import type { QAPair } from '../report/types.js';
import type { LLMClient } from '../llm/client.js';
import { getAllQuestionsSorted, type InterviewQuestion } from './questions.js';
import {
  INTERVIEW_SYSTEM_PROMPT,
  buildFollowUpPrompt,
  COMPLIANCE_FIELD_CHECKLIST,
  detectAdversarialClaim,
  buildAdversarialProbePrompt,
} from '../llm/prompts.js';

export interface InterviewProtocol {
  /** Get the next question to ask, or null if interview is complete */
  nextQuestion(): InterviewQuestion | null;

  /** Record an answer. Returns false if answer was skipped (greeting/repeat). */
  recordAnswer(question: InterviewQuestion, answer: string): boolean;

  /** Generate a follow-up question based on context and missing compliance fields */
  generateFollowUp(category: QAPair['category']): Promise<InterviewQuestion | null>;

  /** Get the full transcript so far */
  getTranscript(): QAPair[];

  /** Check if the interview is complete */
  isComplete(): boolean;

  /** Total number of core questions (excluding follow-ups) */
  totalCoreQuestions: number;

  /**
   * AAP-60 — set the internal follow-up counters to reflect a transcript
   * that was generated by a previous instance. Used by the stateless
   * tool-call question-planner during rehydration: without this, the
   * fresh protocol restarts both counters at zero on every
   * `submit_answer`, and the cap check inside `generateFollowUp` never
   * fires, so the planner produces follow-ups indefinitely.
   *
   * `perQuestion` keys are core-question IDs. Any ID not present
   * stays at zero.
   */
  primeFollowUpCounts(state: {
    global: number;
    perQuestion: Map<string, number>;
  }): void;
}

// ─── Greeting detection ──────────────────────────────────────────────────────

const GREETING_PATTERNS = [
  /^hi\b/i,
  /^hello\b/i,
  /^hey\b/i,
  /ready to answer/i,
  /ready for questions/i,
  /^i am ready/i,
  /^i'm ready/i,
  /let'?s begin/i,
  /let'?s start/i,
  /^greetings/i,
];

/** Detect if an answer is just a greeting with no substantive content */
export function isGreeting(answer: string): boolean {
  const trimmed = answer.trim();
  // Short answers that match greeting patterns
  if (trimmed.length > 200) return false; // Long answers aren't greetings
  return GREETING_PATTERNS.some(p => p.test(trimmed));
}

// ─── Stale / off-topic answer detection ─────────────────────────────────────

/** Topic keywords per compliance field — used to detect answers to the wrong question */
const TOPIC_SIGNALS: Record<string, RegExp[]> = {
  agentProfile: [
    /\bproject.?name\b/i, /\bowner\b/i, /\btrigger/i, /\bwhat I (do|specifically)\b/i,
  ],
  systemId: [
    /\b(→|->)\s*(REST|API|OAuth|SDK|Bot)\b/i, /\bconnect to\b/i, /\bsystems?\s+I\b/i,
  ],
  scopesRequested: [
    /\boauth\s*scop/i, /\bgoogleapis\.com\/auth\//i, /\b(readonly|readwrite|\.edit|\.send|\.admin)\b/i,
  ],
  dataSensitivity: [
    /\b(PII|financial|credentials|confidential|non.?sensitive)\b/i, /\bclassif(y|ied)\b/i,
  ],
  writeOperations: [
    /\b(→|->)\s*(Yes|No)\s*(→|->)/i, /\b(append|insert|create|delete)\s*(row|record|spreadsheet|message)/i,
    /\bvolume\/day\b/i,
  ],
  blastRadius: [
    /\b(worst.?case|single.?record|single.?user|cross.?tenant|org.?wide)\b/i,
    /\bcan it be (undone|recovered)\b/i,
  ],
  frequencyAndVolume: [
    /\b(times?\s+per|runs?\s+per|calls?\s+per|\/week|\/day)\b/i, /\bbatch\s+size\b/i,
  ],
  scopesDelta: [
    /\bnever\s+(actually\s+)?used\b/i, /\bsafely\s+(be\s+)?revoked\b/i, /\bunused\s+permission/i,
  ],
  riskAssessment: [
    /\bworst\s+realistic\s+failure\b/i, /\bwho\s+is\s+affected\b/i, /\bhow\s+bad\s+is\s+the\s+damage\b/i,
    /\bcan\s+it\s+be\s+recovered\b/i,
  ],
};

