/**
 * One template, with its provenance stated rather than implied.
 *
 * The document is the obvious half of this page and the least interesting one.
 * The argument for the whole feature is the chain: this is v3, it supersedes
 * v2, and it exists because an artefact in a real engagement was finalised and
 * promoted. A library that shows only the newest body is a folder of files —
 * the lineage is what makes "every deployment makes the next one easier" a
 * mechanism somebody can audit.
 *
 * The one rule this page has to teach is §7a: a template that has been
 * instantiated is never edited in place, because a live engagement must not
 * have the thing it was copied from change underneath it. So a used template
 * does not show a dead Save button — it says what the rule is and offers the
 * move that is actually available, a new version in the same lineage.
 *
 * `canEdit` and `canPublish` are the server's judgement, read off the detail
 * response. Re-deriving them here from `usageCount` and ownership would put a
 * second copy of the rule in TSX, and the two copies would drift.
 */
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Link, useNavigate, useParams } from 'react-router-dom';
import {
  AlertTriangle,
  ArrowLeft,
  GitBranch,
  History,
  Lock,
  Pencil,
  RefreshCw,
  Send,
  Sparkles,
  Users,
} from 'lucide-react';
import { toast } from 'sonner';
import {
  DEMAND_STAGES,
  DEMAND_STAGE_LABELS,
  MOTION_KIND_DESCRIPTIONS,
  MOTION_VISIBILITY_LABELS,
  toPiggyPageRoute,
  type DemandStage,
  type MotionKind,
  type MotionVisibility,
} from '@pig/core';
import { FieldsView } from '@/components/motion/FieldsView';
import { Markdown } from '@/components/motion/Markdown';
import { MotionKindBadge } from '@/components/motion/MotionKindBadge';
import {
  Badge,
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  EmptyState,
  Input,
  Skeleton,
  cn,
} from '@/components/ui';
import { Label } from '@/components/ui/label';
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { ApiError, get, patch, post, relativeTime, shortDate } from '@/lib/api';
import { useIdentity } from '@/lib/identity';
import { canAny } from '@/lib/permissions';
import { usePiggyContext } from '@/lib/piggy-context';
import { usePageTitle } from '@/lib/title';

// ------------------------------------------------------------------- shapes

interface TemplateSummary {
  id: string;
  kind: MotionKind;
  slug: string;
  version: number;
  title: string;
  summary: string;
  stage: DemandStage;
  visibility: MotionVisibility;
  ownerUserId: string | null;
  supersedesId: string | null;
  originArtifactId: string | null;
  isSystem: boolean;
  usageCount: number;
  archivedAt: string | null;
  createdAt: string;
  updatedAt: string;
}

interface TemplateDetail extends TemplateSummary {
  body: string;
  fields: Record<string, unknown> | null;
}

interface TemplateDetailResponse {
  template: TemplateDetail;
  /** Every version of this slug the reader may see, oldest first. */
  lineage: TemplateSummary[];
  /** Owner or admin, and never instantiated. The §7a rule, already applied. */
  canEdit: boolean;
  canPublish: boolean;
}

interface EngagementOption {
  id: string;
  dealName: string | null;
  accountName: string | null;
  stage: DemandStage | null;
}

interface Promotion {
  templateId: string;
  engagementId: string | null;
  dealName: string | null;
}

interface MotionOverview {
  promotions: Promotion[];
}

interface EditState {
  title: string;
  summary: string;
  body: string;
  stage: DemandStage;
}

// --------------------------------------------------------------------- page

