"""Builder agent -- Code modifier.

Creates branches, writes code, runs sandbox validation, opens PRs.
The ONLY agent that modifies application source code.

Scope boundary: Builder modifies code ONLY through git branches and PRs.
Never pushes to main. Never merges without approval.

This is the ENGINE that makes Hive self-modifying:
  Receive task → Create branch → Generate code via LLM → Run sandbox → Create PR
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from pathlib import Path

from hive.agents.base import AgentMessage, AgentRole, BaseAgent, MessageType, Priority
from hive.git.operations import GitOperations
from hive.llm.client import HiveLLMClient
from hive.sandbox.executor import SandboxExecutor, SandboxResult

logger = logging.getLogger("hive.builder")

# Maximum iterations to fix sandbox failures before escalating
MAX_ITERATIONS = 3


@dataclass
class BuildTask:
    """A task assigned to Builder by CEO."""

    item_id: str
    title: str
    problem: str
    solution: str
    files_to_modify: list[str]
    files_to_create: list[str] = field(default_factory=list)
    tests_to_add: list[str] = field(default_factory=list)
    risk: str = "low"
    branch: str = ""
    status: str = "assigned"
    iterations: int = 0
    created: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
    pr_url: str = ""
    sandbox_results: list[dict[str, Any]] = field(default_factory=list)


class Builder(BaseAgent):
    """Code modifier -- creates branches, writes code, opens PRs.

    Full implementation workflow:
    1. Receive task from CEO (approved proposal)
    2. Create git branch: hive/HIVE-NNN-description
    3. Use LLM to generate code changes
    4. Apply changes to files
    5. Run sandbox validation
    6. If sandbox passes: commit, push, create PR
    7. If sandbox fails: analyze, fix, retry (max 3 iterations)
    8. If still failing after 3: escalate to CEO
    """

    role = AgentRole.BUILDER

    def __init__(
        self,
        workspace_root: Path,
        llm: HiveLLMClient | None = None,
        git: GitOperations | None = None,
        sandbox: SandboxExecutor | None = None,
    ) -> None:
        super().__init__(workspace_root)
        self._llm = llm or HiveLLMClient()
        self._git = git or GitOperations(workspace_root)
        self._sandbox = sandbox or SandboxExecutor(workspace_root)
        self._active_tasks: list[BuildTask] = []
        self._completed_tasks: list[BuildTask] = []

    async def execute_task(self, task: BuildTask) -> BuildTask:
        """Execute a full build cycle for a task.

        This is the core self-modification loop:
        branch → generate code → test → iterate or submit.
        """
        self._active_tasks.append(task)
        logger.info("Builder starting task: %s -- %s", task.item_id, task.title)

        # Step 1: Create branch
        branch_result = self._git.prepare_change(task.item_id, task.title)
        if not branch_result.success:
            task.status = "failed"
            task.branch = ""
            logger.error("Failed to create branch: %s", branch_result.stderr)
            return self._complete_task(task, success=False, notes=f"Branch creation failed: {branch_result.stderr}")

        task.branch = self._git.current_branch() or ""
        task.status = "building"

        # Step 2-5: Generate, apply, test, iterate
        for iteration in range(1, MAX_ITERATIONS + 1):
            task.iterations = iteration
            logger.info("Builder iteration %d/%d for %s", iteration, MAX_ITERATIONS, task.item_id)

            # Generate code changes via LLM
            changes = await self._generate_changes(task, iteration)
            if not changes:
                task.status = "failed"
                return self._complete_task(task, success=False, notes="LLM failed to generate code")

            # Apply changes to disk
            applied = self._apply_changes(changes)
            if not applied:
                task.status = "failed"
                return self._complete_task(task, success=False, notes="Failed to apply changes to files")

            # Run sandbox validation
            sandbox_result = await self._sandbox.validate(
                changed_files=task.files_to_modify + task.files_to_create,
            )
            task.sandbox_results.append(
                {
                    "iteration": iteration,
                    "passed": sandbox_result.passed,
                    "tests": f"{sandbox_result.tests_passed}/{sandbox_result.tests_total}",
                    "lint_errors": sandbox_result.lint_errors,
                }
            )

            if sandbox_result.passed:
                # Submit: commit, push, create PR
                return await self._submit_pr(task, sandbox_result)

            # Sandbox failed -- log and retry with failure context
            logger.warning(
                "Sandbox failed iteration %d: tests=%d/%d, lint=%d",
                iteration,
                sandbox_result.tests_passed,
                sandbox_result.tests_total,
                sandbox_result.lint_errors,
            )

        # All iterations exhausted -- escalate
        task.status = "escalated"
        return self._complete_task(
            task,
            success=False,
            notes=f"Sandbox failed after {MAX_ITERATIONS} iterations. Escalating.",
        )

    async def _generate_changes(self, task: BuildTask, iteration: int) -> dict[str, str] | None:
        """Use LLM to generate code changes for a task.

        Returns: dict mapping file paths to their new content.
        """
        # Build context from codebase
        context_parts = []

        # Read identity and constraints
        identity = self.read_identity()
        constraints = self.read_constraints()
        if identity:
            context_parts.append(f"## Application Identity\n{identity[:2000]}")
        if constraints:
            context_parts.append(f"## Agent Constraints\n{constraints[:1000]}")

        # Read current content of files to modify
        for filepath in task.files_to_modify:
            full_path = self._workspace_root / filepath
            if full_path.exists():
                content = full_path.read_text(encoding="utf-8")
                context_parts.append(f"## Current content of {filepath}\n```python\n{content}\n```")

        # Read code conventions from memory
        patterns = self.read_memory("patterns.md")
        if patterns:
            context_parts.append(f"## Code Conventions\n{patterns[:1000]}")

        # Add failure context for iterations > 1
        failure_context = ""
        if iteration > 1 and task.sandbox_results:
            last_result = task.sandbox_results[-1]
            failure_context = f"""

