'use strict';

const crypto = require('crypto');
const { callBackgroundLlm } = require('./background-llm');

const ACTIVE_STATUS = 'active';
const FAILED_STATUS = 'failed';
const CANDIDATE_STATUS = 'candidate';
const MAX_PATTERN_LENGTH = 320;
const MAX_SAMPLE_LENGTH = 2000;

const ALLOWED_MISS_TYPES = new Set([
  'detector_failure',
  'parser_failure',
  'gate_too_strict',
  'partial_snapshot_race',
  'policy_gap',
  'stale_or_false_positive',
  'unknown',
]);

function _db(options = {}) {
  return options.dbModule || require('../db');
}

function _asString(value) {
  return value == null ? '' : String(value);
}

function _normalizeSpace(value) {
  return _asString(value).replace(/\s+/g, ' ').trim();
}

function redactApprovalText(text) {
  return _asString(text)
    .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '<email>')
    .replace(/(?:sk|pk|rk|xox[baprs]|ghp|github_pat|glpat|AIza)[A-Za-z0-9_\-]{12,}/g, '<secret>')
    .replace(/\/Users\/[^/\s]+/g, '/Users/<user>')
    .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer <token>')
    .slice(-MAX_SAMPLE_LENGTH);
}

function redactTelemetryPattern(pattern) {
  return redactApprovalText(pattern)
    .replace(/\\\$\\s\*[^|)\n]+/g, '\\$\\s*<command>')
    .replace(/\$\\s\*[^|)\n]+/g, '$\\s*<command>')
    .slice(0, MAX_PATTERN_LENGTH);
}

function fingerprintMiss(miss = {}) {
  const h = crypto.createHash('sha256');
  h.update(_asString(miss.providerId || miss.provider_id || ''));
  h.update('\0');
  h.update(_asString(miss.gateReason || miss.gate_reason || miss.reason || ''));
  h.update('\0');
  h.update(_asString(miss.source || ''));
  h.update('\0');
  h.update(_normalizeSpace(redactApprovalText(miss.rawText || miss.text || '')).slice(-1200));
  return h.digest('hex').slice(0, 32);
}

function extractJsonObject(text) {
  const raw = _asString(text).trim();
  const match = raw.match(/\{[\s\S]*\}/);
  if (!match) return null;
  try { return JSON.parse(match[0]); } catch { return null; }
}

function _isSafePattern(pattern) {
  const p = _asString(pattern);
  if (!p) return true;
  if (p.length > MAX_PATTERN_LENGTH) return false;
  if (/\(\?(?:[=!]|<[=!])/.test(p)) return false;
  if (/\\[1-9]/.test(p)) return false;
  if (/\([^)]*[+*][^)]*\)[+*{]/.test(p)) return false;
  if (/(?:\.\*){3,}/.test(p)) return false;
  return true;
}

function _compilePattern(pattern, flags = 'im') {
  const p = _asString(pattern).trim();
  if (!p) return null;
  if (!_isSafePattern(p)) return null;
  try { return new RegExp(p, flags); } catch { return null; }
}

function _firstCapture(match, preferred = []) {
  if (!match) return '';
  for (const key of preferred) {
    if (match.groups && match.groups[key]) return _asString(match.groups[key]).trim();
  }
  for (let i = 1; i < match.length; i++) {
    if (match[i]) return _asString(match[i]).trim();
  }
  return '';
}

function _ruleValue(rule, camel, snake) {
  return rule?.[camel] ?? rule?.[snake] ?? '';
}

