"""Evolve — improve existing skills based on execution stats and LLM analysis."""

from __future__ import annotations

import json
import logging
import shutil
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from rich.columns import Columns
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Confirm
from rich.syntax import Syntax

from alive.llm.base import LLMProvider

_log = logging.getLogger("alive")

SLOW_THRESHOLD_MS = 5000  # 5 seconds


# ── Data structures ──────────────────────────────────────────────────────────


@dataclass
class EvolutionSuggestion:
    """A suggested improvement for a skill."""

    skill_name: str
    reason: str  # "slow", "edge_cases", "deprecated", "general"
    summary: str
    improved_code: str
    original_code: str
    confidence: float  # 0-1, how confident the LLM is this helps


@dataclass
class EvolutionResult:
    """Result of an evolution attempt."""

    skill_name: str
    success: bool
    suggestion: EvolutionSuggestion | None = None
    error: str | None = None
    version: int = 0


# ── LLM prompts ──────────────────────────────────────────────────────────────

_ANALYZE_SYSTEM = """\
You are Alive's skill optimizer. You analyze working skills and suggest \
ONE specific, concrete improvement.

You will receive:
- The current skill code
- Execution statistics (times used, avg duration, success rate)
- The skill's description

Analyze and respond in this EXACT format:

```reason
slow | edge_cases | deprecated | general
```

```summary
One sentence describing the specific improvement.
```

```confidence
0.0 to 1.0 — how confident you are this improvement helps
```

```python improved
# The complete improved skill.py
```

Rules:
- Only suggest ONE improvement per analysis
- The improved code must be a complete, working replacement
- Keep the same run() signature and return schema
- Do NOT change the core behavior — only optimize
- If the skill is already well-written, set confidence < 0.3
- For slow skills: focus on algorithmic improvements, caching, or reducing I/O
- For edge cases: add input validation, error handling, boundary checks
- For deprecated: update to modern Python patterns and current library APIs"""


# ── Core logic ────────────────────────────────────────────────────────────────


async def analyze_skill(
    skill_name: str,
    *,
    provider: LLMProvider,
    reason: str = "general",
    execution_time_ms: int | None = None,
    skills_dir: Path | None = None,
) -> EvolutionSuggestion | None:
    """Ask the LLM to analyze a skill and suggest an improvement.

    Returns None if the skill doesn't need improvement.
    """
    from alive.config import SKILLS_DIR
    from alive.core.skill_registry import get_registry

    skills_dir = skills_dir or SKILLS_DIR
    skill_dir = skills_dir / skill_name
    skill_file = skill_dir / "skill.py"

    if not skill_file.exists():
        raise FileNotFoundError(
            f"Skill '{skill_name}' not found.\n\n"
            f"See available skills with: alive skills"
        )

    code = skill_file.read_text()

    # Gather stats from registry
    registry = get_registry()
    try:
        info = registry.get_skill(skill_name)
        stats = (
            f"Times used: {info.times_used}\n"
            f"Success rate: {info.confidence_score:.0%}\n"
            f"Description: {info.description}\n"
            f"Dependencies: {', '.join(info.dependencies) or 'none'}"
        )
    except KeyError:
        stats = "No execution stats available."

    if execution_time_ms is not None:
        stats += f"\nLast execution time: {execution_time_ms}ms"

    # Determine reason
    if execution_time_ms and execution_time_ms > SLOW_THRESHOLD_MS:
        reason = "slow"

    user_prompt = (
        f"Skill: {skill_name}\n"
        f"Reason for review: {reason}\n\n"
        f"Stats:\n{stats}\n\n"
        f"Current code:\n```python\n{code}\n```\n\n"
        f"Suggest ONE specific improvement."
    )

    response = await provider.complete(_ANALYZE_SYSTEM, user_prompt)
    return _parse_suggestion(skill_name, code, response)


