/**
 * Learn — two tracks, one of which a stranger with the share code can see.
 *
 * The page has to render correctly for two very different callers, and the
 * difference is decided by the server, never by anything held here:
 *
 *   a member  — `GET /api/learn` succeeds and returns all three tracks.
 *   a code-holder — that call answers 401, so the page falls back to the code
 *                   gate, and afterwards reads `GET /api/learn/public`, which
 *                   returns platform walkthroughs and nothing else.
 *
 * Deriving the mode from the 401 rather than from an identity hook is what
 * lets this file work both inside the authenticated shell (where it lives
 * today) and outside it, which is what the anonymous `/learn` route needs. It
 * also means the browser is never the thing deciding what a code-holder may
 * see — it asks, and the API answers with a smaller set.
 *
 * **The anonymous view brings its own chrome.** It renders outside `Shell`, so
 * nothing above it supplies a header, a width or even a page margin — which is
 * why it was previously flush against the left edge of the viewport. The
 * `AnonFrame` below is that missing chrome and belongs to this route only.
 *
 * **The two tracks are rendered as different objects on purpose.** Concepts is
 * market education you browse, so it is a poster-led grid. Platform is a
 * product course you work through in `sortOrder`, so it is a numbered list.
 * Rendering both as one grid was what made the page read as a wireframe.
 *
 * **The locked Concepts panel is deliberate, not an oversight.** A code-holder
 * is shown that supply and demand material exists and is behind sign-in. That
 * was an explicit product decision: the point of the page for an outsider is
 * partly to advertise the rest of it.
 *
 * **An empty walkthrough list is a state this page is designed for, not an
 * accident.** A walkthrough row exists only for a video file that is actually
 * on disk, so a deployment without the rendered media serves a code-holder a
 * page with nothing to play. That is the first — and possibly only — thing a
 * stranger ever sees of PIG, so it gets a written panel of its own rather than
 * the admin empty state, and it is kept distinct from the panel for a read
 * that failed. See `PreviewPending` and `PreviewUnavailable`.
 *
 * Nothing here builds an embed URL. Every source arrives from the API already
 * resolved through the host allowlist in `@pig/core`; a resource the server
 * could not resolve is not in the response at all.
 */
import { useCallback, useState, type ReactNode } from 'react';
import { useQuery } from '@tanstack/react-query';
import {
  BookOpen,
  Check,
  GraduationCap,
  MonitorPlay,
  Plus,
  SlidersHorizontal,
} from 'lucide-react';
import {
  LEARN_CONCEPT_TRACKS,
  LEARN_TRACK_DESCRIPTIONS,
  LEARN_TRACK_LABELS,
  type LearnTrack,
} from '@pig/core';
import { ApiError, get } from '@/lib/api';
import { usePageTitle } from '@/lib/title';
import { usePiggyContext } from '@/lib/piggy-context';
import { toPiggyPageRoute } from '@pig/core';
import { Badge, Button, Card, EmptyState, Label, Skeleton } from '@/components/ui';
import { PageHeader } from '@/components/ui/page-header';
import { PublicHeader } from '@/components/PublicHeader';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AddResourceDialog } from '@/components/learn/AddResourceDialog';
import { LearnAccessHero } from '@/components/learn/LearnAccessHero';
import { LearnPlayerDialog } from '@/components/learn/LearnPlayerDialog';
import { LearnTrackPanel } from '@/components/learn/LearnTrackPanel';
import { LearnVideoCard } from '@/components/learn/LearnVideoCard';
import { LearnWalkthroughList } from '@/components/learn/LearnWalkthroughList';
import {
  bySortOrder,
  type LearnResourceView,
  type MemberFeed,
  type PublicFeed,
} from '@/components/learn/model';

/**
 * sessionStorage, not localStorage.
 *
 * The code is shared, often over a shoulder or in a call, and the token it
 * mints is a session's worth of access to marketing material. Persisting it
 * across browser restarts on a machine that may not be the holder's is the
 * wrong default; retyping a passphrase they were given is not a hardship.
 */
const TOKEN_KEY = 'pig.learn.token';

