"""
InsAIts Pattern Miner
=====================
Reads all session JSONL files (current + saved) and extracts actionable
patterns: anomaly frequency, severity distribution, intervention success
rate, dangerous agent+anomaly combos, and resolution statistics.

Output: session_patterns.json in project root (or custom path).
Runnable standalone: python -m insa_its.learning.pattern_miner

Privacy: NO code content stored — only anomaly types + statistics.
Author: Cristi Bogdan -- Steddy Nova SRL / YuyAI
"""

from __future__ import annotations

import json
import logging
import os
import time
from collections import Counter, defaultdict
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

logger = logging.getLogger("insaits.learning.pattern_miner")

# Non-anomaly event types that should be excluded from anomaly stats
_NON_ANOMALY_TYPES = frozenset({
    "anchor_injected",
    "task_anchor_injected",
    "response_awareness",
    "subagent_spawned",
    "subagent_tool_call",
    "monitoring_started",
    "monitoring_stopped",
})

# Severity ordering for comparisons
_SEV_ORDER = {"INFO": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 3, "CRITICAL": 4}


class PatternMiner:
    """Mines session JSONL files for anomaly patterns and statistics.

    Usage:
        miner = PatternMiner()
        miner.add_jsonl_file("path/to/.insaits_audit_session.jsonl")
        miner.add_jsonl_dir("~/.insaits/")  # all session_*.jsonl
        patterns = miner.mine()
        miner.save(patterns, "session_patterns.json")
    """

    def __init__(self) -> None:
        self._entries: List[Dict[str, Any]] = []
        self._files_loaded: List[str] = []

    def add_jsonl_file(self, path: str) -> int:
        """Load entries from a single JSONL file. Returns count loaded."""
        path = os.path.expanduser(path)
        if not os.path.isfile(path):
            logger.warning("File not found: %s", path)
            return 0
        count = 0
        try:
            with open(path, "r", encoding="utf-8") as fh:
                for line_num, line in enumerate(fh, 1):
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        entry = json.loads(line)
                        entry["_source_file"] = os.path.basename(path)
                        self._entries.append(entry)
                        count += 1
                    except json.JSONDecodeError:
                        logger.debug("Bad JSON at %s:%d", path, line_num)
        except (IOError, OSError) as exc:
            logger.warning("Cannot read %s: %s", path, exc)
            return 0
        if count > 0:
            self._files_loaded.append(path)
        logger.info("Loaded %d entries from %s", count, path)
        return count

    def add_jsonl_dir(self, directory: str) -> int:
        """Load all session_*.jsonl and *audit*.jsonl files from a directory."""
        directory = os.path.expanduser(directory)
        if not os.path.isdir(directory):
            logger.warning("Directory not found: %s", directory)
            return 0
        total = 0
        for fname in sorted(os.listdir(directory)):
            if not fname.endswith(".jsonl"):
                continue
            if fname.startswith("session_") or "audit" in fname:
                total += self.add_jsonl_file(os.path.join(directory, fname))
        return total

    def add_project_dir(self, project_dir: str) -> int:
        """Load from both project root (.insaits_audit_session.jsonl) and ~/.insaits/."""
        total = 0
        # Project root audit log
        audit_path = os.path.join(project_dir, ".insaits_audit_session.jsonl")
        if os.path.isfile(audit_path):
            total += self.add_jsonl_file(audit_path)
        # ~/.insaits/ saved sessions
        insaits_dir = os.path.expanduser("~/.insaits")
        if os.path.isdir(insaits_dir):
            total += self.add_jsonl_dir(insaits_dir)
        return total

    def mine(self) -> Dict[str, Any]:
        """Run pattern mining on all loaded entries.

        Returns a dict with:
        - anomaly_frequency: Counter of anomaly types
        - severity_distribution: Counter of severities
        - agent_anomaly_combos: which agents trigger which anomalies
        - intervention_stats: success/failure rates per anomaly type
        - tool_sequences: common tool sequences before anomalies
        - session_summaries: per-session overview
        - resolution_stats: resolved vs unresolved rates
        - hourly_distribution: anomalies by hour of day
        - metadata: mining timestamp, files loaded, total entries
        """
        if not self._entries:
            return self._empty_result()

        anomaly_freq: Counter = Counter()
        severity_dist: Counter = Counter()
        agent_anomaly: Dict[str, Counter] = defaultdict(Counter)
        intervention_texts: Dict[str, List[str]] = defaultdict(list)
        resolved_counts: Dict[str, List[bool]] = defaultdict(list)
        tool_before_anomaly: Dict[str, Counter] = defaultdict(Counter)
        hourly: Counter = Counter()
        session_stats: Dict[str, Dict[str, Any]] = defaultdict(
            lambda: {"total_calls": 0, "anomaly_count": 0, "resolved": 0,
                     "unresolved": 0, "first_ts": "", "last_ts": "",
                     "tools_used": Counter(), "anomaly_types": Counter()}
        )

        prev_tool = ""
        for entry in self._entries:
            ts_str = entry.get("ts", "")
            session_id = entry.get("session_id", "unknown")
            tool = entry.get("tool", "unknown")
            model = entry.get("model", "unknown")
            anomalies = entry.get("anomalies", [])
            resolved = entry.get("resolved", True)

            # Session stats
            ss = session_stats[session_id]
            ss["total_calls"] += 1
            ss["tools_used"][tool] += 1
            if not ss["first_ts"] or ts_str < ss["first_ts"]:
                ss["first_ts"] = ts_str
            if not ss["last_ts"] or ts_str > ss["last_ts"]:
                ss["last_ts"] = ts_str

            # Hourly distribution
            try:
                hour = int(ts_str[11:13])
                hourly[hour] += 1
            except (ValueError, IndexError):
                pass

            for anom in anomalies:
                atype = anom.get("type", "unknown")
                sev = anom.get("severity", "INFO")
                intervention = anom.get("intervention", "")

                # Skip non-anomaly events
                if atype in _NON_ANOMALY_TYPES:
                    continue

                anomaly_freq[atype] += 1
                severity_dist[sev] += 1
                ss["anomaly_count"] += 1
                ss["anomaly_types"][atype] += 1

                # Agent-anomaly combinations
                agent_key = model.split(":")[-1] if ":" in model else model
                agent_anomaly[agent_key][atype] += 1

                # Intervention tracking
                if intervention:
                    intervention_texts[atype].append(intervention[:200])

                # Resolution
                resolved_counts[atype].append(resolved)
                if resolved:
                    ss["resolved"] += 1
                else:
                    ss["unresolved"] += 1

                # Tool sequence before anomaly
                if prev_tool:
                    tool_before_anomaly[atype][prev_tool] += 1

            prev_tool = tool

        # Build results
        return {
            "anomaly_frequency": dict(anomaly_freq.most_common()),
            "severity_distribution": dict(severity_dist.most_common()),
            "agent_anomaly_combos": self._top_combos(agent_anomaly),
            "intervention_stats": self._intervention_stats(
                intervention_texts, resolved_counts
            ),
            "tool_sequences": {
                atype: dict(counter.most_common(5))
                for atype, counter in tool_before_anomaly.items()
            },
            "resolution_stats": self._resolution_stats(resolved_counts),
            "hourly_distribution": {
                str(h): hourly.get(h, 0) for h in range(24)
            },
            "session_summaries": self._session_summaries(session_stats),
            "top_dangerous_combos": self._dangerous_combos(
                agent_anomaly, severity_dist
            ),
            "metadata": {
                "mined_at": datetime.utcnow().isoformat() + "Z",
                "files_loaded": len(self._files_loaded),
                "total_entries": len(self._entries),
                "total_anomalies": sum(anomaly_freq.values()),
                "unique_sessions": len(session_stats),
            },
        }

    def save(self, patterns: Dict[str, Any], path: str) -> None:
        """Save mined patterns to JSON file."""
        path = os.path.expanduser(path)
        with open(path, "w", encoding="utf-8") as fh:
            json.dump(patterns, fh, indent=2, default=str)
        logger.info("Patterns saved to %s", path)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _empty_result(self) -> Dict[str, Any]:
        return {
            "anomaly_frequency": {},
            "severity_distribution": {},
            "agent_anomaly_combos": [],
            "intervention_stats": {},
            "tool_sequences": {},
            "resolution_stats": {},
            "hourly_distribution": {},
            "session_summaries": [],
            "top_dangerous_combos": [],
            "metadata": {
                "mined_at": datetime.utcnow().isoformat() + "Z",
                "files_loaded": 0,
                "total_entries": 0,
                "total_anomalies": 0,
                "unique_sessions": 0,
            },
        }

    def _top_combos(
        self, agent_anomaly: Dict[str, Counter]
    ) -> List[Dict[str, Any]]:
        """Return top agent+anomaly combinations sorted by frequency."""
        combos = []
        for agent, counter in agent_anomaly.items():
            for atype, count in counter.items():
                combos.append({
                    "agent": agent, "anomaly_type": atype, "count": count
                })
        combos.sort(key=lambda x: x["count"], reverse=True)
        return combos[:30]

    def _intervention_stats(
        self,
        texts: Dict[str, List[str]],
        resolved: Dict[str, List[bool]],
    ) -> Dict[str, Dict[str, Any]]:
        """Per-anomaly intervention effectiveness."""
        stats = {}
        for atype in set(list(texts.keys()) + list(resolved.keys())):
            resolutions = resolved.get(atype, [])
            total = len(resolutions)
            resolved_count = sum(1 for r in resolutions if r)
            stats[atype] = {
                "total_occurrences": total,
                "resolved": resolved_count,
                "unresolved": total - resolved_count,
                "resolution_rate": round(
                    resolved_count / total * 100, 1
                ) if total > 0 else 0.0,
                "has_intervention": len(texts.get(atype, [])) > 0,
                "sample_intervention": (
                    texts[atype][0][:150] if texts.get(atype) else ""
                ),
            }
        return stats

    def _resolution_stats(
        self, resolved: Dict[str, List[bool]]
    ) -> Dict[str, Any]:
        """Overall resolution statistics."""
        all_res = []
        for rlist in resolved.values():
            all_res.extend(rlist)
        total = len(all_res)
        res_count = sum(1 for r in all_res if r)
        return {
            "total": total,
            "resolved": res_count,
            "unresolved": total - res_count,
            "resolution_rate": round(
                res_count / total * 100, 1
            ) if total > 0 else 0.0,
        }

    def _session_summaries(
        self, stats: Dict[str, Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """Per-session summaries sorted by anomaly count (desc)."""
        summaries = []
        for sid, ss in stats.items():
            duration_sec = 0.0
            if ss["first_ts"] and ss["last_ts"]:
                try:
                    t1 = datetime.fromisoformat(
                        ss["first_ts"].replace("Z", "+00:00")
                    )
                    t2 = datetime.fromisoformat(
                        ss["last_ts"].replace("Z", "+00:00")
                    )
                    duration_sec = (t2 - t1).total_seconds()
                except (ValueError, TypeError):
                    pass
            summaries.append({
                "session_id": sid[:12] + "..." if len(sid) > 12 else sid,
                "total_calls": ss["total_calls"],
                "anomaly_count": ss["anomaly_count"],
                "resolved": ss["resolved"],
                "unresolved": ss["unresolved"],
                "duration_minutes": round(duration_sec / 60, 1),
                "top_tools": dict(ss["tools_used"].most_common(5)),
                "top_anomalies": dict(ss["anomaly_types"].most_common(5)),
            })
        summaries.sort(key=lambda x: x["anomaly_count"], reverse=True)
        return summaries

    def _dangerous_combos(
        self,
        agent_anomaly: Dict[str, Counter],
        severity_dist: Counter,
    ) -> List[Dict[str, Any]]:
        """Identify agent+anomaly combos that are frequent AND high severity."""
        combos = []
        # Weight by severity
        for agent, counter in agent_anomaly.items():
            for atype, count in counter.items():
                # Higher count + higher typical severity = more dangerous
                combos.append({
                    "agent": agent,
                    "anomaly_type": atype,
                    "count": count,
                    "danger_score": count,  # simple for now
                })
        combos.sort(key=lambda x: x["danger_score"], reverse=True)
        return combos[:10]


def main() -> None:
    """CLI entry point: mine patterns from default locations."""
    import argparse

    parser = argparse.ArgumentParser(
        description="InsAIts Pattern Miner — extract anomaly patterns from session logs"
    )
    parser.add_argument(
        "--project-dir", "-p",
        default=".",
        help="Project root directory (default: current dir)",
    )
    parser.add_argument(
        "--output", "-o",
        default="session_patterns.json",
        help="Output file path (default: session_patterns.json)",
    )
    parser.add_argument(
        "--jsonl", "-f",
        action="append",
        default=[],
        help="Additional JSONL files to include",
    )
    parser.add_argument(
        "--verbose", "-v",
        action="store_true",
        help="Enable verbose logging",
    )
    args = parser.parse_args()

    logging.basicConfig(
        level=logging.DEBUG if args.verbose else logging.INFO,
        format="%(levelname)s: %(message)s",
    )

    miner = PatternMiner()

    # Load from project + ~/.insaits/
    total = miner.add_project_dir(args.project_dir)

    # Additional files
    for f in args.jsonl:
        total += miner.add_jsonl_file(f)

    if total == 0:
        logger.warning("No entries loaded. Nothing to mine.")
        return

    logger.info("Mining %d entries...", total)
    patterns = miner.mine()

    miner.save(patterns, args.output)

    # Print summary
    meta = patterns["metadata"]
    print(f"\n=== InsAIts Pattern Mining Complete ===")
    print(f"Files loaded:     {meta['files_loaded']}")
    print(f"Total entries:    {meta['total_entries']}")
    print(f"Total anomalies:  {meta['total_anomalies']}")
    print(f"Unique sessions:  {meta['unique_sessions']}")

    freq = patterns["anomaly_frequency"]
    if freq:
        print(f"\nTop anomaly types:")
        for atype, count in list(freq.items())[:10]:
            print(f"  {atype}: {count}")

    res = patterns["resolution_stats"]
    print(f"\nResolution rate:  {res['resolution_rate']}% "
          f"({res['resolved']}/{res['total']})")

    print(f"\nPatterns saved to: {args.output}")


if __name__ == "__main__":
    main()
