/**
 * Behavioral Rule Extractor — detects correction and confirmation patterns
 * in user messages and extracts structured rules.
 */

import type { Message } from '../../types/message.js'
import { logForDebugging } from '../../utils/debug.js'
import { addRule } from './rules.js'
import type { BehaviorRule } from './types.js'

const MAX_MESSAGE_LEN = 2000
const MAX_CAPTURE_LEN = 200
const MIN_CAPTURE_LEN = 3

// Characters that terminate a capture. Stops `.+` greedy swallowing of
// paragraphs, quoted fragments, code, and markup.
const TERMINATOR_CLASS = `[^.!?\\n\\r"\`<>{}\\\\]`
const BODY = `${TERMINATOR_CLASS}{${MIN_CAPTURE_LEN},${MAX_CAPTURE_LEN}}`

type CorrectionMatcher = {
  pattern: RegExp
  toAction: (m: RegExpMatchArray) => string
}

// Correction patterns with explicit polarity-preserving action builders.
// The action string stores the full corrective phrasing (`don't X`, `never X`)
// so the rule reads correctly when injected back into the prompt.
const CORRECTION_PATTERNS: CorrectionMatcher[] = [
  {
    pattern: new RegExp(`(?:no,?\\s*)?don'?t\\s+(${BODY})`, 'i'),
    toAction: (m) => `don't ${m[1].trim()}`,
  },
  {
    pattern: new RegExp(`(?:no,?\\s*)?never\\s+(${BODY})`, 'i'),
    toAction: (m) => `never ${m[1].trim()}`,
  },
  {
    pattern: new RegExp(`stop\\s+(?:doing\\s+)?(${BODY})`, 'i'),
    toAction: (m) => `don't ${m[1].trim()}`,
  },
  {
    pattern: new RegExp(`(?:please\\s+)?use\\s+(${BODY})\\s+instead\\s+of\\s+(${BODY})`, 'i'),
    toAction: (m) => `use ${m[1].trim()} instead of ${m[2].trim()}`,
  },
  {
    pattern: new RegExp(
      `(?:please\\s+)?prefer\\s+(${BODY})\\s+(?:over|instead\\s+of)\\s+(${BODY})`,
      'i',
    ),
    toAction: (m) => `prefer ${m[1].trim()} over ${m[2].trim()}`,
  },
  {
    pattern: new RegExp(`always\\s+(?:make\\s+sure\\s+to\\s+)?(${BODY})`, 'i'),
    toAction: (m) => `always ${m[1].trim()}`,
  },
]