## Previous Attempt Failed
Iteration {iteration - 1} sandbox results: {last_result}
Fix the issues from the previous attempt. Pay attention to test failures and lint errors.
"""

        system_prompt = """You are the Hive Builder agent. You modify application source code to implement approved improvements.

RULES:
- Output ONLY valid Python code for each file
- Follow existing code conventions exactly (indentation, naming, style)
- Minimize diff size -- change only what's needed
- Include type hints on all functions
- Write/update tests for changed behavior
- Never remove existing tests unless explicitly told to
- Output format: JSON mapping file paths to their COMPLETE new content
  Example: {"llmhosts/cache/store.py": "full file content...", "tests/test_cache.py": "full test content..."}
"""

        user_prompt = f"""## Task: {task.item_id} -- {task.title}

**Problem**: {task.problem}
**Solution**: {task.solution}
**Files to modify**: {", ".join(task.files_to_modify)}
**Files to create**: {", ".join(task.files_to_create)}
**Risk level**: {task.risk}

## Codebase Context
{"".join(context_parts)}
{failure_context}

Generate the complete content for each file that needs to be modified or created.
Output as JSON: {{"filepath": "complete file content", ...}}
"""

        try:
            response = await self._llm.complete(
                messages=[{"role": "user", "content": user_prompt}],
                system=system_prompt,
                temperature=0.2,
                max_tokens=8192,
            )

            # Parse JSON from LLM response
            content = response.content.strip()
            # Handle markdown code blocks
            if "```json" in content:
                content = content.split("```json")[1].split("```")[0].strip()
            elif "```" in content:
                content = content.split("```")[1].split("```")[0].strip()

            import json

            changes = json.loads(content)
            if isinstance(changes, dict):
                return changes
            logger.error("LLM returned non-dict response")
            return None
        except Exception as e:
            logger.error("LLM code generation failed: %s", e)
            return None

    def _apply_changes(self, changes: dict[str, str]) -> bool:
        """Write generated code changes to disk."""
        try:
            for filepath, content in changes.items():
                full_path = self._workspace_root / filepath
                full_path.parent.mkdir(parents=True, exist_ok=True)
                full_path.write_text(content, encoding="utf-8")
                logger.info("Written: %s (%d bytes)", filepath, len(content))
            return True
        except Exception as e:
            logger.error("Failed to write changes: %s", e)
            return False

    async def _submit_pr(self, task: BuildTask, sandbox: SandboxResult) -> BuildTask:
        """Commit changes, push, and create a GitHub PR."""
        # Build PR body
        body = f"""## Hive Improvement: {task.item_id}

