/**
 * The library: nine kinds, filtered four ways, in a URL somebody can send.
 *
 * The filters live in the query string rather than in component state, which is
 * a first for this app. It is not tidiness — "the proposal blocks we have for
 * procurement" is a thing one person tells another, and until now the only way
 * to share a view was to describe the clicks. `useSearchParams` makes the view
 * the address.
 *
 * Two rules keep that URL readable, and both are load-bearing:
 * defaults are never written (an unset filter is an absent parameter, not
 * `?kind=all`), and every write is `replace`, so dragging across the stage rail
 * leaves one history entry rather than eight for the back button to walk.
 *
 * An unrecognised value is treated as absent. The API answers `400
 * invalid_request` on an unknown enum, and a hand-edited URL — or one that
 * outlived a kind the ontology dropped — should degrade to the unfiltered
 * library, not to an error page.
 *
 * The rail's figures come from `/api/motion` rather than from the rows on
 * screen: a rail computed from the filtered result empties as you use it, which
 * makes the one control that is meant to navigate the motion useless the moment
 * anything is selected.
 */
import { useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom';
import { AlertTriangle, Layers, Library, Lock, RefreshCw, Search } from 'lucide-react';
import {
  DEMAND_STAGES,
  MOTION_KINDS,
  MOTION_KIND_LABELS,
  MOTION_VISIBILITIES,
  MOTION_VISIBILITY_LABELS,
  isMotionKind,
  isMotionVisibility,
  toPiggyPageRoute,
  type DemandStage,
  type MotionKind,
  type MotionVisibility,
} from '@pig/core';
import { StageRail } from '@/components/motion/StageRail';
import { TemplateCard } from '@/components/motion/TemplateCard';
import type { MotionTemplateView } from '@/components/motion/model';
import { Button, Card, CardContent, EmptyState, Input, Skeleton } from '@/components/ui';
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { get } 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

/**
 * What the list endpoint serialises: the row without `body` or `fields`.
 *
 * Two hundred playbooks is a megabyte of markdown nobody on this page reads, so
 * the API sends the summary and the detail endpoint sends the document.
 */
type TemplateSummary = Omit<MotionTemplateView, 'body' | 'fields'>;

interface TemplateList {
  templates: TemplateSummary[];
  /** The scan hit its bound — the library is wider than this answer. */
  truncated: boolean;
}

interface StageCoverage {
  stage: DemandStage;
  engagements: number;
  templates: number;
}

interface MotionOverview {
  stages: StageCoverage[];
}

interface Filters {
  kind: MotionKind | null;
  stage: DemandStage | null;
  visibility: MotionVisibility | null;
  q: string;
  all: boolean;
}

const STAGE_PARAM = 'stage';
/** Long enough that a typed word is one request, short enough to feel live. */
const SEARCH_SETTLE_MS = 250;

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

export function MotionLibrary() {
  usePageTitle('Library');
  usePiggyContext({
    type: 'page',
    route: toPiggyPageRoute('/motion/library'),
    label: 'Motion library',
  });
  const me = useIdentity();
  const [params, setParams] = useSearchParams();
  const filters = readFilters(params);

  // The input is local and the URL follows it, rather than the other way
  // round: bound directly to the query string, every keystroke is a request
  // against a 400-row scan and a URL that is only copyable between words.
  const [text, setText] = useState(filters.q);
  const pushed = useRef(filters.q);
  useEffect(() => {
    if (text === filters.q) return;
    const timer = setTimeout(() => {
      pushed.current = text;
      setParam(setParams, 'q', text);
    }, SEARCH_SETTLE_MS);
    return () => clearTimeout(timer);
  }, [filters.q, setParams, text]);
  useEffect(() => {
    // The URL moved and it was not us — a pasted link, or a back navigation
    // out of some other filter. Adopt it. Without this the pending input wins
    // the next tick and pushes itself straight back over the address, which
    // reads as a back button that does not work.
    if (filters.q === pushed.current) return;
    pushed.current = filters.q;
    setText(filters.q);
  }, [filters.q]);

  const templates = useQuery({
    queryKey: ['motion', 'templates', filters],
    queryFn: () => get<TemplateList>(`/api/motion/templates${queryString(filters)}`),
    enabled: canAny(me, 'book:read'),
  });
  // Shared with the Motion home, and deliberately unfiltered — see the header.
  const overview = useQuery({
    queryKey: ['motion', 'overview'],
    queryFn: () => get<MotionOverview>('/api/motion'),
    enabled: canAny(me, 'book:read'),
  });

  const railCounts = useMemo(
    () =>
      Object.fromEntries(
        (overview.data?.stages ?? []).map((stage) => [stage.stage, stage.templates]),
      ) as Partial<Record<DemandStage, number>>,
    [overview.data],
  );

  const filtered = Boolean(
    filters.kind || filters.stage || filters.visibility || filters.q || filters.all,
  );

  if (!canAny(me, 'book:read')) {
    return (
      <div className="flex min-w-0 flex-col gap-5">
        <PageHeader />
        <Card>
          <CardContent className="pt-5">
            <EmptyState
              icon={<Lock />}
              title="The library is restricted"
              description="Motion templates sit behind book access. Ask a platform administrator for team membership."
            />
          </CardContent>
        </Card>
      </div>
    );
  }

  const rows = templates.data?.templates ?? [];

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

      <StageRail
        counts={railCounts}
        countNoun={['template', 'templates']}
        activeStage={filters.stage}
        onSelect={(stage) =>
          setParam(setParams, STAGE_PARAM, stage === filters.stage ? '' : stage)
        }
      />

      <div className="grid min-w-0 gap-3 rounded-xl border border-border bg-surface-2/60 p-3 sm:grid-cols-[minmax(0,1fr)_170px_170px]">
        <div className="relative min-w-0">
          <Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted" aria-hidden />
          <Input
            aria-label="Search the library"
            className="pl-9"
            placeholder="Search title, summary or slug"
            value={text}
            onChange={(event) => setText(event.target.value)}
          />
        </div>
        <EnumSelect
          aria-label="Filter by kind"
          value={filters.kind ?? 'all'}
          onValueChange={(value) => setParam(setParams, 'kind', value === 'all' ? '' : value)}
        >
          <SelectItem value="all">All kinds</SelectItem>
          {MOTION_KINDS.map((kind) => (
            <SelectItem key={kind} value={kind}>
              {MOTION_KIND_LABELS[kind]}
            </SelectItem>
          ))}
        </EnumSelect>
        <EnumSelect
          aria-label="Filter by visibility"
          value={filters.visibility ?? 'all'}
          onValueChange={(value) => setParam(setParams, 'visibility', value === 'all' ? '' : value)}
        >
          <SelectItem value="all">Shared and private</SelectItem>
          {MOTION_VISIBILITIES.map((visibility) => (
            <SelectItem key={visibility} value={visibility}>
              {MOTION_VISIBILITY_LABELS[visibility]}
            </SelectItem>
          ))}
        </EnumSelect>
      </div>

      <div className="flex min-h-11 min-w-0 flex-wrap items-center justify-between gap-2">
        <p className="min-w-0 text-sm text-muted">
          <strong className="nums text-fg">{rows.length}</strong>{' '}
          {filters.all ? 'versions' : 'templates'} shown
          {templates.data?.truncated ? ' · more exist than fit in one answer' : ''}
        </p>
        <div className="flex min-w-0 flex-wrap items-center gap-2">
          {/* Lineages collapse to their newest version by default. The whole
              history is what you want when judging whether a template is
              settled or still being rewritten every fortnight. */}
          <Button
            variant={filters.all ? 'secondary' : 'ghost'}
            size="sm"
            aria-pressed={filters.all}
            onClick={() => setParam(setParams, 'all', filters.all ? '' : '1')}
          >
            <Layers aria-hidden />
            {filters.all ? 'Newest versions' : 'Every version'}
          </Button>
          {filtered ? (
            <Button
              variant="ghost"
              size="sm"
              onClick={() => {
                setText('');
                setParams(new URLSearchParams(), { replace: true });
              }}
            >
              Clear filters
            </Button>
          ) : null}
        </div>
      </div>

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

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

      {!templates.isLoading && !templates.isError && rows.length === 0 ? (
        <Card>
          <CardContent className="pt-5">
            <EmptyState
              icon={<Library />}
              title={filtered ? 'No templates match' : 'The library is empty'}
              description={
                filtered
                  ? 'Clear a filter to see the rest of the library. A private template belonging to somebody else will never appear here.'
                  : 'Nine kinds and nothing in them yet. Author the discovery guide you already run from memory, and instantiate it into the next deal.'
              }
              action={
                filtered ? (
                  <Button
                    variant="outline"
                    onClick={() => {
                      setText('');
                      setParams(new URLSearchParams(), { replace: true });
                    }}
                  >
                    Clear filters
                  </Button>
                ) : undefined
              }
            />
          </CardContent>
        </Card>
      ) : null}

      {rows.length ? (
        <div className="grid min-w-0 gap-3 sm:grid-cols-2 xl:grid-cols-3">
          {rows.map((template) => (
            <TemplateCard
              key={template.id}
              // The card reads neither `body` nor `fields`; its prop type asks
              // for them only because one interface describes both endpoints.
              // Asserting beats inventing an empty body, which would be
              // indistinguishable from a template somebody saved blank.
              template={template as MotionTemplateView}
            />
          ))}
        </div>
      ) : null}
    </div>
  );
}

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