const CONCEPT_TRACKS: readonly LearnTrack[] = LEARN_CONCEPT_TRACKS;

export function Learn() {
  usePageTitle('Learn');
  // The dock's route vocabulary is a closed set in @pig/core and does not yet
  // carry '/learn'; until it does this resolves to '/', which is the
  // documented fallback and strictly better than not publishing at all.
  usePiggyContext({ type: 'page', route: toPiggyPageRoute('/learn'), label: 'Learn' });

  const [token, setToken] = useState<string | null>(() => readToken());
  const [playing, setPlaying] = useState<LearnResourceView | null>(null);

  const member = useQuery({
    queryKey: ['learn', 'member'],
    queryFn: () => get<MemberFeed>('/api/learn'),
    retry: false,
  });

  // Only relevant once the member read has actually been refused. Firing it
  // speculatively would put a 401 in the console on every member's first load.
  const locked = member.error instanceof ApiError && member.error.needsSignIn;

  const publicFeed = useQuery({
    queryKey: ['learn', 'public', token],
    enabled: locked && Boolean(token),
    retry: false,
    queryFn: async () => {
      const response = await fetch('/api/learn/public', {
        headers: { authorization: `Bearer ${token ?? ''}` },
      });
      if (response.status === 401) {
        // The code was rotated, or the token expired. Drop it and show the
        // gate again rather than leaving a dead session on screen.
        clearToken();
        setToken(null);
        throw new ApiError('That access has expired.', 401, 'learn_token_expired');
      }
      if (!response.ok) throw new ApiError('Could not load the Learn library.', response.status);
      return (await response.json()) as PublicFeed;
    },
  });

  const play = useCallback((resource: LearnResourceView) => setPlaying(resource), []);

  if (member.isLoading) return <LearnSkeleton />;

  if (locked) {
    return (
      <>
        <CodeHolderView
          token={token}
          feed={publicFeed.data ?? null}
          isLoading={publicFeed.isFetching}
          failed={publicFeed.isError}
          onUnlocked={(minted) => {
            writeToken(minted);
            setToken(minted);
          }}
          onPlay={play}
        />
        <LearnPlayerDialog resource={playing} onClose={() => setPlaying(null)} />
      </>
    );
  }

  if (member.error || !member.data) {
    return (
      <Card>
        <EmptyState
          title="Learn is unavailable"
          description={
            member.error instanceof Error ? member.error.message : 'Could not load the library.'
          }
          action={
            <Button type="button" variant="outline" onClick={() => void member.refetch()}>
              Try again
            </Button>
          }
        />
      </Card>
    );
  }

  return (
    <>
      <MemberView feed={member.data} onPlay={play} />
      <LearnPlayerDialog resource={playing} onClose={() => setPlaying(null)} />
    </>
  );
}

// --------------------------------------------------------------- member view