function parseWithRule(text, rule = {}) {
  const raw = _asString(text);
  if (!raw.trim() || !rule) return null;
  const detector = _compilePattern(_ruleValue(rule, 'detectorPattern', 'detector_pattern'));
  if (detector && !detector.test(raw)) return null;

  const questionPattern = _compilePattern(_ruleValue(rule, 'questionPattern', 'question_pattern'));
  if (questionPattern && !questionPattern.test(raw)) return null;

  const yesPattern = _compilePattern(_ruleValue(rule, 'yesPattern', 'yes_pattern'));
  if (yesPattern && !yesPattern.test(raw)) return null;

  const anchorPattern = _compilePattern(_ruleValue(rule, 'anchorPattern', 'anchor_pattern'));
  if (anchorPattern && !anchorPattern.test(raw)) return null;

  const warningPattern = _compilePattern(_ruleValue(rule, 'warningPattern', 'warning_pattern'));
  const warningMatch = warningPattern ? warningPattern.exec(raw) : null;
  const warning = _firstCapture(warningMatch, ['warning']);

  const commandPattern = _compilePattern(_ruleValue(rule, 'commandPattern', 'command_pattern'));
  const commandMatch = commandPattern ? commandPattern.exec(raw) : null;
  const command = _firstCapture(commandMatch, ['command', 'cmd', 'path']);
  const toolName = _asString(_ruleValue(rule, 'toolName', 'tool_name') || _firstCapture(commandMatch, ['tool'])).trim()
    || 'AI refined approval';

  if (/bash|shell|command/i.test(toolName) && !command) return null;

  const approveShortcut = _asString(_ruleValue(rule, 'approveKey', 'approve_key') || '1').trim() || '1';
  return {
    providerId: _asString(_ruleValue(rule, 'providerId', 'provider_id') || 'generic'),
    toolName,
    command: command.slice(0, 2000),
    warning: warning.slice(0, 1000),
    fullContext: raw.slice(-2000),
    hasAllowAll: false,
    approveShortcut,
    _aiRefinementFingerprint: _asString(rule.fingerprint || ''),
  };
}

function parseWithActiveRules(text, providerId, options = {}) {
  const db = _db(options);
  let rules = [];
  try {
    if (typeof db.findActiveApprovalAiRefinementRules === 'function') {
      rules = db.findActiveApprovalAiRefinementRules(providerId || '');
    } else if (typeof db.listApprovalAiRefinementRules === 'function') {
      rules = db.listApprovalAiRefinementRules({ status: ACTIVE_STATUS, providerId, limit: 50 });
    }
  } catch {
    rules = [];
  }
  for (const rule of rules || []) {
    const ctx = parseWithRule(text, rule);
    if (ctx) return ctx;
  }
  return null;
}

function normalizeProposal(raw = {}) {
  const missType = _asString(raw.missType || raw.miss_type || 'unknown').trim();
  return {
    missType: ALLOWED_MISS_TYPES.has(missType) ? missType : 'unknown',
    safeToInstall: !!(raw.safeToInstall ?? raw.safe_to_install),
    detectorPattern: _asString(raw.detectorPattern || raw.detector_pattern).trim(),
    questionPattern: _asString(raw.questionPattern || raw.question_pattern).trim(),
    yesPattern: _asString(raw.yesPattern || raw.yes_pattern).trim(),
    anchorPattern: _asString(raw.anchorPattern || raw.anchor_pattern).trim(),
    toolName: _asString(raw.toolName || raw.tool_name).trim(),
    commandPattern: _asString(raw.commandPattern || raw.command_pattern).trim(),
    warningPattern: _asString(raw.warningPattern || raw.warning_pattern).trim(),
    approveKey: _asString(raw.approveKey || raw.approve_key || '1').trim() || '1',
    confidence: Math.max(0, Math.min(1, Number(raw.confidence || 0))),
    rationale: _asString(raw.rationale || raw.reasoning).trim().slice(0, 1000),
  };
}

function validateRefinementProposal(proposal, rawText) {
  const errors = [];
  const p = normalizeProposal(proposal);
  if (!p.safeToInstall) errors.push('proposal_not_safe_to_install');
  if (p.confidence < 0.65) errors.push('low_confidence');
  if (!p.detectorPattern) errors.push('missing_detector_pattern');
  if (!p.yesPattern) errors.push('missing_yes_pattern');
  if (!p.toolName) errors.push('missing_tool_name');
  if (!p.commandPattern) errors.push('missing_command_pattern');
  for (const [name, value] of Object.entries({
    detectorPattern: p.detectorPattern,
    questionPattern: p.questionPattern,
    yesPattern: p.yesPattern,
    anchorPattern: p.anchorPattern,
    commandPattern: p.commandPattern,
    warningPattern: p.warningPattern,
  })) {
    if (!_isSafePattern(value)) errors.push(`unsafe_${name}`);
    if (value && !_compilePattern(value)) errors.push(`invalid_${name}`);
  }
  const context = errors.length ? null : parseWithRule(rawText, {
    status: ACTIVE_STATUS,
    detector_pattern: p.detectorPattern,
    question_pattern: p.questionPattern,
    yes_pattern: p.yesPattern,
    anchor_pattern: p.anchorPattern,
    tool_name: p.toolName,
    command_pattern: p.commandPattern,
    warning_pattern: p.warningPattern,
    approve_key: p.approveKey,
  });
  if (!context) errors.push('candidate_rule_did_not_parse_sample');
  return {
    ok: errors.length === 0,
    errors,
    proposal: p,
    context,
  };
}

