"""Dynamic skill library (Voyager-inspired).

The agent creates reusable procedures from successful debugging sessions,
searches them for relevant approaches, and tracks which skills actually work.

Storage: ``.hikaflow/skills.jsonl`` — JSONL, capped at MAX_SKILLS entries.
Skills start unverified and graduate to verified after VERIFY_THRESHOLD
successful uses.  Low-performing skills are auto-pruned.
"""

from __future__ import annotations

import json
import logging
import re
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

MAX_SKILLS = 200
VERIFY_THRESHOLD = 3
PRUNE_MIN_USES = 5
PRUNE_MAX_FAILURE_RATE = 0.70  # prune if success_rate < 30%


@dataclass
class DynamicSkill:
    """A learned debugging procedure."""

    skill_id: str
    description: str
    procedure: List[str]
    tags: List[str] = field(default_factory=list)
    created_from: str = ""
    created_at: str = ""
    verified: bool = False
    times_used: int = 0
    times_succeeded: int = 0

    @property
    def success_rate(self) -> float:
        if self.times_used == 0:
            return 0.0
        return self.times_succeeded / self.times_used

    def to_dict(self) -> dict:
        return {
            "skill_id": self.skill_id,
            "description": self.description,
            "procedure": self.procedure,
            "tags": self.tags,
            "created_from": self.created_from,
            "created_at": self.created_at,
            "verified": self.verified,
            "times_used": self.times_used,
            "times_succeeded": self.times_succeeded,
        }

    @classmethod
    def from_dict(cls, d: dict) -> DynamicSkill:
        return cls(
            skill_id=d.get("skill_id", ""),
            description=d.get("description", ""),
            procedure=d.get("procedure", []),
            tags=d.get("tags", []),
            created_from=d.get("created_from", ""),
            created_at=d.get("created_at", ""),
            verified=d.get("verified", False),
            times_used=d.get("times_used", 0),
            times_succeeded=d.get("times_succeeded", 0),
        )


def _skills_path(project_path: str) -> Path:
    return Path(project_path) / ".hikaflow" / "skills.jsonl"


def _read_all(path: Path) -> List[DynamicSkill]:
    """Read all skills from JSONL file."""
    if not path.is_file():
        return []
    results = []
    try:
        for line in path.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if line:
                try:
                    results.append(DynamicSkill.from_dict(json.loads(line)))
                except (json.JSONDecodeError, KeyError):
                    continue
    except Exception:
        logger.warning("Failed to read skills.jsonl", exc_info=True)
    return results


def _write_all(path: Path, skills: List[DynamicSkill]) -> None:
    """Rewrite the skills file."""
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        for s in skills:
            f.write(json.dumps(s.to_dict()) + "\n")


def _slugify(text: str) -> str:
    """Generate a skill_id slug from description text."""
    slug = re.sub(r"[^a-z0-9]+", "-", text.lower().strip())
    slug = slug.strip("-")
    return slug[:60] if slug else "skill"


def _auto_prune(skills: List[DynamicSkill]) -> List[DynamicSkill]:
    """Remove skills with enough uses but low success rate."""
    return [
        s for s in skills
        if s.times_used < PRUNE_MIN_USES or s.success_rate >= (1 - PRUNE_MAX_FAILURE_RATE)
    ]


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def save_skill(
    project_path: str,
    description: str,
    procedure: List[str],
    tags: Optional[List[str]] = None,
    session_id: str = "",
) -> str:
    """Create and save a new skill.  Returns the skill_id.

    Deduplicates by slug — if a skill with the same slug exists, it is
    updated in place rather than duplicated.
    """
    path = _skills_path(project_path)
    skills = _read_all(path)

    skill_id = _slugify(description)

    # Dedup: update existing skill with same slug
    for existing in skills:
        if existing.skill_id == skill_id:
            existing.description = description
            existing.procedure = procedure
            existing.tags = tags or existing.tags
            _write_all(path, skills)
            return skill_id

    skill = DynamicSkill(
        skill_id=skill_id,
        description=description,
        procedure=procedure,
        tags=tags or [],
        created_from=session_id,
        created_at=datetime.now(timezone.utc).isoformat(),
    )
    skills.append(skill)

    # Auto-prune low performers
    skills = _auto_prune(skills)

    # Cap at MAX_SKILLS — drop oldest unverified first
    if len(skills) > MAX_SKILLS:
        unverified = [s for s in skills if not s.verified]
        verified = [s for s in skills if s.verified]
        # Drop oldest unverified until under cap
        while len(unverified) + len(verified) > MAX_SKILLS and unverified:
            unverified.pop(0)
        skills = verified + unverified

    _write_all(path, skills)
    return skill_id


