/**
 * Deterministic stub scaffolding — the scaffolding principle (harness owns
 * structure, model fills bounded holes) applied to interface header files,
 * mirroring what irScaffold does for test suites.
 *
 * The spec's exports contract already names every symbol each file must
 * export, so the model's job shrinks to per-symbol SIGNATURES. Assembly is
 * the harness's job, and an assembled stub ALWAYS parses: any model-supplied
 * signature that introduces a parse error is replaced by a compilable
 * fallback for that symbol alone — throwing bodies for values, empty-but-
 * growable shapes for types (an empty interface lets the existing additive
 * contract-widening machinery grow it from suite evidence, where a permissive
 * index signature would mask the contract instead). Syntax-class stub errors
 * become impossible by construction; fixStub remains only for semantic
 * signature repair.
 */
import type { FeatureNode, GeneratedFile } from './types';

export interface StubImport {
  /** Sibling stub module, e.g. "./article_model" (extensionless, same dir). */
  from: string;
  symbols: string[];
}

export interface StubSignature {
  symbol: string;
  kind: 'function' | 'class' | 'interface' | 'type' | 'const';
  /**
   * Declaration payload after the symbol name, per kind:
   *   function  → "(id: string, title: string): Article"
   *   class     → "{ create(id: string): Article; count: number }"
   *   interface → "{ id: string; title: string }"
   *   type      → "'draft' | 'published'"   (right-hand side)
   *   const     → "number"                  (type annotation)
   */
  sig?: string;
}

export interface WireStub {
  path: string;
  imports?: StubImport[];
  signatures: StubSignature[];
}

/** A feature as it arrives on the wire: stubs are signatures, not content. */
export type WireFeature = Omit<FeatureNode, 'stubs'> & { stubs: WireStub[] };

/** JSON-schema fragment for the stubs field — spliced into both clients' FEATURE_SCHEMAs. */
export const STUB_WIRE_SCHEMA = {
  type: 'array',
  items: {
    type: 'object',
    additionalProperties: false,
    required: ['path', 'imports', 'signatures'],
    properties: {
      path: { type: 'string' },
      imports: {
        type: 'array',
        items: {
          type: 'object',
          additionalProperties: false,
          required: ['from', 'symbols'],
          properties: {
            from: { type: 'string' },
            symbols: { type: 'array', items: { type: 'string' } },
          },
        },
      },
      signatures: {
        type: 'array',
        items: {
          type: 'object',
          additionalProperties: false,
          required: ['symbol', 'kind', 'sig'],
          properties: {
            symbol: { type: 'string' },
            kind: { type: 'string', enum: ['function', 'class', 'interface', 'type', 'const'] },
            sig: { type: 'string' },
          },
        },
      },
    },
  },
} as const;

const THROW = `{ throw new Error('stub: not implemented'); }`;
const IDENT = /^[A-Za-z_$][\w$]*$/;

/**
 * True iff parens/braces/brackets balance across the snippet (comments and
 * string/template literals skipped). This is deliberately NOT a full parse —
 * stubScaffold's callers only ever check one rendered declaration or import
 * line at a time, where bracket balance is what a malformed model signature
 * actually breaks. Keeping this dependency-free (no `typescript` import) lets
 * the module bundle into browser contexts (the Tier 1 extension runner) where
 * the Node-targeting compiler package cannot load.
 */
export function parsesClean(src: string): boolean {
  let parens = 0;
  let braces = 0;
  let brackets = 0;
  for (let i = 0; i < src.length; i++) {
    const c = src[i];
    if (c === '/' && src[i + 1] === '/') {
      while (i < src.length && src[i] !== '\n') i++;
    } else if (c === '/' && src[i + 1] === '*') {
      i += 2;
      while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i++;
      i++;
    } else if (c === "'" || c === '"' || c === '`') {
      const quote = c;
      i++;
      while (i < src.length && src[i] !== quote) {
        if (src[i] === '\\') i++;
        i++;
      }
    } else if (c === '(') parens++;
    else if (c === ')') { if (--parens < 0) return false; }
    else if (c === '{') braces++;
    else if (c === '}') { if (--braces < 0) return false; }
    else if (c === '[') brackets++;
    else if (c === ']') { if (--brackets < 0) return false; }
  }
  return parens === 0 && braces === 0 && brackets === 0;
}

/** Always-compilable per-symbol fallback (growable for types, throwing for values). */
export function fallbackDecl(symbol: string, kind: StubSignature['kind']): string {
  switch (kind) {
    case 'class': return `export class ${symbol} { constructor(...args: unknown[]) ${THROW} }`;
    case 'interface': return `export interface ${symbol} {}`;
    case 'type': return `export type ${symbol} = unknown;`;
    case 'const': return `export const ${symbol}: unknown = undefined;`;
    default: return `export function ${symbol}(...args: unknown[]): never ${THROW}`;
  }
}

/** Split a class-body sig "{ a(x: T): U; b: V }" into member strings (top-level ';' only). */
function splitMembers(body: string): string[] {
  const inner = body.trim().replace(/^\{/, '').replace(/\}$/, '');
  const out: string[] = [];
  let depth = 0;
  let cur = '';
  for (const ch of inner) {
    if ('({<['.includes(ch)) depth++;
    else if (')}>]'.includes(ch)) depth--;
    if (ch === ';' && depth === 0) {
      if (cur.trim()) out.push(cur.trim());
      cur = '';
    } else cur += ch;
  }
  if (cur.trim()) out.push(cur.trim());
  return out;
}

