"""Skill graph storage and matching."""

from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from uuid import uuid4

from .bytecode import CognitiveOp
from .trace import Episode
from .verifiers import VerificationResult

_TOKEN_RE = re.compile(r"[A-Za-z0-9_]+")


def _tokens(text: str) -> set[str]:
    return {item.lower() for item in _TOKEN_RE.findall(text)}


def _refresh_precision(skill: "Skill") -> None:
    if skill.trigger_attempt_count:
        skill.precision = skill.true_positive_trigger_count / skill.trigger_attempt_count
    else:
        skill.precision = 0.0


@dataclass(slots=True)
class Skill:
    name: str
    description: str
    triggers: list[str]
    steps: list[str]
    verifier: str | None = None
    promoted: bool = False
    success_count: int = 0
    failure_count: int = 0
    version: int = 1
    metadata: dict[str, Any] = field(default_factory=dict)
    skill_id: str | None = None
    family: str | None = None
    status: str = "candidate"
    preconditions: dict[str, Any] = field(default_factory=dict)
    parameter_slots: dict[str, Any] = field(default_factory=dict)
    bytecode_plan: list[str] = field(default_factory=list)
    required_verifier: list[str] = field(default_factory=list)
    promotion_evidence_count: int = 0
    heldout_success_rate: float = 0.0
    average_saved_steps: float = 0.0
    false_trigger_count: int = 0
    last_verified_at: str | None = None
    regression_status: str = "unknown"
    trigger_attempt_count: int = 0
    true_positive_trigger_count: int = 0
    false_positive_trigger_count: int = 0
    verifier_pass_count: int = 0
    verifier_fail_count: int = 0
    fallback_count: int = 0
    quarantine_reason: str | None = None
    precision: float = 0.0
    adversarial_success_rate: float = 0.0

    def __post_init__(self) -> None:
        if self.skill_id is None:
            self.skill_id = uuid4().hex
        if self.promoted and self.status == "candidate":
            self.status = "promoted"
        if self.status == "promoted":
            self.promoted = True

    @property
    def reliability(self) -> float:
        total = self.success_count + self.failure_count
        if total == 0:
            return 0.0
        return self.success_count / total

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "description": self.description,
            "triggers": self.triggers,
            "steps": self.steps,
            "verifier": self.verifier,
            "promoted": self.promoted,
            "success_count": self.success_count,
            "failure_count": self.failure_count,
            "version": self.version,
            "metadata": self.metadata,
            "skill_id": self.skill_id,
            "family": self.family,
            "status": self.status,
            "preconditions": self.preconditions,
            "parameter_slots": self.parameter_slots,
            "bytecode_plan": self.bytecode_plan,
            "required_verifier": self.required_verifier,
            "promotion_evidence_count": self.promotion_evidence_count,
            "heldout_success_rate": self.heldout_success_rate,
            "average_saved_steps": self.average_saved_steps,
            "false_trigger_count": self.false_trigger_count,
            "last_verified_at": self.last_verified_at,
            "regression_status": self.regression_status,
            "trigger_attempt_count": self.trigger_attempt_count,
            "true_positive_trigger_count": self.true_positive_trigger_count,
            "false_positive_trigger_count": self.false_positive_trigger_count,
            "verifier_pass_count": self.verifier_pass_count,
            "verifier_fail_count": self.verifier_fail_count,
            "fallback_count": self.fallback_count,
            "quarantine_reason": self.quarantine_reason,
            "precision": self.precision,
            "adversarial_success_rate": self.adversarial_success_rate,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Skill":
        promoted = bool(data.get("promoted", False))
        status = str(data.get("status") or ("promoted" if promoted else "candidate"))
        verifier = data.get("verifier")
        required_verifier = list(data.get("required_verifier") or ([verifier] if verifier else []))
        return cls(
            name=str(data["name"]),
            description=str(data.get("description", "")),
            triggers=list(data.get("triggers") or []),
            steps=list(data.get("steps") or []),
            verifier=verifier,
            promoted=promoted,
            success_count=int(data.get("success_count", 0)),
            failure_count=int(data.get("failure_count", 0)),
            version=int(data.get("version", 1)),
            metadata=dict(data.get("metadata") or {}),
            skill_id=str(data.get("skill_id") or data["name"]),
            family=str(data["family"]) if data.get("family") is not None else None,
            status=status,
            preconditions=dict(data.get("preconditions") or {}),
            parameter_slots=dict(data.get("parameter_slots") or {}),
            bytecode_plan=[str(item) for item in data.get("bytecode_plan", [])],
            required_verifier=[str(item) for item in required_verifier],
            promotion_evidence_count=int(data.get("promotion_evidence_count", data.get("success_count", 0))),
            heldout_success_rate=float(data.get("heldout_success_rate", 0.0)),
            average_saved_steps=float(data.get("average_saved_steps", 0.0)),
            false_trigger_count=int(data.get("false_trigger_count", 0)),
            last_verified_at=str(data["last_verified_at"]) if data.get("last_verified_at") is not None else None,
            regression_status=str(data.get("regression_status", "unknown")),
            trigger_attempt_count=int(data.get("trigger_attempt_count", 0)),
            true_positive_trigger_count=int(data.get("true_positive_trigger_count", 0)),
            false_positive_trigger_count=int(data.get("false_positive_trigger_count", 0)),
            verifier_pass_count=int(data.get("verifier_pass_count", data.get("success_count", 0))),
            verifier_fail_count=int(data.get("verifier_fail_count", data.get("failure_count", 0))),
            fallback_count=int(data.get("fallback_count", 0)),
            quarantine_reason=str(data["quarantine_reason"]) if data.get("quarantine_reason") is not None else None,
            precision=float(data.get("precision", 0.0)),
            adversarial_success_rate=float(data.get("adversarial_success_rate", 0.0)),
        )