function PageHeader() {
  return (
    <header className="min-w-0">
      <p className="text-xs font-semibold uppercase tracking-[0.16em] text-accent-fg">Motion</p>
      <h1 className="mt-1 text-xl font-semibold tracking-tight sm:text-2xl">Library</h1>
      <p className="mt-1 max-w-2xl text-sm leading-6 text-muted">
        Discovery guides, qualification frameworks, proposal blocks and playbooks — the parts of a
        deployment that should not be rewritten each time. This view is linkable: send the address,
        not the clicks.
      </p>
    </header>
  );
}

type EnumSelectProps = React.ComponentProps<typeof Select> & { 'aria-label': string };

function EnumSelect({ children, 'aria-label': ariaLabel, ...props }: EnumSelectProps) {
  return (
    <Select {...props}>
      <SelectTrigger aria-label={ariaLabel} className="h-11 min-w-0">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        <SelectGroup>{children}</SelectGroup>
      </SelectContent>
    </Select>
  );
}

// ------------------------------------------------------------------ filters

/**
 * Local, because `@pig/core` ships guards for the motion enums but not for the
 * stage list. Declared here rather than added there: this is the only caller,
 * and widening the ontology's surface for one URL parser is not the trade.
 */
function isDemandStage(value: string): value is DemandStage {
  return (DEMAND_STAGES as readonly string[]).includes(value);
}