/**
 * Detect if an answer is clearly responding to a different question (stale session).
 * Returns true if the answer strongly matches a DIFFERENT question's topic
 * but has no relevance to the current question.
 *
 * Conservative: only triggers on long answers (300+ chars) that match 2+ signals
 * from a different topic AND zero signals from the current topic. Skips Q1 entirely
 * since first answers are too varied to classify reliably.
 */
export function isStaleAnswer(question: InterviewQuestion, answer: string): boolean {
  if (answer.length < 300) return false; // Only check long, detailed answers
  const currentField = question.complianceField;
  if (!currentField) return false;

  // Never flag Q1 (agentProfile) — first answers are too varied
  if (currentField === 'agentProfile') return false;

  const currentSignals = TOPIC_SIGNALS[currentField] ?? [];
  const matchesCurrent = currentSignals.some(p => p.test(answer));

  // If the answer matches the current question's topic at all, it's not stale
  if (matchesCurrent) return false;

  // Check if it strongly matches a different question's topic (need 3+ signals)
  let strongOtherMatch = false;
  for (const [field, signals] of Object.entries(TOPIC_SIGNALS)) {
    if (field === currentField) continue;
    const matches = signals.filter(p => p.test(answer)).length;
    if (matches >= 3) { strongOtherMatch = true; break; }
  }

  return strongOtherMatch;
}

// ─── Vagueness detection ─────────────────────────────────────────────────────

/** Vagueness indicators — if an answer matches these patterns, it needs a follow-up */
const VAGUE_PATTERNS = [
  /\b(the database|a database|some database)\b/i,
  /\b(read and write|read\/write|full access)\b/i,
  /\b(user data|some data|the data)\b/i,
  /\b(regularly|periodically|sometimes|occasionally|as needed|when needed)\b/i,
  /\b(could affect|might affect|may affect)\b/i,
  /\b(various|several|multiple|many|some)\s+(systems?|apis?|services?|databases?)/i,
  /\b(everything|all access|full permissions?)\b/i,
  /\bi[' ']?m not sure\b/i,
  /\bnot sure\b/i,
  /\bi don[' ']?t know\b/i,
  // Hedging language — agent describes theoretical capabilities, not actual behavior
  /\bi may (also )?(read|write|access|connect|use|modify|create|delete)\b/i,
  /\bwhen enabled\b/i,
  /\bif the task (requires|involves|needs)\b/i,
  /\bwhen (available|needed|required|the workflow)\b/i,
  /\b(can include|could include|may include)\b/i,
  /\baccess is (environment|session|task).dependent\b/i,
  /\bdepending on (the )?task\b/i,
  /\bconnectors? (are |is )?enabled\b/i,
  // Generic tool descriptions instead of specific project usage
  /\b(local workspace|active workspace|working directory)\b/i,
  /\bconnected (development )?tools\b/i,
];

/** Detect if an answer is too vague for compliance-grade reporting */
export function isVagueAnswer(answer: string): boolean {
  return VAGUE_PATTERNS.some(p => p.test(answer));
}

// ─── Repeated answer detection ───────────────────────────────────────────────

/** Normalize answer text for comparison (trim, lowercase, collapse whitespace) */
function normalizeForComparison(text: string): string {
  return text.trim().toLowerCase().replace(/\s+/g, ' ');
}

/** Check if an answer is a repeat of a previous answer */
function isRepeatedAnswer(answer: string, transcript: QAPair[]): boolean {
  if (transcript.length === 0) return false;
  const normalized = normalizeForComparison(answer);
  // Check for exact or near-exact repeats
  return transcript.some(qa => {
    const prevNormalized = normalizeForComparison(qa.answer);
    // Exact match or >90% overlap (handles minor variations)
    return normalized === prevNormalized ||
      (normalized.length > 50 && prevNormalized.includes(normalized.slice(0, normalized.length * 0.9 | 0)));
  });
}

