import { readFile, stat, realpath, open } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { validateDocument } from '../src/tools/document/model';
import { generateDocumentPdf } from '../src/tools/document/pdf';
import { calculatePlan, exportPlanCsv, validatePlan } from '../src/tools/planner/core';
import { generatePlanPdf } from '../src/tools/planner/pdf';
import { compileGuided, validateProcess } from '../src/tools/mapper/core';
import type { Issue } from '../src/tools/shared/validation';
import { InputError } from '../src/tools/shared/validation';
import { cliCapabilities } from './capabilities';
import { renderLocalProcess } from './local-render';

class CliError extends Error {
  constructor(message: string, readonly exitCode = 2, readonly code = 'invalid_input', readonly issues: Issue[] = []) { super(message); }
}
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const json = (value: unknown) => `${JSON.stringify(value, null, 2)}\n`;
const help = `Essential Tools local CLI ${cliCapabilities.release}\n\n${cliCapabilities.commands.map(command => `  node dist/cli.mjs ${command}`).join('\n')}\n\n--input - reads JSON from stdin (maximum 3 MB). Outputs are never overwritten unless --force is present.\nProcess SVG/PNG/PDF needs local Chromium: npm run browsers. No telemetry or external rendering.\nExit codes: 0 success; 2 invalid input/usage; 1 runtime/export failure. Diagnostics are JSON on stderr.\n`;

function parseArgs(args: string[]) {
  const positional: string[] = []; const flags = new Map<string, string | boolean>();
  for (let index = 0; index < args.length; index++) {
    const arg = args[index];
    if (!arg.startsWith('--')) { positional.push(arg); continue; }
    if (!['--input', '--output', '--format', '--force'].includes(arg)) throw new CliError(`Unknown option ${arg}. Use --help.`);
    if (flags.has(arg)) throw new CliError(`Option ${arg} was supplied more than once.`);
    if (arg === '--force') flags.set(arg, true);
    else { const value = args[++index]; if (!value || value.startsWith('--')) throw new CliError(`${arg} needs a value.`); flags.set(arg, value); }
  }
  return { positional, flags };
}

async function inputJson(path: string): Promise<unknown> {
  let bytes: Uint8Array;
  if (path === '-') {
    const chunks: Uint8Array[] = []; let size = 0;
    for await (const chunk of process.stdin) {
      const data = new Uint8Array(chunk); size += data.byteLength;
      if (size > 3_000_000) throw new CliError('Input exceeds the 3 MB limit.');
      chunks.push(data);
    }
    bytes = Buffer.concat(chunks);
  } else {
    try { if ((await stat(resolve(path))).size > 3_000_000) throw new CliError('Input exceeds the 3 MB limit.'); bytes = await readFile(resolve(path)); } catch (error) { if (error instanceof CliError) throw error; throw new CliError('The input file could not be read.', 1, 'read_failed'); }
  }
  if (bytes.byteLength > 3_000_000) throw new CliError('Input exceeds the 3 MB limit.');
  try { return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); } catch { throw new CliError('Input must be a UTF-8 JSON object.'); }
}

async function assertDifferentInput(inputPath: string, outputPath: string) {
  if (inputPath === '-') return;
  const input = resolve(inputPath);
  const identity = (path: string) => process.platform === 'win32' ? path.toLowerCase() : path;
  let same = identity(input) === identity(outputPath);
  try {
    const [inputReal, outputReal, inputStat, outputStat] = await Promise.all([realpath(input), realpath(outputPath), stat(input, { bigint: true }), stat(outputPath, { bigint: true })]);
    same ||= identity(inputReal) === identity(outputReal) || (inputStat.ino !== 0n && inputStat.dev === outputStat.dev && inputStat.ino === outputStat.ino);
  } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
  if (same) throw new CliError('Choose a different output path; the input file will not be overwritten.');
}

