'use strict';

// Shadow-approver self-adaptation: learn from the user's own corrections.
//
// Two behavioral signals (correlated with the recent approval_decisions log):
//   1. interrupt_after_autoapprove — the approver auto-approved via a learned
//      rule, then the user immediately interrupted (Ctrl-C). Strong signal the
//      rule is over-broad → disable it.
//   2. approve_after_escalation — the approver escalated, then the user manually
//      approved the same prompt. Signal the class is safe → record it so the
//      history-scan / maintenance loop can promote a narrow rule.
//
// Plus a periodic maintenance pass (runSelfAdaptMaintenance) that re-validates
// active AI-refinement detection rules against their stored sample and disables
// ones that no longer parse — so learned detection improves and self-heals over
// time instead of accumulating stale rules.
//
// This module is pure orchestration over an injected dbModule + refinement
// module; all timing is injectable for tests.

const DEFAULT_INTERRUPT_WINDOW_MS = 8_000;
const DEFAULT_APPROVE_WINDOW_MS = 120_000;

// SQLite datetime('now') stores UTC 'YYYY-MM-DD HH:MM:SS'. Parse to epoch ms.
function _sqliteTimeMs(value) {
  if (!value) return 0;
  const s = String(value).trim();
  const iso = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(s) ? s.replace(' ', 'T') + 'Z' : s;
  const ms = Date.parse(iso);
  return Number.isFinite(ms) ? ms : 0;
}

function _recentDecisions(dbModule, sessionId, limit = 6) {
  try {
    return dbModule.listApprovalDecisions({ sessionId, limit }) || [];
  } catch {
    return [];
  }
}

// User interrupted (Ctrl-C) right after we auto-approved via a learned rule →
// retire that rule. Returns { action: 'disabled'|'noop', ruleId?, reason? }.
function learnFromInterrupt({ sessionId, dbModule, now = Date.now(), windowMs = DEFAULT_INTERRUPT_WINDOW_MS } = {}) {
  const decisions = _recentDecisions(dbModule, sessionId, 4);
  const last = decisions[0];
  if (!last) return { action: 'noop', reason: 'no_recent_decision' };
  if (last.decision !== 'approved') return { action: 'noop', reason: 'last_not_approved' };
  if ((now - _sqliteTimeMs(last.created_at)) > windowMs) return { action: 'noop', reason: 'outside_window' };
  // Only retire when an auto-learned rule caused it; never touch user/heuristic-less decisions.
  if (last.decided_by !== 'rule' || !last.rule_id) return { action: 'noop', reason: 'not_rule_based' };
  try {
    if (typeof dbModule.toggleApprovalRule === 'function') {
      dbModule.toggleApprovalRule(last.rule_id, false);
    }
  } catch (e) {
    return { action: 'noop', reason: 'disable_failed', error: e.message };
  }
  return { action: 'disabled', ruleId: last.rule_id, label: last.command_summary || '' };
}

function _escapeRegex(s) {
  return String(s || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// User manually approved right after we escalated → learn it. When the
// escalated decision captured a command signature, we promote it into a narrow
// auto-approve rule keyed on that exact signature (so the same command class is
// auto-approved next time). We never promote blocklist hits or high-risk
// escalations, and a promoted rule keeps the escalation's risk level so the
// verifier still gets a say on medium-risk classes. Always records an
// observation for audit. Returns { action, ... }.
function learnFromApproveAfterEscalation({ sessionId, dbModule, now = Date.now(), windowMs = DEFAULT_APPROVE_WINDOW_MS } = {}) {
  const decisions = _recentDecisions(dbModule, sessionId, 6);
  const escalation = decisions.find(d =>
    d.decision === 'escalated' && (now - _sqliteTimeMs(d.created_at)) <= windowMs);
  if (!escalation) return { action: 'noop', reason: 'no_recent_escalation' };

  // Audit observation (always).
  try {
    if (typeof dbModule.addApprovalObservation === 'function') {
      dbModule.addApprovalObservation({
        sessionId,
        source: 'self-adapt',
        rawDetected: true,
        gated: false,
        policyDecision: 'user_approved_after_escalation',
        decidedBy: 'user',
        keystrokeStatus: 'manual_approve',
        redactedScreenTail: String(escalation.command_summary || escalation.tool_name || '').slice(0, 200),
      });
    }
  } catch { /* non-fatal */ }

  // Promote into a learned rule when we have a reliable signature and the
  // escalation is safe to learn from.
  const signature = escalation.command_signature || '';
  if (!signature) return { action: 'signal_recorded', escalationId: escalation.id, reason: 'no_signature' };
  if (escalation.decided_by === 'blocklist') return { action: 'signal_recorded', escalationId: escalation.id, reason: 'blocklist_not_promotable' };
  if (escalation.risk_level === 'high') return { action: 'signal_recorded', escalationId: escalation.id, reason: 'high_risk_not_promotable' };

  const label = escalation.command_summary || escalation.tool_name || 'Approved after escalation';
  const riskLevel = escalation.risk_level === 'medium' ? 'medium' : 'low';
  try {
    if (typeof dbModule.upsertApprovalRule === 'function') {
      dbModule.upsertApprovalRule({
        pattern: _escapeRegex(signature),
        label,
        description: 'Auto-learned: you approved this after CTM escalated it.',
        category: String(escalation.tool_name || '').toLowerCase().replace(/\s+/g, '-'),
        riskLevel,
        enabled: true,
        commandSignature: signature,
        autoLearnedSource: 'approved_after_escalation',
      });
    }
  } catch (e) {
    return { action: 'signal_recorded', escalationId: escalation.id, reason: 'rule_create_failed', error: e.message };
  }
  return { action: 'rule_created', escalationId: escalation.id, signature, label, riskLevel };
}

// Periodic maintenance: verify each active AI-refinement detection rule is still
// healthy (detector present, all patterns safe + compilable) and retire ones
// that have become corrupt/unsafe, so the learned-detection set self-heals and
// no broken rule shadows detection. (We can't re-parse the stored sample — it is
// redacted — so we guard rule integrity rather than re-deriving from the sample.)
// Returns { checked, disabled, kept }.
function runSelfAdaptMaintenance({ dbModule, refinement, now = Date.now() } = {}) {
  const result = { checked: 0, disabled: 0, kept: 0 };
  let rules = [];
  try {
    rules = dbModule.listApprovalAiRefinementRules
      ? dbModule.listApprovalAiRefinementRules({ status: refinement.ACTIVE_STATUS, limit: 200 })
      : [];
  } catch {
    rules = [];
  }
  for (const rule of rules || []) {
    result.checked += 1;
    const healthy = typeof refinement.isActiveRuleHealthy === 'function'
      ? refinement.isActiveRuleHealthy(rule)
      : true;
    if (!healthy && typeof dbModule.saveApprovalAiRefinementRule === 'function') {
      try {
        dbModule.saveApprovalAiRefinementRule({
          ...rule,
          fingerprint: rule.fingerprint,
          status: refinement.FAILED_STATUS || 'failed',
          validationMessage: 'self-adapt maintenance: rule patterns no longer compile/safe',
          updatedAt: now,
        });
        result.disabled += 1;
        continue;
      } catch { /* fall through to kept */ }
    }
    result.kept += 1;
  }
  return result;
}

module.exports = {
  learnFromInterrupt,
  learnFromApproveAfterEscalation,
  runSelfAdaptMaintenance,
  _sqliteTimeMs,
  DEFAULT_INTERRUPT_WINDOW_MS,
  DEFAULT_APPROVE_WINDOW_MS,
};