export function MotionTemplate() {
  const { id = '' } = useParams<{ id: string }>();
  const me = useIdentity();
  const queryClient = useQueryClient();
  const navigate = useNavigate();
  const [editing, setEditing] = useState(false);
  const [engagementId, setEngagementId] = useState('');

  const detail = useQuery({
    queryKey: ['motion', 'templates', id, 'detail'],
    queryFn: () => get<TemplateDetailResponse>(`/api/motion/templates/${id}`),
    enabled: Boolean(id) && canAny(me, 'book:read'),
    retry: false,
  });
  const engagements = useQuery({
    queryKey: ['motion', 'engagements', 'open'],
    queryFn: () => get<{ engagements: EngagementOption[] }>('/api/motion/engagements?status=open'),
    enabled: Boolean(id) && canAny(me, 'motion:write'),
  });
  /*
   * Read only for the provenance line. The detail response carries
   * `originArtifactId` but not the engagement behind it, and the overview's
   * promotion list is the one place that join already exists — so a recently
   * promoted template can name the deal it came out of, and an older one says
   * plainly that it came from an engagement without inventing which.
   */
  const overview = useQuery({
    queryKey: ['motion', 'overview'],
    queryFn: () => get<MotionOverview>('/api/motion'),
    enabled: Boolean(detail.data?.template.originArtifactId),
  });

  const template = detail.data?.template;
  usePageTitle(template?.title ?? 'Template');
  usePiggyContext({
    type: 'page',
    route: toPiggyPageRoute('/motion/library'),
    label: template ? template.title : 'Motion library',
  });

  const invalidate = async (): Promise<void> => {
    await Promise.all([
      queryClient.invalidateQueries({ queryKey: ['motion', 'templates'] }),
      queryClient.invalidateQueries({ queryKey: ['motion', 'overview'] }),
    ]);
  };

  const save = useMutation({
    mutationFn: (input: EditState) => patch<{ template: TemplateDetail }>(`/api/motion/templates/${id}`, input),
    onSuccess: async () => {
      setEditing(false);
      await invalidate();
      toast.success('Template saved');
    },
    onError: (error: Error) => toast.error(error.message),
  });

  const fork = useMutation({
    mutationFn: () => post<{ template: TemplateDetail }>(`/api/motion/templates/${id}/versions`, {}),
    onSuccess: async ({ template: created }) => {
      await invalidate();
      toast.success(`Version ${created.version} drafted, private to you`);
      navigate(`/motion/library/${created.id}`);
    },
    onError: (error: Error) => toast.error(error.message),
  });

  const publish = useMutation({
    mutationFn: () => post<{ template: TemplateDetail }>(`/api/motion/templates/${id}/publish`, {}),
    onSuccess: async () => {
      await invalidate();
      toast.success('Published to the book');
    },
    onError: (error: Error) => toast.error(error.message),
  });

  const instantiate = useMutation({
    mutationFn: (target: string) =>
      post<{ artifact: { id: string; engagementId: string } }>(
        `/api/motion/engagements/${target}/artifacts`,
        { templateId: id },
      ),
    onSuccess: async ({ artifact }) => {
      await invalidate();
      await queryClient.invalidateQueries({ queryKey: ['motion', 'engagements'] });
      toast.success('Instantiated into the engagement');
      navigate(`/motion/engagements/${artifact.engagementId}`);
    },
    onError: (error: Error) => toast.error(error.message),
  });

  if (!canAny(me, 'book:read')) {
    return (
      <Restricted
        icon={<Lock />}
        title="This template is restricted"
        description="Motion templates sit behind book access. Ask a platform administrator for team membership."
      />
    );
  }

  if (detail.isLoading) {
    return (
      <div className="flex min-w-0 flex-col gap-4">
        <BackLink />
        <Skeleton className="h-96" />
      </div>
    );
  }

  if (detail.error instanceof ApiError && detail.error.status === 404) {
    return (
      <Restricted
        icon={<Lock />}
        title="No such template"
        // The same answer covers both cases on purpose, and saying so is
        // kinder than a bare "not found" that reads as a broken link.
        description="It has been archived, the link was to an id that never existed, or it is private to somebody else."
      />
    );
  }

  if (detail.error || !detail.data || !template) {
    return (
      <div className="flex min-w-0 flex-col gap-4">
        <BackLink />
        <Card>
          <CardContent className="pt-5">
            <EmptyState
              icon={<AlertTriangle />}
              title="Template unavailable"
              description={
                detail.error instanceof Error ? detail.error.message : 'The template could not be loaded.'
              }
              action={
                <Button variant="outline" onClick={() => void detail.refetch()}>
                  <RefreshCw aria-hidden />
                  Try again
                </Button>
              }
            />
          </CardContent>
        </Card>
      </div>
    );
  }

  const { lineage, canEdit, canPublish } = detail.data;
  const mayWrite = canAny(me, 'motion:write');
  const supersedes = lineage.find((version) => version.id === template.supersedesId) ?? null;
  const promotion = overview.data?.promotions.find((row) => row.templateId === template.id) ?? null;
  const openEngagements = engagements.data?.engagements ?? [];

  return (
    <div className="flex min-w-0 flex-col gap-5">
      <BackLink />

      <header className="min-w-0">
        <div className="flex min-w-0 flex-wrap items-center gap-1.5">
          <MotionKindBadge kind={template.kind} />
          <Badge tone="neutral">{DEMAND_STAGE_LABELS[template.stage]}</Badge>
          <Badge tone={template.visibility === 'private' ? 'warning' : 'neutral'}>
            {MOTION_VISIBILITY_LABELS[template.visibility]}
          </Badge>
          {template.isSystem ? (
            <Badge tone="neutral" title="Shipped with PIG rather than authored here">
              <Sparkles className="size-3 shrink-0" aria-hidden />
              Starter
            </Badge>
          ) : null}
        </div>
        <h1 className="mt-2 min-w-0 break-words text-xl font-semibold tracking-tight sm:text-2xl">
          {template.title}
        </h1>
        <p className="mt-1 max-w-2xl min-w-0 break-words text-sm leading-6 text-muted">
          {template.summary}
        </p>
        <p className="mt-2 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
          <span className="nums whitespace-nowrap">Version {template.version}</span>
          <span className="nums inline-flex items-center gap-1 whitespace-nowrap">
            <Users className="size-3 shrink-0" aria-hidden />
            {template.usageCount === 1 ? 'Used once' : `Used ${template.usageCount} times`}
          </span>
          <span className="min-w-0 truncate">{MOTION_KIND_DESCRIPTIONS[template.kind]}</span>
        </p>
      </header>

      <Provenance
        template={template}
        supersedes={supersedes}
        engagementId={promotion?.engagementId ?? null}
        dealName={promotion?.dealName ?? null}
      />

      {/*
        The §7a rule, said in full where the edit button would otherwise be.
        A disabled control with no explanation teaches the reader that the app
        is broken; this one names the reason and the move that replaces it.
      */}
      {template.usageCount > 0 ? (
        <div className="flex min-w-0 gap-3 rounded-xl border border-info/30 bg-info/10 p-3 text-sm">
          <History className="size-4 shrink-0 text-info" aria-hidden />
          <div className="min-w-0">
            <p className="font-medium">This template is closed to edits</p>
            <p className="mt-1 text-muted">
              {template.usageCount === 1 ? 'An engagement has' : `${template.usageCount} engagements have`}{' '}
              already instantiated it, and their artefacts record that they came from v
              {template.version}. Changing it now would rewrite their provenance. Draft a new
              version instead — the lineage keeps both.
            </p>
          </div>
        </div>
      ) : null}

      <div className="grid min-w-0 gap-2 sm:flex sm:flex-wrap">
        {canEdit ? (
          <Button
            variant={editing ? 'secondary' : 'outline'}
            aria-pressed={editing}
            onClick={() => setEditing((current) => !current)}
          >
            <Pencil aria-hidden />
            {editing ? 'Stop editing' : 'Edit draft'}
          </Button>
        ) : null}
        <Button
          variant={template.usageCount > 0 ? 'primary' : 'outline'}
          disabled={!mayWrite || fork.isPending}
          title={mayWrite ? undefined : WRITE_DENIED}
          onClick={() => fork.mutate()}
        >
          <GitBranch aria-hidden />
          {template.usageCount > 0 ? 'New version' : 'Fork to a private draft'}
        </Button>
        {canPublish ? (
          <Button variant="primary" disabled={publish.isPending} onClick={() => publish.mutate()}>
            <Send aria-hidden />
            Publish to the book
          </Button>
        ) : null}
      </div>

      {editing && canEdit ? (
        <TemplateEditor
          initial={{
            title: template.title,
            summary: template.summary,
            body: template.body,
            stage: template.stage,
          }}
          pending={save.isPending}
          onCancel={() => setEditing(false)}
          onSave={(input) => save.mutate(input)}
        />
      ) : null}

      <Card>
        <CardHeader className="flex-row items-start justify-between gap-3">
          <div className="min-w-0">
            <CardTitle>Use this template</CardTitle>
            <p className="text-sm text-muted">
              Instantiating copies the body and fields into the engagement as a draft artefact, and
              records which version it came from.
            </p>
          </div>
        </CardHeader>
        <CardContent className="grid min-w-0 gap-2 sm:grid-cols-[minmax(0,1fr)_auto]">
          {/*
            Pending and failed are answered before empty. "No open engagement to
            instantiate into" is a claim about the book, and it was being made
            while the query was still in flight and again when it had failed —
            so a reader was told to go and open an engagement they already have.
            `isPending` is a safe reading of "still loading" here only because
            both halves of this query's `enabled` are already established:
            `mayWrite` by the arm above, `id` by the detail data this branch is
            rendered from.
          */}
          {!mayWrite ? (
            <p className="text-sm text-muted">{WRITE_DENIED}</p>
          ) : engagements.isPending ? (
            <Select disabled>
              <SelectTrigger aria-label="Choose an engagement" className="h-11 min-w-0">
                <SelectValue placeholder="Loading engagements…" />
              </SelectTrigger>
            </Select>
          ) : engagements.isError ? (
            <>
              <p className="text-sm text-muted">
                {engagements.error instanceof Error
                  ? engagements.error.message
                  : 'The open engagements could not be loaded.'}
              </p>
              <Button variant="outline" onClick={() => void engagements.refetch()}>
                <RefreshCw aria-hidden />
                Try again
              </Button>
            </>
          ) : openEngagements.length === 0 ? (
            <p className="text-sm text-muted">
              No open engagement to instantiate into.{' '}
              <Link to="/motion/engagements" className="underline underline-offset-4">
                Open one against a demand deal
              </Link>{' '}
              first.
            </p>
          ) : (
            <>
              <Select value={engagementId || undefined} onValueChange={setEngagementId}>
                <SelectTrigger aria-label="Choose an engagement" className="h-11 min-w-0">
                  <SelectValue placeholder="Choose an engagement" />
                </SelectTrigger>
                <SelectContent>
                  <SelectGroup>
                    {openEngagements.map((engagement) => (
                      <SelectItem key={engagement.id} value={engagement.id}>
                        {engagement.dealName ?? 'Unnamed deal'}
                        {engagement.accountName ? ` · ${engagement.accountName}` : ''}
                      </SelectItem>
                    ))}
                  </SelectGroup>
                </SelectContent>
              </Select>
              <Button
                variant="primary"
                disabled={!engagementId || instantiate.isPending}
                onClick={() => instantiate.mutate(engagementId)}
              >
                Instantiate
              </Button>
            </>
          )}
        </CardContent>
      </Card>

      <Card>
        <CardHeader>
          <CardTitle>The document</CardTitle>
        </CardHeader>
        <CardContent>
          {template.body.trim() ? (
            <Markdown content={template.body} />
          ) : (
            <p className="text-sm text-muted">This version has no body.</p>
          )}
        </CardContent>
      </Card>

      <FieldsView kind={template.kind} fields={template.fields} />

      <Lineage lineage={lineage} currentId={template.id} slug={template.slug} />
    </div>
  );
}