@dataclass(slots=True)
class SkillMatch:
    skill: Skill
    score: float


@dataclass(slots=True)
class SkillPromotionReport:
    skill_name: str | None
    promoted: bool
    reasons: list[str]
    support_count: int = 0
    regression_rate: float = 0.0
    verifier_result: dict[str, object] | None = None

    def to_dict(self) -> dict[str, object]:
        return {
            "skill_name": self.skill_name,
            "promoted": self.promoted,
            "reasons": self.reasons,
            "support_count": self.support_count,
            "regression_rate": self.regression_rate,
            "verifier_result": self.verifier_result,
        }

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> "SkillPromotionReport":
        return cls(
            skill_name=str(data["skill_name"]) if data.get("skill_name") is not None else None,
            promoted=bool(data.get("promoted", False)),
            reasons=[str(item) for item in data.get("reasons", [])],
            support_count=int(data.get("support_count", 0)),
            regression_rate=float(data.get("regression_rate", 0.0)),
            verifier_result=dict(data["verifier_result"]) if data.get("verifier_result") is not None else None,
        )


class SkillGraph:
    def __init__(self, path: str | Path) -> None:
        self.path = Path(path)
        if self.path.parent:
            self.path.parent.mkdir(parents=True, exist_ok=True)
        self.skills: dict[str, Skill] = {}
        self.load()

    def load(self) -> None:
        if not self.path.exists():
            self.skills = {}
            return
        data = json.loads(self.path.read_text(encoding="utf-8"))
        self.skills = {item["name"]: Skill.from_dict(item) for item in data.get("skills", [])}

    def save(self) -> None:
        payload = {"skills": [skill.to_dict() for skill in sorted(self.skills.values(), key=lambda item: item.name)]}
        self.path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")

    def add_or_update(self, skill: Skill) -> None:
        existing = self.skills.get(skill.name)
        if existing is not None and existing.version >= skill.version:
            skill.version = existing.version + 1
        self.skills[skill.name] = skill
        self.save()

    def get(self, name: str) -> Skill | None:
        return self.skills.get(name)

    def match(
        self,
        query: str,
        limit: int = 5,
        promoted_only: bool = True,
        include_statuses: list[str] | None = None,
    ) -> list[SkillMatch]:
        query_tokens = _tokens(query)
        matches: list[SkillMatch] = []
        allowed_statuses = set(include_statuses) if include_statuses is not None else ({"promoted"} if promoted_only else {"candidate", "promoted"})
        for skill in self.skills.values():
            if skill.status not in allowed_statuses:
                continue
            if skill.status in {"quarantined", "deprecated"} and include_statuses is None:
                continue
            if promoted_only and not skill.promoted:
                continue
            haystack = " ".join([skill.name, skill.description, " ".join(skill.triggers), " ".join(skill.steps)])
            overlap = len(query_tokens & _tokens(haystack))
            if overlap:
                score = overlap / max(1, len(query_tokens))
                if skill.promoted:
                    score += 0.1
                score += min(skill.reliability, 1.0) * 0.05
                matches.append(SkillMatch(skill=skill, score=score))
        matches.sort(key=lambda item: item.score, reverse=True)
        return matches[:limit]

    def record_trigger(self, name: str, true_positive: bool) -> None:
        skill = self.skills[name]
        skill.trigger_attempt_count += 1
        if true_positive:
            skill.true_positive_trigger_count += 1
        else:
            skill.false_positive_trigger_count += 1
            skill.false_trigger_count += 1
            if skill.status == "promoted" and skill.false_positive_trigger_count >= 2:
                self._quarantine_skill(skill, "false_positive_trigger_threshold")
        _refresh_precision(skill)
        self.save()

    def record_result(self, name: str, success: bool, fallback_used: bool = False) -> None:
        skill = self.skills[name]
        if success:
            skill.success_count += 1
            skill.verifier_pass_count += 1
        else:
            skill.failure_count += 1
            skill.verifier_fail_count += 1
            if skill.status == "promoted" and skill.verifier_fail_count >= 2:
                self._quarantine_skill(skill, "verifier_failure_threshold")
        if fallback_used:
            skill.fallback_count += 1
        _refresh_precision(skill)
        self.save()

    def record_false_trigger(self, name: str) -> None:
        skill = self.skills[name]
        skill.false_trigger_count += 1
        skill.false_positive_trigger_count += 1
        skill.trigger_attempt_count += 1
        if skill.status == "promoted" and skill.false_positive_trigger_count >= 2:
            self._quarantine_skill(skill, "false_positive_trigger_threshold")
        _refresh_precision(skill)
        self.save()

    def quarantine(self, name: str, reason: str) -> None:
        skill = self.skills[name]
        self._quarantine_skill(skill, reason)
        self.save()

    def _quarantine_skill(self, skill: Skill, reason: str) -> None:
        skill.status = "quarantined"
        skill.quarantine_reason = reason
        skill.metadata = {**skill.metadata, "quarantine_reason": reason}


