/** Helpers for scriba-engine contract (SSE logs, conversion failure UX, accounting). NEXT doc §§2–5. */

const VENDOR_LEAK_RE =
  /\b(openai|anthropic|google|gpt-?\d*|claude|gemini|chatgpt)\b|provider\s*:\s*\S+/gi;

/**
 * NEVER show LLM vendor names in streamed logs (privacy contract §1).
 * If the backend leaked a term, scrub before rendering.
 */
export function sanitizeEngineLogForDisplay(message: string): string {
  if (!message || typeof message !== 'string') return '';
  const cleaned = message.replace(VENDOR_LEAK_RE, '—').replace(/\s{2,}/g, ' ').trim();
  if (cleaned.length === 0) return 'Engine activity (detail omitted for privacy).';
  return cleaned;
}

/** §4.3 — all log categories (FRONTEND-integration §4.3). */
export type EngineLogCategory =
  | 'preflight'
  | 'analysis'
  | 'translate'
  | 'compile'
  | 'sanitize'
  | 'scaffold'
  | 'validate'
  | 'repair'
  | 'quality'
  | 'decisions'
  | 'stage'
  | 'idiom'
  | 'antipattern'
  | 'property'
  | 'structural'
  | 'signing'
  | 'timestamp'
  | 'accounting'
  | 'test-gen'
  | 'info'
  | 'generic';

/** §4.3 — categorize stream `log.message` values for badges / grouping. */
export function categorizeEngineLogMessage(message: string): EngineLogCategory {
  if (!message || typeof message !== 'string') return 'generic';
  const m = message.toLowerCase();
  if (/(preflight|size limit|loc exceeded|too many files)/i.test(message)) return 'preflight';
  if (m.includes('analysis') || m.includes('repository')) return 'analysis';
  if (m.includes('compile-check') || m.includes('compiler') || /(compile check|sandbox)/i.test(message)) return 'compile';
  if (/sanitized/i.test(message)) return 'sanitize';
  if (/(build file|scaffolding)/i.test(message)) return 'scaffold';
  if (m.includes('validation') || m.includes('parity')) return 'validate';
  if (/repair loop/i.test(message)) return 'repair';
  if (m.includes('quality gate') || m.includes('quality index') || /(quality (index|gate)|marker gate)/i.test(message)) return 'quality';
  if (/decision journal/i.test(message) || m.includes('decision')) return 'decisions';
  if (/idioms?:/i.test(message)) return 'idiom';
  if (/anti-patterns/i.test(message)) return 'antipattern';
  if (/property checks/i.test(message)) return 'property';
  if (/structural validation/i.test(message)) return 'structural';
  if (/(bundle signed|signing (failed|module)|signing not configured)/i.test(message)) return 'signing';
  if (/(timestamp|tsa)/i.test(message)) return 'timestamp';
  if (/accounting|cost/i.test(message)) return 'accounting';
  if (/generated \d+ test/i.test(message)) return 'test-gen';
  if (
    m.includes('translation') ||
    m.includes('translating') ||
    m.includes('transient backend') ||
    /(staged translation|bodiesstage|polishstage|using multi-stage|structure → bodies)/i.test(message)
  ) {
    return m.includes('multi-stage') || m.includes('bodiesstage') ? 'stage' : 'translate';
  }
  return 'info';
}

/** §6 — error category for UX-specific error banners. */
export type ConversionErrorCategory =
  | 'preflight'
  | 'size-guard'
  | 'quality-gate'
  | 'runtime'
  | 'cancelled'
  | 'validation'
  | null;

export function categorizeConversionErrors(errs: string[]): ConversionErrorCategory {
  if (!Array.isArray(errs) || errs.length === 0) return null;
  if (errs.some(e => /Preflight grammar check/i.test(e))) return 'preflight';
  if (errs.some(e => /(size limit|loc exceeded)/i.test(e))) return 'size-guard';
  if (errs.some(e => /Quality gate failed/i.test(e))) return 'quality-gate';
  if (errs.some(e => /Cancelled/i.test(e))) return 'cancelled';
  if (errs.some(e => /Validation failed/i.test(e))) return 'validation';
  if (errs.length > 0) return 'runtime';
  return null;
}

export type ConversionFailureKind =
  | 'preflight'
  | 'compile'
  | 'validation'
  | 'quality-gate'
  | 'idiomatic-gate'
  | 'runtime'
  | 'user-cancelled'
  | 'unknown';

/**
 * SSE log lines from the engine that carry structured failure semantics.
 * Used when `metadata.errors` is empty but the stream already surfaced the reason.
 */
export const STRUCTURAL_FAILURE_LINE_RE =
  /\b(?:quality\s+gate\s+failed|validation\s+parity\s+below\s+threshold|compile-check\s+failed|idiomatic\s+gate\s+failed|unsupported\s+language\s+pair|source\s+size\s+exceeds|too\s+many\s+files|rate\s+limit\s+exceeded)\b/i;

export function isStructuralFailureEngineLogLine(message: string): boolean {
  return typeof message === 'string' && STRUCTURAL_FAILURE_LINE_RE.test(message.trim());
}