// ---------------------------------------------------------------- fragments

const WRITE_DENIED = 'Authoring in the library needs the motion:write permission.';

function BackLink() {
  return (
    <Link
      to="/motion/library"
      className="tap inline-flex min-h-11 w-fit items-center gap-2 text-sm text-muted hover:text-fg"
    >
      <ArrowLeft className="size-4" aria-hidden />
      Back to the library
    </Link>
  );
}

function Restricted({
  icon,
  title,
  description,
}: {
  icon: React.ReactNode;
  title: string;
  description: string;
}) {
  return (
    <div className="flex min-w-0 flex-col gap-4">
      <BackLink />
      <Card>
        <CardContent className="pt-5">
          <EmptyState icon={icon} title={title} description={description} />
        </CardContent>
      </Card>
    </div>
  );
}

/**
 * Where this version came from, in one sentence a reader can act on.
 *
 * Three facts, and each is absent for a legitimate reason: v1 supersedes
 * nothing, an authored template was never promoted, and a promotion older than
 * the overview's window is known to exist without its engagement being named.
 */
function Provenance({
  template,
  supersedes,
  engagementId,
  dealName,
}: {
  template: TemplateSummary;
  supersedes: TemplateSummary | null;
  engagementId: string | null;
  dealName: string | null;
}) {
  const promoted = Boolean(template.originArtifactId);
  if (!supersedes && !promoted) return null;

  return (
    <div className="flex min-w-0 gap-3 rounded-xl border border-border bg-surface-2/60 p-3 text-sm">
      <GitBranch className="size-4 shrink-0 text-muted" aria-hidden />
      <p className="min-w-0 break-words">
        <span className="nums font-medium">Version {template.version}</span>
        {supersedes ? (
          <>
            , superseding{' '}
            <Link
              to={`/motion/library/${supersedes.id}`}
              className="underline underline-offset-4 hover:text-accent-fg"
            >
              v{supersedes.version}
            </Link>
          </>
        ) : null}
        {promoted ? (
          <>
            , promoted from an artefact
            {engagementId ? (
              <>
                {' '}on{' '}
                <Link
                  to={`/motion/engagements/${engagementId}`}
                  className="underline underline-offset-4 hover:text-accent-fg"
                >
                  {dealName ?? 'its engagement'}
                </Link>
              </>
            ) : (
              ' proved in an engagement'
            )}
          </>
        ) : null}
        . <span className="text-muted">Created {shortDate(template.createdAt)}.</span>
      </p>
    </div>
  );
}

