/**
 * The starter library — twelve authored templates, shipped with the product.
 *
 * These are **not** demo data and carry no `DEMO — ` prefix. They are written
 * content in the same class as `docs/learn-scripts.md`: a customer who
 * self-hosts PIG gets them, uses them against real deals, and promotes their
 * own versions back over the top. That is why they seed `isSystem: true`,
 * `visibility: 'shared'`, `version: 1` and `ownerUserId: null` — a shared
 * system row belongs to the deployment rather than to whoever ran the seed,
 * and version 1 is the base of a lineage that a customer's promotions extend.
 *
 * The prose lives in JSON beside this file rather than in a TypeScript literal
 * because the bodies are long-form markdown — the shortest is 7kB and the
 * longest 24kB — and a 200kB module of backtick strings is unreviewable and
 * unmergeable by two people at once. They are imported statically with an
 * import attribute rather than read with `readdir`, which was tried first: a
 * static import makes a missing or renamed file a typecheck failure instead of
 * a seed that quietly ships eleven templates. Both `tsx` and `tsc` were run
 * against this to confirm the attribute syntax is understood by each, since
 * the server runs the TypeScript directly.
 *
 * Idempotency is keyed on `(slug, version)`, which is enforceable **only**
 * because of `motion_templates_slug_version_key`. AGENTS.md §5 is blunt about
 * what `onConflictDoNothing` does without a constraint to fire on — it silently
 * duplicated seed data here twice — so `assertNoDuplicates` below checks the
 * outcome rather than trusting the schema, and CI counts this table in its
 * idempotency gate alongside `contacts`.
 *
 * A second run REFRESHES a starter row rather than skipping it, but only while
 * that row is still ours: `is_system`, nobody has forked or instantiated it
 * (`usage_count = 0`) and it has no owner. Insert-only was the first version and
 * is wrong for the one thing this library is for — the content improves, and a
 * deployment seeded in August would otherwise be frozen on August's wording for
 * ever, with no upgrade path short of hand-editing production rows. The
 * pristine test is what keeps that from becoming a write over somebody's work:
 * the moment a template has been used, §7a says it is never edited in place,
 * and this respects that with the same condition the API enforces.
 */
import { DEMAND_STAGES, isMotionKind, type DemandStage, type MotionKind } from '@pig/core';
import { and, eq, inArray } from 'drizzle-orm';
import type { Database } from '../../client';
import { motionTemplates } from '../../schema/index';

import caseStudyFrame from './case-study-frame.json' with { type: 'json' };
import dataAndSecurityBrief from './data-and-security-brief.json' with { type: 'json' };
import postTrainingPoc from './post-training-poc.json' with { type: 'json' };
import pricingAndPackaging from './pricing-and-packaging.json' with { type: 'json' };
import procurementAndBudgetPath from './procurement-and-budget-path.json' with { type: 'json' };
import productionReadiness from './production-readiness.json' with { type: 'json' };
import proposalBlocks from './proposal-blocks.json' with { type: 'json' };
import referenceArchitectures from './reference-architectures.json' with { type: 'json' };
import strategicDeploymentPlaybook from './strategic-deployment-playbook.json' with { type: 'json' };
import technicalDiscovery from './technical-discovery.json' with { type: 'json' };
import technicalNarratives from './technical-narratives.json' with { type: 'json' };
import trainabilityQualification from './trainability-qualification.json' with { type: 'json' };

/**
 * The authored shape. `kind` and `stage` are `string` here and narrowed at the
 * boundary below, because `resolveJsonModule` infers `string` for a JSON string
 * and there is no honest way to tell TypeScript otherwise without a cast that
 * would also swallow a genuine typo in the file.
 */
interface StarterTemplate {
  readonly kind: string;
  readonly slug: string;
  readonly title: string;
  readonly summary: string;
  readonly stage: string;
  readonly body: string;
  readonly fields: Record<string, unknown>;
  /** Guidance for whoever runs it. Folded into `fields` — see `toRow`. */
  readonly notes: string;
}

const STARTER_LIBRARY: readonly StarterTemplate[] = [
  technicalDiscovery,
  dataAndSecurityBrief,
  trainabilityQualification,
  referenceArchitectures,
  technicalNarratives,
  postTrainingPoc,
  proposalBlocks,
  pricingAndPackaging,
  procurementAndBudgetPath,
  productionReadiness,
  caseStudyFrame,
  strategicDeploymentPlaybook,
];

const isDemandStage = (value: string): value is DemandStage =>
  (DEMAND_STAGES as readonly string[]).includes(value);

/**
 * `notes` has no column, deliberately.
 *
 * It is guidance for the person running the template — when to reach for it,
 * what it is not for — rather than a field the artifact renders, so a column
 * would put it on every engagement artifact's editor for no reason. Keeping it
 * inside `fields` means promotion carries it forward with the rest of the
 * structured payload without any special handling in the promote path.
 */
/*
 * Key order is not preserved by `jsonb`: Postgres stores an object with its
 * keys sorted by length and then bytewise, so the value that comes back is
 * rarely the value that went in. A plain `JSON.stringify` comparison therefore
 * reported every starter template as changed on every run, and the seed
 * rewrote nine rows each time while claiming to be idempotent. Found by
 * running it twice and reading the count, not by reading the code.
 */