def _parse_suggestion(skill_name: str, original_code: str, response: str) -> EvolutionSuggestion | None:
    """Parse the LLM's evolution suggestion."""
    import re

    reason = _extract(r"```reason\s*\n(.*?)```", response) or "general"
    summary = _extract(r"```summary\s*\n(.*?)```", response) or ""
    conf_str = _extract(r"```confidence\s*\n(.*?)```", response) or "0.5"
    improved = _extract(r"```python\s+improved\s*\n(.*?)```", response) or ""

    if not improved or not summary:
        return None

    try:
        confidence = float(conf_str.strip())
    except ValueError:
        confidence = 0.5

    # Low confidence = not worth suggesting
    if confidence < 0.3:
        return None

    return EvolutionSuggestion(
        skill_name=skill_name,
        reason=reason.strip(),
        summary=summary.strip(),
        improved_code=improved,
        original_code=original_code,
        confidence=min(1.0, max(0.0, confidence)),
    )


def _extract(pattern: str, text: str) -> str:
    """Extract first group from a regex pattern."""
    import re

    match = re.search(pattern, text, re.DOTALL)
    return match.group(1).strip() if match else ""


# ── Apply evolution ──────────────────────────────────────────────────────────


def apply_evolution(
    suggestion: EvolutionSuggestion,
    *,
    skills_dir: Path | None = None,
) -> EvolutionResult:
    """Apply an evolution suggestion — backup old code, write new code, test it."""
    from alive.config import SKILLS_DIR
    from alive.sandbox.runner import run_skill_code

    skills_dir = skills_dir or SKILLS_DIR
    skill_dir = skills_dir / suggestion.skill_name
    skill_file = skill_dir / "skill.py"
    meta_file = skill_dir / "metadata.json"

    if not skill_file.exists():
        return EvolutionResult(
            skill_name=suggestion.skill_name,
            success=False,
            error=f"Skill file not found: {skill_file}",
        )

    # Determine version number
    version = 1
    if meta_file.exists():
        try:
            meta = json.loads(meta_file.read_text())
            version = meta.get("evolution_version", 0) + 1
        except (json.JSONDecodeError, KeyError):
            pass

    # Backup current code
    backup_path = skill_dir / f"skill_v{version - 1}.py"
    shutil.copy2(str(skill_file), str(backup_path))

    # Test the new code first
    sample_input = {}
    if meta_file.exists():
        try:
            meta = json.loads(meta_file.read_text())
            sample_input = meta.get("sample_input", {})
        except (json.JSONDecodeError, KeyError):
            pass

    result = run_skill_code(suggestion.improved_code, sample_input)

    if not result.success:
        # Rollback — don't apply the change
        backup_path.unlink(missing_ok=True)
        return EvolutionResult(
            skill_name=suggestion.skill_name,
            success=False,
            suggestion=suggestion,
            error=f"Improved code failed testing: {result.error}",
            version=version,
        )

    # Apply — write new code
    skill_file.write_text(suggestion.improved_code)

    # Update metadata with version info
    if meta_file.exists():
        try:
            meta = json.loads(meta_file.read_text())
            meta["evolution_version"] = version
            meta.setdefault("evolution_history", []).append({
                "version": version,
                "reason": suggestion.reason,
                "summary": suggestion.summary,
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "confidence": suggestion.confidence,
            })
            meta_file.write_text(json.dumps(meta, indent=2) + "\n")
        except (json.JSONDecodeError, KeyError) as exc:
            _log.debug("Failed to update metadata for evolution: %s", exc)

    return EvolutionResult(
        skill_name=suggestion.skill_name,
        success=True,
        suggestion=suggestion,
        version=version,
    )


def rollback_skill(skill_name: str, *, skills_dir: Path | None = None) -> bool:
    """Rollback a skill to the previous version."""
    from alive.config import SKILLS_DIR

    skills_dir = skills_dir or SKILLS_DIR
    skill_dir = skills_dir / skill_name
    skill_file = skill_dir / "skill.py"
    meta_file = skill_dir / "metadata.json"

    if not skill_file.exists():
        return False

    # Find latest backup
    version = 0
    if meta_file.exists():
        try:
            meta = json.loads(meta_file.read_text())
            version = meta.get("evolution_version", 0)
        except (json.JSONDecodeError, KeyError):
            pass

    if version == 0:
        return False

    backup_path = skill_dir / f"skill_v{version - 1}.py"
    if not backup_path.exists():
        return False

    # Restore backup
    shutil.copy2(str(backup_path), str(skill_file))

    # Update metadata
    if meta_file.exists():
        try:
            meta = json.loads(meta_file.read_text())
            meta["evolution_version"] = version - 1
            history = meta.get("evolution_history", [])
            if history:
                history.pop()
            meta_file.write_text(json.dumps(meta, indent=2) + "\n")
        except (json.JSONDecodeError, KeyError) as exc:
            _log.debug("Failed to update metadata during rollback: %s", exc)

    return True


