/**
 * SET 95b — hole-only draw frames.
 *
 * The harness assembles the deterministic boilerplate (the `it()` wrapper for
 * a case fill, the signature-and-imports skeleton for a file build) and the
 * model fills ONLY the marked hole(s). This module is the sole owner of that
 * boilerplate + the splice/extract/overreach gate around it — no other
 * module may reproduce a `describe(...)` string or a stub-skeleton string.
 *
 * Wire format (SET 95a — see `IR_SYSTEM_HOLE` / `BUILD_SYSTEM_HOLE` in
 * `src/core/prompts.ts`): the model's entire hole answer is ONE
 * `⟨HOLE⟩ … ⟨/END⟩` block. Case frames carry a single unnamed `⟨HOLE⟩`
 * marker (one hole per case); impl frames carry one `⟨BODY:<symbol>⟩` marker
 * per exported symbol (one hole per function).
 */
import type { FeatureNode, FeatureSpec } from '../core/types';
import type { IRCaseGen } from '../core/irScaffold';

/** Wire markers — both appear in the system prompt so the model is primed on the pair. */
export const HOLE_OPEN = '⟨HOLE⟩';
export const HOLE_CLOSE = '⟨/END⟩';

/** Typical assertion-body budget (chars) for a single case fill. */
const DEFAULT_CASE_HOLE_BUDGET_BYTES = 400;
/** Typical function-body budget (chars) for a single impl-file hole. */
const DEFAULT_IMPL_HOLE_BUDGET_BYTES = 500;

const KIND_PREFIX: Record<IRCaseGen['kind'], string> = {
  behavior: '',
  edge: 'edge: ',
  integration: 'integration: ',
};

export interface CaseFrame {
  /** The frame with the ⟨HOLE⟩ marker in place of the assertion body. */
  bytes: string;
  holeName: 'ASSERTION';
  holeBudgetBytes: number;
}

/**
 * Deterministic `it()` frame with a single hole where the assertion body
 * goes. This is CONTEXT shown to the model (never the model's output) — the
 * harness still owns the real `describe(...)` wrapper via `assembleTestFile`;
 * this frame exists only so the draw prompt shows the model exactly the
 * shape it is filling.
 */
export function assembleCaseFrame(
  feature: FeatureNode,
  caseName: string,
  kind: IRCaseGen['kind'],
): CaseFrame {
  const prefix = KIND_PREFIX[kind] ?? '';
  const name = prefix && !caseName.startsWith(prefix) ? prefix + caseName : caseName;
  const bytes = [
    `it(${JSON.stringify(name)}, () => {`,
    `  ${HOLE_OPEN}`,
    `});`,
  ].join('\n');
  return { bytes, holeName: 'ASSERTION', holeBudgetBytes: DEFAULT_CASE_HOLE_BUDGET_BYTES };
}

export interface ImplFrame {
  bytes: string;
  /** One hole per exported symbol. */
  holes: { name: string; symbol: string; budgetBytes: number }[];
}

