"""Self-Extension Engine — builds new skills from natural language descriptions."""

from __future__ import annotations

import ast
import json
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from rich.console import Console
from rich.panel import Panel
from rich.syntax import Syntax

from alive.core.replay import (
    BattleContestant,
    BattleLog,
    BirthStoryRecorder,
    save_birth_story,
)
from alive.core.skill_registry import SkillInfo, SkillRegistry, get_registry
from alive.llm.base import LLMProvider
from alive.sandbox.dependency import DependencyManager, get_dependency_manager
from alive.sandbox.runner import run_skill_code

MAX_ATTEMPTS = 5
BATTLE_BENCHMARK_RUNS = 3


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


@dataclass
class BuildResult:
    """Parsed output from the LLM generation phase."""

    skill_name: str
    description: str
    code: str
    test_code: str
    dependencies: list[str]
    input_schema: dict[str, Any]
    output_schema: dict[str, Any]
    skill_doc: str
    sample_input: dict[str, Any]


@dataclass
class BuildAttempt:
    """Record of one build attempt."""

    attempt_number: int
    code: str
    test_code: str
    success: bool
    error: str | None = None
    stdout: str | None = None
    stderr: str | None = None


# ── LLM prompt templates ────────────────────────────────────────────────────

_ANALYZE_SYSTEM = """\
You are Alive's skill analyzer. You analyze capability requests to plan \
Python skill implementations.

Given a capability description and a list of existing skills, respond with \
a brief analysis covering:
- What the skill needs to do (inputs, outputs, edge cases)
- What Python libraries would be best suited
- Whether any existing skills can be composed or referenced
- A proposed function signature for run()

Be concise. No code yet — just the plan."""

_GENERATE_SYSTEM = """\
You are Alive's skill generator. You write Python skills that follow a strict template.

SKILL TEMPLATE RULES:
- A single file with a `run()` function as the entry point
- The function returns a dict
- Type hints on all parameters and return type
- Handle errors gracefully — return {{"success": False, "error": "..."}} \
rather than raising exceptions
- Only use standard library OR well-known PyPI packages
- Never use input() or any interactive I/O
- Keep the code focused — one skill, one purpose

You MUST respond in EXACTLY this format with these labeled code blocks:

```name
skill_name_here
```

```description
One-line description here.
```

```dependencies
package1
package2
```

```input_schema
{{"param1": "str", "param2": "int (optional, default 10)"}}
```

```output_schema
{{"result": "str", "success": "bool"}}
```

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

```python test
# Tests — must use: from skill import run
```

```python sample_input
{{"param1": "example value"}}
```

```markdown doc
# skill_name
Full SKILL.md documentation here.
```

PERSISTENT MEMORY (optional):
If the skill needs to remember data between runs (e.g. tracking prices, \
monitoring changes, counting events, caching results), use SkillMemory:

    from alive.memory import SkillMemory
    mem = SkillMemory("skill_name_here")
    mem.set("key", value)          # store any JSON-serializable value
    prev = mem.get("key")          # retrieve latest value (None if missing)
    mem.history("key")             # list of (timestamp, value) tuples

Only use SkillMemory when the skill genuinely needs to persist state across \
invocations. Most skills do NOT need memory.

IMPORTANT:
- The skill name must be valid snake_case
- Tests must import with `from skill import run` (not a package path)
- sample_input must be a valid Python dict that exercises the happy path
- Dependencies must NOT include standard library modules

{integrations_context}"""

_FIX_SUFFIX = """

You are fixing a skill that failed testing. You will receive the original \
code, the test code, and the error output. Fix the code to make it work.

IMPORTANT: Respond with the COMPLETE set of blocks again (not just a diff)."""

_BATTLE_SIMPLE_SUFFIX = (
    "\n\nIMPORTANT: Use the SIMPLEST, most straightforward approach. "
    "Prefer standard library. Prioritize readability and correctness."
)

_BATTLE_ALT_SUFFIX = (
    "\n\nIMPORTANT: Use a DIFFERENT approach than the obvious/simple one. "
    "Optimize for speed and robustness. Use a different algorithm or library. "
    "This is for a battle — be creative."
)