def search_skills(
    project_path: str,
    query: str,
    limit: int = 3,
) -> List[DynamicSkill]:
    """Multi-token keyword search across description, tags, and procedure."""
    path = _skills_path(project_path)
    skills = _read_all(path)
    if not skills or not query.strip():
        return []

    tokens = query.lower().split()
    scored: List[tuple[float, DynamicSkill]] = []

    for s in skills:
        searchable = (
            s.description.lower()
            + " " + " ".join(s.tags).lower()
            + " " + " ".join(s.procedure).lower()
        )
        match_count = sum(1 for t in tokens if t in searchable)
        if match_count == 0:
            continue
        # Tie-break: verified status + success_rate
        score = match_count * 10 + (5 if s.verified else 0) + s.success_rate
        scored.append((score, s))

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


def get_skill(project_path: str, skill_id: str) -> Optional[DynamicSkill]:
    """Load a single skill by ID."""
    path = _skills_path(project_path)
    for s in _read_all(path):
        if s.skill_id == skill_id:
            return s
    return None


def record_skill_outcome(
    project_path: str,
    skill_id: str,
    success: bool,
) -> bool:
    """Increment usage counters.  Auto-verifies at threshold.  Returns True if found."""
    path = _skills_path(project_path)
    skills = _read_all(path)

    found = False
    for s in skills:
        if s.skill_id == skill_id:
            s.times_used += 1
            if success:
                s.times_succeeded += 1
            if s.times_succeeded >= VERIFY_THRESHOLD and not s.verified:
                s.verified = True
            found = True
            break

    if found:
        skills = _auto_prune(skills)
        _write_all(path, skills)
    return found


def load_top_skills(project_path: str, limit: int = 5) -> List[DynamicSkill]:
    """Top skills for system prompt injection.  Verified first, then newest."""
    path = _skills_path(project_path)
    skills = _read_all(path)
    if not skills:
        return []

    verified = sorted(
        [s for s in skills if s.verified],
        key=lambda s: s.success_rate,
        reverse=True,
    )
    unverified = sorted(
        [s for s in skills if not s.verified],
        key=lambda s: s.created_at,
        reverse=True,
    )
    combined = verified + unverified
    return combined[:limit]


def render_skills_for_prompt(skills: List[DynamicSkill]) -> str:
    """Compact markdown list for system prompt injection."""
    if not skills:
        return ""

    lines = ["## Learned Skills\n"]
    for s in skills:
        badge = " [verified]" if s.verified else ""
        lines.append(f"- **{s.skill_id}**{badge}: {s.description[:80]}")
    lines.append("")
    lines.append(
        "Use `search_skills(query)` to find relevant skills or "
        "`use_skill(skill_id)` to load a full procedure."
    )
    return "\n".join(lines)


def auto_create_skill_from_session(
    project_path: str,
    blocks: Any,
) -> Optional[str]:
    """Extract a skill from a successful session's state.

    Only creates a skill if:
    1. Session has meaningful session_state (>50 chars)
    2. Outcome inferred as success
    3. Content has 2+ numbered/bulleted steps (structured procedure)

    Returns skill_id or None.
    """
    if blocks is None:
        return None

    session_state = blocks.get("session_state")
    if not session_state or len(session_state.strip()) < 50:
        return None

    # Infer outcome — only create skills from successful sessions
    lower = session_state.lower()
    if not any(w in lower for w in ["fixed", "resolved", "success", "done", "completed"]):
        return None

    # Extract procedure steps (numbered: "1." or bulleted: "- Step")
    lines = session_state.strip().split("\n")
    steps = []
    for line in lines:
        stripped = line.strip()
        if re.match(r"^\d+[\.\)]\s+", stripped) or stripped.startswith("- "):
            steps.append(stripped)

    if len(steps) < 2:
        return None

    # First line as description
    description = lines[0].strip()[:120]

    # Extract tags from content
    tags = _extract_tags(session_state)

    try:
        session_id = blocks.session_id if hasattr(blocks, "session_id") else ""
        return save_skill(
            project_path=project_path,
            description=description,
            procedure=steps,
            tags=tags,
            session_id=session_id,
        )
    except Exception:
        logger.warning("Failed to auto-create skill from session", exc_info=True)
        return None


def _extract_tags(text: str) -> List[str]:
    """Extract likely tags from session text."""
    tag_keywords = [
        "async", "race", "concurrency", "flaky", "timeout", "memory",
        "leak", "deadlock", "auth", "security", "sql", "api", "test",
        "import", "dependency", "config", "deploy", "docker", "redis",
        "database", "network", "retry", "cache", "permission",
    ]
    lower = text.lower()
    return [t for t in tag_keywords if t in lower]
