// lib/validators/next-session-prompt-validator.mjs
// Validate NEXT-SESSION-PROMPT.local.md frontmatter (Bug 3 contract).
//
// Producers (ultraexecute-local Phase 8/2.55/4, ultraplan-end-session-local
// Phase 3) MUST write `produced_by:` and `produced_at:` (ISO-8601) frontmatter.
// Consumers (/ultracontinue Phase 1.5) compare two candidate files and refuse
// when producers disagree on a non-stale pair.
//
// Schema is forward-compatible: unknown frontmatter keys are tolerated.

import { readFileSync, existsSync } from 'node:fs';
import { issue, fail } from '../util/result.mjs';
import { splitFrontmatter, parseFrontmatter } from '../util/frontmatter.mjs';

export const NEXT_SESSION_PROMPT_REQUIRED_FIELDS = Object.freeze(['produced_by', 'produced_at']);

const ONE_DAY_MS = 24 * 60 * 60 * 1000;

export function validateNextSessionPromptContent(text) {
  const split = splitFrontmatter(text);
  if (!split.hasFrontmatter) {
    return {
      valid: true,
      errors: [],
      warnings: [issue(
        'NEXT_SESSION_PROMPT_NO_FRONTMATTER',
        'NEXT-SESSION-PROMPT.local.md has no YAML frontmatter',
        'Producers should write produced_by and produced_at; legacy files are tolerated.',
      )],
      parsed: null,
    };
  }
  const fm = parseFrontmatter(split.frontmatter);
  if (!fm.valid) {
    return { valid: false, errors: fm.errors, warnings: [], parsed: fm.parsed || null };
  }
  return validateNextSessionPromptObject(fm.parsed);
}

export function validateNextSessionPromptObject(parsed) {
  const errors = [];
  const warnings = [];

  if (typeof parsed !== 'object' || parsed === null) {
    return fail(issue('NEXT_SESSION_PROMPT_NOT_OBJECT', 'Frontmatter is not an object'));
  }

  for (const k of NEXT_SESSION_PROMPT_REQUIRED_FIELDS) {
    if (!(k in parsed)) {
      errors.push(issue(
        'NEXT_SESSION_PROMPT_MISSING_FIELD',
        `Required frontmatter field missing: ${k}`,
      ));
    }
  }

  if (parsed.produced_at !== undefined) {
    if (typeof parsed.produced_at !== 'string' || Number.isNaN(Date.parse(parsed.produced_at))) {
      errors.push(issue(
        'NEXT_SESSION_PROMPT_INVALID_TIMESTAMP',
        `produced_at "${parsed.produced_at}" is not a valid ISO-8601 timestamp`,
      ));
    }
  }

  if (parsed.produced_by !== undefined) {
    if (typeof parsed.produced_by !== 'string' || parsed.produced_by.length === 0) {
      errors.push(issue(
        'NEXT_SESSION_PROMPT_INVALID_PRODUCER',
        'produced_by must be a non-empty string',
      ));
    }
  }

  return { valid: errors.length === 0, errors, warnings, parsed };
}

export function validateNextSessionPrompt(filePath) {
  if (!existsSync(filePath)) {
    return fail(issue('NEXT_SESSION_PROMPT_NOT_FOUND', `File not found: ${filePath}`));
  }
  let text;
  try { text = readFileSync(filePath, 'utf-8'); }
  catch (e) {
    return fail(issue('NEXT_SESSION_PROMPT_READ_ERROR', `Cannot read ${filePath}: ${e.message}`));
  }
  return validateNextSessionPromptContent(text);
}

/**
 * Compare two NEXT-SESSION-PROMPT files for consistency.
 * Optional state object enables state-anchored staleness check.
 *
 * @param {{path:string, parsed:object|null}} a
 * @param {{path:string, parsed:object|null}} b
 * @param {{state?: {updated_at?: string}, now?: number}} opts
 */