_APPROACH_SYSTEM = """\
You are Alive's approach namer. Given Python code for a skill, respond with \
a 2-5 word description of the approach used. Examples: "regex line scan", \
"json.loads recursive walk", "pandas dataframe", "itertools pipeline". \
Respond with ONLY the short description, nothing else."""


def _build_integrations_context() -> str:
    """Build dynamic integrations context for the skill generation prompt.

    Checks which integrations are configured and returns prompt text
    instructing the LLM to use them when appropriate.
    """
    try:
        from alive.integrations import get_available_integrations

        integrations = get_available_integrations()
    except Exception:
        return ""

    blocks: list[str] = []

    blocks.append("AVAILABLE INTEGRATIONS (use these in your skill code when relevant):")

    # Web search — always available
    ws = integrations.get("web_search", {})
    if ws.get("configured"):
        backend = ws.get("backend", "duckduckgo")
        blocks.append(f"""
WEB SEARCH — Always available ({backend}, no API key needed):
  from alive.integrations.web_search import search_web, search_and_read, search_news
  results = search_web('query', max_results=5)  # Returns [{{title, url, snippet}}]
  full_text = search_and_read('query')  # Search + fetch top result content
  news = search_news('query')  # Recent news [{{title, url, date, snippet}}]

  USE THIS whenever the task needs current/live information: prices,
  weather, news, stock data, product info, anything that changes.""")

    # Telegram
    tg = integrations.get("telegram", {})
    if tg.get("configured"):
        blocks.append("""
TELEGRAM ALERTS:
  from alive.integrations.telegram import send_telegram, send_telegram_photo
  send_telegram('Alert: Price dropped!')
  send_telegram_photo('/path/to/image.png', caption='Screenshot')

  USE THIS when the user says: alert me, notify me, message me,
  tell me, let me know, send me.""")

    # Browser
    br = integrations.get("browser", {})
    if br.get("configured"):
        blocks.append("""
BROWSER AUTOMATION (for JavaScript-heavy sites):
  from alive.integrations.browser import BrowserTool
  with BrowserTool() as browser:
      page = browser.new_page()
      page.goto('https://example.com')
      page.wait_for_load_state('networkidle')
      content = page.content()  # full rendered HTML
      page.screenshot(path='/tmp/screenshot.png')
      page.click('button#submit')
      page.fill('input#search', 'query')
      text = page.inner_text('.results')
      page.close()

  USE THIS when: the site uses JavaScript rendering, you need to
  click/fill forms, simple HTTP fetch doesn't get the content,
  or the user explicitly mentions a website to interact with.
  PREFER simple web_search for basic pages. Use browser only
  when JavaScript rendering is needed.""")

    # Webhook
    wh = integrations.get("webhook", {})
    if wh.get("configured"):
        names = wh.get("names", [])
        name_hint = f"'{names[0]}'" if names else "'default'"
        blocks.append(f"""
WEBHOOK NOTIFICATIONS:
  from alive.integrations.webhook import send_webhook
  send_webhook('Alert message', webhook_name={name_hint})
  send_webhook('Message', data={{'key': 'value'}})

  USE THIS when the user mentions: slack, discord, webhook,
  or wants to send data to an external service.""")

    rules = [
        "IMPORTANT RULES FOR SKILL GENERATION:",
        "- ALWAYS use web_search for current/live data. Never hardcode URLs",
        "  for data that changes. Search first, then fetch.",
        "- When the user asks to \"monitor\" or \"track\" something, build the",
        "  skill to do ONE check. The scheduler handles repetition externally.",
        "  The skill should return the current state + whether a condition is met.",
        "- When the user says \"alert me\" or \"notify me\", include the",
        "  appropriate notification call at the end of the skill.",
    ]
    if br.get("configured"):
        rules.append("- When building web scraping skills, TRY simple httpx fetch first.")
        rules.append("  Only use BrowserTool if the content requires JavaScript rendering.")
    else:
        rules.append("- When building web scraping skills, use httpx for fetching content.")
    blocks.append("\n" + "\n".join(rules))

    return "\n".join(blocks)


# ── Battle result ─────────────────────────────────────────────────────────


