// lib/parsers/manifest-yaml.mjs
// Extract the `manifest:` YAML block from each step body.
//
// Plan v1.7 contract: every step has a fenced ```yaml ... ``` block whose
// top-level key is `manifest:` and which contains the keys:
//   expected_paths, min_file_count, commit_message_pattern, bash_syntax_check,
//   forbidden_paths, must_contain.

import { issue, ok, fail } from '../util/result.mjs';
import { parseFrontmatter } from '../util/frontmatter.mjs';

const FENCED_YAML_RE = /```ya?ml\s*\n([\s\S]*?)\n[ \t]*```/g;

const REQUIRED_KEYS = [
  'expected_paths',
  'min_file_count',
  'commit_message_pattern',
  'bash_syntax_check',
  'forbidden_paths',
  'must_contain',
];

// Optional manifest keys (plan-v2 Step 4). Absence == false.
// `skip_commit_check`: opt out of the per-step commit assertion (e.g. memory-only steps).
// `memory_write`     : marks a step that writes to ~/.claude/projects/.../memory/
//                      so the executor can route it through the memory truth gate.
const OPTIONAL_KEYS = [
  'skip_commit_check',
  'memory_write',
];

const OPTIONAL_BOOLEAN_KEYS = new Set(OPTIONAL_KEYS);

export { OPTIONAL_KEYS };

/**
 * Extract the first fenced YAML block whose first non-blank line begins with
 * `manifest:`.
 * @returns {string|null} Inner YAML body without the leading `manifest:` line.
 */
export function extractManifestYaml(stepBody) {
  if (typeof stepBody !== 'string') return null;
  FENCED_YAML_RE.lastIndex = 0;
  let m;
  while ((m = FENCED_YAML_RE.exec(stepBody)) !== null) {
    const block = m[1];
    const firstNonBlank = block.split(/\r?\n/).find(l => l.trim() !== '');
    if (firstNonBlank && /^manifest\s*:/.test(firstNonBlank.trim())) {
      const after = block.replace(/^[\s\S]*?manifest[ \t]*:[ \t]*\n?/, '');
      return after;
    }
  }
  return null;
}

/**
 * Parse a single step's manifest into an object.
 * Reuses the frontmatter parser (same restricted YAML subset).
 * @returns {import('../util/result.mjs').Result}
 */
export function parseManifest(stepBody) {
  const yamlText = extractManifestYaml(stepBody);
  if (yamlText === null) {
    return fail(issue('MANIFEST_MISSING', 'No `manifest:` YAML block found in step body'));
  }
  const dedented = dedent(yamlText);
  const result = parseFrontmatter(dedented);
  if (!result.valid) return result;

  const errors = [];
  const warnings = [];
  const parsed = result.parsed || {};

  for (const k of REQUIRED_KEYS) {
    if (!(k in parsed)) {
      errors.push(issue('MANIFEST_MISSING_KEY', `Manifest is missing required key: ${k}`));
    }
  }

  if ('commit_message_pattern' in parsed) {
    const pat = parsed.commit_message_pattern;
    if (typeof pat !== 'string') {
      errors.push(issue('MANIFEST_PATTERN_TYPE', 'commit_message_pattern must be a string'));
    } else {
      try { new RegExp(pat); }
      catch (e) {
        errors.push(issue('MANIFEST_PATTERN_INVALID', `commit_message_pattern is not a valid regex: ${e.message}`));
      }
    }
  }

  if ('expected_paths' in parsed && !Array.isArray(parsed.expected_paths)) {
    errors.push(issue('MANIFEST_PATHS_TYPE', 'expected_paths must be a list'));
  }

  if ('min_file_count' in parsed && typeof parsed.min_file_count !== 'number') {
    errors.push(issue('MANIFEST_COUNT_TYPE', 'min_file_count must be a number'));
  }

  for (const k of OPTIONAL_BOOLEAN_KEYS) {
    if (k in parsed) {
      if (typeof parsed[k] !== 'boolean') {
        errors.push(issue(
          'MANIFEST_OPTIONAL_TYPE',
          `${k} must be boolean if present (got ${typeof parsed[k]})`,
        ));
      }
    } else {
      parsed[k] = false; // default: absence == false
    }
  }

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

function dedent(text) {
  const lines = text.split(/\r?\n/);
  const indents = lines
    .filter(l => l.trim() !== '')
    .map(l => (l.match(/^(\s*)/) || ['', ''])[1].length);
  if (indents.length === 0) return text;
  const min = Math.min(...indents);
  if (min === 0) return text;
  return lines.map(l => l.slice(min)).join('\n');
}

/**
 * Validate every step in a parsed plan has a manifest.
 * @param {Array<{n: number, body: string}>} steps
 * @returns {import('../util/result.mjs').Result}
 */
export function validateAllManifests(steps) {
  const errors = [];
  const warnings = [];
  const parsed = [];
  for (const s of steps) {
    const r = parseManifest(s.body);
    if (!r.valid) {
      for (const e of r.errors) errors.push(issue(e.code, `Step ${s.n}: ${e.message}`, e.hint));
    }
    parsed.push({ n: s.n, manifest: r.parsed, valid: r.valid });
  }
  return { valid: errors.length === 0, errors, warnings, parsed };
}
