/**
 * The Learn library: the PIG-hosted recordings.
 *
 * There used to be two illustrative rows here as well, embedding public
 * recordings from the Cap instance at video.karti.ai under `DEMO — ` titles, so
 * that the `supply` and `demand` concept tracks were not empty. Both are gone,
 * for two reasons that arrived together.
 *
 * One of the two recordings had since been removed from that instance:
 * `sjqqvthbfma27bm` answers 404 on both `/s/` and `/embed/`, so the card
 * promised six seconds of teaching and played a dead frame. That is the exact
 * failure the previous version of this comment was written to record — an id
 * that 404s behind a card that claims content — and it came back because the
 * footage lives somewhere this repository does not control.
 *
 * The other is judgement: /learn is the page deliberately shown to outsiders,
 * and a `DEMO — ` card sitting beside genuine product footage makes the whole
 * page read as half-placeholder to exactly the audience it exists to convince.
 * An empty concept track renders a proper empty state saying nothing has been
 * published yet, which is true and says so.
 *
 * If `supply` and `demand` get purpose-shot recordings, they belong in
 * `HOSTED_LEARN_MANIFEST` below and rendered by `scripts/learn-film.mjs` like
 * the platform track, not embedded from an instance whose contents can change
 * underneath this file.
 */
import { LEARN_MEDIA_PATH_PREFIX, isLearnMediaFilename, learnMediaContentType } from '@pig/core';
import { eq } from 'drizzle-orm';
import { readdir } from 'node:fs/promises';
import { resolve } from 'node:path';
import { learnResources, users } from '../../schema/index';
import type { DemoContext } from './index';

// ------------------------------------------------------- PIG-hosted learn
//
// Real videos, served by PIG itself from PIG_MEDIA_DIR — not demo data. They
// carry no `DEMO — ` prefix precisely because they are genuine product
// walkthroughs, which also means `--clear` leaves them alone; `--clear-hosted`
// is their own switch.
//
// **A row is written only when its file is actually on disk.** A learn row
// whose media 404s is worse than a missing row: the card renders, the play
// button does nothing, and the feature reads as broken. So the manifest below
// declares the curriculum, and the seed inserts the entries it can find.
// Running it before the videos are generated is a no-op with a printed list,
// and running it again afterwards fills them in.
//
// That choice was re-examined, because the audience for the absence is not an
// operator: the anonymous share-code view of /learn shows this track and
// nothing else, so a missing file is a stranger's first impression of the
// product. A "coming soon" ROW was rejected. It would need a filename to point
// at, the card would carry a play button, and pressing it would fail — which
// is the one outcome worse than an empty section, and the same reason the
// access hero draws redacted bars rather than invented thumbnails. The absence
// is handled where it belongs instead: `apps/web/src/pages/Learn.tsx` gives a
// code-holder with nothing published a finished panel that says so and offers
// a way on, rather than the admin empty state.
//
// `media/` is gitignored — hundreds of megabytes of rendered MP4 are a release
// artefact, not source — so a fresh clone and every git worktree start without
// it, and that is the ordinary case rather than a fault. Point PIG_MEDIA_DIR
// at a directory holding the renders (the API route reads the same variable)
// and the five rows appear.
//
// **The filename is discovered, not written down.** Files are
// content-addressed — `<slug>.<hash>.mp4` — so the hash changes every time a
// video is re-rendered, and a manifest carrying the hash would be a file that
// has to be edited in lockstep with a render. Instead the slug is the stable
// identity and the directory supplies the rest. Idempotency then rests on the
// unique key (track, provider, external_id) exactly as the DEMO rows do.
//
// A re-render produces a NEW hash and therefore a new row; the old row keeps
// pointing at a file that is no longer there. That is reported rather than
// resolved automatically, because deleting rows on the strength of a missing
// file would empty the curriculum the first time someone ran this with the
// media directory unmounted.

interface HostedLearnEntry {
  /** Stable identity. Also the filename stem: `<slug>.<hash>.<ext>`. */
  slug: string;
  title: string;
  summary: string;
  track: 'supply' | 'demand' | 'platform';
  visibility: 'members' | 'code';
  /**
   * Measured from the render, to the nearest second — not estimated from the
   * script. Every one below was re-checked against the file it names, because
   * a duration is the only claim a card makes that a viewer can verify before
   * pressing play.
   */
  durationSeconds: number;
  sortOrder: number;
}

