"""Architecture Tracker - Tracks global architecture to prevent context starvation and code duplication."""

import ast
import json
import logging
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

from app_factory.core.claude_client import ClaudeSDKClient

logger = logging.getLogger(__name__)


class ArchitectureTracker:
    """Tracks global architecture to prevent Dev Agent context starvation and code duplication."""

    def __init__(
        self,
        data_dir: str = "app_factory/data",
        api_key: str = None,
        auth_token: str = None,
        debug: bool = False,
        observability=None,
    ):
        """Initialize ArchitectureTracker.

        Args:
            data_dir: Directory for storing global_architecture.json.
            api_key: Optional API key for AI-powered summarization.
        """
        self.data_dir = Path(data_dir)
        self.data_dir.mkdir(parents=True, exist_ok=True)
        self._arch_path = self.data_dir / "global_architecture.json"
        self.observability = observability

        self._client = None
        resolved_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
        resolved_auth = auth_token or os.environ.get("ANTHROPIC_AUTH_TOKEN")
        if resolved_key or resolved_auth:
            try:
                self._client = ClaudeSDKClient(
                    api_key=resolved_key,
                    auth_token=resolved_auth,
                    enable_debug=debug,
                )
            except Exception as exc:
                logger.warning("Claude SDK unavailable (%s). AI summarization disabled.", exc)

        self._architecture = self.load_architecture()

    def _default_architecture(self) -> dict:
        """Return default architecture schema."""
        return {
            "modules": [],
            "utilities": [],
            "design_patterns": [],
            "naming_conventions": {
                "variables": "snake_case",
                "classes": "PascalCase",
                "functions": "snake_case",
                "constants": "UPPER_SNAKE_CASE",
            },
            "tech_stack": {
                "language": "Python",
                "framework": "LangGraph",
            },
            "version": 1,
            "last_updated": datetime.now(timezone.utc).isoformat(),
        }

    def load_architecture(self) -> dict:
        """Load from global_architecture.json or return default."""
        if self._arch_path.exists():
            try:
                with open(self._arch_path, "r") as f:
                    return json.load(f)
            except (json.JSONDecodeError, OSError) as exc:
                logger.warning("Failed to load architecture file (%s). Using default.", exc)
        return self._default_architecture()

    def save_architecture(self, data: dict):
        """Save to global_architecture.json with timestamp update."""
        data["last_updated"] = datetime.now(timezone.utc).isoformat()
        with open(self._arch_path, "w") as f:
            json.dump(data, f, indent=2)
        self._architecture = data

    async def update_architecture(self, completed_task: dict, files_changed: list):
        """Update architecture based on completed task and changed files.

        Args:
            completed_task: Dict with task info (e.g. title, description).
            files_changed: List of file paths that were modified.
        """
        new_modules = []
        new_utilities = []

        for file_path in files_changed:
            if not os.path.exists(file_path) or not file_path.endswith(".py"):
                continue

            try:
                with open(file_path, "r") as f:
                    source = f.read()
            except OSError:
                continue

            if self._client:
                await self._ai_extract(source, file_path, new_modules, new_utilities)
            else:
                self._basic_extract(source, file_path, new_modules, new_utilities)

        existing_module_names = {m["name"] for m in self._architecture["modules"]}
        for mod in new_modules:
            if mod["name"] not in existing_module_names:
                self._architecture["modules"].append(mod)
                existing_module_names.add(mod["name"])

        existing_utility_names = {u["name"] for u in self._architecture["utilities"]}
        for util in new_utilities:
            if util["name"] not in existing_utility_names:
                self._architecture["utilities"].append(util)
                existing_utility_names.add(util["name"])

        self.save_architecture(self._architecture)

    async def _ai_extract(
        self, source: str, file_path: str, modules: list, utilities: list
    ):
        """Use Claude to extract architecture info from source code."""
        prompt = (
            "Analyze this Python source file and extract:\n"
            "1. Module-level classes (name, purpose)\n"
            "2. Utility functions (name, description)\n"
            "Respond ONLY with valid JSON: "
            '{"classes": [{"name": "...", "purpose": "..."}], '
            '"functions": [{"name": "...", "description": "..."}]}\n\n'
            f"File: {file_path}\n```python\n{source[:4000]}\n```"
        )
        try:
            response = await self._client.complete(
                prompt=prompt,
                model="claude-sonnet-4-6",
                max_turns=100,
                observability=self.observability,
                agent_name="architecture_tracker",
                task_id=f"ai_extract:{Path(file_path).name}",
            )
            if self.observability:
                self.observability.log_token_usage(
                    "architecture_tracker",
                    f"ai_extract:{Path(file_path).name}",
                    input_tokens=response.input_tokens,
                    output_tokens=response.output_tokens,
                    model="claude-sonnet-4-6",
                )
            text = response.text
            # Extract JSON from response
            start = text.find("{")
            end = text.rfind("}") + 1
            if start >= 0 and end > start:
                data = json.loads(text[start:end])
                for cls in data.get("classes", []):
                    modules.append({
                        "name": cls["name"],
                        "purpose": cls.get("purpose", ""),
                        "file_path": file_path,
                    })
                for func in data.get("functions", []):
                    utilities.append({
                        "name": func["name"],
                        "description": func.get("description", ""),
                        "file_path": file_path,
                    })
        except Exception as exc:
            logger.warning("AI extraction failed (%s). Falling back to basic.", exc)
            self._basic_extract(source, file_path, modules, utilities)

    def _basic_extract(
        self, source: str, file_path: str, modules: list, utilities: list
    ):
        """Extract architecture info using AST parsing."""
        try:
            tree = ast.parse(source)
        except SyntaxError:
            return

        for node in ast.iter_child_nodes(tree):
            if isinstance(node, ast.ClassDef):
                docstring = ast.get_docstring(node) or ""
                modules.append({
                    "name": node.name,
                    "purpose": docstring.split("\n")[0] if docstring else "",
                    "file_path": file_path,
                })
            elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
                if node.name.startswith("_"):
                    continue
                docstring = ast.get_docstring(node) or ""
                utilities.append({
                    "name": node.name,
                    "description": docstring.split("\n")[0] if docstring else "",
                    "file_path": file_path,
                })

    def get_architecture_summary(self, max_tokens: int = 2000) -> str:
        """Generate concise text summary from architecture data.

        Args:
            max_tokens: Approximate max tokens for the summary (~4 chars per token).

        Returns:
            Formatted string for injection into Dev Agent prompts.
        """
        max_chars = max_tokens * 4
        parts = []

        parts.append("## Project Architecture Summary")
        parts.append("")

        # Tech stack
        tech = self._architecture.get("tech_stack", {})
        if tech:
            parts.append("### Tech Stack")
            for key, value in tech.items():
                parts.append(f"- {key}: {value}")
            parts.append("")

        # Modules
        mods = self._architecture.get("modules", [])
        if mods:
            parts.append("### Modules")
            for m in mods:
                line = f"- **{m['name']}** ({m.get('file_path', '')}): {m.get('purpose', '')}"
                parts.append(line)
            parts.append("")

        # Utilities
        utils = self._architecture.get("utilities", [])
        if utils:
            parts.append("### Shared Utilities")
            for u in utils:
                line = f"- **{u['name']}** ({u.get('file_path', '')}): {u.get('description', '')}"
                parts.append(line)
            parts.append("")

        # Design patterns
        patterns = self._architecture.get("design_patterns", [])
        if patterns:
            parts.append("### Design Patterns")
            for p in patterns:
                parts.append(f"- {p.get('pattern', '')}: {p.get('usage', '')}")
            parts.append("")

        # Naming conventions
        conventions = self._architecture.get("naming_conventions", {})
        if conventions:
            parts.append("### Naming Conventions")
            for key, value in conventions.items():
                parts.append(f"- {key}: {value}")
            parts.append("")

        summary = "\n".join(parts)

        if len(summary) > max_chars:
            summary = summary[:max_chars - 3] + "..."

        return summary

    def add_module(self, name: str, purpose: str, file_path: str):
        """Manually add a module to the architecture.

        Args:
            name: Module/class name.
            purpose: Brief description of what it does.
            file_path: Path to the source file.
        """
        self._architecture["modules"].append({
            "name": name,
            "purpose": purpose,
            "file_path": file_path,
        })
        self.save_architecture(self._architecture)

    def add_utility(self, name: str, description: str, file_path: str):
        """Manually add a utility function to the architecture.

        Args:
            name: Function name.
            description: Brief description of what it does.
            file_path: Path to the source file.
        """
        self._architecture["utilities"].append({
            "name": name,
            "description": description,
            "file_path": file_path,
        })
        self.save_architecture(self._architecture)
