/**
 * Every engagement, one per demand deal.
 *
 * The list answers a different question from the Motion home: not "is the
 * motion repeating" but "which deals are being worked with it, and what does
 * qualification say about them now". So the columns are the ones that move —
 * stage, artefact count, latest score — and the status filter lives in the URL
 * so a view of everything still open can be pasted to somebody.
 *
 * `Unscored` is a stat rather than a column because it is the finding: an open
 * engagement nobody has qualified is a deal being worked on instinct, and it is
 * invisible in a table that only shows the scores that exist.
 */
import { useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Link, useSearchParams } from 'react-router-dom';
import {
  AlertTriangle,
  ChevronRight,
  Handshake,
  Lock,
  Plus,
  RefreshCw,
} from 'lucide-react';
import { toast } from 'sonner';
import {
  DEMAND_STAGE_LABELS,
  ENGAGEMENT_STATUSES,
  ENGAGEMENT_STATUS_LABELS,
  isEngagementStatus,
  toPiggyPageRoute,
  type DemandStage,
  type EngagementStatus,
  type MotionBandTone,
} from '@pig/core';
import { percent } from '@/components/motion/format';
import { get, post, shortDate } from '@/lib/api';
import { usePageTitle } from '@/lib/title';
import { usePiggyContext } from '@/lib/piggy-context';
import { useIdentity } from '@/lib/identity';
import { canAny } from '@/lib/permissions';
import {
  Badge,
  Button,
  Card,
  CardContent,
  EmptyState,
  Skeleton,
  Stat,
  cn,
} from '@/components/ui';
import { Label } from '@/components/ui/label';
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Textarea } from '@/components/ui/textarea';

/** The score the list carries — the latest one, or none yet. */
interface LatestScore {
  basisPoints: number;
  band: string;
  tone: MotionBandTone;
  scoredAt: string;
}

/** `GET /api/motion/engagements` rows. `stage` is the deal's, not the engagement's. */
interface EngagementRow {
  id: string;
  demandDealId: string;
  dealName: string | null;
  stage: DemandStage | null;
  accountId: string | null;
  accountName: string | null;
  status: EngagementStatus;
  summary: string | null;
  openedAt: string;
  closedAt: string | null;
  artifactCount: number;
  latestScore: LatestScore | null;
}

interface DemandDealRow {
  deal: { id: string; name: string; stage: DemandStage };
  accountName: string | null;
}

const WRITE_DENIED = 'Opening an engagement needs the motion:write permission.';