/** An unrecognised value reads as absent — see the header. */
function readFilters(params: URLSearchParams): Filters {
  const kind = params.get('kind');
  const stage = params.get(STAGE_PARAM);
  const visibility = params.get('visibility');
  return {
    kind: kind && isMotionKind(kind) ? kind : null,
    stage: stage && isDemandStage(stage) ? stage : null,
    visibility: visibility && isMotionVisibility(visibility) ? visibility : null,
    q: params.get('q')?.trim() ?? '',
    all: params.get('all') === '1',
  };
}

/**
 * Set one parameter, dropping it when it falls back to the default.
 *
 * Functional form, because two filters can settle within one tick — the search
 * debounce firing while a stage is being pressed — and reading `params` from
 * the closure would make the second write discard the first.
 */
function setParam(
  setParams: ReturnType<typeof useSearchParams>[1],
  key: string,
  value: string,
): void {
  setParams(
    (current) => {
      const next = new URLSearchParams(current);
      if (value) next.set(key, value);
      else next.delete(key);
      return next;
    },
    { replace: true },
  );
}

function queryString(filters: Filters): string {
  const query = new URLSearchParams();
  if (filters.kind) query.set('kind', filters.kind);
  if (filters.stage) query.set(STAGE_PARAM, filters.stage);
  if (filters.visibility) query.set('visibility', filters.visibility);
  if (filters.q) query.set('q', filters.q);
  if (filters.all) query.set('all', '1');
  const rendered = query.toString();
  return rendered ? `?${rendered}` : '';
}
