// Semantic validator for the reflection applier. Returns per-change decisions
// using the ReflectionRejectionCode union (10 codes) defined in ./types.ts.
//
// The same thresholds (name pattern, four-value type, max body bytes) are
// surfaced through the manual-edit path at `src/bridge/memory-api.ts` PUT
// /entries/:name. Keep the two in sync — if you add or rename a rejection
// code, update both files plus the UI message map in MemoryLearningForm.tsx
// (see docs/maintenance-checklist.md item #10).

import { parse as parseYaml } from 'yaml';
import { isValidMemoryName } from '../../user-folder/memory.js';
import type {
  MemoryChange,
  PieceChanges,
  ReflectionInput,
  ReflectionResult,
  ReflectionRejectionCode,
} from './types.js';

// ── Public types ──────────────────────────────────────────────────────────────

export interface ValidatorOpts {
  maxBodyBytes: number;
}

export interface MemoryDecision {
  index: number;
  accepted: boolean;
  code?: ReflectionRejectionCode;
  reason?: string;
}

export interface PieceDecision {
  accepted: boolean;
  code?: ReflectionRejectionCode;
  reason?: string;
}

export interface ValidatorOutput {
  memoryDecisions: MemoryDecision[];
  pieceDecision: PieceDecision | null; // null when should_edit=false
}

// ── Constants ─────────────────────────────────────────────────────────────────

const ALLOWED_TYPES = new Set(['user', 'feedback', 'project', 'reference']);

/** Sentinels that are forbidden in rules[].next — engine-internal only. */
const SENTINELS = new Set(['COMPLETE', 'ABORT', 'ASK']);

// ── Main export ───────────────────────────────────────────────────────────────

/**
 * Semantic validator for a parsed LLM ReflectionResult.
 *
 * Validates all memory_changes (up to the 3-entry cap) and piece_changes
 * statically — without touching the filesystem. Returns a ValidatorOutput
 * with per-item decisions (accepted/rejected + reason code).
 *
 * Rejection codes covered:
 *   rejected_unknown_type       — type not in {user, feedback, project, reference}
 *   rejected_bad_name           — name fails isValidMemoryName (pattern / length)
 *   rejected_body_too_large     — body > maxBodyBytes in UTF-8
 *   rejected_missing_target     — update/merge_into/remove missing merge_target field OR
 *                                 merge_target does not exist in the current memory index
 *   rejected_name_collision     — add with a name that already exists
 *   rejected_target_piece_mismatch — piece_changes.target_piece ≠ input.pieceName
 *   rejected_invalid_yaml       — piece_changes.new_yaml fails YAML parse
 *   rejected_invalid_piece      — new_yaml parses but fails piece-lint
 *   rejected_dangerous_piece    — COMPLETE/ABORT/ASK appear in rules[].next
 *
 * Note: rejected_stale_target (CAS revision mismatch) is raised by the applier
 * at write time, NOT by this static validator — the revision comparison requires
 * reading the live file inside the per-user lock.
 */
export function validateReflectionResult(
  r: ReflectionResult,
  input: ReflectionInput,
  opts: ValidatorOpts,
): ValidatorOutput {
  // Build a set of existing memory entry names for collision / target checks.
  const existing = new Set(input.memoryEntries.map((e) => e.name));

  // Validate each memory change (cap at 3).
  const memoryDecisions: MemoryDecision[] = r.memory_changes
    .slice(0, 3)
    .map((c, i): MemoryDecision => validateMemoryChange(c, i, existing, opts.maxBodyBytes));

  // Validate piece changes (only when should_edit=true).
  let pieceDecision: PieceDecision | null = null;
  if (r.piece_changes?.should_edit) {
    pieceDecision = validatePiece(r.piece_changes, input);
  }

  return { memoryDecisions, pieceDecision };
}

// ── Memory change validator ───────────────────────────────────────────────────

function validateMemoryChange(
  c: MemoryChange,
  index: number,
  existing: Set<string>,
  maxBodyBytes: number,
): MemoryDecision {
  // 1. Type check
  if (!ALLOWED_TYPES.has(c.type)) {
    return {
      index,
      accepted: false,
      code: 'rejected_unknown_type',
      reason: `type="${c.type}" not in {user, feedback, project, reference}`,
    };
  }

  // 2. Name validation
  if (!isValidMemoryName(c.name)) {
    return {
      index,
      accepted: false,
      code: 'rejected_bad_name',
      reason: `name="${c.name}" fails name pattern (1-64 chars, [a-zA-Z0-9_-] only)`,
    };
  }

  // 3. Body size
  if (Buffer.byteLength(c.body, 'utf8') > maxBodyBytes) {
    return {
      index,
      accepted: false,
      code: 'rejected_body_too_large',
      reason: `body is ${Buffer.byteLength(c.body, 'utf8')} bytes, limit is ${maxBodyBytes}`,
    };
  }

  // 4. Collision check (add only)
  if (c.op === 'add' && existing.has(c.name)) {
    return {
      index,
      accepted: false,
      code: 'rejected_name_collision',
      reason: `add with name="${c.name}" but that entry already exists`,
    };
  }

  // 5. merge_target required for update/merge_into/remove
  if (c.op !== 'add') {
    if (!c.merge_target) {
      return {
        index,
        accepted: false,
        code: 'rejected_missing_target',
        reason: `op="${c.op}" requires merge_target field`,
      };
    }
    if (!existing.has(c.merge_target)) {
      return {
        index,
        accepted: false,
        code: 'rejected_missing_target',
        reason: `merge_target="${c.merge_target}" does not exist in current memory index`,
      };
    }
  }

  return { index, accepted: true };
}

// ── Piece change validator ────────────────────────────────────────────────────

function validatePiece(p: PieceChanges, input: ReflectionInput): PieceDecision {
  // 1. Target piece must match the running piece
  if (p.target_piece && p.target_piece !== input.pieceName) {
    return {
      accepted: false,
      code: 'rejected_target_piece_mismatch',
      reason: `target_piece="${p.target_piece}" but running piece is "${input.pieceName}"`,
    };
  }

  // 2. new_yaml must be present
  if (!p.new_yaml) {
    return {
      accepted: false,
      code: 'rejected_invalid_yaml',
      reason: 'new_yaml is null or absent',
    };
  }

  // 3. YAML parse
  let doc: unknown;
  try {
    doc = parseYaml(p.new_yaml);
  } catch (e) {
    return {
      accepted: false,
      code: 'rejected_invalid_yaml',
      reason: String(e),
    };
  }

  // 4. Piece-lint: movements must be a non-empty array
  if (
    !doc ||
    typeof doc !== 'object' ||
    !Array.isArray((doc as Record<string, unknown>)['movements']) ||
    ((doc as Record<string, unknown>)['movements'] as unknown[]).length === 0
  ) {
    return {
      accepted: false,
      code: 'rejected_invalid_piece',
      reason: 'movements must be a non-empty array',
    };
  }

  // 5. Dangerous sentinel check: COMPLETE/ABORT/ASK must not appear in rules[].next
  const movements = (doc as Record<string, unknown>)['movements'] as Array<Record<string, unknown>>;
  for (const movement of movements) {
    const rules = (movement['rules'] ?? []) as Array<Record<string, unknown>>;
    for (const rule of rules) {
      const next = rule['next'];
      if (typeof next === 'string' && SENTINELS.has(next)) {
        return {
          accepted: false,
          code: 'rejected_dangerous_piece',
          reason: `"${next}" appears in rules[].next — use complete() tool instead`,
        };
      }
    }
  }

  return { accepted: true };
}