function Lineage({
  lineage,
  currentId,
  slug,
}: {
  lineage: TemplateSummary[];
  currentId: string;
  slug: string;
}) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Version history</CardTitle>
        <p className="text-sm text-muted">
          Every version of <span className="font-mono text-xs">{slug}</span> you can see, oldest
          first. Versions private to somebody else are not listed.
        </p>
      </CardHeader>
      <CardContent className="flex min-w-0 flex-col gap-2">
        {lineage.map((version) => (
          <LineageRow
            key={version.id}
            version={version}
            current={version.id === currentId}
          />
        ))}
      </CardContent>
    </Card>
  );
}

/**
 * One version in the history.
 *
 * The whole row navigates rather than the title alone, which was a single line
 * of text and about 20px of target on a phone. The version already open is a
 * plain div instead: a link to where you already are is a tap that does
 * nothing, which is worse than no target at all.
 */
function LineageRow({ version, current }: { version: TemplateSummary; current: boolean }) {
  const body = (
    <>
      <div className="min-w-0">
        <p className="min-w-0 break-words font-medium">{version.title}</p>
        <p className="mt-1 min-w-0 truncate text-xs text-muted">
          {MOTION_VISIBILITY_LABELS[version.visibility]} ·{' '}
          <span className="nums">{version.usageCount}</span> use
          {version.usageCount === 1 ? '' : 's'} · {relativeTime(version.updatedAt)}
        </p>
      </div>
      <Badge tone={current ? 'accent' : 'neutral'} className="nums shrink-0">
        v{version.version}
      </Badge>
    </>
  );
  const shape = 'flex min-h-11 min-w-0 items-start justify-between gap-3 rounded-lg p-3';

  if (current) {
    return <div className={cn(shape, 'bg-surface-2 ring-1 ring-border')}>{body}</div>;
  }
  return (
    <Link
      to={`/motion/library/${version.id}`}
      className={cn(shape, 'tap bg-surface-2/60 hover:bg-surface-2')}
    >
      {body}
    </Link>
  );
}