export function validateNextSessionPromptConsistency(a, b, opts = {}) {
  const errors = [];
  const warnings = [];
  const now = typeof opts.now === 'number' ? opts.now : Date.now();
  const stateUpdatedAt = opts.state && opts.state.updated_at
    ? Date.parse(opts.state.updated_at)
    : NaN;

  const stale = (cand) => {
    if (!cand || !cand.parsed || !cand.parsed.produced_at) return false;
    if (Number.isNaN(stateUpdatedAt)) return false;
    const t = Date.parse(cand.parsed.produced_at);
    if (Number.isNaN(t)) return false;
    return t < stateUpdatedAt;
  };

  const aStale = stale(a);
  const bStale = stale(b);
  const aFm = a && a.parsed;
  const bFm = b && b.parsed;

  if (aFm && bFm) {
    const producerMismatch = aFm.produced_by !== bFm.produced_by;
    const bothFresh = !aStale && !bStale;
    if (producerMismatch && bothFresh) {
      errors.push(issue(
        'NEXT_SESSION_PROMPT_PRODUCER_MISMATCH',
        `Frontmatter "produced_by" disagrees: "${aFm.produced_by}" (${a.path}) vs "${bFm.produced_by}" (${b.path})`,
        'One file is stale or producers wrote conflicting frontmatter. Resolve manually.',
      ));
    } else if (producerMismatch && (aStale || bStale)) {
      const fresh = aStale ? b : a;
      warnings.push(issue(
        'NEXT_SESSION_PROMPT_STALE_IGNORED',
        `Stale candidate ignored; using fresher prompt from ${fresh.path}`,
      ));
    }

    for (const cand of [a, b]) {
      if (!cand || !cand.parsed || !cand.parsed.produced_at) continue;
      const t = Date.parse(cand.parsed.produced_at);
      if (Number.isNaN(t)) continue;
      if (now - t > ONE_DAY_MS) {
        warnings.push(issue(
          'NEXT_SESSION_PROMPT_WALL_CLOCK_DRIFT',
          `${cand.path} produced_at is more than 24h old (${cand.parsed.produced_at})`,
          'Soft warning only. Resuming after a long pause is fine; verify state is still relevant.',
        ));
      }
    }
  }

  return { valid: errors.length === 0, errors, warnings, parsed: { a: aFm || null, b: bFm || null } };
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const args = process.argv.slice(2);
  const positionals = args.filter(a => !a.startsWith('--'));
  const wantJson = args.includes('--json');
  const consistency = args.includes('--consistency');
  const stateIdx = args.indexOf('--state-file');
  const stateFile = stateIdx >= 0 ? args[stateIdx + 1] : null;

  function emit(r) {
    if (wantJson) {
      process.stdout.write(JSON.stringify({ valid: r.valid, errors: r.errors, warnings: r.warnings }, null, 2) + '\n');
    } else {
      process.stdout.write(`next-session-prompt-validator: ${r.valid ? 'PASS' : 'FAIL'}\n`);
      for (const e of r.errors) process.stderr.write(`  ERROR  [${e.code}] ${e.message}\n`);
      for (const w of r.warnings) process.stderr.write(`  WARN   [${w.code}] ${w.message}\n`);
    }
    process.exit(r.valid ? 0 : 1);
  }

  if (consistency) {
    const fileArgs = positionals;
    if (fileArgs.length !== 2) {
      process.stderr.write('Usage: next-session-prompt-validator.mjs --json --consistency <path-a> <path-b> [--state-file <state.json>]\n');
      process.exit(2);
    }
    const [pathA, pathB] = fileArgs;
    const ra = validateNextSessionPrompt(pathA);
    const rb = validateNextSessionPrompt(pathB);
    let stateObj = null;
    if (stateFile) {
      try {
        const txt = readFileSync(stateFile, 'utf-8');
        stateObj = JSON.parse(txt);
      } catch (_e) {
        stateObj = null;
      }
    }
    const r = validateNextSessionPromptConsistency(
      { path: pathA, parsed: ra.parsed },
      { path: pathB, parsed: rb.parsed },
      { state: stateObj },
    );
    emit({
      valid: r.valid && ra.valid !== false,
      errors: [...(ra.errors || []), ...(rb.errors || []), ...r.errors],
      warnings: [...(ra.warnings || []), ...(rb.warnings || []), ...r.warnings],
    });
  } else {
    if (positionals.length !== 1) {
      process.stderr.write('Usage: next-session-prompt-validator.mjs [--json] <NEXT-SESSION-PROMPT.local.md>\n');
      process.exit(2);
    }
    const r = validateNextSessionPrompt(positionals[0]);
    emit(r);
  }
}