@dataclass
class BattleResult:
    """Outcome of a battle between two approaches."""

    winner: BuildResult  # the winning BuildResult
    battle_log: BattleLog
    used_fallback: bool = False


# ── SkillBuilder class ──────────────────────────────────────────────────────


class SkillBuilder:
    """Builds a new skill from a natural language description."""

    def __init__(
        self,
        description: str,
        *,
        console: Console | None = None,
        verbose: bool = False,
        provider: LLMProvider | None = None,
        registry: SkillRegistry | None = None,
        battle: bool = True,
    ) -> None:
        self._description = description
        self._console = console or Console()
        self._verbose = verbose
        self._provider = provider
        self._registry = registry
        self._battle = battle
        self._attempts: list[BuildAttempt] = []
        self._recorder: BirthStoryRecorder | None = None

    async def build(self) -> SkillInfo:
        """Run the full build pipeline: analyze -> battle/generate -> test -> retry -> save."""
        provider = self._get_provider()
        mode_label = "Battle Mode" if self._battle else "Self-Extension Engine"
        self._console.print(
            Panel(
                f"[bold]Building skill:[/] {self._description}\n"
                f"[dim]Provider: {provider.name} | Mode: {'battle' if self._battle else 'standard'}[/]",
                title=f"[bold cyan]{mode_label}[/]",
                border_style="cyan",
            )
        )

        # Phase 1: Analyze
        with self._console.status("[bold cyan]  Analyzing capability...", spinner="dots"):
            analysis = await self._analyze()

        # Phase 2: Generate (battle or single)
        battle_log: BattleLog | None = None

        if self._battle:
            battle_result = await self._battle_build(analysis)
            if battle_result:
                result = battle_result.winner
                battle_log = battle_result.battle_log
            else:
                # Battle failed entirely — fallback to normal
                with self._console.status("[bold cyan]  Generating code...", spinner="dots"):
                    result = await self._generate(analysis)
        else:
            with self._console.status("[bold cyan]  Generating code...", spinner="dots"):
                result = await self._generate(analysis)

        if self._verbose:
            self._show_code(result)

        # Start recording the birth story
        self._recorder = BirthStoryRecorder(
            skill_name=result.skill_name,
            original_request=self._description,
            plan_step=analysis[:200],
        )

        # If battle already produced a passing result, skip retry loop
        battle_already_passed = battle_log is not None and battle_log.winner != "fallback"

        if not battle_already_passed:
            # Phase 3+4: Test with retry loop
            for attempt_num in range(1, MAX_ATTEMPTS + 1):
                self._console.print(f"\n[bold cyan]  Attempt {attempt_num}/{MAX_ATTEMPTS}[/]")

                with self._console.status("[bold cyan]  Installing dependencies...", spinner="dots"):
                    self._install_dependencies(result.dependencies)

                with self._console.status("[bold cyan]  Testing in sandbox...", spinner="dots"):
                    attempt = self._test(result)
                self._attempts.append(attempt)

                # Record attempt in birth story
                if self._recorder:
                    self._recorder.record_attempt(
                        attempt_num,
                        result.code,
                        success=attempt.success,
                        error=attempt.error,
                    )

                if attempt.success:
                    self._console.print("[bold green]  Tests passed.[/]")
                    break

                # Show error
                error_text = attempt.stderr or attempt.error or "Unknown error"
                self._console.print(
                    Panel(
                        error_text[:500],
                        title=f"[bold red]Test Failed (attempt {attempt_num})[/]",
                        border_style="red",
                    )
                )

                if attempt_num == MAX_ATTEMPTS:
                    self._console.print(
                        Panel(
                            f"Failed to build skill after {MAX_ATTEMPTS} attempts.\n"
                            f"Description: {self._description}",
                            title="[bold red]Build Failed[/]",
                            border_style="red",
                        )
                    )
                    try:
                        from alive.core.self_model import record_gap

                        record_gap(self._description, error_text[:200])
                    except Exception as exc:
                        import logging
                        logging.getLogger("alive").debug("Gap tracking failed: %s", exc)
                    raise RuntimeError(
                        f"Failed to build skill '{result.skill_name}' "
                        f"after {MAX_ATTEMPTS} attempts. "
                        f"Last error: {error_text[:200]}"
                    )

                # Fix
                with self._console.status("[bold yellow]  Fixing code...", spinner="dots"):
                    result = await self._fix(result, attempt)

                if self._verbose:
                    self._show_code(result)

        # Phase 5: Save
        with self._console.status("[bold cyan]  Saving skill...", spinner="dots"):
            skill_info = self._save(result)

        # Save the birth story
        if self._recorder:
            try:
                n_attempts = len(self._attempts)
                if battle_already_passed:
                    approach = f"Won in battle mode."
                elif n_attempts <= 1:
                    approach = "Built successfully on first try."
                else:
                    approach = f"Took {n_attempts} attempts. Final code resolved earlier failures."
                story = self._recorder.finalize(final_approach=approach)
                story.battle_log = battle_log
                save_birth_story(story)
            except Exception as exc:
                import logging
                logging.getLogger("alive").debug("Failed to save birth story: %s", exc)

        battle_suffix = ""
        if battle_log and battle_log.winner != "fallback":
            battle_suffix = f"\n[dim]Mode:[/] battle (winner selected from 2 approaches)"

        self._console.print(
            Panel(
                f"[bold green]{result.skill_name}[/]\n\n"
                f"{result.description}\n\n"
                f"[dim]Dependencies:[/] {', '.join(result.dependencies) or 'none'}\n"
                f"[dim]Attempts:[/] {len(self._attempts) or 'battle winner'}"
                f"{battle_suffix}\n"
                f"[dim]Location:[/] ~/.alive/skills/{result.skill_name}/",
                title="[bold green]Skill Built Successfully[/]",
                border_style="green",
            )
        )
        return skill_info

    # ── Battle mode ──────────────────────────────────────────────────────

    async def _battle_build(self, analysis: str) -> BattleResult | None:
        """Generate two implementations, test both, benchmark, pick the winner."""
        self._console.print()
        self._console.print("[bold yellow]  ⚔️  Battle Mode — generating two approaches...[/]")

        # Generate both approaches concurrently
        prompt_base = (
            f'Build a skill for: "{self._description}"\n\n'
            f"Analysis:\n{analysis}"
        )

        gen_system = self._get_generate_system()

        with self._console.status("[bold yellow]  Generating Approach A (simple)...", spinner="dots"):
            try:
                resp_a = await self._llm(gen_system, prompt_base + _BATTLE_SIMPLE_SUFFIX)
                result_a = self._parse_llm_response(resp_a)
            except (ValueError, Exception) as exc:
                import logging
                logging.getLogger("alive").debug("Battle Approach A generation failed: %s", exc)
                return None

        with self._console.status("[bold yellow]  Generating Approach B (alternative)...", spinner="dots"):
            try:
                resp_b = await self._llm(gen_system, prompt_base + _BATTLE_ALT_SUFFIX)
                result_b = self._parse_llm_response(resp_b)
            except (ValueError, Exception) as exc:
                import logging
                logging.getLogger("alive").debug("Battle Approach B generation failed: %s", exc)
                return None

        # Name the approaches
        approach_a = await self._name_approach(result_a.code)
        approach_b = await self._name_approach(result_b.code)

        # Install deps for both
        all_deps = list(set(result_a.dependencies + result_b.dependencies))
        if all_deps:
            with self._console.status("[bold yellow]  Installing dependencies...", spinner="dots"):
                self._install_dependencies(all_deps)

        # Test Approach A
        self._console.print(f"\n  [bold]Approach A:[/] {approach_a}")
        with self._console.status("[bold yellow]    Testing Approach A...", spinner="dots"):
            test_a = self._test(result_a)
        a_passed = test_a.success

        if a_passed:
            self._console.print(f"    [green]→ Tests passed.[/]")
        else:
            error_preview = (test_a.error or "")[:80]
            self._console.print(f"    [red]→ Tests failed.[/] {error_preview}")

        # Test Approach B
        self._console.print(f"\n  [bold]Approach B:[/] {approach_b}")
        with self._console.status("[bold yellow]    Testing Approach B...", spinner="dots"):
            test_b = self._test(result_b)
        b_passed = test_b.success

        if b_passed:
            self._console.print(f"    [green]→ Tests passed.[/]")
        else:
            error_preview = (test_b.error or "")[:80]
            self._console.print(f"    [red]→ Tests failed.[/] {error_preview}")

        # Neither passed → fallback
        if not a_passed and not b_passed:
            self._console.print(
                "\n  [yellow]Neither approach passed. Falling back to standard build...[/]"
            )
            fallback_log = BattleLog(
                contestant_a=BattleContestant(
                    label="A", approach=approach_a, code=result_a.code,
                    passed=False, error=test_a.error,
                ),
                contestant_b=BattleContestant(
                    label="B", approach=approach_b, code=result_b.code,
                    passed=False, error=test_b.error,
                ),
                winner="fallback",
            )
            return BattleResult(winner=result_a, battle_log=fallback_log, used_fallback=True)

        # Only one passed
        if a_passed and not b_passed:
            avg_a = await self._benchmark(result_a)
            self._console.print(f"    Avg time: {avg_a / 1000:.1f}s")
            self._display_battle_panel(
                result_a.skill_name, approach_a, approach_b,
                a_passed, b_passed, avg_a, 0, test_a.error, test_b.error,
            )
            return BattleResult(
                winner=result_a,
                battle_log=BattleLog(
                    contestant_a=BattleContestant(
                        label="A", approach=approach_a, code=result_a.code,
                        passed=True, avg_time_ms=avg_a,
                    ),
                    contestant_b=BattleContestant(
                        label="B", approach=approach_b, code=result_b.code,
                        passed=False, error=test_b.error,
                    ),
                    winner="A",
                ),
            )

        if b_passed and not a_passed:
            avg_b = await self._benchmark(result_b)
            self._console.print(f"    Avg time: {avg_b / 1000:.1f}s")
            self._display_battle_panel(
                result_b.skill_name, approach_a, approach_b,
                a_passed, b_passed, 0, avg_b, test_a.error, test_b.error,
            )
            return BattleResult(
                winner=result_b,
                battle_log=BattleLog(
                    contestant_a=BattleContestant(
                        label="A", approach=approach_a, code=result_a.code,
                        passed=False, error=test_a.error,
                    ),
                    contestant_b=BattleContestant(
                        label="B", approach=approach_b, code=result_b.code,
                        passed=True, avg_time_ms=avg_b,
                    ),
                    winner="B",
                ),
            )

        # Both passed → benchmark both
        self._console.print(f"\n  [bold cyan]Both passed! Benchmarking ({BATTLE_BENCHMARK_RUNS} runs each)...[/]")

        with self._console.status("[bold yellow]    Benchmarking Approach A...", spinner="dots"):
            avg_a = await self._benchmark(result_a)
        self._console.print(f"    Approach A avg: {avg_a / 1000:.1f}s")

        with self._console.status("[bold yellow]    Benchmarking Approach B...", spinner="dots"):
            avg_b = await self._benchmark(result_b)
        self._console.print(f"    Approach B avg: {avg_b / 1000:.1f}s")

        # Pick winner
        if avg_a <= avg_b:
            winner_label = "A"
            winner_result = result_a
            speedup = avg_b / avg_a if avg_a > 0 else 1.0
        else:
            winner_label = "B"
            winner_result = result_b
            speedup = avg_a / avg_b if avg_b > 0 else 1.0

        self._display_battle_panel(
            winner_result.skill_name, approach_a, approach_b,
            True, True, avg_a, avg_b, None, None,
        )

        return BattleResult(
            winner=winner_result,
            battle_log=BattleLog(
                contestant_a=BattleContestant(
                    label="A", approach=approach_a, code=result_a.code,
                    passed=True, avg_time_ms=avg_a,
                ),
                contestant_b=BattleContestant(
                    label="B", approach=approach_b, code=result_b.code,
                    passed=True, avg_time_ms=avg_b,
                ),
                winner=winner_label,
                speedup=round(speedup, 1),
            ),
        )

    async def _benchmark(self, result: BuildResult) -> float:
        """Run a skill multiple times and return the average duration in ms."""
        dm = self._get_dep_manager()
        total = 0
        for _ in range(BATTLE_BENCHMARK_RUNS):
            sr = run_skill_code(result.code, result.sample_input, dep_manager=dm)
            total += sr.duration_ms
        return total / BATTLE_BENCHMARK_RUNS

    async def _name_approach(self, code: str) -> str:
        """Ask the LLM to name an approach in 2-5 words."""
        try:
            name = await self._llm(_APPROACH_SYSTEM, code[:1500])
            # Clean — strip quotes, limit length
            name = name.strip().strip('"\'').strip()
            if len(name) > 50:
                name = name[:50]
            return name or "unknown approach"
        except Exception:
            return "unknown approach"

    def _display_battle_panel(
        self,
        skill_name: str,
        approach_a: str,
        approach_b: str,
        a_passed: bool,
        b_passed: bool,
        avg_a: float,
        avg_b: float,
        error_a: str | None,
        error_b: str | None,
    ) -> None:
        """Display the final battle result panel."""
        lines = [f"[bold]Building:[/] {skill_name}", ""]

        # Approach A
        lines.append(f"[bold]Approach A:[/] {approach_a}")
        if a_passed:
            lines.append(f"  → [green]Tests passed.[/] Avg time: {avg_a / 1000:.1f}s")
        else:
            lines.append(f"  → [red]Tests failed.[/]")
        lines.append("")

        # Approach B
        lines.append(f"[bold]Approach B:[/] {approach_b}")
        if b_passed:
            lines.append(f"  → [green]Tests passed.[/] Avg time: {avg_b / 1000:.1f}s")
        else:
            lines.append(f"  → [red]Tests failed.[/]")
        lines.append("")

        # Winner
        if a_passed and b_passed:
            if avg_a <= avg_b:
                speedup = avg_b / avg_a if avg_a > 0 else 1.0
                lines.append(f"[bold green]🏆 Winner: Approach A ({speedup:.1f}x faster)[/]")
            else:
                speedup = avg_a / avg_b if avg_b > 0 else 1.0
                lines.append(f"[bold green]🏆 Winner: Approach B ({speedup:.1f}x faster)[/]")
        elif a_passed:
            lines.append("[bold green]🏆 Winner: Approach A (only one that passed)[/]")
        elif b_passed:
            lines.append("[bold green]🏆 Winner: Approach B (only one that passed)[/]")

        self._console.print()
        self._console.print(Panel(
            "\n".join(lines),
            title="[bold yellow]⚔️  Battle Mode[/]",
            border_style="yellow",
            padding=(1, 2),
        ))

    # ── Phase methods ────────────────────────────────────────────────────

    async def _analyze(self) -> str:
        """Phase 1: Analyze the capability request."""
        registry = self._registry or get_registry()
        existing = registry.search(self._description)[:5]

        if existing:
            skills_text = "\n".join(
                f"- {s.name}: {s.description} "
                f"(inputs: {s.input_schema}, outputs: {s.output_schema})"
                for s in existing
            )
        else:
            skills_text = "(none relevant)"

        user_prompt = (
            f'Capability requested: "{self._description}"\n\n'
            f"Existing skills that might help:\n{skills_text}\n\n"
            f"Analyze this capability and plan the implementation."
        )
        return await self._llm(_ANALYZE_SYSTEM, user_prompt)

    def _get_generate_system(self) -> str:
        """Return the generation system prompt with integrations context injected."""
        ctx = _build_integrations_context()
        return _GENERATE_SYSTEM.format(integrations_context=ctx)

    async def _generate(self, analysis: str) -> BuildResult:
        """Phase 2: Ask LLM to generate skill code."""
        user_prompt = (
            f'Build a skill for: "{self._description}"\n\n'
            f"Analysis:\n{analysis}"
        )
        response = await self._llm(self._get_generate_system(), user_prompt)
        return self._parse_llm_response(response)

    def _test(self, result: BuildResult) -> BuildAttempt:
        """Phase 3: Run generated code in a subprocess sandbox."""
        dm = self._get_dep_manager()
        sr = run_skill_code(
            result.code,
            result.sample_input,
            dep_manager=dm,
        )
        return BuildAttempt(
            attempt_number=len(self._attempts) + 1,
            code=result.code,
            test_code=result.test_code,
            success=sr.success,
            stdout=json.dumps(sr.output, default=str) if sr.output else None,
            stderr=sr.error,
            error=sr.error,
        )

    async def _fix(self, result: BuildResult, attempt: BuildAttempt) -> BuildResult:
        """Phase 4: Send error back to LLM for fixing."""
        attempt_num = len(self._attempts)
        different = (
            "\n\nThe previous approach is NOT WORKING. "
            "Try a FUNDAMENTALLY DIFFERENT approach — "
            "different algorithm, different library, different strategy."
            if attempt_num >= 3
            else ""
        )

        error_output = (attempt.stderr or "") + (attempt.stdout or "")
        user_prompt = (
            f"The skill failed testing. Fix it.\n\n"
            f"ORIGINAL CODE:\n```python\n{result.code}\n```\n\n"
            f"TEST CODE:\n```python\n{result.test_code}\n```\n\n"
            f"ERROR OUTPUT:\n```\n{error_output[:2000]}\n```\n\n"
            f"Attempt: {attempt_num}/{MAX_ATTEMPTS}"
            f"{different}\n\n"
            f"Respond with the COMPLETE set of blocks again."
        )
        response = await self._llm(self._get_generate_system() + _FIX_SUFFIX, user_prompt)
        return self._parse_llm_response(response)

    def _install_dependencies(self, dependencies: list[str]) -> None:
        """Install dependencies into the shared venv."""
        if not dependencies:
            return
        dm = self._get_dep_manager()
        dm.install(dependencies)

    def _save(self, result: BuildResult) -> SkillInfo:
        """Phase 5: Register the completed skill."""
        registry = self._registry or get_registry()

        # Handle name collision
        name = result.skill_name
        if name in {s.name for s in registry.list_all()}:
            for i in range(2, 100):
                candidate = f"{name}_{i}"
                if candidate not in {s.name for s in registry.list_all()}:
                    name = candidate
                    break

        return registry.register(
            name=name,
            code=result.code,
            description=result.description,
            dependencies=result.dependencies,
            input_schema=result.input_schema,
            output_schema=result.output_schema,
            sample_input=result.sample_input,
            test_code=result.test_code,
            skill_doc=result.skill_doc,
        )

    # ── Helpers ──────────────────────────────────────────────────────────

    def _get_provider(self) -> LLMProvider:
        if self._provider is None:
            from alive.llm import get_provider

            self._provider = get_provider()
        return self._provider

    async def _llm(self, system_prompt: str, user_prompt: str) -> str:
        provider = self._get_provider()
        return await provider.complete(system_prompt, user_prompt)

    def _get_dep_manager(self) -> DependencyManager:
        """Return the dependency manager (lazy singleton)."""
        return get_dependency_manager()

    def _get_venv_python(self) -> Path:
        """Return path to the venv Python binary."""
        dm = self._get_dep_manager()
        dm.ensure_venv()
        return dm.python

    def _parse_llm_response(self, response: str) -> BuildResult:
        """Parse the structured LLM response into a BuildResult."""
        code = _extract_block("skill", response)
        if not code:
            raise ValueError("LLM response missing skill code block")

        raw_name = _extract_block("name", response) or self._derive_name()
        skill_name = re.sub(r"[^a-z0-9_]", "", raw_name.lower().replace("-", "_").replace(" ", "_"))
        skill_name = skill_name.strip("_") or "unnamed_skill"

        description = _extract_block("description", response) or self._description

        test_code = _extract_block("test", response) or (
            f'"""Auto-generated tests for {skill_name}."""\n\n'
            f"from skill import run\n\n\n"
            f"def test_{skill_name}_runs():\n"
            f"    assert callable(run)\n"
        )

        skill_doc = _extract_block("doc", response) or f"# {skill_name}\n\n{description}\n"

        deps_raw = _extract_block("dependencies", response)
        dependencies = [d.strip() for d in deps_raw.splitlines() if d.strip()] if deps_raw else []

        input_schema = _safe_parse_json(_extract_block("input_schema", response))
        output_schema = _safe_parse_json(_extract_block("output_schema", response))

        sample_raw = _extract_block("sample_input", response)
        sample_input = _safe_eval_dict(sample_raw) if sample_raw else {}

        return BuildResult(
            skill_name=skill_name,
            description=description,
            code=code,
            test_code=test_code,
            dependencies=dependencies,
            input_schema=input_schema,
            output_schema=output_schema,
            skill_doc=skill_doc,
            sample_input=sample_input,
        )

    def _derive_name(self) -> str:
        """Derive a skill name from the description."""
        words = re.findall(r"[a-z]+", self._description.lower())
        # Filter stop words
        stop = {"a", "an", "the", "to", "from", "and", "or", "of", "in", "for", "with", "that", "this", "is"}
        significant = [w for w in words if w not in stop][:4]
        return "_".join(significant) or "unnamed_skill"

    def _show_code(self, result: BuildResult) -> None:
        """Display generated code with syntax highlighting (verbose mode)."""
        self._console.print(
            Panel(
                Syntax(result.code, "python", theme="monokai", line_numbers=True),
                title=f"[bold]{result.skill_name}/skill.py[/]",
                border_style="dim",
            )
        )