export function MotionEngagements() {
  usePageTitle('Engagements');
  usePiggyContext({
    type: 'page',
    route: toPiggyPageRoute('/motion/engagements'),
    label: 'Engagements',
  });
  const me = useIdentity();
  const mayWrite = canAny(me, 'motion:write');
  const [params, setParams] = useSearchParams();
  const [opening, setOpening] = useState(false);

  // The filter lives in the URL so a view of "everything still open" can be
  // pasted to somebody, which is the whole reason anyone filters a list.
  const statusParam = params.get('status');
  const status = statusParam && isEngagementStatus(statusParam) ? statusParam : null;

  const engagements = useQuery({
    queryKey: ['motion', 'engagements', { status }],
    queryFn: () =>
      get<{ engagements: EngagementRow[] }>(
        `/api/motion/engagements${status ? `?status=${status}` : ''}`,
      ),
    // The permission short-circuit below is an early return, and a hook cannot
    // sit behind one — so without this the read fires for exactly the people
    // the page is about to tell they have no access, and react-query retries
    // the 403 while they read the empty state.
    enabled: canAny(me, 'book:read'),
  });

  const rows = engagements.data?.engagements ?? [];
  const totals = useMemo(
    () => ({
      open: rows.filter((row) => row.status === 'open').length,
      artifacts: rows.reduce((sum, row) => sum + row.artifactCount, 0),
      unscored: rows.filter((row) => row.status === 'open' && !row.latestScore).length,
    }),
    [rows],
  );

  if (!canAny(me, 'book:read')) {
    return (
      <div className="flex min-w-0 flex-col gap-5">
        <header>
          <h1 className="text-xl font-semibold tracking-tight sm:text-2xl">Engagements</h1>
        </header>
        <Card>
          <CardContent className="pt-5">
            <EmptyState
              icon={<Lock />}
              title="Engagements are restricted"
              description="Reading the book behind an engagement needs the book permission. Ask a platform administrator for team membership."
            />
          </CardContent>
        </Card>
      </div>
    );
  }

  return (
    <div className="flex min-w-0 flex-col gap-5">
      <header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
        <div className="min-w-0">
          <h1 className="text-xl font-semibold tracking-tight sm:text-2xl">Engagements</h1>
          <p className="mt-1 max-w-2xl text-sm text-muted">
            One per demand deal: the artefacts it has produced, and what qualification says about
            it now rather than when it was opened.
          </p>
        </div>
        <Button
          type="button"
          variant="primary"
          disabled={!mayWrite}
          title={mayWrite ? undefined : WRITE_DENIED}
          onClick={() => setOpening(true)}
        >
          <Plus aria-hidden />
          Open an engagement
        </Button>
      </header>

      {rows.length ? (
        <section className="grid grid-cols-2 gap-2 sm:gap-3 xl:grid-cols-3" aria-label="Engagement summary">
          <Stat label="Open" value={totals.open} hint="Still in the motion" />
          <Stat label="Artefacts" value={totals.artifacts} hint="Across the list below" />
          <Stat
            label="Unscored"
            value={totals.unscored}
            hint="Open, never qualified"
            tone={totals.unscored ? 'warning' : 'default'}
          />
        </section>
      ) : null}

      <div className="flex min-w-0 flex-wrap items-center gap-2">
        <StatusFilter
          value={status}
          onChange={(next) => {
            const updated = new URLSearchParams(params);
            if (next) updated.set('status', next);
            else updated.delete('status');
            setParams(updated, { replace: true });
          }}
        />
      </div>

      {engagements.isLoading ? <Skeleton className="h-72" /> : null}

      {engagements.isError ? (
        <Card>
          <CardContent className="pt-5">
            <EmptyState
              icon={<AlertTriangle />}
              title="Engagements unavailable"
              description={
                engagements.error instanceof Error
                  ? engagements.error.message
                  : 'The list could not be loaded.'
              }
              action={
                <Button variant="outline" onClick={() => void engagements.refetch()}>
                  <RefreshCw aria-hidden />
                  Try again
                </Button>
              }
            />
          </CardContent>
        </Card>
      ) : null}

      {!engagements.isLoading && !engagements.isError && rows.length === 0 ? (
        <Card>
          <CardContent className="pt-5">
            <EmptyState
              icon={<Handshake />}
              title={status ? 'No engagement in this state' : 'No engagements yet'}
              description={
                status
                  ? 'Clear the filter to see the rest of them.'
                  : 'An engagement binds the library to a demand deal. Open one on the deal you are working now, and the artefacts it produces can be promoted back.'
              }
              action={
                status ? undefined : (
                  <Button
                    variant="primary"
                    disabled={!mayWrite}
                    title={mayWrite ? undefined : WRITE_DENIED}
                    onClick={() => setOpening(true)}
                  >
                    Open the first one
                  </Button>
                )
              }
            />
          </CardContent>
        </Card>
      ) : null}

      {rows.length ? (
        <>
          <Card className="hidden md:block">
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>Deal</TableHead>
                  <TableHead>Stage</TableHead>
                  <TableHead>Status</TableHead>
                  <TableHead>Artefacts</TableHead>
                  <TableHead>Qualification</TableHead>
                  <TableHead>Opened</TableHead>
                  <TableHead className="w-12">
                    <span className="sr-only">Open</span>
                  </TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {rows.map((row) => (
                  <TableRow key={row.id}>
                    <TableCell>
                      <div className="min-w-0">
                        <Link
                          to={`/motion/engagements/${row.id}`}
                          className="font-medium underline-offset-4 hover:text-accent-fg hover:underline"
                        >
                          {row.dealName ?? 'Untitled deal'}
                        </Link>
                        <p className="truncate text-xs text-muted">
                          {row.accountName ?? 'Unknown account'}
                        </p>
                      </div>
                    </TableCell>
                    <TableCell className="text-sm">
                      {row.stage ? DEMAND_STAGE_LABELS[row.stage] : '—'}
                    </TableCell>
                    <TableCell>
                      <StatusBadge status={row.status} />
                    </TableCell>
                    <TableCell className="nums text-sm">{row.artifactCount}</TableCell>
                    <TableCell>
                      <ScoreCell score={row.latestScore} />
                    </TableCell>
                    <TableCell className="nums text-sm">{shortDate(row.openedAt)}</TableCell>
                    <TableCell>
                      <Link
                        to={`/motion/engagements/${row.id}`}
                        className="tap inline-flex items-center justify-center text-muted"
                        aria-label={`Open ${row.dealName ?? 'engagement'}`}
                      >
                        <ChevronRight aria-hidden />
                      </Link>
                    </TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          </Card>

          <div className="grid gap-3 md:hidden">
            {rows.map((row) => (
              <Link
                key={row.id}
                to={`/motion/engagements/${row.id}`}
                className="card min-h-11 min-w-0 p-4"
              >
                <div className="flex min-w-0 items-start justify-between gap-3">
                  <div className="min-w-0">
                    <div className="flex min-w-0 flex-wrap items-center gap-2">
                      <StatusBadge status={row.status} />
                      {row.stage ? (
                        <Badge tone="neutral" className="min-w-0">
                          <span className="truncate">{DEMAND_STAGE_LABELS[row.stage]}</span>
                        </Badge>
                      ) : null}
                    </div>
                    <p className="mt-2 truncate font-medium">{row.dealName ?? 'Untitled deal'}</p>
                    <p className="mt-0.5 truncate text-sm text-muted">
                      {row.accountName ?? 'Unknown account'}
                    </p>
                  </div>
                  <ChevronRight className="shrink-0 text-muted" aria-hidden />
                </div>
                <div className="mt-3 flex min-w-0 items-end justify-between gap-3 border-t border-border pt-3 text-xs text-muted">
                  <p className="nums min-w-0">
                    {row.artifactCount} artifact{row.artifactCount === 1 ? '' : 's'} · opened{' '}
                    {shortDate(row.openedAt)}
                  </p>
                  <ScoreCell score={row.latestScore} />
                </div>
              </Link>
            ))}
          </div>
        </>
      ) : null}

      <OpenEngagementSheet open={opening} onOpenChange={setOpening} />
    </div>
  );
}

/**
 * Opening an engagement on a deal that already has one is a 409, so the deals
 * that already have one are not offered. The server still refuses — this is a
 * courtesy, not the rule — but an option that can only fail is a worse thing
 * to ship than a shorter list.
 */
function OpenEngagementSheet({
  open,
  onOpenChange,
}: {
  open: boolean;
  onOpenChange: (open: boolean) => void;
}) {
  const queryClient = useQueryClient();
  // Its own check rather than a prop. The trigger is already disabled without
  // `motion:write`, but these two reads are what the sheet costs, and a query
  // that fires for somebody who cannot use the result is a 403 in the network
  // tab and a retry loop behind a dialog they were never meant to open.
  const mayWrite = canAny(useIdentity(), 'motion:write');
  const [demandDealId, setDemandDealId] = useState('');
  const [summary, setSummary] = useState('');

  const deals = useQuery({
    queryKey: ['deals', 'demand'],
    queryFn: () => get<{ deals: DemandDealRow[] }>('/api/deals/demand'),
    enabled: open && mayWrite,
  });
  /*
   * Its own unfiltered query rather than the rows the page is showing. The
   * status filter is the trap: filtered to "Won", the page holds none of the
   * open engagements, so every deal already running would be offered here and
   * every one of them would answer 409. The key matches the page's own
   * unfiltered query, so the two share a cache entry rather than racing.
   */
  const allEngagements = useQuery({
    queryKey: ['motion', 'engagements', { status: null }],
    queryFn: () => get<{ engagements: EngagementRow[] }>('/api/motion/engagements'),
    enabled: open && mayWrite,
  });

  const taken = new Set((allEngagements.data?.engagements ?? []).map((row) => row.demandDealId));
  const available = (deals.data?.deals ?? []).filter((row) => !taken.has(row.deal.id));
  const failed = deals.isError || allEngagements.isError;

  const create = useMutation({
    mutationFn: () =>
      post<{ engagement: { id: string } }>('/api/motion/engagements', {
        demandDealId,
        summary: summary.trim() ? summary.trim() : null,
      }),
    onSuccess: async () => {
      await queryClient.invalidateQueries({ queryKey: ['motion'] });
      toast.success('Engagement opened');
      setDemandDealId('');
      setSummary('');
      onOpenChange(false);
    },
    onError: (error: Error) => toast.error(error.message),
  });

  return (
    <Sheet open={open} onOpenChange={onOpenChange}>
      <SheetContent side="right" className="w-full overflow-y-auto sm:max-w-lg">
        <SheetHeader>
          <SheetTitle>Open an engagement</SheetTitle>
          <SheetDescription>
            An engagement hangs off a demand deal rather than replacing it — the deal keeps its
            stage, and the engagement collects what the motion produces against it.
          </SheetDescription>
        </SheetHeader>

        <form
          className="mt-5 flex min-w-0 flex-col gap-4 pb-6"
          onSubmit={(event) => {
            event.preventDefault();
            create.mutate();
          }}
        >
          <div className="flex min-w-0 flex-col gap-1.5">
            <Label htmlFor="engagement-deal">Demand deal</Label>
            <Select value={demandDealId || undefined} onValueChange={setDemandDealId}>
              <SelectTrigger id="engagement-deal" aria-label="Demand deal" className="h-11 min-w-0">
                <SelectValue placeholder={deals.isLoading || allEngagements.isLoading ? 'Loading deals…' : 'Choose a deal'} />
              </SelectTrigger>
              <SelectContent>
                <SelectGroup>
                  {available.map((row) => (
                    <SelectItem key={row.deal.id} value={row.deal.id}>
                      {row.deal.name} · {row.accountName ?? 'Unknown account'}
                    </SelectItem>
                  ))}
                </SelectGroup>
              </SelectContent>
            </Select>
            {/*
              A failed read is not an empty book. Without this the sheet told
              people "every demand deal already has an engagement" when the deal
              query had simply errored. The engagement query's failure has to be
              said too rather than tolerated: `taken` is then empty, so the list
              is every deal including the ones already running, and choosing one
              of those can only answer 409.
            */}
            {failed ? (
              <>
                <p className="text-xs text-danger">
                  {(deals.error ?? allEngagements.error)?.message ??
                    'The deal list could not be loaded.'}
                </p>
                <p className="text-xs text-muted">
                  {deals.isError
                    ? 'No deal can be offered until this loads.'
                    : 'Deals that already have an engagement cannot be filtered out, so a choice here may be refused.'}
                </p>
                <Button
                  type="button"
                  variant="outline"
                  className="w-fit"
                  onClick={() => {
                    void deals.refetch();
                    void allEngagements.refetch();
                  }}
                >
                  <RefreshCw aria-hidden />
                  Try again
                </Button>
              </>
            ) : !deals.isLoading && !allEngagements.isLoading && available.length === 0 ? (
              <p className="text-xs text-muted">
                Every demand deal already has an engagement. Open one from the deal in Pipeline
                once there is a new deal to run.
              </p>
            ) : null}
          </div>

          <div className="flex min-w-0 flex-col gap-1.5">
            <Label htmlFor="engagement-summary">What this engagement is for</Label>
            <Textarea
              id="engagement-summary"
              className="min-h-24"
              value={summary}
              onChange={(event) => setSummary(event.target.value)}
              placeholder="The deployment being scoped, in one or two sentences."
            />
          </div>

          {create.isError ? <p className="text-sm text-danger">{create.error.message}</p> : null}

          <div className="flex min-w-0 flex-wrap justify-end gap-2">
            <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
              Cancel
            </Button>
            <Button type="submit" variant="primary" disabled={!demandDealId || create.isPending}>
              Open engagement
            </Button>
          </div>
        </form>
      </SheetContent>
    </Sheet>
  );
}

// ------------------------------------------------------------------ helpers

function StatusFilter({
  value,
  onChange,
}: {
  value: EngagementStatus | null;
  onChange: (value: EngagementStatus | null) => void;
}) {
  return (
    <div role="tablist" aria-label="Filter by status" className="scroll-x -mx-1 flex gap-1 px-1 pb-1">
      {([null, ...ENGAGEMENT_STATUSES] as const).map((option) => (
        <button
          key={option ?? 'all'}
          type="button"
          role="tab"
          aria-selected={value === option}
          onClick={() => onChange(option)}
          className={cn(
            'tap min-h-11 shrink-0 rounded-lg px-4 text-sm font-medium transition-colors',
            value === option
              ? 'bg-surface text-fg shadow-sm ring-1 ring-border'
              : 'text-muted hover:bg-surface-2',
          )}
        >
          {option ? ENGAGEMENT_STATUS_LABELS[option] : 'All'}
        </button>
      ))}
    </div>
  );
}

function StatusBadge({ status }: { status: EngagementStatus }) {
  const tone =
    status === 'won' ? 'positive' : status === 'lost' ? 'danger' : status === 'paused' ? 'warning' : 'info';
  return <Badge tone={tone}>{ENGAGEMENT_STATUS_LABELS[status]}</Badge>;
}

/**
 * The band leads and the figure follows it.
 *
 * "Qualified" is what a seller acts on; 6,200 basis points is what an analyst
 * checks afterwards. Showing the number alone invites the reading that 62 is
 * nearly 70 and therefore nearly good, when the band boundary at 7500 is the
 * only place anything changes.
 */
function ScoreCell({ score }: { score: LatestScore | null }) {
  if (!score) return <span className="text-sm text-muted">Not scored</span>;
  return (
    <span className="inline-flex min-w-0 flex-wrap items-center gap-1.5">
      <Badge tone={score.tone}>{score.band}</Badge>
      <span className="nums whitespace-nowrap text-xs text-muted" title={`${score.basisPoints} basis points`}>
        {percent(score.basisPoints)}
      </span>
    </span>
  );
}