function MemberView({ feed, onPlay }: { feed: MemberFeed; onPlay: (r: LearnResourceView) => void }) {
  const [concept, setConcept] = useState<string>(CONCEPT_TRACKS[0] ?? 'supply');
  const [managing, setManaging] = useState(false);
  const [adding, setAdding] = useState(false);
  const total = Object.values(feed.tracks).reduce((sum, list) => sum + (list?.length ?? 0), 0);
  const platform = feed.tracks.platform ?? [];

  return (
    <div className="flex min-w-0 flex-col gap-8">
      {/* Precise about what the code opens: an earlier line promised that
          "anything on the Platform track" could be sent to someone with no
          account, which is true only of the rows marked by code. */}
      <PageHeader
        title="Learn"
        description="How this market works, and how PIG works. The Platform track is what the share code opens; Concepts stays with members."
        actions={<>
          <Badge tone="neutral" className="nums">
            {total} video{total === 1 ? '' : 's'}
          </Badge>
          {/*
            Admin controls hide behind one toggle rather than sitting on every
            card. Archive is destructive and useful to a handful of people; it
            was competing for attention with the content for everyone else.
          */}
          {feed.canManage ? (
            <Button
              variant={managing ? 'secondary' : 'ghost'}
              size="sm"
              aria-pressed={managing}
              onClick={() => setManaging((current) => !current)}
            >
              {managing ? (
                <Check className="size-4" aria-hidden />
              ) : (
                <SlidersHorizontal className="size-4" aria-hidden />
              )}
              {managing ? 'Done' : 'Manage'}
            </Button>
          ) : null}
          {feed.canManage && managing ? (
            <Button variant="primary" size="sm" onClick={() => setAdding(true)}>
              <Plus className="size-4" aria-hidden />
              Add a video
            </Button>
          ) : null}
        </>}
      />

      <TrackSection
        id="learn-concepts"
        kicker="Market education"
        icon={<BookOpen className="size-3.5 shrink-0" aria-hidden />}
        title="Concepts"
        description="How this market actually works, taught side by side with the book you run. Members only."
      >
        <Tabs value={concept} onValueChange={setConcept} className="flex min-w-0 flex-col gap-4">
          {/* `self-start` because the list is `inline-flex` inside a column
              flex container, which stretches it to the full width regardless
              of `w-auto`. The palette, the 52px list and the 44px trigger all
              live in the primitive now, so nothing is overridden here. */}
          <TabsList className="w-full justify-start overflow-x-auto sm:w-auto sm:self-start">
            {CONCEPT_TRACKS.map((track) => (
              <TabsTrigger key={track} value={track} className="shrink-0 px-4">
                {LEARN_TRACK_LABELS[track]}
              </TabsTrigger>
            ))}
          </TabsList>
          {CONCEPT_TRACKS.map((track) => (
            <TabsContent key={track} value={track} className="mt-0 flex min-w-0 flex-col gap-3">
              <p className="min-w-0 text-sm leading-6 text-muted">
                {LEARN_TRACK_DESCRIPTIONS[track]}
              </p>
              <ConceptGrid
                resources={feed.tracks[track] ?? []}
                managing={managing}
                canManage={feed.canManage}
                emptyTitle={`No ${LEARN_TRACK_LABELS[track].toLowerCase()} material yet`}
                onPlay={onPlay}
              />
            </TabsContent>
          ))}
        </Tabs>
      </TrackSection>

      <TrackSection
        id="learn-platform"
        kicker="Product how-to"
        icon={<MonitorPlay className="size-3.5 shrink-0" aria-hidden />}
        title="Platform"
        description="A short course on PIG itself, in order. This is the one track the share code opens."
      >
        {platform.length === 0 ? (
          <LearnEmpty title="No walkthroughs yet" description={emptyHint(feed.canManage)} />
        ) : (
          // Capped: a one-line summary set the full width of a desktop shell
          // is a line nobody can track back from.
          <div className="min-w-0 max-w-4xl">
            <LearnWalkthroughList resources={platform} managing={managing} onPlay={onPlay} />
          </div>
        )}
      </TrackSection>

      <AddResourceDialog open={adding} onOpenChange={setAdding} />
    </div>
  );
}

function ConceptGrid({
  resources,
  managing,
  canManage,
  emptyTitle,
  onPlay,
}: {
  resources: LearnResourceView[];
  managing: boolean;
  canManage: boolean;
  emptyTitle: string;
  onPlay: (resource: LearnResourceView) => void;
}) {
  if (resources.length === 0) return <LearnEmpty title={emptyTitle} description={emptyHint(canManage)} />;

  return (
    <div className="grid min-w-0 gap-4 sm:grid-cols-2 xl:grid-cols-3">
      {bySortOrder(resources).map((resource) => (
        <LearnVideoCard
          key={resource.id}
          resource={resource}
          managing={managing}
          onPlay={onPlay}
        />
      ))}
    </div>
  );
}

// ---------------------------------------------------------- code-holder view