// ─── Missing compliance fields ───────────────────────────────────────────────

/** Check which compliance fields are missing from the transcript for a given category */
function findMissingFields(transcript: QAPair[]): string[] {
  const allText = transcript.map(qa => qa.answer.toLowerCase()).join(' ');
  const missing: string[] = [];

  // Check for specific system identifiers
  if (!/\b(api|oauth|sdk|via|using)\b/i.test(allText)) {
    missing.push('systemId (specific API names, auth methods)');
  }

  // Check for specific scopes
  if (!/\b(scope|permission|role|\.readonly|\.send|\.modify|\.admin)\b/i.test(allText)) {
    missing.push('scopesRequested (specific OAuth scopes, API permissions)');
  }

  // Check for data sensitivity classification
  if (!/\b(pii|sensitive|confidential|financial|personal|classified)\b/i.test(allText)) {
    missing.push('dataSensitivity (PII, financial, confidential classification)');
  }

  // Check for blast radius
  if (!/\b(single.?record|single.?user|team|org.?wide|cross.?tenant|mailbox|affected)\b/i.test(allText)) {
    missing.push('blastRadius (scope of impact: single-record/user/team/org-wide)');
  }

  // Check for frequency numbers
  if (!/\b(\d+\s*(times?|per|\/)\s*(day|hour|minute|week|session)|batch)\b/i.test(allText)) {
    missing.push('frequencyAndVolume (specific numbers: times/day, batch size)');
  }

  // Check for write reversibility
  if (!/\b(revers|rollback|undo|irrevers|cannot be undone|can be restored)\b/i.test(allText)) {
    missing.push('writeOperations.reversible (whether writes can be rolled back)');
  }

  return missing;
}

// ─── Protocol factory ────────────────────────────────────────────────────────