# ── Parsing helpers (module-level) ──────────────────────────────────────────


def _extract_block(label: str, response: str) -> str:
    """Extract content from a labeled markdown code block."""
    patterns = [
        # ```python skill  or  ```python test  etc.
        rf"```\w+\s+{re.escape(label)}\s*\n(.*?)```",
        # ```markdown doc
        rf"```\w+\s+{re.escape(label)}\s*\n(.*?)```",
        # ```name  or  ```description  etc.
        rf"```{re.escape(label)}\s*\n(.*?)```",
    ]
    for pattern in patterns:
        match = re.search(pattern, response, re.DOTALL)
        if match:
            return match.group(1).strip()
    return ""


def _safe_parse_json(text: str) -> dict[str, Any]:
    """Parse JSON, falling back to ast.literal_eval for Python dicts."""
    if not text:
        return {}
    try:
        result = json.loads(text)
        return result if isinstance(result, dict) else {}
    except json.JSONDecodeError:
        try:
            result = ast.literal_eval(text)
            return result if isinstance(result, dict) else {}
        except (ValueError, SyntaxError, TypeError):
            return {}


def _safe_eval_dict(text: str) -> dict[str, Any]:
    """Safely evaluate a Python dict literal."""
    if not text:
        return {}
    try:
        result = ast.literal_eval(text)
        return result if isinstance(result, dict) else {}
    except (ValueError, SyntaxError, TypeError):
        try:
            return json.loads(text)
        except json.JSONDecodeError:
            return {}