/**
 * The draft editor, shown only while `canEdit` holds.
 *
 * No `kind` and no `visibility` field, matching the endpoint: a lineage that
 * changes kind halfway is a different template wearing the same slug, and
 * visibility moves through Publish, where the capability check lives.
 */
function TemplateEditor({
  initial,
  pending,
  onCancel,
  onSave,
}: {
  initial: EditState;
  pending: boolean;
  onCancel(): void;
  onSave(input: EditState): void;
}) {
  const [form, setForm] = useState(initial);
  const set = <Key extends keyof EditState>(key: Key, value: EditState[Key]) =>
    setForm((current) => ({ ...current, [key]: value }));

  return (
    <Card>
      <CardContent className="pt-5">
        <form
          className="flex min-w-0 flex-col gap-4"
          onSubmit={(event) => {
            event.preventDefault();
            onSave(form);
          }}
        >
          <div className="grid min-w-0 gap-3 sm:grid-cols-2">
            <div className="flex min-w-0 flex-col gap-1.5">
              <Label htmlFor="template-title">Title</Label>
              <Input
                id="template-title"
                required
                value={form.title}
                onChange={(event) => set('title', event.target.value)}
              />
            </div>
            <div className="flex min-w-0 flex-col gap-1.5">
              <Label htmlFor="template-stage">Stage it serves</Label>
              <Select value={form.stage} onValueChange={(value) => set('stage', value as DemandStage)}>
                <SelectTrigger id="template-stage" className="h-11 min-w-0">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectGroup>
                    {DEMAND_STAGES.map((stage) => (
                      <SelectItem key={stage} value={stage}>
                        {DEMAND_STAGE_LABELS[stage]}
                      </SelectItem>
                    ))}
                  </SelectGroup>
                </SelectContent>
              </Select>
            </div>
          </div>
          <div className="flex min-w-0 flex-col gap-1.5">
            <Label htmlFor="template-summary">Summary</Label>
            <Textarea
              id="template-summary"
              required
              rows={2}
              value={form.summary}
              onChange={(event) => set('summary', event.target.value)}
            />
          </div>
          <div className="flex min-w-0 flex-col gap-1.5">
            <Label htmlFor="template-body">Body</Label>
            <Textarea
              id="template-body"
              rows={16}
              className="font-mono text-xs"
              value={form.body}
              onChange={(event) => set('body', event.target.value)}
            />
            <p className="text-xs text-muted">
              Markdown, with GFM tables and task lists. Structured fields are edited through the
              API for now.
            </p>
          </div>
          <div className="flex min-w-0 flex-wrap justify-end gap-2">
            <Button type="button" variant="ghost" onClick={onCancel}>
              Cancel
            </Button>
            <Button type="submit" variant="primary" disabled={pending}>
              Save draft
            </Button>
          </div>
        </form>
      </CardContent>
    </Card>
  );
}