/**
 * Models routinely restate context inside the sig payload — the symbol name
 * itself ("lockThread(…)" for symbol lockThread), or keywords ("export
 * function …"). Naive concatenation then mints doubled names
 * (lockThreadlockThread) that typecheck but are wrong. Strip every leading
 * restatement deterministically before assembly.
 */
export function sanitizeSig(symbol: string, sig: string): string {
  let out = sig.trim();
  const esc = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const leading = [
    /^export\s+/i,
    /^declare\s+/i,
    /^async\s+/i,
    /^(?:function|class|interface|type|const)\s+/i,
    new RegExp(`^${esc}\\s*`),
  ];
  let changed = true;
  while (changed) {
    changed = false;
    for (const re of leading) {
      const next = out.replace(re, '');
      if (next !== out) {
        out = next.trim();
        changed = true;
      }
    }
  }
  return out;
}

function renderDecl(s: StubSignature): string {
  if (!IDENT.test(s.symbol)) return ''; // dropped; caller substitutes nothing
  const sig = sanitizeSig(s.symbol, s.sig ?? '');
  if (!sig) return fallbackDecl(s.symbol, s.kind);
  switch (s.kind) {
    case 'function':
      return `export function ${s.symbol}${sig} ${THROW}`;
    case 'class': {
      const members = splitMembers(sig).map(m =>
        // A parameter list means a method → throwing body; otherwise a
        // property → `declare` needs no initializer under strict mode.
        /\(/.test(m.split(':')[0] ?? m) ? `  ${m} ${THROW}` : `  declare ${m};`,
      );
      return `export class ${s.symbol} {\n${members.join('\n')}\n}`;
    }
    case 'interface':
      return `export interface ${s.symbol} ${sig.startsWith('{') ? sig : `{ ${sig} }`}`;
    case 'type':
      return `export type ${s.symbol} = ${sig.replace(/^=\s*/, '').replace(/;$/, '')};`;
    case 'const':
      return `export const ${s.symbol}: ${sig.replace(/;$/, '')} = undefined as never;`;
  }
}

function renderImports(imports: StubImport[] | undefined): string[] {
  const lines: string[] = [];
  for (const imp of imports ?? []) {
    const syms = (imp.symbols ?? []).filter(s => IDENT.test(s.trim())).map(s => s.trim());
    if (!syms.length) continue;
    let from = String(imp.from ?? '').trim().replace(/\.ts$/, '');
    if (!from) continue;
    // Normalize to a same-dir relative module: "src/foo" | "foo" → "./foo".
    from = from.replace(/^src\//, './');
    if (!from.startsWith('.')) from = `./${from}`;
    lines.push(`import type { ${[...new Set(syms)].join(', ')} } from '${from}';`);
  }
  return lines;
}

/**
 * Assemble one stub file. Every contracted symbol is guaranteed an export;
 * every emitted file is guaranteed to parse.
 */
export function assembleStubFile(
  featureId: string,
  filePath: string,
  contractedSymbols: string[],
  wire?: WireStub,
): string {
  const header = `/** Interface stub for ${filePath} — ${featureId}. Harness-assembled; bodies throw until built. */`;
  const importLines = renderImports(wire?.imports).filter(l => parsesClean(l));

  const seen = new Set<string>();
  const decls: string[] = [];
  for (const s of wire?.signatures ?? []) {
    const sym = String(s.symbol ?? '').trim();
    if (!IDENT.test(sym) || seen.has(sym)) continue;
    seen.add(sym);
    const rendered = renderDecl({ ...s, symbol: sym });
    decls.push(rendered && parsesClean(rendered) ? rendered : fallbackDecl(sym, s.kind ?? 'function'));
  }
  // Contract completeness: every symbol dependents import MUST exist.
  for (const sym of contractedSymbols) {
    if (!IDENT.test(sym) || seen.has(sym)) continue;
    seen.add(sym);
    decls.push(fallbackDecl(sym, 'function'));
  }
  if (!decls.length) decls.push('export {};');

  let content = [header, ...importLines, '', ...decls, ''].join('\n');
  if (!parsesClean(content)) {
    // Imports were individually clean and declarations are parse-checked or
    // fallbacks, so this is unreachable in practice — but the guarantee is
    // absolute: degrade to all-fallback, which always parses.
    content = [
      header,
      ...[...seen].map(sym => fallbackDecl(sym, 'function')),
      '',
    ].join('\n');
  }
  return content;
}

/**
 * Turn a wire feature's signature stubs into on-disk stub files ({path, content}).
 * Every owned file gets a stub (spec validation requires it); wire stubs for
 * files the feature does not own are dropped (ownership is disjoint).
 */
export function assembleFeatureStubs(
  feature: Pick<WireFeature, 'id' | 'files' | 'exports'>,
  wire: WireStub[],
): GeneratedFile[] {
  const norm = (p: string) => p.replace(/\\/g, '/').replace(/^\.\//, '');
  const byPath = new Map<string, WireStub>();
  for (const w of wire ?? []) {
    if (w?.path) byPath.set(norm(w.path), w);
  }
  return (feature.files ?? []).map(file => {
    const p = norm(file);
    const contracted = (feature.exports ?? []).filter(e => norm(e.file) === p).map(e => e.symbol);
    return { path: p, content: assembleStubFile(feature.id, p, contracted, byPath.get(p)) };
  });
}

/** Map wire features (signature stubs) → internal FeatureNodes (content stubs). */
export function assembleSpecStubs(features: WireFeature[]): FeatureNode[] {
  return (features ?? []).map(f => ({
    ...f,
    stubs: assembleFeatureStubs(f, f.stubs ?? []),
  }));
}
