"""
Skill Engine - Memory Skill Learning Infrastructure (Phase 21).

Transforms agent experience into reusable, executable skills:
  experience → memory → pattern → playbook → skill

Core capabilities:
  - Skill Extraction: converts patterns/playbooks into structured skills
  - Skill Registry: stores, retrieves, and ranks skills
  - Skill Execution: suggest or auto-execute skill steps
  - Skill Evolution: merge similar, deprecate weak, promote strong skills
"""

import uuid
import time
import logging
from abc import ABC, abstractmethod
from typing import List, Optional, Callable
from dataclasses import dataclass, field

logger = logging.getLogger("openmemo")

SKILL_STATUS_ACTIVE = "active"
SKILL_STATUS_VERIFIED = "verified"
SKILL_STATUS_DEPRECATED = "deprecated"

EXEC_MODE_SUGGEST = "suggest"
EXEC_MODE_AUTO = "auto"


@dataclass
class Skill:
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    name: str = ""
    description: str = ""
    pattern: str = ""
    scene: str = ""
    trigger: str = ""
    steps: list = field(default_factory=list)
    tools: list = field(default_factory=list)
    confidence: float = 0.0
    usage_count: int = 0
    success_rate: float = 0.0
    skill_version: int = 1
    status: str = SKILL_STATUS_ACTIVE
    created_at: float = field(default_factory=time.time)
    updated_at: float = field(default_factory=time.time)
    metadata: dict = field(default_factory=dict)

    def record_usage(self, success: bool):
        total_success = self.success_rate * self.usage_count + (1.0 if success else 0.0)
        self.usage_count += 1
        self.success_rate = total_success / self.usage_count
        self.updated_at = time.time()

    def to_dict(self):
        return {
            "id": self.id,
            "name": self.name,
            "description": self.description,
            "pattern": self.pattern,
            "scene": self.scene,
            "trigger": self.trigger,
            "steps": self.steps,
            "tools": self.tools,
            "confidence": self.confidence,
            "usage_count": self.usage_count,
            "success_rate": self.success_rate,
            "skill_version": self.skill_version,
            "status": self.status,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "metadata": self.metadata,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "Skill":
        fields = cls.__dataclass_fields__
        kwargs = {k: v for k, v in data.items() if k in fields}
        return cls(**kwargs)


class SkillExtractor(ABC):
    @abstractmethod
    def should_extract(self, pattern: str, count: int, successes: int) -> bool:
        pass


class DefaultSkillExtractor(SkillExtractor):
    def __init__(self, config=None):
        self._config = config

    def should_extract(self, pattern: str, count: int, successes: int) -> bool:
        from openmemo._internal import get_skill_params
        params = get_skill_params()
        return count >= params["pattern_threshold"]


class RelevanceScorer(ABC):
    @abstractmethod
    def score(self, skill: dict, context: str) -> float:
        pass


class DefaultRelevanceScorer(RelevanceScorer):
    def score(self, skill: dict, context: str) -> float:
        context_lower = context.lower()
        score = 0.0

        trigger = skill.get("trigger", "").lower()
        if trigger and trigger in context_lower:
            score += 0.4

        pattern = skill.get("pattern", "").lower()
        if pattern and pattern in context_lower:
            score += 0.3

        scene = skill.get("scene", "").lower()
        if scene and scene in context_lower:
            score += 0.1

        name = skill.get("name", "").lower()
        if name:
            name_words = set(name.replace("_", " ").split())
            context_words = set(context_lower.split())
            overlap = name_words & context_words
            if overlap:
                score += 0.1 * len(overlap) / max(len(name_words), 1)

        if score > 0:
            confidence = skill.get("confidence", 0.0)
            success_rate = skill.get("success_rate", 0.0)
            usage = min(skill.get("usage_count", 0) / 100, 0.1)
            score += confidence * 0.05 + success_rate * 0.05 + usage

        return min(score, 1.0)


def compute_skill_score(skill: dict) -> float:
    confidence = skill.get("confidence", 0.0)
    usage = min(skill.get("usage_count", 0) / 50.0, 1.0)
    success = skill.get("success_rate", 0.0)
    recency = 0.0
    updated = skill.get("updated_at", 0)
    if updated:
        age_days = (time.time() - updated) / 86400
        recency = max(0, 1.0 - age_days / 365)

    return (
        confidence * 0.3
        + usage * 0.2
        + success * 0.3
        + recency * 0.2
    )


class SkillEngine:
    def __init__(self, store=None, extractor: SkillExtractor = None,
                 scorer: RelevanceScorer = None, config=None,
                 llm_fn: Optional[Callable] = None):
        self.store = store
        self._extractor = extractor or DefaultSkillExtractor(config=config)
        self._scorer = scorer or DefaultRelevanceScorer()
        self._pattern_counts = {}
        self._llm_fn = llm_fn

    def observe(self, action: str, context: str = "", success: bool = True):
        key = self._normalize(action)
        if key not in self._pattern_counts:
            self._pattern_counts[key] = {"count": 0, "successes": 0, "context": context}

        self._pattern_counts[key]["count"] += 1
        if success:
            self._pattern_counts[key]["successes"] += 1

    def extract_skills(self) -> List[Skill]:
        new_skills = []

        for pattern, data in self._pattern_counts.items():
            if self._extractor.should_extract(pattern, data["count"], data["successes"]):
                skill = Skill(
                    name=pattern,
                    description=f"Learned from {data['count']} observations",
                    pattern=pattern,
                    usage_count=data["count"],
                    success_rate=data["successes"] / data["count"] if data["count"] > 0 else 0,
                    confidence=data["successes"] / data["count"] if data["count"] > 0 else 0,
                )
                new_skills.append(skill)

                if self.store:
                    self.store.put_skill(skill.to_dict())

        return new_skills

    def extract_from_patterns(self, patterns: List[dict],
                              playbooks: List[dict] = None) -> List[Skill]:
        new_skills = []

        for p in patterns:
            content = p.get("content", "")
            scene = p.get("scene", "")
            confidence = p.get("confidence", 0.5)

            skill_data = self._pattern_to_skill(content, scene, confidence)
            if skill_data:
                skill = Skill.from_dict(skill_data)
                new_skills.append(skill)
                if self.store:
                    self.store.put_skill(skill.to_dict())

        for pb in (playbooks or []):
            content = pb.get("content", "")
            scene = pb.get("scene", "")
            confidence = pb.get("confidence", 0.5)
            meta = pb.get("metadata", {})
            steps_raw = meta.get("steps", [])

            skill_data = self._playbook_to_skill(content, scene, confidence, steps_raw)
            if skill_data:
                skill = Skill.from_dict(skill_data)
                new_skills.append(skill)
                if self.store:
                    self.store.put_skill(skill.to_dict())

        logger.info("[openmemo:skill] extracted %d skills from patterns/playbooks", len(new_skills))
        return new_skills

    def _pattern_to_skill(self, content: str, scene: str, confidence: float) -> Optional[dict]:
        if self._llm_fn:
            try:
                prompt = (
                    f"Given this pattern:\n\n{content}\n\n"
                    f"Scene: {scene}\n\n"
                    "Convert it into an executable skill. Return JSON with fields: "
                    "skill_name, scene, trigger, steps (list), tools (list), description."
                )
                result = self._llm_fn(prompt)
                if isinstance(result, dict):
                    return {
                        "id": str(uuid.uuid4()),
                        "name": result.get("skill_name", content[:50]),
                        "description": result.get("description", content[:100]),
                        "pattern": content[:200],
                        "scene": result.get("scene", scene),
                        "trigger": result.get("trigger", ""),
                        "steps": result.get("steps", []),
                        "tools": result.get("tools", []),
                        "confidence": confidence,
                        "created_at": time.time(),
                        "updated_at": time.time(),
                    }
            except Exception as e:
                logger.warning("[openmemo:skill] LLM extraction failed: %s", e)

        name = self._extract_name(content)
        trigger = content[:100].strip()
        steps = [s.strip() for s in content.split(".") if s.strip()][:5]

        return {
            "id": str(uuid.uuid4()),
            "name": name,
            "description": content[:200],
            "pattern": content[:200],
            "scene": scene,
            "trigger": trigger,
            "steps": steps,
            "tools": [],
            "confidence": confidence,
            "created_at": time.time(),
            "updated_at": time.time(),
        }

    def _playbook_to_skill(self, content: str, scene: str,
                           confidence: float, steps: list) -> Optional[dict]:
        if self._llm_fn:
            try:
                prompt = (
                    f"Given this playbook:\n\n{content}\n\n"
                    f"Steps: {steps}\n"
                    f"Scene: {scene}\n\n"
                    "Convert it into an executable skill. Return JSON with fields: "
                    "skill_name, scene, trigger, steps (list), tools (list), description."
                )
                result = self._llm_fn(prompt)
                if isinstance(result, dict):
                    return {
                        "id": str(uuid.uuid4()),
                        "name": result.get("skill_name", content[:50]),
                        "description": result.get("description", content[:100]),
                        "pattern": content[:200],
                        "scene": result.get("scene", scene),
                        "trigger": result.get("trigger", ""),
                        "steps": result.get("steps", steps),
                        "tools": result.get("tools", []),
                        "confidence": confidence,
                        "created_at": time.time(),
                        "updated_at": time.time(),
                    }
            except Exception as e:
                logger.warning("[openmemo:skill] LLM playbook extraction failed: %s", e)

        name = self._extract_name(content)
        trigger = content[:100].strip()
        if not steps:
            steps = [s.strip() for s in content.split(".") if s.strip()][:5]

        return {
            "id": str(uuid.uuid4()),
            "name": name,
            "description": content[:200],
            "pattern": content[:200],
            "scene": scene,
            "trigger": trigger,
            "steps": steps,
            "tools": [],
            "confidence": confidence,
            "created_at": time.time(),
            "updated_at": time.time(),
        }

    def get_relevant_skills(self, context: str, scene: str = "",
                            top_k: int = 5) -> List[Skill]:
        if not self.store:
            return []

        all_skills = self.store.list_skills(scene=scene)

        scored = []
        for s in all_skills:
            if s.get("status") == SKILL_STATUS_DEPRECATED:
                continue
            relevance = self._scorer.score(s, context)
            if relevance > 0:
                scored.append((Skill.from_dict(s), relevance))

        scored.sort(key=lambda x: x[1], reverse=True)
        return [s for s, _ in scored[:top_k]]

    def recall_skills(self, query: str, scene: str = "",
                      top_k: int = 5) -> List[dict]:
        skills = self.get_relevant_skills(query, scene=scene, top_k=top_k)
        return [s.to_dict() for s in skills]

    def execute_skill(self, skill_id: str,
                      mode: str = EXEC_MODE_SUGGEST) -> dict:
        if not self.store:
            return {"error": "no store"}

        skill_data = self.store.get_skill(skill_id)
        if not skill_data:
            return {"error": "skill not found", "skill_id": skill_id}

        skill = Skill.from_dict(skill_data)
        steps = skill.steps or []

        if mode == EXEC_MODE_SUGGEST:
            return {
                "mode": "suggest",
                "skill_id": skill.id,
                "skill_name": skill.name,
                "suggested_steps": steps,
                "tools": skill.tools,
                "confidence": skill.confidence,
            }
        elif mode == EXEC_MODE_AUTO:
            results = []
            for i, step in enumerate(steps):
                results.append({
                    "step": i + 1,
                    "action": step,
                    "status": "executed",
                })
            return {
                "mode": "auto",
                "skill_id": skill.id,
                "skill_name": skill.name,
                "steps_executed": len(results),
                "results": results,
            }
        else:
            return {"error": f"unknown mode: {mode}"}

    def record_feedback(self, skill_id: str, success: bool,
                        result: str = "") -> dict:
        if not self.store:
            return {"error": "no store"}

        skill_data = self.store.get_skill(skill_id)
        if not skill_data:
            return {"error": "skill not found"}

        skill = Skill.from_dict(skill_data)
        skill.record_usage(success)
        self.store.put_skill(skill.to_dict())

        feedback = {
            "feedback_id": str(uuid.uuid4())[:12],
            "skill_id": skill_id,
            "result": result,
            "success": success,
            "timestamp": time.time(),
        }
        self.store.put_skill_feedback(feedback)

        logger.info("[openmemo:skill] feedback recorded for %s (success=%s)", skill_id, success)
        return {"status": "recorded", "feedback": feedback}

    def evolve_skills(self) -> dict:
        if not self.store:
            return {"merged": 0, "deprecated": 0, "promoted": 0}

        all_skills = self.store.list_skills()
        active_skills = [s for s in all_skills if s.get("status") != SKILL_STATUS_DEPRECATED]

        merged = self._merge_similar_skills(active_skills)
        deprecated = self._deprecate_weak_skills(active_skills)
        promoted = self._promote_strong_skills(active_skills)

        logger.info("[openmemo:skill] evolution: merged=%d deprecated=%d promoted=%d",
                     merged, deprecated, promoted)
        return {"merged": merged, "deprecated": deprecated, "promoted": promoted}

    def _merge_similar_skills(self, skills: List[dict]) -> int:
        merged_count = 0
        merged_ids = set()

        for i, a in enumerate(skills):
            if a["id"] in merged_ids:
                continue
            for j, b in enumerate(skills):
                if j <= i or b["id"] in merged_ids:
                    continue
                sim = self._skill_similarity(a, b)
                if sim > 0.9:
                    winner = a if compute_skill_score(a) >= compute_skill_score(b) else b
                    loser = b if winner is a else a

                    winner_skill = Skill.from_dict(winner)
                    loser_skill = Skill.from_dict(loser)
                    winner_skill.usage_count += loser_skill.usage_count
                    if winner_skill.usage_count > 0:
                        total = (winner_skill.success_rate * (winner_skill.usage_count - loser_skill.usage_count)
                                 + loser_skill.success_rate * loser_skill.usage_count)
                        winner_skill.success_rate = total / winner_skill.usage_count
                    winner_skill.confidence = max(winner_skill.confidence, loser_skill.confidence)
                    winner_skill.skill_version += 1
                    winner_skill.updated_at = time.time()

                    for step in loser_skill.steps:
                        if step not in winner_skill.steps:
                            winner_skill.steps.append(step)
                    for tool in loser_skill.tools:
                        if tool not in winner_skill.tools:
                            winner_skill.tools.append(tool)

                    self.store.put_skill(winner_skill.to_dict())

                    loser_skill.status = SKILL_STATUS_DEPRECATED
                    loser_skill.updated_at = time.time()
                    self.store.put_skill(loser_skill.to_dict())

                    merged_ids.add(loser["id"])
                    merged_count += 1

        return merged_count

    def _deprecate_weak_skills(self, skills: List[dict]) -> int:
        deprecated = 0
        for s in skills:
            usage = s.get("usage_count", 0)
            success_rate = s.get("success_rate", 0.0)
            if usage >= 5 and success_rate < 0.2:
                skill = Skill.from_dict(s)
                skill.status = SKILL_STATUS_DEPRECATED
                skill.updated_at = time.time()
                self.store.put_skill(skill.to_dict())
                deprecated += 1
        return deprecated

    def _promote_strong_skills(self, skills: List[dict]) -> int:
        promoted = 0
        for s in skills:
            if s.get("status") == SKILL_STATUS_VERIFIED:
                continue
            usage = s.get("usage_count", 0)
            success_rate = s.get("success_rate", 0.0)
            if usage > 5 and success_rate > 0.8:
                skill = Skill.from_dict(s)
                skill.status = SKILL_STATUS_VERIFIED
                skill.updated_at = time.time()
                self.store.put_skill(skill.to_dict())
                promoted += 1
        return promoted

    def _skill_similarity(self, a: dict, b: dict) -> float:
        name_a = set(a.get("name", "").lower().replace("_", " ").split())
        name_b = set(b.get("name", "").lower().replace("_", " ").split())
        if not name_a or not name_b:
            return 0.0
        name_sim = len(name_a & name_b) / max(len(name_a | name_b), 1)

        steps_a = set(s.lower() for s in a.get("steps", []))
        steps_b = set(s.lower() for s in b.get("steps", []))
        steps_sim = 0.0
        if steps_a and steps_b:
            steps_sim = len(steps_a & steps_b) / max(len(steps_a | steps_b), 1)

        scene_match = 1.0 if (a.get("scene", "") == b.get("scene", "") and a.get("scene")) else 0.0

        return name_sim * 0.4 + steps_sim * 0.4 + scene_match * 0.2

    def _extract_name(self, content: str) -> str:
        words = content.strip().split()[:5]
        name = "_".join(w.lower() for w in words if w.isalnum())
        return name or "unnamed_skill"

    def _normalize(self, action: str) -> str:
        return action.strip().lower()