async function proposeRefinement(miss = {}, options = {}) {
  const callModel = options.callModel || callBackgroundLlm;
  if (typeof callModel !== 'function') {
    return { ok: false, error: 'model_unavailable' };
  }
  const redacted = redactApprovalText(miss.rawText || miss.text || '');
  const prompt = `You are CTM's approval prompt refinement engine.

The deterministic terminal approval pipeline saw an approval-shaped screen but missed or rejected it before the normal approval policy could run.

Your job is NOT to decide whether the command is allowed.
Your job is to propose a narrow parser/detector refinement rule so CTM can extract a normal approval context, then the existing blocklist/policy/live-terminal gates will decide.

Hard rules:
- Only propose a rule for an active approval widget or active permission prompt.
- Never create a durable "always allow" rule.
- Never approve by policy. This rule only extracts detector/parser context.
- Prefer provider-specific text anchors over broad words like "allow" alone.
- Regexes must be JavaScript-compatible, case-insensitive safe, and under ${MAX_PATTERN_LENGTH} characters.
- commandPattern must capture the operation in a named group "command" when possible.
- yesPattern must match the one-time Yes/Allow option, not an always-allow option.
- If uncertain or stale, set safeToInstall false.

Provider: ${miss.providerId || miss.provider_id || 'unknown'}
Source: ${miss.source || 'unknown'}
Gate reason: ${miss.gateReason || miss.gate_reason || miss.reason || 'unknown'}
Raw detected: ${miss.rawDetected ? 'yes' : 'no'}
Hint detected: ${miss.hintDetected ? 'yes' : 'no'}
Parse status: ${miss.parseStatus || miss.parse_status || 'unknown'}

Redacted terminal tail:
${redacted}

Return only JSON:
{
  "missType": "detector_failure" | "parser_failure" | "gate_too_strict" | "partial_snapshot_race" | "policy_gap" | "stale_or_false_positive" | "unknown",
  "safeToInstall": true,
  "detectorPattern": "regex that proves this is the approval widget",
  "questionPattern": "optional regex for the active question",
  "yesPattern": "regex for the one-time yes/allow option",
  "anchorPattern": "optional provider/tool anchor regex",
  "toolName": "short tool label",
  "commandPattern": "regex with named capture (?<command>...) or first capture",
  "warningPattern": "optional regex with named capture (?<warning>...)",
  "approveKey": "1",
  "confidence": 0.0,
  "rationale": "one sentence"
}`;

  try {
    const response = await callModel(prompt, {
      task: 'approval-ai-refinement',
      modelTier: 'fast',
      maxTokens: 700,
      temperature: 0,
      thinking: 'disabled',
      reasoningEffort: 'low',
      timeoutMs: Number(options.modelTimeoutMs || 45000),
    });
    const parsed = extractJsonObject(response?.text || response);
    if (!parsed) return { ok: false, error: 'invalid_model_json' };
    return { ok: true, proposal: normalizeProposal(parsed), model: response?.model || '' };
  } catch (err) {
    return { ok: false, error: err?.message || String(err) };
  }
}