/** Every function/method/constructor stub body the compiler emits. */
const STUB_THROW_RE = /\{\s*throw new Error\(\s*(['"])stub: not implemented\1\s*\)\s*;?\s*\}/;

/**
 * Signature-and-imports-only stub frame: for every exported symbol whose
 * stub body is the compiler's canonical throwing stub, that body is replaced
 * with a named `⟨BODY:<symbol>⟩` hole. Symbols whose stub body does not match
 * the canonical throw shape (e.g. a class member the stub-fix stage widened
 * unusually) are left as-is — no hole, no fill expected.
 */
export function assembleImplFrame(feature: FeatureNode, _spec: FeatureSpec): ImplFrame {
  const holes: ImplFrame['holes'] = [];
  const fileBlocks: string[] = [];

  for (const stub of feature.stubs) {
    let content = stub.content;
    const symbols = feature.exports
      .filter(e => e.file === stub.path)
      .map(e => e.symbol);
    for (const symbol of symbols) {
      const esc = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      const declRe = new RegExp(`\\b${esc}\\b[^{]*` + STUB_THROW_RE.source);
      const m = declRe.exec(content);
      if (!m) continue;
      const marker = `⟨BODY:${symbol}⟩`;
      const spliced = m[0].replace(STUB_THROW_RE, `{ ${marker} }`);
      content = content.slice(0, m.index) + spliced + content.slice(m.index + m[0].length);
      holes.push({ name: `BODY:${symbol}`, symbol, budgetBytes: DEFAULT_IMPL_HOLE_BUDGET_BYTES });
    }
    fileBlocks.push(`--- ${stub.path} ---\n${content}`);
  }

  return { bytes: fileBlocks.join('\n\n'), holes };
}

/**
 * Replace the named hole with `body`, verbatim. Deterministic; a frame whose
 * marker was already spliced away is returned unchanged (idempotent).
 */
export function spliceHole(frame: string, holeName: string, body: string): string {
  const named = `⟨BODY:${holeName}⟩`;
  if (frame.includes(named)) return frame.split(named).join(body);
  if (frame.includes(HOLE_OPEN)) return frame.replace(HOLE_OPEN, body);
  return frame;
}

const HOLE_BODY_RE = new RegExp(`${HOLE_OPEN}\\s*([\\s\\S]*?)\\s*${HOLE_CLOSE}`);

/**
 * Extract the content of a `⟨HOLE⟩ … ⟨/END⟩` block. Returns `body: null`
 * when no well-formed block is present anywhere in the response — the
 * doctrine-95 rejection `no hole marker in response`.
 */
export function extractHole(response: string): { body: string | null; reason?: string } {
  const m = HOLE_BODY_RE.exec(response);
  if (!m) return { body: null, reason: 'no hole marker in response' };
  return { body: m[1] ?? '' };
}

const BOILERPLATE_RE = /\b(describe|it|test)\s*\(|(^|[\n;])\s*import\s/;

/**
 * `overreach|body` predicate (SET 95 doctrine): no marker → over; extracted
 * body byte length exceeding `budgetBytes * 1.5` → over; boilerplate tokens
 * (`describe(`, `it(`, `test(`, `import `) found OUTSIDE the hole → over.
 */
export function isOverreach(response: string, budgetBytes: number): { over: boolean; reason?: string } {
  const { body, reason } = extractHole(response);
  if (body === null) return { over: true, reason: reason ?? 'no hole marker in response' };

  const budget = budgetBytes * 1.5;
  if (body.length > budget) {
    return { over: true, reason: `response ${body.length} > budget ${budget}` };
  }

  const holeStart = response.indexOf(HOLE_OPEN);
  const holeEnd = response.indexOf(HOLE_CLOSE);
  const outside = holeStart >= 0 && holeEnd >= 0
    ? response.slice(0, holeStart) + response.slice(holeEnd + HOLE_CLOSE.length)
    : response;
  if (BOILERPLATE_RE.test(outside)) {
    return { over: true, reason: 'boilerplate reproduction outside hole' };
  }

  return { over: false };
}

const IMPL_BOILERPLATE_RE = /\b(describe|it|test)\s*\(/;

/**
 * SET 95d — content-level overreach check for `generateImplementation`
 * draws (build / entropy-reset). There is no `⟨HOLE⟩` marker to extract
 * against here: `generateImplementation` returns whole `GeneratedFile[]`
 * content, not a hole-only response, because widening that contract to
 * hole-body strings would break every `LlmClient` implementation's schema
 * (LocalLlmClient/ClaudeClient/MockLlm all return full file content). The
 * predicate that DOES generalize regardless of wire shape: an
 * implementation file must never contain test-scaffolding tokens
 * (`describe(`/`it(`/`test(`) — that is always a transmission overreach.
 */
export function isBuildOverreach(content: string): { over: boolean; reason?: string } {
  if (IMPL_BOILERPLATE_RE.test(content)) {
    return { over: true, reason: 'test-scaffold token in implementation file' };
  }
  return { over: false };
}