def induce_skill_from_episodes(episodes: list[Episode], min_support: int = 2) -> Skill | None:
    """Propose a candidate skill from repeated successful operation sequences.

    This is a conservative MVP placeholder. It does not promote the skill; it
    only proposes one when the exact operation sequence appears often enough.
    """

    sequences: dict[tuple[str, ...], list[Episode]] = {}
    for episode in episodes:
        if episode.outcome != "success":
            continue
        ops = tuple(step.instruction.op.value for step in episode.steps if step.success)
        if len(ops) < 2:
            continue
        sequences.setdefault(ops, []).append(episode)

    if not sequences:
        return None

    ops, supporting = max(sequences.items(), key=lambda item: len(item[1]))
    if len(supporting) < min_support:
        return None

    token_counts: dict[str, int] = {}
    for episode in supporting:
        for token in _tokens(episode.task):
            token_counts[token] = token_counts.get(token, 0) + 1
    trigger_words = [
        token
        for token, _count in sorted(token_counts.items(), key=lambda item: (-item[1], item[0]))[:12]
    ]
    name = "candidate_" + "_".join(op.lower() for op in ops[:3])
    steps = [f"Execute {op}" for op in ops]
    return Skill(
        name=name[:80],
        description="Candidate skill induced from repeated successful traces.",
        triggers=trigger_words,
        steps=steps,
        verifier="manual_or_regression_suite",
        promoted=False,
        success_count=len(supporting),
        failure_count=0,
        metadata={
            "supporting_episode_ids": [episode.episode_id for episode in supporting],
            "op_sequence": list(ops),
            "source": "dream_consolidation",
        },
        status="candidate",
        bytecode_plan=list(ops),
        required_verifier=["manual_or_regression_suite"],
        promotion_evidence_count=len(supporting),
    )


def op_sequence_contains(episode: Episode, op: CognitiveOp) -> bool:
    return any(step.instruction.op == op for step in episode.steps)


def promote_candidate_skill(
    skill_graph: SkillGraph,
    candidate: Skill | None,
    verifier_result: VerificationResult,
    min_support: int = 2,
    regression_rate: float = 0.0,
) -> SkillPromotionReport:
    """Persist a candidate only after support, verifier, and regression gates pass."""

    if candidate is None:
        return SkillPromotionReport(
            skill_name=None,
            promoted=False,
            reasons=["missing_candidate"],
            verifier_result=verifier_result.to_dict(),
            regression_rate=regression_rate,
        )

    reasons: list[str] = []
    if candidate.success_count < min_support:
        reasons.append("insufficient_support")
    if not candidate.triggers:
        reasons.append("missing_triggers")
    if not verifier_result.passed:
        reasons.append("verifier_failed")
    if regression_rate > 0.0:
        reasons.append("regression_detected")

    if reasons:
        return SkillPromotionReport(
            skill_name=candidate.name,
            promoted=False,
            reasons=reasons,
            support_count=candidate.success_count,
            regression_rate=regression_rate,
            verifier_result=verifier_result.to_dict(),
        )

    promoted = Skill.from_dict(candidate.to_dict())
    promoted.promoted = True
    promoted.status = "promoted"
    promoted.promotion_evidence_count = max(promoted.promotion_evidence_count, candidate.success_count)
    promoted.heldout_success_rate = 1.0
    promoted.regression_status = "passed"
    promoted.metadata = {
        **promoted.metadata,
        "promotion": {
            "min_support": min_support,
            "regression_rate": regression_rate,
            "verifier_result": verifier_result.to_dict(),
        },
    }
    skill_graph.add_or_update(promoted)

    return SkillPromotionReport(
        skill_name=promoted.name,
        promoted=True,
        reasons=["promoted"],
        support_count=promoted.success_count,
        regression_rate=regression_rate,
        verifier_result=verifier_result.to_dict(),
    )