function _ruleRow(fingerprint, miss, proposal, validation, extra = {}) {
  return {
    fingerprint,
    providerId: miss.providerId || miss.provider_id || '',
    missType: proposal.missType,
    source: miss.source || '',
    status: validation.ok ? ACTIVE_STATUS : FAILED_STATUS,
    detectorPattern: proposal.detectorPattern,
    questionPattern: proposal.questionPattern,
    yesPattern: proposal.yesPattern,
    anchorPattern: proposal.anchorPattern,
    toolName: proposal.toolName,
    commandPattern: proposal.commandPattern,
    warningPattern: proposal.warningPattern,
    approveKey: proposal.approveKey,
    confidence: proposal.confidence,
    rationale: proposal.rationale,
    sampleRedactedTail: redactApprovalText(miss.rawText || miss.text || ''),
    validationStatus: validation.ok ? 'passed' : 'failed',
    validationMessage: validation.ok ? 'candidate rule parsed the sample' : validation.errors.join(','),
    telemetryStatus: extra.telemetryStatus || '',
    telemetryError: extra.telemetryError || '',
    lastSeenAtMs: Date.now(),
  };
}

function telemetryRulePayload(proposal = {}) {
  const p = normalizeProposal(proposal);
  return {
    miss_type: p.missType,
    tool_name: p.toolName,
    detector_pattern: redactTelemetryPattern(p.detectorPattern),
    question_pattern: redactTelemetryPattern(p.questionPattern),
    yes_pattern: redactTelemetryPattern(p.yesPattern),
    command_pattern: redactTelemetryPattern(p.commandPattern),
    warning_pattern: redactTelemetryPattern(p.warningPattern),
    approve_key: p.approveKey,
  };
}

async function _submitTelemetry(eventClient, meta) {
  if (!eventClient || typeof eventClient.submitTelemetryEvent !== 'function') {
    return { ok: false, skipped: true, reason: 'telemetry_client_unavailable' };
  }
  try {
    return await eventClient.submitTelemetryEvent('ctm_approval_ai_refinement', meta);
  } catch (err) {
    return { ok: false, error: err?.message || String(err) };
  }
}

function _warningRow(miss, fingerprint, title, message, evidence = '') {
  return {
    fingerprint,
    sessionId: miss.sessionId || miss.session_id || '',
    providerId: miss.providerId || miss.provider_id || '',
    severity: 'warning',
    status: 'open',
    title,
    message,
    evidence: evidence || redactApprovalText(miss.rawText || miss.text || ''),
  };
}

function _saveWarning(db, warning) {
  try {
    return db.saveApprovalAiRefinementWarning?.(warning) || warning;
  } catch {
    return warning;
  }
}

async function handleMiss(miss = {}, options = {}) {
  const rawText = _asString(miss.rawText || miss.text || '');
  const db = _db(options);
  const fingerprint = miss.fingerprint || fingerprintMiss({ ...miss, rawText });
  const existing = db.getApprovalAiRefinementRule?.(fingerprint);
  if (existing?.status === ACTIVE_STATUS) {
    const context = parseWithRule(rawText, existing);
    if (context) return { status: ACTIVE_STATUS, fingerprint, rule: existing, context, reused: true };
  }

  const proposalResult = await proposeRefinement({ ...miss, rawText }, options);
  if (!proposalResult.ok) {
    const warning = _saveWarning(db, _warningRow(
      miss,
      fingerprint,
      'AI approval refinement failed',
      `CTM detected a missed approval prompt, but AI refinement could not propose a rule: ${proposalResult.error || 'unknown error'}.`,
    ));
    const fallbackProposal = normalizeProposal({ missType: 'unknown', safeToInstall: false });
    const validation = { ok: false, errors: [proposalResult.error || 'proposal_failed'], proposal: fallbackProposal, context: null };
    db.saveApprovalAiRefinementRule?.(_ruleRow(fingerprint, miss, fallbackProposal, validation));
    await _submitTelemetry(options.remoteFeedbackClient || options.telemetryClient, {
      fingerprint,
      provider_id: miss.providerId || miss.provider_id || '',
      source: miss.source || '',
      miss_type: 'unknown',
      status: FAILED_STATUS,
      validation_status: 'proposal_failed',
      error: proposalResult.error || 'proposal_failed',
    });
    return { status: FAILED_STATUS, fingerprint, warning, error: proposalResult.error || 'proposal_failed' };
  }

  const validation = validateRefinementProposal(proposalResult.proposal, rawText);
  const telemetry = await _submitTelemetry(options.remoteFeedbackClient || options.telemetryClient, {
    fingerprint,
    provider_id: miss.providerId || miss.provider_id || '',
    source: miss.source || '',
    gate_reason: miss.gateReason || miss.gate_reason || miss.reason || '',
    miss_type: validation.proposal.missType,
    status: validation.ok ? ACTIVE_STATUS : FAILED_STATUS,
    validation_status: validation.ok ? 'passed' : 'failed',
    validation_message: validation.ok ? 'candidate rule parsed the sample' : validation.errors.join(','),
    confidence: validation.proposal.confidence,
    has_detector_pattern: !!validation.proposal.detectorPattern,
    has_command_pattern: !!validation.proposal.commandPattern,
    rule: telemetryRulePayload(validation.proposal),
  });
  const rule = db.saveApprovalAiRefinementRule?.(_ruleRow(fingerprint, miss, validation.proposal, validation, {
    telemetryStatus: telemetry.ok ? 'sent' : (telemetry.skipped ? 'skipped' : 'failed'),
    telemetryError: telemetry.ok ? '' : (telemetry.error || telemetry.reason || ''),
  })) || _ruleRow(fingerprint, miss, validation.proposal, validation);

  if (!validation.ok) {
    const warning = _saveWarning(db, _warningRow(
      miss,
      fingerprint,
      'AI approval refinement did not validate',
      `CTM detected a missed approval prompt, but the generated rule did not pass local validation: ${validation.errors.join(', ')}.`,
    ));
    return { status: FAILED_STATUS, fingerprint, rule, warning, validation };
  }

  return { status: ACTIVE_STATUS, fingerprint, rule, context: validation.context, validation };
}