# ── Display ──────────────────────────────────────────────────────────────────


def display_suggestion(suggestion: EvolutionSuggestion, console: Console | None = None) -> None:
    """Show a before/after comparison for an evolution suggestion."""
    console = console or Console()

    reason_icons = {
        "slow": "🐌",
        "edge_cases": "🛡️",
        "deprecated": "📦",
        "general": "✨",
    }
    icon = reason_icons.get(suggestion.reason, "✨")

    console.print()
    console.print(Panel(
        f"[bold]{suggestion.skill_name}[/]\n\n"
        f"{icon}  [bold]{suggestion.summary}[/]\n\n"
        f"[dim]Reason:[/] {suggestion.reason}\n"
        f"[dim]Confidence:[/] {suggestion.confidence:.0%}",
        title="[bold yellow]Evolution Suggestion[/]",
        border_style="yellow",
        padding=(1, 2),
    ))

    # Side-by-side code comparison
    orig_lines = suggestion.original_code.splitlines()
    new_lines = suggestion.improved_code.splitlines()

    # Truncate for display
    max_lines = 40
    orig_display = "\n".join(orig_lines[:max_lines])
    if len(orig_lines) > max_lines:
        orig_display += f"\n# ... ({len(orig_lines) - max_lines} more lines)"

    new_display = "\n".join(new_lines[:max_lines])
    if len(new_lines) > max_lines:
        new_display += f"\n# ... ({len(new_lines) - max_lines} more lines)"

    console.print()
    console.print(Panel(
        Syntax(orig_display, "python", theme="monokai", line_numbers=True),
        title="[bold red]Before[/]",
        border_style="red",
    ))

    console.print(Panel(
        Syntax(new_display, "python", theme="monokai", line_numbers=True),
        title="[bold green]After[/]",
        border_style="green",
    ))


def display_evolution_result(result: EvolutionResult, console: Console | None = None) -> None:
    """Show the result of applying an evolution."""
    console = console or Console()

    if result.success:
        console.print(Panel(
            f"[bold green]{result.skill_name}[/] evolved to v{result.version}\n\n"
            f"[dim]Previous version backed up as skill_v{result.version - 1}.py[/]\n"
            f"[dim]Rollback with:[/] alive evolve {result.skill_name} --rollback",
            title="[bold green]Evolution Applied[/]",
            border_style="green",
            padding=(1, 2),
        ))
    else:
        console.print(Panel(
            f"Evolution failed for [bold]{result.skill_name}[/]\n\n"
            f"{result.error}\n\n"
            f"[dim]The original code was not modified.[/]",
            title="[bold red]Evolution Failed[/]",
            border_style="red",
            padding=(1, 2),
        ))


# ── Auto-suggest (called after skill execution) ─────────────────────────────


async def check_and_suggest(
    skill_name: str,
    execution_time_ms: int,
    *,
    provider: LLMProvider,
    console: Console | None = None,
) -> EvolutionSuggestion | None:
    """Check if a skill should be evolved after execution.

    Called automatically when a skill takes > SLOW_THRESHOLD_MS.
    Returns the suggestion if one was generated, None otherwise.
    """
    if execution_time_ms < SLOW_THRESHOLD_MS:
        return None

    console = console or Console()
    console.print()
    console.print(
        f"[yellow]⚡ {skill_name} took {execution_time_ms / 1000:.1f}s — "
        f"analyzing for optimizations...[/]"
    )

    try:
        suggestion = await analyze_skill(
            skill_name,
            provider=provider,
            reason="slow",
            execution_time_ms=execution_time_ms,
        )
    except (FileNotFoundError, Exception) as exc:
        _log.debug("Auto-evolution analysis failed: %s", exc)
        return None

    if suggestion:
        display_suggestion(suggestion, console)
        console.print()
        console.print("[dim]Apply with:[/] alive evolve " + skill_name)

    return suggestion