function canonical(value: unknown): string {
  const order = (node: unknown): unknown => {
    if (Array.isArray(node)) return node.map(order);
    if (node && typeof node === 'object') {
      return Object.fromEntries(
        Object.keys(node as Record<string, unknown>)
          .sort()
          .map((key) => [key, order((node as Record<string, unknown>)[key])]),
      );
    }
    return node;
  };
  return JSON.stringify(order(value));
}

function toRow(
  template: StarterTemplate,
  kind: MotionKind,
  stage: DemandStage,
): typeof motionTemplates.$inferInsert {
  return {
    kind,
    stage,
    slug: template.slug,
    version: 1,
    title: template.title,
    summary: template.summary,
    body: template.body,
    fields: { ...template.fields, notes: template.notes },
    visibility: 'shared',
    ownerUserId: null,
    isSystem: true,
  };
}

/**
 * Confirm the outcome, not the constraint.
 *
 * Reading `pg_constraint` would prove the unique index exists; counting the
 * rows proves the clause did its job, which is the thing that has actually
 * gone wrong here before. It throws rather than warns: a duplicated starter
 * library is a corrupt library, and every subsequent run would double it again.
 *
 * Scoped to version 1, and found by running it: the demo book promotes an
 * artifact into `case-study-frame` v2, so counting every row for these slugs
 * reported the loop working as a duplicated seed. Version 1 is the only row
 * this loader writes and therefore the only one it may assert about.
 */
async function assertNoDuplicates(db: Database, slugs: readonly string[]): Promise<number> {
  const rows = await db
    .select({ slug: motionTemplates.slug })
    .from(motionTemplates)
    .where(and(inArray(motionTemplates.slug, [...slugs]), eq(motionTemplates.version, 1)));

  if (rows.length > slugs.length) {
    const counts = new Map<string, number>();
    for (const row of rows) counts.set(row.slug, (counts.get(row.slug) ?? 0) + 1);
    const duplicated = [...counts.entries()].filter(([, count]) => count > 1).map(([slug]) => slug);
    throw new Error(
      `The starter library has duplicate rows for: ${duplicated.join(', ')}. ` +
        'The unique constraint motion_templates_slug_version_key is missing or the ' +
        'conflict target no longer matches it — see AGENTS.md §5.',
    );
  }

  return rows.length;
}

export async function seedMotionLibrary(
  db: Database,
): Promise<{
  total: number;
  added: number;
  refreshed: number;
  held: readonly string[];
  rejected: readonly string[];
}> {
  const rejected: string[] = [];
  const held: string[] = [];
  let added = 0;
  let refreshed = 0;

  for (const template of STARTER_LIBRARY) {
    /*
     * A bad `kind` or `stage` is skipped, not thrown on. These files are
     * authored by hand and the CHECK constraints would reject the row anyway —
     * but as an aborted transaction partway through the base seed, taking the
     * eleven good templates and everything after them down with it. Reporting
     * the one bad file by name is more useful and leaves the deployment usable.
     */
    if (!isMotionKind(template.kind) || !isDemandStage(template.stage)) {
      rejected.push(`${template.slug} (kind=${template.kind}, stage=${template.stage})`);
      continue;
    }

    const row = toRow(template, template.kind, template.stage);

    const [existing] = await db
      .select({
        id: motionTemplates.id,
        isSystem: motionTemplates.isSystem,
        usageCount: motionTemplates.usageCount,
        ownerUserId: motionTemplates.ownerUserId,
        title: motionTemplates.title,
        summary: motionTemplates.summary,
        body: motionTemplates.body,
        fields: motionTemplates.fields,
        kind: motionTemplates.kind,
        stage: motionTemplates.stage,
      })
      .from(motionTemplates)
      .where(and(eq(motionTemplates.slug, template.slug), eq(motionTemplates.version, 1)))
      .limit(1);

    if (!existing) {
      const [created] = await db
        .insert(motionTemplates)
        .values(row)
        .onConflictDoNothing({ target: [motionTemplates.slug, motionTemplates.version] })
        .returning({ id: motionTemplates.id });
      if (created) added += 1;
      continue;
    }

    const pristine =
      existing.isSystem && existing.usageCount === 0 && existing.ownerUserId === null;
    const changed =
      existing.title !== row.title ||
      existing.summary !== row.summary ||
      existing.body !== row.body ||
      existing.kind !== row.kind ||
      existing.stage !== row.stage ||
      canonical(existing.fields) !== canonical(row.fields);

    if (pristine && changed) {
      await db
        .update(motionTemplates)
        .set({
          title: row.title,
          summary: row.summary,
          body: row.body,
          fields: row.fields,
          kind: row.kind,
          stage: row.stage,
          updatedAt: new Date(),
        })
        .where(eq(motionTemplates.id, existing.id));
      refreshed += 1;
    } else if (changed) {
      // Reported rather than forced: the row has been used or adopted, so
      // overwriting it would edit a template a live engagement was cut from.
      held.push(template.slug);
    }
  }

  const total = await assertNoDuplicates(
    db,
    STARTER_LIBRARY.map((template) => template.slug),
  );

  return { total, added, refreshed, held, rejected };
}