function recordRerunFailure(miss = {}, refinement = {}, rerunResult = {}, options = {}) {
  const db = _db(options);
  const fingerprint = refinement.fingerprint || miss.fingerprint || fingerprintMiss(miss);
  const reason = rerunResult?.reason || rerunResult?.outcome || 'rerun_failed';
  const warning = _warningRow(
    miss,
    fingerprint,
    'AI approval refinement failed after install',
    `CTM installed a local AI approval parser rule, but rerunning the approval detection did not prove it worked (${reason}). The prompt needs user attention.`,
  );
  const saved = _saveWarning(db, warning);
  try {
    const existing = db.getApprovalAiRefinementRule?.(fingerprint);
    if (existing) {
      db.saveApprovalAiRefinementRule?.({
        ...existing,
        status: FAILED_STATUS,
        validationStatus: 'rerun_failed',
        validationMessage: reason,
        lastSeenAtMs: Date.now(),
      });
    }
  } catch {}
  return saved;
}

// Integrity check for an active learned rule: its detector pattern must be
// present, safe (no catastrophic backtracking / backrefs / lookaround), and
// compilable, and any other supplied pattern must compile. Used by the
// self-adapt maintenance pass to retire rules that have become corrupt/unsafe.
function isActiveRuleHealthy(rule = {}) {
  const detector = _ruleValue(rule, 'detectorPattern', 'detector_pattern');
  if (!_asString(detector).trim()) return false;
  for (const [camel, snake] of [
    ['detectorPattern', 'detector_pattern'],
    ['questionPattern', 'question_pattern'],
    ['yesPattern', 'yes_pattern'],
    ['anchorPattern', 'anchor_pattern'],
    ['commandPattern', 'command_pattern'],
    ['warningPattern', 'warning_pattern'],
  ]) {
    const p = _asString(_ruleValue(rule, camel, snake)).trim();
    if (p && !_compilePattern(p)) return false;
  }
  return true;
}

module.exports = {
  ACTIVE_STATUS,
  FAILED_STATUS,
  CANDIDATE_STATUS,
  redactApprovalText,
  redactTelemetryPattern,
  fingerprintMiss,
  extractJsonObject,
  isActiveRuleHealthy,
  parseWithRule,
  parseWithActiveRules,
  normalizeProposal,
  validateRefinementProposal,
  telemetryRulePayload,
  proposeRefinement,
  handleMiss,
  recordRerunFailure,
};