// Patterns that indicate user confirmation of an approach.
const CONFIRMATION_PATTERNS = [
  /(?:yes|yeah|yep),?\s+(?:exactly|perfect|that'?s?\s+(?:right|correct|it))/i,
  /(?:good|great|nice)\s+(?:approach|choice|decision|call)/i,
  /keep\s+(?:doing|using)\s+(?:that|this)/i,
]

// Lower initial confidence so a fresh correction needs at least one
// reinforcement before it crosses the injection threshold. See
// rules.ts#MIN_INJECTION_CONFIDENCE.
const INITIAL_CORRECTION_CONFIDENCE = 0.4
const INITIAL_CONFIRMATION_CONFIDENCE = 0.3

export type RuleCandidate = Pick<
  BehaviorRule,
  'trigger' | 'action' | 'context' | 'confidence' | 'source'
>

/**
 * Reject messages that look like pasted content rather than a direct
 * correction typed by the user. These tend to contain code fences,
 * XML-ish tags, JSON-escaped sequences, or exceed a conversational
 * length — all strong indicators the extractor will over-capture.
 */
function looksLikePaste(text: string): boolean {
  if (text.length > MAX_MESSAGE_LEN) return true
  if (/\\n|\\"/.test(text)) return true // JSON-escaped content
  if (/```|~~~/.test(text)) return true // code fences
  if (/<[A-Z_][A-Z_\-]*>/i.test(text)) return true // tags like <EXTREMELY_IMPORTANT>
  return false
}

/**
 * Pure extraction: scan a single text blob for correction/confirmation
 * patterns and return candidate rules. Does not touch disk.
 */
export function findRuleCandidates(
  text: string,
  opts: { context?: string; trigger?: string; approach?: string } = {},
): RuleCandidate[] {
  const out: RuleCandidate[] = []
  if (!text || text.length < 10) return out
  if (looksLikePaste(text)) return out

  const trigger = opts.trigger ?? 'when working in this project'
  const context = opts.context ?? 'global'

  for (const { pattern, toAction } of CORRECTION_PATTERNS) {
    const match = text.match(pattern)
    if (!match) continue
    const action = toAction(match).trim()
    if (action.length < 5 || action.length > MAX_CAPTURE_LEN) continue
    out.push({
      trigger,
      action,
      context,
      confidence: INITIAL_CORRECTION_CONFIDENCE,
      source: 'correction',
    })
    return out // one correction rule per message
  }

  if (opts.approach) {
    for (const pattern of CONFIRMATION_PATTERNS) {
      if (pattern.test(text)) {
        out.push({
          trigger,
          action: opts.approach,
          context,
          confidence: INITIAL_CONFIRMATION_CONFIDENCE,
          source: 'confirmation',
        })
        break
      }
    }
  }

  return out
}

/**
 * Analyze user messages for correction/confirmation patterns.
 * Call this after each conversation turn.
 */
export async function extractBehaviorRules(messages: Message[]): Promise<BehaviorRule[]> {
  const extracted: BehaviorRule[] = []

  const recentUserMessages = messages.filter((m) => m.type === 'user').slice(-2)

  for (const msg of recentUserMessages) {
    const text = extractUserText(msg)
    if (!text) continue

    const approach = extractRecentApproach(messages) ?? undefined
    const candidates = findRuleCandidates(text, {
      context: inferContext(messages),
      approach,
    })

    for (const candidate of candidates) {
      const rule = await addRule(candidate)
      if (rule) {
        extracted.push(rule)
        logForDebugging(`Extracted ${candidate.source} rule: ${rule.action}`)
      }
    }
  }

  return extracted
}

/**
 * Extract plain text from a user message.
 */
function extractUserText(msg: Message): string | null {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const m = msg as any
  const content = m.message?.content
  if (typeof content === 'string') return content
  if (Array.isArray(content)) {
    return content
      .filter((b: { type: string }) => b.type === 'text')
      .map((b: { text: string }) => b.text)
      .join(' ')
  }
  return null
}

/**
 * Infer the scope context from recent file operations.
 */
function inferContext(messages: Message[]): string {
  const recentAssistant = messages.filter((m) => m.type === 'assistant').slice(-3)

  for (const msg of recentAssistant) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const content = (msg as any).message?.content
    if (!Array.isArray(content)) continue

    for (const block of content) {
      if (block.type === 'tool_use' && block.input?.file_path) {
        const filePath = block.input.file_path as string
        const parts = filePath.split(/[/\\]/)
        if (parts.length >= 2) {
          return parts.slice(0, -1).join('/')
        }
      }
    }
  }

  return 'global'
}

/**
 * Extract a description of what the assistant recently did.
 */
function extractRecentApproach(messages: Message[]): string | null {
  const lastAssistant = messages.filter((m) => m.type === 'assistant').at(-1)

  if (!lastAssistant) return null

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const content = (lastAssistant as any).message?.content
  if (!Array.isArray(content)) return null

  const toolUses = content.filter((b: { type: string }) => b.type === 'tool_use')

  if (toolUses.length > 0) {
    const toolNames = toolUses.map((t: { name: string }) => t.name).join(', ')
    return `approach using ${toolNames}`
  }

  const textBlock = content.find((b: { type: string }) => b.type === 'text')
  if (textBlock?.text) {
    return textBlock.text.slice(0, 80)
  }

  return null
}
