"""Advisor agent -- Improvement engine.

Identifies optimization opportunities, prioritizes the backlog,
generates improvement proposals, and tracks outcomes.

Scope boundary: Advisor proposes but never implements.
All proposals go through the approval queue.

This agent uses LLM reasoning to analyze the codebase and generate
actionable improvement proposals with code diff previews.
"""

from __future__ import annotations

import json
import logging
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from pathlib import Path

from hive.agents.base import AgentRole, BaseAgent
from hive.llm.client import HiveLLMClient

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


class Advisor(BaseAgent):
    """Improvement engine -- identifies what should be improved.

    Uses LLM reasoning to:
    - Analyze codebase map for optimization opportunities
    - Research solutions using institutional memory
    - Generate detailed proposals with code diff previews
    - Prioritize backlog using impact/effort matrix
    - Detect pattern crystallization opportunities
    """

    role = AgentRole.ADVISOR

    def __init__(
        self,
        workspace_root: Path,
        llm: HiveLLMClient | None = None,
    ) -> None:
        super().__init__(workspace_root)
        self._llm = llm or HiveLLMClient()
        self._proposals: list[dict[str, Any]] = []
        self._pattern_tracker: dict[str, int] = {}

    async def analyze_and_propose(
        self,
        codebase_map: dict[str, Any],
        observations: list[dict[str, Any]],
    ) -> list[dict[str, Any]]:
        """Full analysis cycle: analyze observations → research → propose.

        This is the LLM-powered improvement engine.
        """
        proposals = []

        for obs in observations:
            if obs.get("severity") in ("critical", "high"):
                # For critical/high: immediate proposal
                proposal = await self._research_and_propose(obs, codebase_map)
                if proposal:
                    proposals.append(proposal)
            elif obs.get("severity") == "medium":
                # For medium: batch analysis
                proposal = await self._research_and_propose(obs, codebase_map)
                if proposal:
                    proposals.append(proposal)
            # Low severity observations are logged but not immediately acted on

        self._proposals.extend(proposals)
        return proposals

    async def _research_and_propose(
        self,
        observation: dict[str, Any],
        codebase_map: dict[str, Any],
    ) -> dict[str, Any] | None:
        """Research solutions and create a proposal for a single observation.

        Steps 2-3 of the 8-step improvement cycle.
        """
        # Step 2: RESEARCH
        # Check institutional memory first
        memory_context = self._check_memory(observation)

        # Build codebase context for the affected area
        affected_modules = self._find_affected_modules(observation, codebase_map)

        # Use LLM to research and generate solution
        system_prompt = """You are the Hive Advisor agent. You research solutions for detected issues
and generate improvement proposals.

OUTPUT FORMAT (JSON):
{
    "title": "short improvement title",
    "problem": "what's wrong, with evidence",
    "solution": "what to do, specifically",
    "files_to_modify": ["list", "of", "files"],
    "files_to_create": ["new", "files", "if any"],
    "estimated_impact": 1-10,
    "estimated_effort_days": 0.5-10,
    "risk": "low|medium|high",
    "diff_preview": "brief description of code changes",
    "rollback_plan": "how to undo if it fails",
    "tests_needed": ["test descriptions"]
}

RULES:
- Be specific about file paths (use the codebase map)
- Estimate effort honestly (don't minimize to get approval)
- Risk assessment must be honest
- Solution must be actionable by Builder agent
- Include diff preview so admin knows what will change
"""

        user_prompt = f"""## Observation
- Category: {observation.get("category", "unknown")}
- Severity: {observation.get("severity", "unknown")}
- Title: {observation.get("title", "untitled")}
- Evidence: {observation.get("evidence", "none")}
- Frequency: {observation.get("frequency", "unknown")}

## Institutional Memory
{memory_context or "No relevant past incidents."}

## Affected Modules
{json.dumps(affected_modules, indent=2)[:3000]}

## Codebase Stats
- Total files: {codebase_map.get("stats", {}).get("total_files", 0)}
- Total lines: {codebase_map.get("stats", {}).get("total_lines", 0)}
- Languages: {codebase_map.get("stats", {}).get("languages", {})}

Research this issue and propose a specific improvement. Output as JSON.
"""

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

            # Parse the LLM's proposal
            content = response.content.strip()
            if "```json" in content:
                content = content.split("```json")[1].split("```")[0].strip()
            elif "```" in content:
                content = content.split("```")[1].split("```")[0].strip()

            proposal: dict[str, Any] = json.loads(content)
            proposal["source"] = "advisor"
            proposal["observation_id"] = observation.get("id", "unknown")
            proposal["status"] = "proposed"
            proposal["created"] = datetime.now(timezone.utc).isoformat()
            proposal["priority"] = self._calculate_priority(proposal)
            proposal["llm_model"] = response.model
            proposal["llm_cost"] = response.cost_usd

            # Quality gate: verify proposal meets minimum standards
            if self._validate_proposal(proposal):
                return proposal
            else:
                logger.warning("Proposal failed quality gate for observation: %s", observation.get("title"))
                return None

        except Exception as e:
            logger.error("LLM proposal generation failed: %s", e)
            # Fallback: create basic proposal without LLM
            return self._create_basic_proposal(observation)

    def analyze_codebase(self, codebase_map: dict[str, Any]) -> list[dict[str, Any]]:
        """Analyze the codebase for improvement opportunities.

        Returns a list of observations with category, severity, and evidence.
        This is the non-LLM static analysis (fast, runs every cycle).
        """
        observations: list[dict[str, Any]] = []
        obs_counter = 0

        stats = codebase_map.get("stats", {})
        modules = codebase_map.get("modules", [])

        # Check 1: Low test coverage
        total_files = stats.get("total_files", 0)
        if total_files > 0:
            source_files = [m for m in modules if not m["path"].startswith("tests/")]
            test_files = [m for m in modules if m["path"].startswith("tests/")]

            if len(test_files) < len(source_files) * 0.5:
                obs_counter += 1
                observations.append(
                    {
                        "id": f"OBS-{obs_counter:03d}",
                        "category": "quality",
                        "severity": "medium",
                        "title": "Low test coverage",
                        "evidence": f"{len(test_files)} test files for {len(source_files)} source files ({len(test_files) / max(len(source_files), 1) * 100:.0f}%)",
                        "frequency": "persistent",
                        "estimated_impact": 5,
                        "estimated_effort_days": 3,
                    }
                )

        # Check 2: Large files (complexity indicator)
        for module in modules:
            lines = module.get("lines", 0)
            if lines > 500:
                obs_counter += 1
                observations.append(
                    {
                        "id": f"OBS-{obs_counter:03d}",
                        "category": "quality",
                        "severity": "low",
                        "title": f"Large file: {module['path']} ({lines} lines)",
                        "evidence": "File exceeds 500 lines. Consider splitting into focused modules.",
                        "frequency": "persistent",
                        "estimated_impact": 3,
                        "estimated_effort_days": 1,
                    }
                )

        # Check 3: Missing __init__.py exports
        for module in modules:
            if module["path"].endswith("__init__.py"):
                exports = module.get("exports", [])
                if not exports:
                    obs_counter += 1
                    observations.append(
                        {
                            "id": f"OBS-{obs_counter:03d}",
                            "category": "quality",
                            "severity": "low",
                            "title": f"Empty __init__.py: {module['path']}",
                            "evidence": "No public exports. Consider adding __all__ or removing if unnecessary.",
                            "frequency": "persistent",
                            "estimated_impact": 1,
                            "estimated_effort_days": 0.5,
                        }
                    )

        # Check 4: Circular dependencies
        dep_graph = codebase_map.get("dependency_graph", {})
        cycles = self._detect_cycles(dep_graph)
        for cycle in cycles:
            obs_counter += 1
            observations.append(
                {
                    "id": f"OBS-{obs_counter:03d}",
                    "category": "quality",
                    "severity": "high",
                    "title": f"Circular dependency detected: {' → '.join(cycle)}",
                    "evidence": f"Import cycle: {' → '.join(cycle)}. Breaks clean architecture.",
                    "frequency": "persistent",
                    "estimated_impact": 6,
                    "estimated_effort_days": 2,
                }
            )

        # Check 5: Missing documentation directory
        docs_dir = self._workspace_root / "docs"
        if not docs_dir.exists() or not any(docs_dir.rglob("*.md")):
            obs_counter += 1
            observations.append(
                {
                    "id": f"OBS-{obs_counter:03d}",
                    "category": "quality",
                    "severity": "low",
                    "title": "Missing documentation",
                    "evidence": "No docs/ directory or markdown files found",
                    "frequency": "persistent",
                    "estimated_impact": 3,
                    "estimated_effort_days": 1,
                }
            )

        return observations

    def _check_memory(self, observation: dict[str, Any]) -> str:
        """Check institutional memory for related past incidents."""
        incidents = self.read_memory("incidents.md")
        decisions = self.read_memory("decisions.md")

        context_parts = []
        title = observation.get("title", "").lower()

        if incidents:
            # Simple keyword matching (LLM does the deeper analysis)
            for line in incidents.splitlines():
                if any(word in line.lower() for word in title.split()[:3]):
                    context_parts.append(line)

        if decisions:
            for line in decisions.splitlines():
                if any(word in line.lower() for word in title.split()[:3]):
                    context_parts.append(line)

        return "\n".join(context_parts[:20]) if context_parts else ""

    def _find_affected_modules(
        self,
        observation: dict[str, Any],
        codebase_map: dict[str, Any],
    ) -> list[dict[str, Any]]:
        """Find modules affected by an observation."""
        title = observation.get("title", "")
        evidence = observation.get("evidence", "")
        search_text = f"{title} {evidence}".lower()

        affected = []
        for module in codebase_map.get("modules", []):
            path = module["path"].lower()
            if any(word in path for word in search_text.split()[:5] if len(word) > 3):
                affected.append(module)

        return affected[:10]  # Cap at 10 for context size

    def _calculate_priority(self, proposal: dict[str, Any]) -> str:
        """Calculate priority using impact/effort/urgency matrix."""
        impact = proposal.get("estimated_impact", 1)
        effort = max(proposal.get("estimated_effort_days", 1), 0.5)
        risk = proposal.get("risk", "low")

        urgency = 5
        if risk == "high":
            urgency = 8
        elif proposal.get("category") == "security":
            urgency = min(urgency + 3, 10)

        priority_score = (impact * urgency) / effort

        if priority_score >= 7:
            return "critical"
        if priority_score >= 4:
            return "high"
        if priority_score >= 2:
            return "medium"
        return "low"

    def _validate_proposal(self, proposal: dict[str, Any]) -> bool:
        """Quality gate: verify proposal meets minimum standards."""
        required_fields = ["title", "problem", "solution", "files_to_modify"]
        for f in required_fields:
            if not proposal.get(f):
                return False

        # Must have honest risk assessment
        if proposal.get("risk") not in ("low", "medium", "high"):
            return False

        # Must have rollback plan
        return bool(proposal.get("rollback_plan"))

    def _create_basic_proposal(self, observation: dict[str, Any]) -> dict[str, Any]:
        """Create a basic proposal without LLM (fallback)."""
        return {
            "title": observation.get("title", "untitled improvement"),
            "status": "proposed",
            "priority": self._calculate_priority(observation),
            "source": "advisor",
            "created": datetime.now(timezone.utc).isoformat(),
            "problem": observation.get("evidence", ""),
            "solution": "Requires manual analysis -- LLM proposal generation failed.",
            "files_to_modify": [],
            "estimated_impact": observation.get("estimated_impact", 0),
            "estimated_effort_days": observation.get("estimated_effort_days", 0),
            "risk": "medium",
            "rollback_plan": "Revert the PR",
            "category": observation.get("category", "general"),
        }

    def _detect_cycles(self, graph: dict[str, list[str]]) -> list[list[str]]:
        """Detect circular dependencies in the module graph."""
        cycles: list[list[str]] = []
        visited: set[str] = set()
        path: list[str] = []
        on_path: set[str] = set()

        def dfs(node: str) -> None:
            if node in on_path:
                # Found a cycle
                cycle_start = path.index(node)
                cycle = [*path[cycle_start:], node]
                cycles.append(cycle)
                return
            if node in visited:
                return

            visited.add(node)
            on_path.add(node)
            path.append(node)

            for neighbor in graph.get(node, []):
                dfs(neighbor)

            path.pop()
            on_path.discard(node)

        for node in graph:
            if node not in visited:
                dfs(node)

        return cycles

    def track_pattern(self, pattern_name: str, success: bool) -> dict[str, Any] | None:
        """Track a recurring pattern for potential skill crystallization.

        Returns a crystallization proposal if threshold is met (5+ instances, 70%+ success).
        """
        key = f"{pattern_name}:{'success' if success else 'fail'}"
        self._pattern_tracker[key] = self._pattern_tracker.get(key, 0) + 1

        success_count = self._pattern_tracker.get(f"{pattern_name}:success", 0)
        total = success_count + self._pattern_tracker.get(f"{pattern_name}:fail", 0)

        if total >= 5 and (success_count / total) >= 0.70:
            return {
                "type": "skill_crystallization",
                "pattern": pattern_name,
                "instances": total,
                "success_rate": round(success_count / total, 2),
                "suggested_name": f"auto_{pattern_name.lower().replace(' ', '_')}",
            }

        return None

    def reprioritize_backlog(self, evolution_context: dict[str, Any] | None = None) -> list[dict[str, Any]]:
        """Re-evaluate all proposals against current priorities.

        If evolution_context is provided, adjusts priorities based on
        new application direction.
        """
        if evolution_context:
            logger.info("Reprioritizing backlog for evolution: %s", evolution_context.get("type", "unknown"))
            # Annotate pre-pivot proposals
            for proposal in self._proposals:
                if proposal.get("status") == "proposed":
                    proposal["pre_pivot"] = True

        priority_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
        return sorted(
            self._proposals,
            key=lambda p: priority_order.get(p.get("priority", "low"), 4),
        )

    def get_status(self) -> dict[str, Any]:
        """Advisor-specific status."""
        status = super().get_status()
        status.update(
            {
                "total_proposals": len(self._proposals),
                "pending_proposals": len([p for p in self._proposals if p["status"] == "proposed"]),
                "tracked_patterns": len(self._pattern_tracker),
            }
        )
        return status
