#!/usr/bin/env tsx
/**
 * Generate capability and recipe registries from YAML master data.
 *
 * @remarks
 * Reads `capabilities.yaml` and `recipes.yaml`, validates via Zod schemas,
 * and writes 5 output files:
 *
 * 1. `src/ai/capability-registry.generated.ts`
 * 2. `src/ai/recipe-registry.generated.ts`
 * 3. `docs/capabilities.md`
 * 4. `skills/playwright-praman-sap-testing/capabilities-reference.md`
 * 5. `capabilities.json` (machine-readable manifest for AI agents)
 *
 * @see capabilities.yaml
 * @see recipes.yaml
 */

import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { parse as parseYaml } from 'yaml';

import { CapabilitiesYamlSchema } from '../src/ai/schemas/capability.schema.js';
import { RecipesYamlSchema } from '../src/ai/schemas/recipe.schema.js';
import type { CapabilityEntry } from '../src/ai/schemas/capability.schema.js';
import type { CapabilitiesYaml } from '../src/ai/schemas/capability.schema.js';
import type { RecipeEntry } from '../src/ai/schemas/recipe.schema.js';
import type { RecipesYaml } from '../src/ai/schemas/recipe.schema.js';

const ROOT = resolve(import.meta.dirname ?? '.', '..');

/* ── Paths ────────────────────────────────────────────────────────────────── */

const CAPABILITIES_YAML = resolve(ROOT, 'capabilities.yaml');
const RECIPES_YAML = resolve(ROOT, 'recipes.yaml');
const CAPABILITY_REGISTRY_TS = resolve(ROOT, 'src/ai/capability-registry.generated.ts');
const RECIPE_REGISTRY_TS = resolve(ROOT, 'src/ai/recipe-registry.generated.ts');
const CAPABILITIES_MD = resolve(ROOT, 'docs/capabilities.md');
const CAPABILITIES_SKILL_MD = resolve(
  ROOT,
  'skills/playwright-praman-sap-testing/capabilities-reference.md',
);
const CAPABILITIES_JSON = resolve(ROOT, 'capabilities.json');

/* ── Helpers ──────────────────────────────────────────────────────────────── */

const today = new Date().toISOString().split('T')[0];

/**
 * Trim trailing whitespace from `usageExample` values.
 *
 * @remarks
 * YAML block scalars (`|` or `>`) append a trailing newline that must be
 * removed before embedding in TypeScript source.
 */
function trimExamples(entries: readonly CapabilityEntry[]): CapabilityEntry[] {
  return entries.map((entry) => ({
    ...entry,
    usageExample: entry.usageExample.trim(),
  }));
}

/**
 * Trim trailing whitespace from recipe `pattern` values.
 */
function trimPatterns(entries: readonly RecipeEntry[]): RecipeEntry[] {
  return entries.map((entry) => ({
    ...entry,
    description: entry.description.trim(),
    pattern: entry.pattern.trim(),
  }));
}

const PRINT_WIDTH = 98;

/**
 * Quote a string value using single quotes, falling back to double quotes
 * when the value contains single quotes (matching Prettier singleQuote).
 * Escape sequences (\n, \t, \\) are preserved as literals in the output.
 */