/** Prefer the latest ERROR-level structural line; else the latest matching line at any level. */
export function inferPrimaryFailureFromLogs(
  logs: ReadonlyArray<{ level?: string; message?: string }>,
): string | undefined {
  let fallback: string | undefined;
  for (let i = logs.length - 1; i >= 0; i--) {
    const entry = logs[i];
    const msg = typeof entry?.message === 'string' ? entry.message.trim() : '';
    if (!msg || !STRUCTURAL_FAILURE_LINE_RE.test(msg)) continue;
    if (entry?.level === 'error') return msg;
    if (!fallback) fallback = msg;
  }
  return fallback;
}

/** Prefer `metadata.errors[0]`; map to UX-friendly copy (NEXT §5.4). */
export function classifyConversionFailure(primaryError: string | undefined): {
  kind: ConversionFailureKind;
  title: string;
  detail: string;
} {
  const raw = typeof primaryError === 'string' ? primaryError.trim() : '';
  if (!raw) {
    return {
      kind: 'unknown',
      title: 'Conversion failed',
      detail: 'No error details were returned. Retry or contact support.',
    };
  }

  const lower = raw.toLowerCase();

  if (/^cancelled\b|cancellation|cancelled by user/i.test(raw)) {
    return {
      kind: 'user-cancelled',
      title: 'Conversion cancelled',
      detail: 'The run was cancelled before completion.',
    };
  }

  if (/unsupported language pair/i.test(lower)) {
    return {
      kind: 'preflight',
      title: 'Language pair not supported',
      detail: raw,
    };
  }

  if (/source size exceeds|too many files/i.test(lower)) {
    return {
      kind: 'preflight',
      title: 'Repository too large',
      detail: raw,
    };
  }

  if (
    /preflight|does not look like|grammar check failed|encoding|binary content/i.test(raw) ||
    /size limit|files,\s*limit|loc exceeded/i.test(lower)
  ) {
    let title = 'Source check failed';
    let detail = raw;
    if (/size limit|loc exceeded/i.test(lower)) {
      title = 'Project exceeds limits';
      detail =
        'This migration exceeds the configured file or line-of-code limits. Try narrower include patterns or contact your team.';
    } else if (/does not look like|grammar/i.test(lower)) {
      title = 'Source may not match the selected language';
      detail = raw;
    }
    return { kind: 'preflight', title, detail };
  }

  if (/validation parity below threshold/i.test(lower)) {
    return {
      kind: 'validation',
      title: 'Quality threshold not reached',
      detail: raw,
    };
  }

  if (/idiomatic gate failed/i.test(lower)) {
    return {
      kind: 'idiomatic-gate',
      title: 'Idiomatic gate failed',
      detail: raw,
    };
  }

  if (/quality gate failed|below required|threshold/i.test(lower) && /quality/i.test(lower)) {
    return {
      kind: 'quality-gate',
      title: 'Blocked by quality gate',
      detail: raw,
    };
  }

  if (/compile-check failed/i.test(lower) || (/compile|javac|tsc|pyright|mypy|sandbox/i.test(lower) && /fail|error/i.test(lower))) {
    return {
      kind: 'compile',
      title: 'Compilation check failed',
      detail: raw,
    };
  }

  return {
    kind: 'runtime',
    title: 'Conversion error',
    detail: raw,
  };
}

export type AccountingStage =
  | 'analyzer'
  | 'translator'
  | 'validator'
  | 'structure'
  | 'bodies'
  | 'repair';

export type AccountingSnapshot = {
  totals: {
    calls: number;
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
    costUsd: number;
  };
  byStage: Array<{
    stage: AccountingStage | string;
    calls: number;
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
    costUsd: number;
  }>;
};

export function isAccountingSnapshot(v: unknown): v is AccountingSnapshot {
  if (!v || typeof v !== 'object') return false;
  const o = v as Record<string, unknown>;
  const totals = o.totals;
  const byStage = o.byStage;
  if (!totals || typeof totals !== 'object' || !Array.isArray(byStage)) return false;
  return typeof (totals as { costUsd?: unknown }).costUsd === 'number';
}

/** §2.1 UI labels — per stage, never per provider. */
export function labelAccountingStage(stage: string): string {
  const labels: Record<string, string> = {
    analyzer: 'Analysis',
    translator: 'Translation',
    validator: 'Validation',
    structure: 'Translation (structure)',
    bodies: 'Translation (bodies)',
    repair: 'Repair',
  };
  return labels[stage] ?? stage;
}

/** Per-file journals + heuristic aggregate locations (NEXT §3). */
export function buildMigrationDecisionPaths(engineOutputPaths: string[]): {
  sidecars: string[];
  aggregateCandidates: string[];
} {
  const norm = engineOutputPaths
    .filter((p): p is string => typeof p === 'string' && p.length > 0)
    .map((p) => p.replace(/\\/g, '/'));

  const sidecars = [...new Set(norm.map((p) => `${p}.scriba-decisions.md`))];

  const aggregateCandidatesSet = new Set<string>();
  for (const p of norm) {
    const d = p.replace(/\/[^/]+$/, '');
    if (d && d !== p) aggregateCandidatesSet.add(`${d}/MIGRATION-DECISIONS.md`);
    const d2 = d.replace(/\/[^/]+$/, '');
    if (d2 && d2 !== d) aggregateCandidatesSet.add(`${d2}/MIGRATION-DECISIONS.md`);
    const d3 = d2.replace(/\/[^/]+$/, '');
    if (d3 && d3 !== d2) aggregateCandidatesSet.add(`${d3}/MIGRATION-DECISIONS.md`);
  }
  return { sidecars, aggregateCandidates: [...aggregateCandidatesSet] };
}