function CodeHolderView({
  token,
  feed,
  isLoading,
  failed,
  onUnlocked,
  onPlay,
}: {
  token: string | null;
  feed: PublicFeed | null;
  isLoading: boolean;
  /** The read was refused or never answered — distinct from an empty library. */
  failed: boolean;
  onUnlocked: (token: string) => void;
  onPlay: (resource: LearnResourceView) => void;
}) {
  if (!token) {
    return (
      <AnonFrame>
        <LearnAccessHero onUnlocked={onUnlocked} />
        <LearnTrackPanel
          heading="What's inside"
          description="Two kinds of material. The product walkthroughs open with the code you were given; the market training is for the go-to-market team."
          teasers={[
            { track: 'platform', access: 'code' },
            { track: 'supply', access: 'members' },
            { track: 'demand', access: 'members' },
          ]}
          showSignIn={false}
        />
      </AnonFrame>
    );
  }

  const expiry = feed ? formatExpiry(feed.expiresAt) : null;
  const resources = feed?.resources ?? [];

  return (
    <AnonFrame>
      <section aria-labelledby="learn-platform-public" className="flex min-w-0 flex-col gap-5">
        <div className="flex min-w-0 flex-col gap-3">
          <Label className="flex min-w-0 items-center gap-1.5">
            <MonitorPlay className="size-3.5 shrink-0" aria-hidden />
            Prime Intellect Growth
          </Label>
          <h1
            id="learn-platform-public"
            className="min-w-0 break-words text-xl font-semibold tracking-tight sm:text-2xl"
          >
            Platform walkthroughs
          </h1>
          {/* The running-order sentence is a claim about videos that are on
              the page. With none published it describes nothing, so it goes. */}
          <p className="min-w-0 max-w-2xl text-sm leading-6 text-muted">
            {LEARN_TRACK_DESCRIPTIONS.platform}
            {resources.length > 0
              ? ' Work through them in order — the first one assumes nothing.'
              : ''}
          </p>
          <div className="flex min-w-0 flex-wrap items-center gap-2">
            <Badge tone="accent">
              <Check className="size-3.5 shrink-0" aria-hidden />
              Access unlocked
            </Badge>
            {expiry ? <span className="text-xs text-muted">Expires in {expiry}</span> : null}
          </div>
        </div>

        {isLoading && !feed ? (
          <ListSkeleton />
        ) : failed && !feed ? (
          <PreviewUnavailable />
        ) : resources.length === 0 ? (
          <PreviewPending />
        ) : (
          <LearnWalkthroughList resources={resources} managing={false} onPlay={onPlay} />
        )}
      </section>

      <LearnTrackPanel
        heading="Concepts"
        description="Behind sign-in: the market training the go-to-market team runs on. Not part of the shared preview."
        teasers={[
          { track: 'supply', access: 'members' },
          { track: 'demand', access: 'members' },
        ]}
      />
    </AnonFrame>
  );
}

/**
 * The page chrome the anonymous route has no shell to give it.
 *
 * A max width and a page margin, plus the one piece of identity a stranger
 * needs: whose page this is and where to sign in. `app-canvas` is the same
 * background the authenticated shell paints, so the two do not read as two
 * different products.
 */
function AnonFrame({ children }: { children: ReactNode }) {
  return (
    <div className="app-canvas flex min-h-dvh min-w-0 flex-col">
      <PublicHeader current="learn" />
      <main className="mx-auto flex w-full min-w-0 max-w-5xl flex-1 flex-col gap-8 px-4 pb-14 pt-6 sm:px-6 sm:pb-20">
        {children}
      </main>
      <footer className="mx-auto w-full min-w-0 max-w-5xl px-4 pb-8 text-xs text-muted sm:px-6">
        Prime Intellect Growth · shared preview
      </footer>
    </div>
  );
}

// -------------------------------------------------------------------- pieces

function TrackSection({
  id,
  kicker,
  icon,
  title,
  description,
  children,
}: {
  id: string;
  kicker: string;
  icon: ReactNode;
  title: string;
  description: string;
  children: ReactNode;
}) {
  return (
    <section aria-labelledby={id} className="flex min-w-0 flex-col gap-4">
      <div className="min-w-0">
        <Label className="flex min-w-0 items-center gap-1.5">
          {icon}
          {kicker}
        </Label>
        <h2 id={id} className="mt-1 text-base font-semibold leading-tight">
          {title}
        </h2>
        <p className="mt-1 max-w-2xl text-sm leading-6 text-muted">{description}</p>
      </div>
      {children}
    </section>
  );
}