async function main() {
  const args = process.argv.slice(2);
  if (args.length === 0 || args.includes('--help') || args.includes('-h')) { process.stdout.write(help); return; }
  const { positional, flags } = parseArgs(args);
  if (positional.length === 1 && positional[0] === 'capabilities') { if (flags.size) throw new CliError('capabilities does not take options.'); process.stdout.write(json(cliCapabilities)); return; }
  if (positional.length !== 2) throw new CliError('Choose a documented command. Use --help.');
  const [command, operation] = positional;
  const inputPath = flags.get('--input');
  if (typeof inputPath !== 'string') throw new CliError('--input <file|-> is required.');
  const data = await inputJson(inputPath);
  const kind = command === 'validate' ? operation : command;
  if (!['document', 'plan', 'process'].includes(kind)) throw new CliError('Choose document, plan or process.');
  const checked = kind === 'document' ? validateDocument(data) : kind === 'plan' ? validatePlan(data) : validateProcess(data);
  if (!checked.ok) throw new CliError('The input did not pass validation.', 2, 'validation_failed', checked.errors);
  if (command === 'validate') { process.stdout.write(json({ ok: true, tool: kind, schemaVersion: 1, warnings: checked.warnings })); return; }
  if (command === 'plan' && operation === 'calculate') {
    const result = validatePlan(data); if (!result.ok) throw new CliError('Invalid plan.');
    process.stdout.write(json({ ok: true, ...calculatePlan(result.value), warnings: result.warnings })); return;
  }
  const output = flags.get('--output');
  if (typeof output !== 'string' || output === '-') throw new CliError('--output <file> is required for exports.');
  const outputPath = resolve(output);
  await assertDifferentInput(inputPath, outputPath);
  let format = flags.get('--format');
  let result: string | Uint8Array;
  const pdfAssets = async () => ({ fontBytes: new Uint8Array(await readFile(resolve(root, 'public/tools-assets/fonts/NotoSans-Regular.ttf'))) });
  if (command === 'document' && operation === 'pdf') {
    if (format && format !== 'pdf') throw new CliError('document pdf only exports PDF.');
    format = 'pdf'; const valid = validateDocument(data); if (!valid.ok) throw new CliError('Invalid document.');
    result = await generateDocumentPdf(valid.value, await pdfAssets());
  } else if (command === 'plan' && operation === 'export') {
    if (!['json', 'csv', 'pdf'].includes(String(format))) throw new CliError('Plan format must be json, csv or pdf.');
    const valid = validatePlan(data); if (!valid.ok) throw new CliError('Invalid plan.');
    result = format === 'json' ? json(valid.value) : format === 'csv' ? exportPlanCsv(valid.value) : await generatePlanPdf(valid.value, await pdfAssets());
  } else if (command === 'process' && operation === 'render') {
    if (!['json', 'mermaid', 'svg', 'png', 'pdf'].includes(String(format))) throw new CliError('Process format must be json, mermaid, svg, png or pdf.');
    const valid = validateProcess(data); if (!valid.ok) throw new CliError('Invalid process.');
    if (format === 'json') result = json(valid.value);
    else if (format === 'mermaid') result = `${valid.value.mode === 'guided' ? compileGuided(valid.value.graph) : valid.value.source}\n`;
    else result = await renderLocalProcess(valid.value, format as 'svg' | 'png' | 'pdf', root);
  } else throw new CliError('Unknown command. Use --help.');
  try {
    // Open existing forced outputs without truncation, so aliases can be checked
    // against the actual file handle before any input bytes could be destroyed.
    let handle;
    try { handle = await open(outputPath, flags.has('--force') ? 'r+' : 'wx'); }
    catch (error) { if (!flags.has('--force') || (error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; handle = await open(outputPath, 'wx'); }
    try {
      if (inputPath !== '-') {
        const [inputStat, outputStat] = await Promise.all([stat(resolve(inputPath), { bigint: true }), handle.stat({ bigint: true })]);
        if (inputStat.ino !== 0n && inputStat.dev === outputStat.dev && inputStat.ino === outputStat.ino) throw new CliError('Choose a different output path; the input file will not be overwritten.');
      }
      await assertDifferentInput(inputPath, outputPath);
      await handle.truncate(0);
      await handle.writeFile(result);
    } finally { await handle.close(); }
  }
  catch (error) {
    if (error instanceof CliError) throw error;
    if ((error as NodeJS.ErrnoException).code === 'EEXIST') throw new CliError('The output file already exists. Choose a new path or explicitly use --force.', 2, 'output_exists');
    throw new CliError('The output could not be written. Check the path and directory permissions.', 1, 'write_failed');
  }
  process.stdout.write(json({ ok: true, format, output: outputPath, bytes: typeof result === 'string' ? Buffer.byteLength(result) : result.byteLength, warnings: checked.warnings }));
}

main().catch(error => {
  const known = error instanceof CliError;
  const message = known || error instanceof InputError ? error.message : /Executable doesn't exist/.test(String(error?.message)) ? 'Local Chromium is missing. Run npm run browsers, then retry.' : 'The local export failed. Check the installation and input, then retry. No output was reported as complete.';
  process.stderr.write(json({ ok: false, error: { code: known ? error.code : 'runtime_failure', message, ...(known && error.issues.length ? { issues: error.issues } : {}) } }));
  process.exitCode = known ? error.exitCode : 1;
});