export const HOSTED_LEARN_MANIFEST: readonly HostedLearnEntry[] = [
  {
    slug: 'overview-and-margin',
    title: 'Overview and the margin question',
    summary:
      'Which contracted capacity is sold, at what margin, and what is idle right now — and why cost is charged against the full commitment.',
    track: 'platform',
    visibility: 'code',
    durationSeconds: 29,
    sortOrder: 1,
  },
  {
    slug: 'quarterly-calendar',
    title: 'The quarterly calendar',
    summary:
      'What closes, what renews, what expires and when capacity lands — with export authorisations and compliance artefacts at the top.',
    track: 'platform',
    visibility: 'code',
    durationSeconds: 30,
    sortOrder: 2,
  },
  {
    slug: 'capacity-to-allocations',
    title: 'Capacity to allocations',
    summary:
      'Joining a commitment you bought to a deal you sold — the availability book, the matcher, and the allocation the ledger is built on.',
    track: 'platform',
    visibility: 'code',
    durationSeconds: 26,
    sortOrder: 3,
  },
  {
    slug: 'importing-your-book',
    title: 'Importing your book',
    summary:
      'Getting off the spreadsheet — CSV, Excel, Notion or a bounded Google Sheets range, with a dry run you review before anything is written.',
    track: 'platform',
    visibility: 'code',
    durationSeconds: 28,
    sortOrder: 4,
  },
  {
    slug: 'piggy-and-its-boundary',
    title: 'Piggy, and where it stops',
    // This said Piggy had "no ability to write CRM records" — the headline
    // capability of the release it ships beside, denied on the one track a
    // public share code opens, two clicks from a front door reading "Piggy can
    // change it, with your approval." The boundary is real and still worth
    // stating; it is approval, not incapability.
    summary:
      'The docked agent reads through scoped, page-specific PIG tools, and proposes any change as a card that only a person can apply — no shell, no filesystem, and nothing written without a human pressing Apply.',
    track: 'platform',
    visibility: 'code',
    durationSeconds: 28,
    sortOrder: 5,
  },
  /*
   * Six and seven are the first vertical (9:16) films in the set. The five
   * above are landscape captures of the desktop app; these are cut for a phone,
   * because the way this material actually gets watched is somebody opening the
   * link on the way to a meeting. `scripts/learn-film.mjs` renders them and is
   * the pipeline the first five never had.
   */
  {
    slug: 'motion-the-repeatable-motion',
    title: 'Motion: the repeatable go-to-market',
    summary:
      'A tour of Motion — the library of what we know how to do, bound to the stages of a real deal, getting better every time we run it.',
    track: 'platform',
    visibility: 'code',
    durationSeconds: 32,
    sortOrder: 6,
  },
  {
    slug: 'the-sales-motion',
    title: 'How a deal moves through Motion',
    summary:
      "The practitioner's path: open an engagement on a live deal, score it, pull the template for your stage, fill it with the customer's facts, and watch the score move both ways.",
    track: 'platform',
    visibility: 'code',
    durationSeconds: 34,
    sortOrder: 7,
  },
];

/**
 * Where the served files live. The same variable the API route reads.
 *
 * The default is resolved from this file's location, not from `process.cwd()`,
 * because pnpm runs the seed with the working directory at `packages/db` while
 * the server runs at the repository root — so a relative default would mean
 * two different directories, and the seed would write rows for files the API
 * cannot find.
 *
 * Five levels up, because this file sits one deeper than the seed it was split
 * out of: `packages/db/src/seed/demo` → repository root.
 */
function mediaDirectory(): string {
  const configured = process.env.PIG_MEDIA_DIR?.trim();
  if (configured && configured.length > 0) return resolve(configured);
  return resolve(import.meta.dirname, '../../../../../media');
}

/**
 * Find the one VIDEO belonging to a slug.
 *
 * Two videos for one slug is an error rather than a choice: picking the newest
 * would silently publish whichever render happened to finish last, and the
 * operator has an old file to delete.
 *
 * Video extensions only, and that is the whole point of the second predicate.
 * A poster is `<slug>.<hash>.jpg` — deliberately the video's own stem, so it
 * cannot go stale against the clip it shows (`learnPosterFilename`) — and it
 * sits in this same directory. Matching on the slug alone therefore found two
 * files for every complete entry and declared each of them an ambiguous stale
 * render, so a directory containing five finished walkthroughs AND their
 * posters seeded exactly nothing, and the share-code page it feeds rendered
 * empty on the one box that had the media.
 */