**Problem**: {task.problem}
**Solution**: {task.solution}
**Impact**: See backlog item for estimates
**Risk**: {task.risk}
**Iterations**: {task.iterations}

### Sandbox Validation
- Tests: {sandbox.tests_passed}/{sandbox.tests_total} passed
- Lint errors: {sandbox.lint_errors}
- Execution time: {sandbox.execution_time_seconds:.1f}s

### Files Changed
{chr(10).join(f"- `{f}`" for f in task.files_to_modify + task.files_to_create)}

---
*Generated by Hive.CEO Builder agent. Human review required before merge.*
"""

        # Submit via git operations
        pr_result = self._git.submit_change(
            item_id=task.item_id,
            title=task.title,
            body=body,
            files=task.files_to_modify + task.files_to_create,
        )

        if pr_result.success:
            task.pr_url = pr_result.stdout
            task.status = "pr_created"
            logger.info("PR created: %s", task.pr_url)
            return self._complete_task(task, success=True, notes=f"PR: {task.pr_url}")
        else:
            task.status = "pr_failed"
            logger.error("PR creation failed: %s", pr_result.stderr)
            return self._complete_task(task, success=False, notes=f"PR creation failed: {pr_result.stderr}")

    def _complete_task(self, task: BuildTask, success: bool, notes: str) -> BuildTask:
        """Mark a task as completed and move to history."""
        task.status = "completed" if success else "failed"
        if task in self._active_tasks:
            self._active_tasks.remove(task)
        self._completed_tasks.append(task)

        # Record in memory
        memory_entry = (
            f"\n### {task.item_id}: {task.title}\n"
            f"- date: {datetime.now(timezone.utc).isoformat()}\n"
            f"- status: {'success' if success else 'failed'}\n"
            f"- iterations: {task.iterations}\n"
            f"- notes: {notes}\n"
        )
        self.write_memory("decisions.md", memory_entry)

        logger.info("Task %s %s: %s", task.item_id, "completed" if success else "failed", notes)
        return task

    def receive_message(self, message: AgentMessage) -> AgentMessage | None:
        """Process a task assignment from CEO."""
        self._message_log.append(message)

        if message.message_type == MessageType.TASK:
            payload = message.payload
            task = BuildTask(
                item_id=payload.get("id", "HIVE-000"),
                title=payload.get("title", "untitled"),
                problem=payload.get("problem", ""),
                solution=payload.get("solution", ""),
                files_to_modify=payload.get("files", []),
                files_to_create=payload.get("new_files", []),
                risk=payload.get("risk", "low"),
            )
            # Task will be executed by the engine loop calling execute_task()
            self._active_tasks.append(task)
            return AgentMessage(
                from_agent=AgentRole.BUILDER,
                to_agent=AgentRole.CEO,
                message_type=MessageType.RESULT,
                priority=Priority.NORMAL,
                payload={"status": "accepted", "item_id": task.item_id},
            )

        return None

    def get_status(self) -> dict[str, Any]:
        """Builder-specific status."""
        status = super().get_status()
        status.update(
            {
                "active_tasks": len(self._active_tasks),
                "completed_tasks": len(self._completed_tasks),
                "success_rate": self._success_rate(),
                "max_iterations": MAX_ITERATIONS,
            }
        )
        return status

    def _success_rate(self) -> float:
        """Calculate success rate of completed tasks."""
        if not self._completed_tasks:
            return 0.0
        successes = sum(1 for t in self._completed_tasks if t.status == "completed")
        return round(successes / len(self._completed_tasks), 2)
