# Copyright (c) 2024-2026 YuyAI / InsAIts Team. All rights reserved.
# Proprietary and confidential. See LICENSE.premium for terms.
"""
InsAIts Adaptive Context Manager (ACM) - Premium Core
======================================================
Monitors the message stream and promotes HOT context to COLD when
settlement conditions are met. Never deletes — only compresses.

HOT: load-bearing, must stay in full fidelity
COLD: settled, safe to compress to a 1-line summary

Core principle: quality is the primary constraint. Token reduction
is secondary. Never sacrifice reasoning quality for token savings.

Author: Cristi Bogdan -- Steddy Nova SRL / YuyAI
"""

from __future__ import annotations

import logging
import re
import time
from enum import Enum
from typing import Any, Dict, List, Optional, Set, Tuple

from insa_its.context.settlement_detector import SettlementDetector
from insa_its.context.quality_guard import QualityGuard
from insa_its.context.base import (
    ApprovalResult,
    CompressionApproval,
)

logger = logging.getLogger("insaits.context.acm")


class ContextState(Enum):
    HOT = "hot"
    COLD = "cold"


# Patterns that indicate a message must stay HOT regardless
_ALWAYS_HOT_PATTERNS: List[re.Pattern] = [
    re.compile(r"\b(?:TODO|FIXME|HACK|BLOCKED|WAITING|PENDING)\b"),
    re.compile(r"\b(?:unresolved|failed|error|retry)\b", re.I),
    re.compile(r"\b(?:CRITICAL|HIGH)\b"),
    re.compile(r"\bgotchas?\.md\b", re.I),
    re.compile(r"\bCLAUDE\.md\b"),
    re.compile(r"\bsession.?rule\b", re.I),
    re.compile(r"\b(?:ACTIVE_RULES|CONTEXT|SESSION_PARAMS|CONSTRAINT)\b"),
    re.compile(r"\btask.?anchor\b", re.I),
    re.compile(r"\banchor.?inject\b", re.I),
]

# INFO-severity anomaly types that are safe to compress when resolved
_SAFE_INFO_TYPES: Set[str] = {
    "subagent_tool_call", "subagent_spawned",
    "task_anchor_injected", "anchor_injected",
    "monitoring_gap_recovered", "response_awareness",
}

# Estimate: average tokens per message for savings calculation
_AVG_TOKENS_PER_MSG: int = 800
_SETTLED_LINE_TOKENS: int = 30