function mediaFileFor(
  slug: string,
  filenames: readonly string[],
): { filename: string | null; ambiguous: readonly string[] } {
  const matches = filenames.filter(
    (name) =>
      isLearnMediaFilename(name) &&
      name.startsWith(`${slug}.`) &&
      learnMediaContentType(name)?.startsWith('video/'),
  );
  /*
   * Reported and skipped, not thrown.
   *
   * This runs from the middle of seedDemo(), so throwing on a duplicate took
   * out every later section — facts, activities, the lot — and left the
   * operator working out why `pnpm db:demo` died on two MP4s sharing a slug.
   * A stale render is a condition of one directory entry; its blast radius
   * should be that entry. Missing files are already handled this way.
   */
  if (matches.length > 1) return { filename: null, ambiguous: matches };
  return { filename: matches[0] ?? null, ambiguous: [] };
}

export async function seedHostedLearn(context: DemoContext): Promise<{
  present: number;
  added: number;
  missing: number;
}> {
  const { db } = context;

  const directory = mediaDirectory();
  let filenames: string[] = [];
  try {
    filenames = await readdir(directory);
  } catch {
    // No directory is the normal state of a fresh checkout, not a failure.
    console.log(
      `  (no media directory at ${directory} — skipping PIG-hosted learn videos; ` +
        'set PIG_MEDIA_DIR if the renders live elsewhere)',
    );
    return { present: 0, added: 0, missing: HOSTED_LEARN_MANIFEST.length };
  }

  const [owner] = await db.select({ id: users.id }).from(users).limit(1);
  let present = 0;
  let added = 0;
  const absent: string[] = [];

  for (const entry of HOSTED_LEARN_MANIFEST) {
    const { filename, ambiguous } = mediaFileFor(entry.slug, filenames);
    if (ambiguous.length > 0) {
      console.warn(
        `  ! ${entry.slug}: ${ambiguous.length} files match (${ambiguous.join(', ')}) — ` +
          'content-addressed names mean one is a stale render. Delete it and re-run. Skipped.',
      );
      absent.push(entry.slug);
      continue;
    }
    if (!filename) {
      absent.push(entry.slug);
      continue;
    }
    present += 1;

    const inserted = await db
      .insert(learnResources)
      .values({
        track: entry.track,
        title: entry.title,
        summary: entry.summary,
        // The same path the resolver builds, from the same constant — so a
        // seeded row and a row created through the API are indistinguishable.
        url: `${LEARN_MEDIA_PATH_PREFIX}${filename}`,
        provider: 'pig',
        externalId: filename,
        visibility: entry.visibility,
        durationSeconds: entry.durationSeconds,
        sortOrder: entry.sortOrder,
        addedByUserId: owner?.id ?? null,
      })
      .onConflictDoNothing({
        target: [learnResources.track, learnResources.provider, learnResources.externalId],
      })
      .returning({ id: learnResources.id });
    if (inserted.length) added += 1;
  }

  if (absent.length) {
    console.log(
      `  (no file yet in ${directory} for: ${absent.join(', ')} — ` +
        'drop <slug>.<hash>.mp4 there, or point PIG_MEDIA_DIR at the renders, and run this again)',
    );
  }

  // Rows whose file has gone: reported, never deleted. See the note above.
  const orphans = (
    await db
      .select({ externalId: learnResources.externalId, title: learnResources.title })
      .from(learnResources)
      .where(eq(learnResources.provider, 'pig'))
  ).filter((row) => !filenames.includes(row.externalId));
  for (const orphan of orphans) {
    console.log(`  ! "${orphan.title}" points at ${orphan.externalId}, which is not on disk`);
  }

  return { present, added, missing: absent.length };
}

/**
 * `--clear-hosted`. Kept beside the seed it undoes rather than in `clear.ts`,
 * because it is the opposite of a different command: `--clear` removes the
 * invented book and must leave these genuine rows exactly where they are.
 */
export async function clearHostedLearn(context: DemoContext): Promise<void> {
  const removed = await context.db
    .delete(learnResources)
    .where(eq(learnResources.provider, 'pig'))
    .returning({ id: learnResources.id });
  console.log(`Removed ${removed.length} PIG-hosted learn resource(s). Files on disk are untouched.`);
}