function quoteString(value: string): string {
  let escaped = value
    .replace(/\\/g, '\\\\')
    .replace(/\n/g, '\\n')
    .replace(/\r/g, '\\r')
    .replace(/\t/g, '\\t');

  if (value.includes("'")) {
    escaped = escaped.replace(/"/g, '\\"');
    return `"${escaped}"`;
  }
  return `'${escaped}'`;
}

/**
 * Serialize a JS value as a Prettier-compatible literal (single quotes,
 * unquoted keys, trailing commas, printWidth-aware array wrapping).
 */
function toJsLiteral(value: unknown, indent: number): string {
  if (value === null || value === undefined) return 'undefined';
  if (typeof value === 'boolean' || typeof value === 'number') return String(value);
  if (typeof value === 'string') return quoteString(value);

  if (Array.isArray(value)) {
    if (value.length === 0) return '[]';
    const items = value.map((item: unknown) => toJsLiteral(item, indent + 2));
    const inline = `[${items.join(', ')}]`;
    if (indent + inline.length <= PRINT_WIDTH) return inline;
    const pad = ' '.repeat(indent + 2);
    return `[\n${items.map((item) => `${pad}${item},`).join('\n')}\n${' '.repeat(indent)}]`;
  }

  if (typeof value === 'object') {
    const obj = value as Record<string, unknown>;
    const pad = ' '.repeat(indent + 2);
    const lines: string[] = [];
    for (const [key, val] of Object.entries(obj)) {
      if (val === undefined) continue;
      const serializedVal = toJsLiteral(val, indent + 2);
      const line = `${pad}${key}: ${serializedVal},`;
      if (line.length <= PRINT_WIDTH) {
        lines.push(line);
      } else if (Array.isArray(val)) {
        const items = val.map((item: unknown) => toJsLiteral(item, indent + 4));
        const innerPad = ' '.repeat(indent + 4);
        lines.push(
          `${pad}${key}: [\n${items.map((item) => `${innerPad}${item},`).join('\n')}\n${pad}],`,
        );
      } else {
        lines.push(`${pad}${key}:\n${pad}  ${serializedVal},`);
      }
    }
    return `{\n${lines.join('\n')}\n${' '.repeat(indent)}}`;
  }

  return String(value);
}

/**
 * Serialize a capability entry to a Prettier-formatted JS object literal,
 * omitting undefined optional fields.
 */
function serializeEntry(entry: CapabilityEntry): string {
  const ordered: Record<string, unknown> = {
    id: entry.id,
    qualifiedName: entry.qualifiedName,
    name: entry.name,
    description: entry.description,
    category: entry.category,
    priority: entry.priority,
    usageExample: entry.usageExample,
    registryVersion: entry.registryVersion,
  };
  if (entry.intent !== undefined) ordered['intent'] = entry.intent;
  if (entry.sapModule !== undefined) ordered['sapModule'] = entry.sapModule;
  if (entry.controlTypes !== undefined) ordered['controlTypes'] = entry.controlTypes;
  if (entry.async !== undefined) ordered['async'] = entry.async;
  if (entry.aiSteering !== undefined) ordered['aiSteering'] = entry.aiSteering;
  return toJsLiteral(ordered, 0);
}

/**
 * Serialize a recipe entry to a Prettier-formatted JS object literal.
 */
function serializeRecipeEntry(entry: RecipeEntry): string {
  return toJsLiteral(entry, 0);
}

/**
 * Ensure the parent directory of a file path exists.
 */
async function ensureDir(filePath: string): Promise<void> {
  await mkdir(dirname(filePath), { recursive: true });
}

/* ── Generators ───────────────────────────────────────────────────────────── */

/**
 * Generate `capability-registry.generated.ts`.
 */
function generateCapabilityRegistryTs(entries: readonly CapabilityEntry[]): string {
  const arrayBody = entries.map((entry) => {
    const literal = serializeEntry(entry);
    return literal
      .split('\n')
      .map((line) => `  ${line}`)
      .join('\n');
  });

  return `/**
 * @license
 * Copyright (c) ZesTest 2025-2030. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 *
 * This file may contain AI-assisted code.
 * See LICENSE and NOTICE files for details.
 */

/* eslint-disable max-lines, sonarjs/no-duplicate-string */
/**
 * AUTO-GENERATED — do not edit manually.
 *
 * @remarks
 * This file is overwritten by \`npm run generate:capabilities\`.
 * Edit \`capabilities.yaml\` to add or modify capabilities, then re-run the generator.
 *
 * @module ai
 */

import type { CapabilityEntry } from './schemas/capability.schema.js';

/**
 * Static list of generated capability entries.
 *
 * @remarks
 * Generated on ${today} with ${String(entries.length)} entries.
 */
export const GENERATED_CAPABILITIES: readonly CapabilityEntry[] = [
${arrayBody.join(',\n')},
] as const;
`;
}

/**
 * Generate `recipe-registry.generated.ts`.
 */
function generateRecipeRegistryTs(entries: readonly RecipeEntry[]): string {
  const serialized = entries.map((entry) => {
    const literal = serializeRecipeEntry(entry);
    return literal
      .split('\n')
      .map((line) => `  ${line}`)
      .join('\n');
  });

  return `/**
 * @license
 * Copyright (c) ZesTest 2025-2030. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 *
 * This file may contain AI-assisted code.
 * See LICENSE and NOTICE files for details.
 */

/* eslint-disable sonarjs/no-duplicate-string */
/**
 * AUTO-GENERATED — do not edit manually.
 *
 * @remarks
 * This file is overwritten by \`npm run generate:capabilities\`.
 * Edit \`recipes.yaml\` to add or modify recipes, then re-run the generator.
 *
 * @module ai
 */

import type { RecipeEntry } from './schemas/recipe.schema.js';

/**
 * Static list of generated recipe entries.
 *
 * @remarks
 * Generated on ${today} with ${String(entries.length)} entries.
 */
export const GENERATED_RECIPES: readonly RecipeEntry[] = [
${serialized.join(',\n')},
] as const;
`;
}

/**
 * Generate `docs/capabilities.md`.
 */
function generateCapabilitiesMd(
  entries: readonly CapabilityEntry[],
  yamlData: CapabilitiesYaml,
): string {
  const lines: string[] = [];

  lines.push('# Praman Capabilities Reference');
  lines.push('');
  lines.push(
    `> **Generated**: ${today} — do not edit manually, run \`npm run generate:capabilities\``,
  );
  lines.push(
    `> **Total**: ${String(entries.length)} capabilities across ${String(Object.keys(yamlData.categories).length)} categories`,
  );
  lines.push('');
  lines.push('---');
  lines.push('');

  // Category summary table
  lines.push('## Categories');
  lines.push('');
  lines.push('| Category | Prefix | Description | Count |');
  lines.push('|----------|--------|-------------|-------|');

  const categoryCounts = new Map<string, number>();
  for (const entry of entries) {
    categoryCounts.set(entry.category, (categoryCounts.get(entry.category) ?? 0) + 1);
  }

  for (const [catKey, catInfo] of Object.entries(yamlData.categories)) {
    const count = categoryCounts.get(catKey) ?? 0;
    lines.push(
      `| ${catKey} | \`UI5-${catInfo.prefix}\` | ${catInfo.description} | ${String(count)} |`,
    );
  }

  lines.push('');
  lines.push('---');
  lines.push('');

  // Per-category sections
  const grouped = new Map<string, CapabilityEntry[]>();
  for (const entry of entries) {
    const list = grouped.get(entry.category) ?? [];
    list.push(entry);
    grouped.set(entry.category, list);
  }

  for (const [catKey, catInfo] of Object.entries(yamlData.categories)) {
    const catEntries = grouped.get(catKey);
    if (!catEntries || catEntries.length === 0) continue;

    lines.push(`## ${catKey} — ${catInfo.description}`);
    lines.push('');
    lines.push('| ID | Name | Description | Usage Example |');
    lines.push('|-----|------|-------------|---------------|');

    for (const entry of catEntries) {
      const example = entry.usageExample.split('\n')[0] ?? '';
      lines.push(`| \`${entry.id}\` | ${entry.name} | ${entry.description} | \`${example}\` |`);
    }

    lines.push('');
  }

  return lines.join('\n');
}

/**
 * Generate `skills/playwright-praman-sap-testing/capabilities-reference.md`.
 */
function generateSkillCapabilitiesMd(
  entries: readonly CapabilityEntry[],
  yamlData: CapabilitiesYaml,
): string {
  const lines: string[] = [];

  lines.push('# Praman Capabilities Reference (Agent)');
  lines.push('');
  lines.push(`> Generated: ${today} — do not edit manually, run \`npm run generate:capabilities\``);
  lines.push(`> Total: ${String(entries.length)} capabilities`);
  lines.push('');
  lines.push('---');
  lines.push('');

  // Per-category sections
  const grouped = new Map<string, CapabilityEntry[]>();
  for (const entry of entries) {
    const list = grouped.get(entry.category) ?? [];
    list.push(entry);
    grouped.set(entry.category, list);
  }

  for (const [catKey, catInfo] of Object.entries(yamlData.categories)) {
    const catEntries = grouped.get(catKey);
    if (!catEntries || catEntries.length === 0) continue;

    lines.push(`## ${catKey} — ${catInfo.description}`);
    lines.push('');

    for (const entry of catEntries) {
      lines.push(`- **${entry.qualifiedName}** — ${entry.description}`);
    }

    lines.push('');
  }

  return lines.join('\n');
}

/**
 * Generate `capabilities.json` — machine-readable manifest for AI agents.
 *
 * @remarks
 * Produces a self-describing JSON file that AI coding agents (Claude Code,
 * Copilot, Cursor, Codex) can parse programmatically without a YAML library.
 */
function generateCapabilitiesJson(
  entries: readonly CapabilityEntry[],
  yamlData: CapabilitiesYaml,
): string {
  const manifest = {
    $schema: 'https://json-schema.org/draft/2020-12/schema',
    name: 'playwright-praman',
    description: 'Capability manifest for Praman SAP UI5 test automation',
    registryVersion: yamlData.registryVersion,
    generatedAt: today,
    totalCapabilities: entries.length,
    categories: yamlData.categories,
    capabilities: entries.map((entry) => {
      const obj: Record<string, unknown> = {
        id: entry.id,
        qualifiedName: entry.qualifiedName,
        name: entry.name,
        description: entry.description,
        category: entry.category,
        priority: entry.priority,
        usageExample: entry.usageExample,
        registryVersion: entry.registryVersion,
      };
      if (entry.intent !== undefined) obj['intent'] = entry.intent;
      if (entry.sapModule !== undefined) obj['sapModule'] = entry.sapModule;
      if (entry.controlTypes !== undefined) obj['controlTypes'] = entry.controlTypes;
      if (entry.async !== undefined) obj['async'] = entry.async;
      if (entry.aiSteering !== undefined) obj['aiSteering'] = entry.aiSteering;
      return obj;
    }),
  };

  return `${JSON.stringify(manifest, null, 2)}\n`;
}

/* ── Main ─────────────────────────────────────────────────────────────────── */

async function main(): Promise<void> {
  console.log('Scanning YAML master data...');

  // 1. Read YAML files
  console.log(`  Reading ${CAPABILITIES_YAML}`);
  const capabilitiesRaw = await readFile(CAPABILITIES_YAML, 'utf-8');
  console.log(`  Reading ${RECIPES_YAML}`);
  const recipesRaw = await readFile(RECIPES_YAML, 'utf-8');

  // 2. Parse YAML
  const capabilitiesParsed: unknown = parseYaml(capabilitiesRaw);
  const recipesParsed: unknown = parseYaml(recipesRaw);

  // 3. Validate with Zod schemas
  console.log('Validating capabilities.yaml...');
  let capabilitiesYaml: CapabilitiesYaml;
  try {
    capabilitiesYaml = CapabilitiesYamlSchema.parse(capabilitiesParsed);
  } catch (error: unknown) {
    console.error('Validation failed for capabilities.yaml:');
    console.error(error);
    process.exitCode = 1;
    return;
  }

  console.log('Validating recipes.yaml...');
  let recipesYaml: RecipesYaml;
  try {
    recipesYaml = RecipesYamlSchema.parse(recipesParsed);
  } catch (error: unknown) {
    console.error('Validation failed for recipes.yaml:');
    console.error(error);
    process.exitCode = 1;
    return;
  }

  // 4. Trim trailing whitespace from usage examples and patterns
  const capabilities = trimExamples(capabilitiesYaml.capabilities);
  const recipes = trimPatterns(recipesYaml.recipes);

  console.log(`  Found ${String(capabilities.length)} capabilities`);
  console.log(`  Found ${String(recipes.length)} recipes`);

  // 5. Generate and write output files
  console.log('Writing output files...');

  // Output 1: capability-registry.generated.ts
  await ensureDir(CAPABILITY_REGISTRY_TS);
  const capRegistryContent = generateCapabilityRegistryTs(capabilities);
  await writeFile(CAPABILITY_REGISTRY_TS, capRegistryContent, 'utf-8');
  console.log(`  Written: ${CAPABILITY_REGISTRY_TS} (${String(capabilities.length)} entries)`);

  // Output 2: recipe-registry.generated.ts
  await ensureDir(RECIPE_REGISTRY_TS);
  const recipeRegistryContent = generateRecipeRegistryTs(recipes);
  await writeFile(RECIPE_REGISTRY_TS, recipeRegistryContent, 'utf-8');
  console.log(`  Written: ${RECIPE_REGISTRY_TS} (${String(recipes.length)} entries)`);

  // Output 3: docs/capabilities.md
  await ensureDir(CAPABILITIES_MD);
  const capsMdContent = generateCapabilitiesMd(capabilities, capabilitiesYaml);
  await writeFile(CAPABILITIES_MD, capsMdContent, 'utf-8');
  console.log(`  Written: ${CAPABILITIES_MD}`);

  // Output 4: skills capabilities-reference.md
  await ensureDir(CAPABILITIES_SKILL_MD);
  const skillMdContent = generateSkillCapabilitiesMd(capabilities, capabilitiesYaml);
  await writeFile(CAPABILITIES_SKILL_MD, skillMdContent, 'utf-8');
  console.log(`  Written: ${CAPABILITIES_SKILL_MD}`);

  // Output 5: capabilities.json (machine-readable manifest for AI agents)
  const capsJsonContent = generateCapabilitiesJson(capabilities, capabilitiesYaml);
  await writeFile(CAPABILITIES_JSON, capsJsonContent, 'utf-8');
  console.log(`  Written: ${CAPABILITIES_JSON} (${String(capabilities.length)} entries)`);

  console.log('Done.');
}

main().catch((err: unknown) => {
  console.error('generate-capabilities failed:', err);
  process.exitCode = 1;
});