function LearnEmpty({ title, description }: { title: string; description: string }) {
  return (
    <Card>
      <EmptyState
        icon={<GraduationCap className="size-6" aria-hidden />}
        title={title}
        description={description}
      />
    </Card>
  );
}

/**
 * The second line of an empty track, which is an instruction only for the one
 * person who can act on it. "Paste a video link to start the collection" was
 * shown to every member, none of whom the API would let write, and it is
 * exactly the wrong sentence to put in front of an outsider.
 */
function emptyHint(canManage: boolean): string {
  return canManage
    ? 'Paste a video link to start the collection.'
    : 'Nothing has been published to this track yet.';
}

/**
 * What a code-holder sees when the walkthroughs have not been published.
 *
 * This is the whole page for someone whose only view of PIG is a share code —
 * it happens whenever the rendered media is not on the box, since a
 * walkthrough row is written only for a file that exists — so it has to read
 * as a finished page rather than a failed one.
 *
 * No ghost cards, and no "coming soon" tiles. A card carries a play button,
 * and a play button that does nothing is worse than an honest absence; it is
 * the same reason the access hero draws redacted bars rather than invented
 * thumbnails. What the visitor gets instead is the truth and a human to ask.
 */
function PreviewPending() {
  return (
    <Card>
      <EmptyState
        icon={<MonitorPlay className="size-6" aria-hidden />}
        title="No walkthroughs published yet"
        description="Your code worked — nothing has been published to this preview so far. Whoever shared the code will know when the first one lands."
      />
    </Card>
  );
}

/**
 * Distinct from `PreviewPending` on purpose: a library that did not answer is
 * not a library that is empty, and telling a visitor "nothing published yet"
 * when the request failed is a page inventing a fact about itself.
 */
function PreviewUnavailable() {
  return (
    <Card>
      <EmptyState
        icon={<MonitorPlay className="size-6" aria-hidden />}
        title="Could not load the walkthroughs"
        description="The library did not answer. Reload the page, or try again in a few minutes."
      />
    </Card>
  );
}

function LearnSkeleton() {
  return (
    <div className="flex min-w-0 flex-col gap-6 p-4 sm:p-0">
      <div className="flex min-w-0 flex-col gap-2">
        <Skeleton className="h-4 w-24 rounded" />
        <Skeleton className="h-9 w-40 rounded-lg" />
      </div>
      <ListSkeleton />
    </div>
  );
}

function ListSkeleton() {
  return (
    <div className="flex min-w-0 flex-col gap-3">
      {[0, 1, 2].map((key) => (
        <Skeleton key={key} className="h-28 rounded-2xl" />
      ))}
    </div>
  );
}

/**
 * Whole hours while there are any, minutes once there are not. A code-holder
 * needs to know whether they can finish the course now, not to the second.
 */
function formatExpiry(expiresAt: string): string | null {
  const remaining = Date.parse(expiresAt) - Date.now();
  if (!Number.isFinite(remaining) || remaining <= 0) return null;
  const hours = Math.floor(remaining / 3_600_000);
  if (hours >= 1) return `${hours} hour${hours === 1 ? '' : 's'}`;
  const minutes = Math.max(1, Math.round(remaining / 60_000));
  return `${minutes} minute${minutes === 1 ? '' : 's'}`;
}

// --------------------------------------------------------------------- token

function readToken(): string | null {
  try {
    return window.sessionStorage.getItem(TOKEN_KEY);
  } catch {
    // Private-mode Safari throws on storage access. A code-holder who has to
    // retype the code is a worse experience than a crash is a bug.
    return null;
  }
}

function writeToken(token: string): void {
  try {
    window.sessionStorage.setItem(TOKEN_KEY, token);
  } catch {
    /* See readToken. */
  }
}

function clearToken(): void {
  try {
    window.sessionStorage.removeItem(TOKEN_KEY);
  } catch {
    /* See readToken. */
  }
}