export function createProtocol(llmClient: LLMClient, maxFollowUps = 6): InterviewProtocol {
  const coreQuestions = getAllQuestionsSorted();
  let currentIndex = 0;
  const transcript: QAPair[] = [];
  let globalFollowUpCount = 0;
  const followUpCountPerQuestion = new Map<string, number>();
  // AAP-43 P3 #10/#11: limit adversarial probes per session so the
  // conversation doesn't devolve into interrogation.
  let adversarialProbeCount = 0;
  const MAX_ADVERSARIAL_PROBES = 2;
  const probedClaimKinds = new Set<string>();
  let repeatedAnswerCount = 0;

  // Follow-up queue
  const followUpQueue: InterviewQuestion[] = [];

  return {
    totalCoreQuestions: coreQuestions.length,

    nextQuestion(): InterviewQuestion | null {
      // Drain follow-up queue first
      if (followUpQueue.length > 0) {
        return followUpQueue.shift()!;
      }

      if (currentIndex >= coreQuestions.length) {
        return null;
      }

      return coreQuestions[currentIndex++];
    },

    recordAnswer(question: InterviewQuestion, answer: string): boolean {
      // Skip greetings — don't record them as answers
      if (transcript.length === 0 && isGreeting(answer)) {
        // Don't rewind currentIndex — SessionManager will re-ask pendingQuestion directly
        return false;
      }

      // Detect stale answers from a lost/different session
      if (isStaleAnswer(question, answer)) {
        // Don't rewind — SessionManager will re-ask pendingQuestion directly
        return false;
      }

      // Detect repeated/canned answers
      if (isRepeatedAnswer(answer, transcript)) {
        repeatedAnswerCount++;
        // Still record it (for transparency in transcript) but mark the category
        transcript.push({
          question: question.text,
          answer: `[REPEATED RESPONSE] ${answer}`,
          category: question.category,
        });
        // After 3+ repeats, stop generating follow-ups — agent is stuck
        return true;
      }

      transcript.push({
        question: question.text,
        answer,
        category: question.category,
      });
      return true;
    },

    async generateFollowUp(category: QAPair['category']): Promise<InterviewQuestion | null> {
      // Global cap
      if (globalFollowUpCount >= maxFollowUps) return null;

      // Per-question cap (2 follow-ups per core question)
      const lastCoreQ = coreQuestions.find(q =>
        q.category === category && transcript.some(t => t.question === q.text)
      );
      if (lastCoreQ) {
        const count = followUpCountPerQuestion.get(lastCoreQ.id) ?? 0;
        if (count >= 2) return null;
      }

      // Don't follow up if agent is repeating canned responses
      if (repeatedAnswerCount >= 3) return null;

      const categoryQA = transcript.filter(qa => qa.category === category);
      if (categoryQA.length === 0) return null;

      // Check if the last answer was vague
      const lastAnswer = categoryQA[categoryQA.length - 1].answer;
      const vague = isVagueAnswer(lastAnswer);

      // Find missing compliance fields across all transcript
      const missingFields = findMissingFields(transcript);

      // AAP-43 P3: scan recent answers for adversarial-claim opportunities
      // (HITL, narrow-scope claim, monitored, deletion policy, etc.). If
      // one is detected and we haven't exhausted the per-session budget,
      // prefer an adversarial probe over a generic vagueness follow-up.
      const recentText = transcript.slice(-4).map((qa) => qa.answer).join(' \n ');
      const adversarialClaim =
        adversarialProbeCount < MAX_ADVERSARIAL_PROBES
          ? detectAdversarialClaim(recentText)
          : null;
      const isNewClaim = adversarialClaim !== null && !probedClaimKinds.has(adversarialClaim.kind);

      // Only generate follow-up if answer was vague, fields are missing,
      // or a new adversarial claim was detected
      if (!vague && missingFields.length === 0 && !isNewClaim) return null;

      try {
        const prompt =
          isNewClaim && adversarialClaim
            ? buildAdversarialProbePrompt(adversarialClaim.kind, adversarialClaim.probe, categoryQA)
            : buildFollowUpPrompt(category, categoryQA, missingFields.length > 0 ? missingFields : undefined);

        const followUpText = await llmClient.chat(INTERVIEW_SYSTEM_PROMPT, prompt);

        if (!followUpText.trim()) return null;

        globalFollowUpCount++;
        if (lastCoreQ) {
          followUpCountPerQuestion.set(lastCoreQ.id, (followUpCountPerQuestion.get(lastCoreQ.id) ?? 0) + 1);
        }
        if (isNewClaim && adversarialClaim) {
          adversarialProbeCount++;
          probedClaimKinds.add(adversarialClaim.kind);
        }

        const idPrefix = isNewClaim && adversarialClaim ? `probe_${adversarialClaim.kind}` : `followup_${category}`;
        const followUp: InterviewQuestion = {
          id: `${idPrefix}_${globalFollowUpCount}`,
          category,
          text: followUpText.trim(),
          priority: 100 + globalFollowUpCount,
        };
        return followUp;
      } catch {
        return null;
      }
    },

    getTranscript(): QAPair[] {
      return [...transcript];
    },

    isComplete(): boolean {
      return currentIndex >= coreQuestions.length && followUpQueue.length === 0;
    },

    primeFollowUpCounts(state: { global: number; perQuestion: Map<string, number> }): void {
      // Clamp negative inputs to zero and trust the caller's count
      // otherwise. The protocol's cap checks (`globalFollowUpCount >=
      // maxFollowUps`, `count >= 2`) use `>=`, so honest counts work
      // directly and over-counts are still safe (just slightly more
      // conservative).
      globalFollowUpCount = Math.max(0, state.global | 0);
      for (const [qid, count] of state.perQuestion) {
        followUpCountPerQuestion.set(qid, Math.max(0, count | 0));
      }
    },
  };
}

/**
 * Enqueue a follow-up question to be asked next.
 * Used by SessionManager to avoid monkey-patching protocol.nextQuestion.
 */
export function enqueueFollowUp(protocol: InterviewProtocol, followUp: InterviewQuestion): void {
  // The follow-up queue is internal to createProtocol, so we need another approach.
  // Instead, we expose a method to push into the queue via the protocol's nextQuestion behavior.
  // Actually, the clean way is to just have SessionManager track its own queue — see sessions.ts.
  // This function exists for the scan/CLI path where the interviewer handles follow-ups inline.
  void protocol;
  void followUp;
}
