/**
 * uat-plan/generate.ts — PURE assembly of a `.plantest.yml` from discovery data.
 *
 * Ties the projection (project-roles) to the contract (plantest-schema): assemble
 * the raw plan, parse it through the Zod schema (fills defaults), check the
 * structural invariants, serialise to deterministic YAML, and emit the artifact +
 * its `.signature` sidecar. No I/O, no clock — `generatedAt` is injected so the
 * plan is byte-stable under a fixture.
 */
import { createHash } from 'node:crypto';
import { dump } from 'js-yaml';
import {
  PlanTestSchema,
  validateInvariants,
  type PlanTest,
  type Route,
  type Endpoint,
  type InvariantViolation,
} from '../lib/plantest-schema.js';
import {
  canonicalRoleOrder,
  projectRoutes,
  projectEndpoints,
} from './project-roles.js';
import type {
  DiscoveryResult,
  SourceSignature,
  GeneratedFile,
  CapsOverride,
} from './types.js';

/** Run-time-interpolated screenshot template (literal `${...}` placeholders — F2). */
const SCREENSHOTS_OUT_DIR = '.application-test/runs/${run}/screenshots/${role}/${route_id}';

/** sha256 of three canonical source strings → the plan's drift signature. PURE. */
export function computeSignature(parts: { nav: string; rbac: string; registry: string }): SourceSignature {
  const sha = (s: string): string => createHash('sha256').update(s, 'utf8').digest('hex');
  return { nav_sha: sha(parts.nav), rbac_sha: sha(parts.rbac), registry_sha: sha(parts.registry) };
}

export interface AssemblePlanArgs {
  application: string;
  path: string;
  /** Canonically ordered (caller passes canonicalRoleOrder output). */
  roles: string[];
  routes: Route[];
  endpoints: Endpoint[];
  signature: SourceSignature;
  modes?: ('bfs' | 'goto')[];
  caps?: CapsOverride;
  generatedAt?: string;
}

/**
 * Assemble + validate a plan. Returns the parsed plan (defaults filled) and any
 * structural invariant violations. Throws only on an internal assembly bug (a
 * raw plan that fails the SCHEMA — distinct from invariant violations, which are
 * returned for the caller to surface).
 */
export function assemblePlan(args: AssemblePlanArgs): { plan: PlanTest; violations: InvariantViolation[] } {
  const execution: Record<string, unknown> = {
    screenshots: { out_dir: SCREENSHOTS_OUT_DIR },
  };
  if (args.modes) execution.modes = args.modes;
  // A partial caps override is parsed normally by Zod, so omitted fields still get
  // their per-field defaults (the v4 `.default()` footgun only bites the absent case).
  if (args.caps) execution.caps = args.caps;
  if (args.roles.includes('anonymous')) {
    execution.per_role = { anonymous: { mode: 'goto', expect: 'redirect_login' } };
  }

  const meta: Record<string, unknown> = {
    application: args.application,
    path: args.path,
    source_signature: args.signature,
  };
  if (args.generatedAt) meta.generated_at = args.generatedAt;

  const raw = {
    schema_version: '1.0.0',
    meta,
    roles: args.roles,
    execution,
    routes: args.routes,
    endpoints: args.endpoints,
  };

  const parsed = PlanTestSchema.safeParse(raw);
  if (!parsed.success) {
    throw new Error(
      `uat-plan assembled an invalid plan (internal bug): ${parsed.error.issues
        .map((i) => `${i.path.join('.')}: ${i.message}`)
        .join('; ')}`,
    );
  }
  return { plan: parsed.data, violations: validateInvariants(parsed.data) };
}

/** Serialise a plan to deterministic YAML (insertion order preserved, no line-wrap). */
export function serializePlan(plan: PlanTest): string {
  return dump(plan, { lineWidth: -1, noRefs: true, indent: 2 });
}

/** Emit the plan + its `.signature` sidecar under `outDir`. */
export function emitArtifacts(
  name: string,
  outDir: string,
  yaml: string,
  signature: SourceSignature,
): GeneratedFile[] {
  const dir = outDir.replace(/\/+$/, '');
  return [
    { path: `${dir}/${name}.plantest.yml`, content: yaml },
    { path: `${dir}/${name}.plantest.signature`, content: `${JSON.stringify(signature, null, 2)}\n` },
  ];
}

export interface GeneratePlanOptions {
  /** Artifact basename (`{name}.plantest.yml`). */
  name: string;
  /** Artifact directory (relative to projectPath). */
  outDir: string;
  modes?: ('bfs' | 'goto')[];
  caps?: CapsOverride;
  generatedAt?: string;
  /** Emit the API axis (`endpoints[]`). */
  includeApi: boolean;
}

export interface GeneratePlanResult {
  plan: PlanTest;
  yaml: string;
  violations: InvariantViolation[];
  files: GeneratedFile[];
  signature: SourceSignature;
}

/** End-to-end PURE pipeline: discovery → projection → plan → YAML + artifacts. */
export function generatePlan(discovery: DiscoveryResult, opts: GeneratePlanOptions): GeneratePlanResult {
  const roles = canonicalRoleOrder(discovery.rbac.roles);
  const routes = projectRoutes(discovery.routes, roles, discovery.rbac.grantsByRole);
  const endpoints = opts.includeApi
    ? projectEndpoints(discovery.endpoints, roles, discovery.rbac.grantsByRole)
    : [];
  const { plan, violations } = assemblePlan({
    application: discovery.application,
    path: discovery.path,
    roles,
    routes,
    endpoints,
    signature: discovery.signature,
    modes: opts.modes,
    caps: opts.caps,
    generatedAt: opts.generatedAt,
  });
  const yaml = serializePlan(plan);
  const files = emitArtifacts(opts.name, opts.outDir, yaml, discovery.signature);
  return { plan, yaml, violations, files, signature: discovery.signature };
}