class AdaptiveContextManager:
    """Classifies and compresses context items in the message stream.

    Compression is batched — runs every `compression_cycle_interval` tool
    calls, only after the `compression_trigger` threshold is reached.
    """

    RECENCY_WINDOW: int = 15
    COMPRESSION_TRIGGER: int = 80
    DUPLICATE_THRESHOLD: float = 0.85
    MIN_HOT_ITEMS: int = 20

    def __init__(
        self,
        recency_window: int = 15,
        compression_trigger: int = 80,
        duplicate_threshold: float = 0.85,
        min_hot_items: int = 20,
        max_compression_pct: float = 40.0,
        recent_critical_window: int = 10,
    ) -> None:
        self.RECENCY_WINDOW = recency_window
        self.COMPRESSION_TRIGGER = compression_trigger
        self.DUPLICATE_THRESHOLD = duplicate_threshold
        self.MIN_HOT_ITEMS = min_hot_items

        self._detector = SettlementDetector(lookback_window=recency_window)
        self._guard = QualityGuard(
            min_hot_items=min_hot_items,
            max_compression_pct=max_compression_pct,
            recent_critical_window=recent_critical_window,
        )

        # State
        self._classifications: Dict[int, ContextState] = {}
        self._compressed_summaries: Dict[int, str] = {}
        self._compression_cycles: int = 0
        self._total_tokens_saved: int = 0
        self._duplicates_compressed: int = 0

    def classify(
        self,
        message_idx: int,
        messages: List[Dict[str, Any]],
    ) -> ContextState:
        """Classify a message as HOT or COLD.

        Classification rules (first match wins):
        1. Within RECENCY_WINDOW -> HOT
        2. Contains open task markers -> HOT
        3. Cross-agent dependency -> HOT
        4. Session rule / gotcha / CLAUDE.md -> HOT
        5. CRITICAL/HIGH unresolved anomaly -> HOT
        6. Settled bash/tool output -> COLD
        7. Settled file read -> COLD
        8. Duplicate subagent report -> COLD
        9. INFO anomaly, auto-resolved -> COLD
        10. Default -> HOT
        """
        if message_idx in self._classifications:
            return self._classifications[message_idx]

        msg = messages[message_idx]
        total = len(messages)

        # Rule 1: Recency window -- always HOT
        if total - message_idx <= self.RECENCY_WINDOW:
            return self._set(message_idx, ContextState.HOT)

        # Get message text for pattern matching
        text = self._get_text(msg)

        # Rule 2-4: Always-hot patterns (tasks, rules, gotchas)
        for pattern in _ALWAYS_HOT_PATTERNS:
            if pattern.search(text):
                return self._set(message_idx, ContextState.HOT)

        # Rule 5: CRITICAL/HIGH unresolved anomaly
        anomalies = msg.get("anomalies", [])
        if isinstance(anomalies, list):
            for a in anomalies:
                if isinstance(a, dict):
                    sev = a.get("severity", "")
                    atype = a.get("type", "")
                    if sev in ("CRITICAL", "HIGH") and not msg.get("resolved", False):
                        return self._set(message_idx, ContextState.HOT)

        # Rule 9: INFO anomaly, auto-resolved -> COLD
        if anomalies and msg.get("resolved", False):
            all_info_safe = all(
                isinstance(a, dict)
                and a.get("severity", "").upper() == "INFO"
                and a.get("type", "") in _SAFE_INFO_TYPES
                for a in anomalies
            )
            if all_info_safe:
                return self._set(message_idx, ContextState.COLD)

        # Rule 6-7: Settlement check (bash output, file read acted on)
        if self._detector.is_settled(message_idx, messages):
            return self._set(message_idx, ContextState.COLD)

        # Rule 8: Duplicate subagent report
        dup_idx = self._detector.is_duplicate(
            message_idx, messages, self.DUPLICATE_THRESHOLD,
        )
        if dup_idx is not None:
            return self._set(message_idx, ContextState.COLD)

        # Rule 10: Default -> HOT
        return self._set(message_idx, ContextState.HOT)

    def compress(
        self,
        message_idx: int,
        messages: List[Dict[str, Any]],
    ) -> str:
        """Convert a COLD message to its 1-line settled summary.

        Format:
        [SETTLED: <timestamp> | tool=<tool> | agent=<agent> | result=<outcome> | action_taken=<action>]

        If any field cannot be determined confidently, returns empty string
        (message stays HOT -- do not compress).
        """
        if message_idx >= len(messages):
            return ""

        msg = messages[message_idx]
        ts = msg.get("ts", "")
        tool = msg.get("tool", "unknown")
        agent = msg.get("model", msg.get("agent", "unknown"))
        text = self._get_text(msg)

        if not text:
            return ""

        # Extract a 1-line result summary
        result_summary = self._summarize_result(text, tool)
        if not result_summary:
            return ""

        # Determine action taken from subsequent messages
        action = self._determine_action(message_idx, messages)

        # Check for duplicates
        dup_idx = self._detector.is_duplicate(
            message_idx, messages, self.DUPLICATE_THRESHOLD,
        )
        if dup_idx is not None:
            self._duplicates_compressed += 1
            return (
                f"[DUPLICATE: kept_idx={dup_idx} | "
                f"agent={agent} | {result_summary}]"
            )

        summary = (
            f"[SETTLED: {ts[:19] if ts else 'unknown'} | "
            f"tool={tool} | agent={agent} | "
            f"result={result_summary} | "
            f"action_taken={action}]"
        )
        return summary

    def build_compression_plan(
        self,
        messages: List[Dict[str, Any]],
    ) -> CompressionApproval:
        """Build and approve a compression plan for the message stream.

        Classifies all eligible messages, builds compression candidates,
        and runs through the QualityGuard for approval.

        Returns:
            CompressionApproval with approved/vetoed indices.
        """
        self._compression_cycles += 1
        total = len(messages)

        if total < self.COMPRESSION_TRIGGER:
            return CompressionApproval(
                result=ApprovalResult.APPROVED,
                approved_indices=[],
                vetoed_indices=[],
                reasons=[],
            )

        # Classify all messages
        hot_count = 0
        cold_indices: List[int] = []

        for i in range(total):
            state = self.classify(i, messages)
            if state == ContextState.HOT:
                hot_count += 1
            else:
                cold_indices.append(i)

        # Only compress items that are already classified COLD
        # and haven't been compressed yet
        uncompressed_cold = [
            i for i in cold_indices if i not in self._compressed_summaries
        ]

        if not uncompressed_cold:
            return CompressionApproval(
                result=ApprovalResult.APPROVED,
                approved_indices=[],
                vetoed_indices=[],
                reasons=[],
            )

        # Run through quality guard
        approval = self._guard.approve(
            compression_plan=uncompressed_cold,
            messages=messages,
            hot_count=hot_count,
            total_count=total,
        )

        # Generate summaries for approved items
        if approval.approved:
            for idx in approval.approved_indices:
                summary = self.compress(idx, messages)
                if summary:
                    self._compressed_summaries[idx] = summary
                    self._total_tokens_saved += (
                        _AVG_TOKENS_PER_MSG - _SETTLED_LINE_TOKENS
                    )
                else:
                    # Cannot compress confidently -- reclassify as HOT
                    self._classifications[idx] = ContextState.HOT

        return approval

    def apply(
        self,
        messages: List[Dict[str, Any]],
    ) -> List[Dict[str, Any]]:
        """Apply compression to the message stream.

        Replaces COLD compressed messages with their 1-line summaries.
        Original messages are preserved (summaries are additive metadata).
        """
        result = []
        for i, msg in enumerate(messages):
            if i in self._compressed_summaries:
                compressed = dict(msg)
                compressed["_acm_compressed"] = True
                compressed["_acm_summary"] = self._compressed_summaries[i]
                compressed["_acm_original_size"] = len(self._get_text(msg))
                result.append(compressed)
            else:
                result.append(msg)
        return result

    def get_compression_report(self) -> Dict[str, Any]:
        """Return stats for the dashboard CONTEXT HEALTH panel."""
        hot_count = sum(
            1 for s in self._classifications.values()
            if s == ContextState.HOT
        )
        cold_count = sum(
            1 for s in self._classifications.values()
            if s == ContextState.COLD
        )
        total = hot_count + cold_count
        compressed_count = len(self._compressed_summaries)
        compression_ratio = (
            (compressed_count / max(total, 1)) * 100
        )
        quality_risk = self._guard.get_quality_risk(
            hot_count, cold_count, total,
        )

        return {
            "hot_items": hot_count,
            "cold_items": cold_count,
            "compressed_items": compressed_count,
            "duplicates_compressed": self._duplicates_compressed,
            "estimated_tokens_saved": self._total_tokens_saved,
            "compression_ratio": round(compression_ratio, 1),
            "compression_cycles": self._compression_cycles,
            "quality_risk": quality_risk,
            "guard_stats": self._guard.get_stats(),
        }

    def _set(self, idx: int, state: ContextState) -> ContextState:
        """Set and return classification for an index."""
        self._classifications[idx] = state
        return state

    @staticmethod
    def _get_text(msg: Dict[str, Any]) -> str:
        """Extract text from message."""
        for field in ("text", "content", "description", "result", "output"):
            val = msg.get(field, "")
            if isinstance(val, str) and val:
                return val
        anomalies = msg.get("anomalies", [])
        if isinstance(anomalies, list):
            descs = [
                a.get("description", "")
                for a in anomalies
                if isinstance(a, dict) and a.get("description")
            ]
            if descs:
                return " ".join(descs)
        return ""

    @staticmethod
    def _summarize_result(text: str, tool: str) -> str:
        """Create a 1-line summary of a tool result."""
        if not text:
            return ""
        # Take first meaningful line, max 80 chars
        lines = [l.strip() for l in text.split("\n") if l.strip()]
        if not lines:
            return ""
        # Skip common noise lines
        for line in lines:
            if line.startswith("#") or line.startswith("---"):
                continue
            # Clean and truncate
            clean = re.sub(r"\s+", " ", line).strip()
            if len(clean) > 80:
                clean = clean[:77] + "..."
            return clean
        return lines[0][:80] if lines else ""

    @staticmethod
    def _determine_action(
        message_idx: int,
        messages: List[Dict[str, Any]],
    ) -> str:
        """Determine what action was taken after this message."""
        # Look at the next few messages for action keywords
        for i in range(message_idx + 1, min(message_idx + 5, len(messages))):
            msg = messages[i]
            tool = msg.get("tool", "")
            if tool:
                return f"proceeded_to_{tool.lower()}"
        return "continued"