# ── Public API ──────────────────────────────────────────────────────────────


async def build_skill(
    description: str,
    *,
    console: Console | None = None,
    verbose: bool = False,
    provider: LLMProvider | None = None,
    registry: SkillRegistry | None = None,
    battle: bool = True,
) -> SkillInfo:
    """Build a new skill from a natural language capability description.

    This is the main entry point for the Self-Extension Engine. It:
    1. Analyzes the capability request
    2. Generates two competing implementations (battle mode) or a single one
    3. Tests the code in a sandboxed subprocess
    4. Retries up to 5 times on failure
    5. Saves the skill to the registry on success

    Args:
        description: What the skill should do, in plain English.
        console: Rich console for output. Defaults to a new Console.
        verbose: If True, show generated code with syntax highlighting.
        provider: LLM provider override. Defaults to ALIVE_PROVIDER.
        registry: SkillRegistry override. Defaults to global registry.
        battle: If True, use battle mode (two competing implementations).
                Defaults to True.

    Returns:
        SkillInfo for the newly registered skill.

    Raises:
        RuntimeError: If the skill could not be built after 5 attempts.
    """
    builder = SkillBuilder(
        description,
        console=console,
        verbose=verbose,
        provider=provider,
        registry=registry,
        battle=battle,
    )
    return await builder.build()
