"""
Claude Code CLI Adapter - Wrapper for Claude Code in headless mode.

This adapter runs Claude Code as a subprocess with --output-format json
to capture structured responses and usage metrics.

CRITICAL: Claude Code headless mode is per-session. There is NO persistent
state between invocations. State must be:
1. Injected via the prompt at the start of each run
2. Captured from the StatusPacket at the end of each run
3. Stored externally in project_state.json and SQLite

Usage:
    from agent_orchestrator.adapters.claude_code_cli import ClaudeCodeCLIAdapter

    adapter = ClaudeCodeCLIAdapter(
        agent_id="claude-code-main",
        workspace_path="/path/to/repo",
    )
    response = await adapter.execute("Implement user auth", context)
"""

import asyncio
import json
import os
import shutil
import subprocess
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, AsyncIterator, Optional

from .base import (
    CLIAgentAdapter,
    AgentResponse,
    AgentStatus,
    PromptBuilder,
)
from .pricing import estimate_cost
from ..journal.status_packet import (
    StatusPacket,
    TaskArtifacts,
    create_success_packet,
    create_failed_packet,
)
from ..secrets.redactor import SecretRedactor


@dataclass
class ClaudeCodeConfig:
    """Configuration for Claude Code CLI adapter."""

    # Path to claude executable (default: assumes in PATH)
    executable: str = "claude"

    # Output format (json or stream-json)
    output_format: str = "json"

    # Timeout for execution in seconds
    timeout_seconds: int = 600  # 10 minutes

    # Working directory (defaults to workspace_path)
    working_dir: Optional[str] = None

    # Additional CLI flags
    additional_flags: list[str] = None

    # Whether to allow tool use
    allow_tools: bool = True

    # Maximum turns for agentic behavior
    max_turns: int = 10

    # Auth check prompt (set to None to skip)
    auth_check_prompt: Optional[str] = "Auth check: respond with OK."

    # Auth check timeout in seconds
    auth_check_timeout_seconds: int = 30

    def __post_init__(self):
        if self.additional_flags is None:
            self.additional_flags = []


class ClaudeCodeCLIAdapter(CLIAgentAdapter):
    """
    Adapter for Claude Code CLI in headless mode.

    Runs Claude Code as a subprocess and parses the JSON output
    to extract responses and usage metrics.

    Key behaviors:
    - Injects project state at start of each invocation
    - Captures structured output from JSON response
    - Extracts usage metrics for budget tracking
    - Handles the fact that sessions don't persist
    """

    def __init__(
        self,
        agent_id: str,
        workspace_path: Optional[str] = None,
        config: Optional[ClaudeCodeConfig] = None,
    ) -> None:
        """
        Initialize the Claude Code CLI adapter.

        Args:
            agent_id: Unique identifier for this agent
            workspace_path: Path to the git repository/workspace
            config: Optional configuration overrides
        """
        super().__init__(agent_id, workspace_path)
        self.config = config or ClaudeCodeConfig()
        self._last_packet: Optional[StatusPacket] = None
        self._process: Optional[asyncio.subprocess.Process] = None

        # Verify claude is available
        self._claude_available = self._check_claude_available()

    def _check_claude_available(self) -> bool:
        """Check if claude CLI is available."""
        return shutil.which(self.config.executable) is not None

    async def execute(self, task: str, context: dict[str, Any]) -> AgentResponse:
        """
        Execute a task using Claude Code CLI.

        IMPORTANT: Each invocation is a fresh session. State is injected
        via the prompt and must be captured from the response.

        Args:
            task: The task description/prompt
            context: Project state, constraints, recent decisions

        Returns:
            AgentResponse with result and usage metrics
        """
        if not self._claude_available:
            return AgentResponse(
                content="",
                success=False,
                error="Claude Code CLI not found in PATH",
            )

        self._status = AgentStatus.RUNNING
        start_time = datetime.now()

        try:
            # Build the full prompt with context injection
            full_prompt = self._build_prompt(task, context)

            # Build command
            cmd = self._build_command(full_prompt)

            # Set working directory
            cwd = self.config.working_dir or self.workspace_path or os.getcwd()

            # Execute
            result = await self._run_command(cmd, cwd)

            # Parse response
            response = self._parse_response(result, task)

            # Update usage stats
            self._usage_stats.add_response(response)

            # Create status packet
            self._last_packet = create_success_packet(
                agent_id=self.agent_id,
                task_id=self._current_task_id or "",
                summary=f"Completed: {task[:100]}...",
                files_modified=response.artifacts.files_modified,
                next_steps=["Review changes", "Run tests"],
            )
            self._last_packet.artifacts = response.artifacts

            self._status = AgentStatus.IDLE
            return response

        except asyncio.TimeoutError:
            error_msg = f"Claude Code timed out after {self.config.timeout_seconds}s"
            self._last_packet = create_failed_packet(
                agent_id=self.agent_id,
                task_id=self._current_task_id or "",
                error_message=error_msg,
            )
            self._status = AgentStatus.IDLE
            self._usage_stats.errors_count += 1

            return AgentResponse(
                content="",
                success=False,
                error=error_msg,
            )

        except Exception as e:
            error_msg = SecretRedactor.redact(str(e))
            self._last_packet = create_failed_packet(
                agent_id=self.agent_id,
                task_id=self._current_task_id or "",
                error_message=error_msg,
            )
            self._status = AgentStatus.IDLE
            self._usage_stats.errors_count += 1

            return AgentResponse(
                content="",
                success=False,
                error=error_msg,
            )

    async def stream(self, task: str, context: dict[str, Any]) -> AsyncIterator[str]:
        """
        Stream response from Claude Code using stream-json format.

        Args:
            task: The task description
            context: Additional context

        Yields:
            Response chunks as they become available
        """
        if not self._claude_available:
            yield "Error: Claude Code CLI not found in PATH"
            return

        self._status = AgentStatus.RUNNING

        try:
            full_prompt = self._build_prompt(task, context)

            # Use stream-json format for streaming
            cmd = self._build_command(full_prompt, stream=True)
            cwd = self.config.working_dir or self.workspace_path or os.getcwd()

            # Create subprocess
            process = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                cwd=cwd,
            )

            # Stream output
            accumulated_content = ""
            async for line in process.stdout:
                line_str = line.decode("utf-8").strip()
                if not line_str:
                    continue

                try:
                    # Parse JSON line
                    data = json.loads(line_str)

                    # Extract content based on message type
                    if data.get("type") == "content_block_delta":
                        delta = data.get("delta", {})
                        if delta.get("type") == "text_delta":
                            text = delta.get("text", "")
                            accumulated_content += text
                            yield text

                    elif data.get("type") == "assistant":
                        # Final message
                        content = data.get("content", "")
                        if content and content != accumulated_content:
                            yield content

                except json.JSONDecodeError:
                    # Non-JSON output, yield as-is
                    yield line_str

            await process.wait()

        except Exception as e:
            yield f"Error: {SecretRedactor.redact(str(e))}"

        finally:
            self._status = AgentStatus.IDLE

    def _build_prompt(self, task: str, context: dict[str, Any]) -> str:
        """
        Build the full prompt with context injection.

        Since Claude Code doesn't persist state, we must inject
        all relevant context at the start of each invocation.
        """
        builder = PromptBuilder.for_cli_agent()
        return builder.build(
            task,
            context,
            output_requirements=PromptBuilder.default_output_requirements(),
        )

    def _build_command(self, prompt: str, stream: bool = False) -> list[str]:
        """Build the claude CLI command."""
        cmd = [self.config.executable]

        # Add prompt
        cmd.extend(["-p", prompt])

        # Add output format
        output_format = "stream-json" if stream else self.config.output_format
        cmd.extend(["--output-format", output_format])

        # Add max turns if specified
        if self.config.max_turns:
            cmd.extend(["--max-turns", str(self.config.max_turns)])

        # Add additional flags
        cmd.extend(self.config.additional_flags)

        return cmd

    async def _run_command(
        self,
        cmd: list[str],
        cwd: str,
        timeout_seconds: Optional[int] = None,
    ) -> dict[str, Any]:
        """Run the claude command and return parsed output."""
        process = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=cwd,
        )

        try:
            stdout, stderr = await asyncio.wait_for(
                process.communicate(),
                timeout=timeout_seconds or self.config.timeout_seconds,
            )
        except asyncio.TimeoutError:
            process.kill()
            await process.wait()
            raise

        stdout_str = stdout.decode("utf-8")
        stderr_str = stderr.decode("utf-8")

        if process.returncode != 0:
            error = stderr_str or stdout_str or f"Exit code {process.returncode}"
            raise RuntimeError(f"Claude Code failed: {SecretRedactor.redact(error)}")

        # Parse JSON output
        try:
            return json.loads(stdout_str)
        except json.JSONDecodeError:
            # If not valid JSON, wrap in a dict
            return {
                "content": stdout_str,
                "raw_output": True,
            }

    def _parse_response(self, result: dict[str, Any], task: str) -> AgentResponse:
        """Parse the JSON response from Claude Code."""
        # Extract content
        content = ""
        if "content" in result:
            content = result["content"]
        elif "result" in result:
            content = result["result"]
        elif "message" in result:
            msg = result["message"]
            if isinstance(msg, dict):
                content = msg.get("content", "")
            else:
                content = str(msg)

        # Extract usage
        usage = result.get("usage", {})
        tokens_input = usage.get("input_tokens", 0)
        tokens_output = usage.get("output_tokens", 0)

        # Extract cost if available
        cost = usage.get("cost", 0.0)
        if not cost and tokens_input:
            # Estimate cost using centralized pricing
            cost = estimate_cost("claude-code", tokens_input, tokens_output)

        # Extract files modified from result or parse from content
        files_modified = result.get("files_modified", [])
        if not files_modified and "modified" in content.lower():
            # Try to extract from content (basic heuristic)
            import re
            file_pattern = r'`([^`]+\.[a-z]+)`'
            matches = re.findall(file_pattern, content)
            files_modified = list(set(matches))[:10]  # Limit to 10

        # Build artifacts
        artifacts = TaskArtifacts(
            diff_summary=result.get("diff_summary", ""),
            files_modified=files_modified,
            tokens_input=tokens_input,
            tokens_output=tokens_output,
            cost_usd=cost,
            next_action_recommendation=result.get("next_action", ""),
        )

        # Check for test results
        if "test_results" in result:
            artifacts.test_results = result["test_results"]
        elif "tests" in result:
            artifacts.test_results = result["tests"]

        return AgentResponse(
            content=SecretRedactor.redact(content),
            tokens_input=tokens_input,
            tokens_output=tokens_output,
            tokens_used=tokens_input + tokens_output,
            cost=cost,
            model="claude-code",
            artifacts=artifacts,
            success=True,
            metadata={
                "session_id": result.get("session_id"),
                "raw_output": result.get("raw_output", False),
            },
        )

    def write_status_packet(self) -> StatusPacket:
        """Return the last status packet."""
        if self._last_packet:
            return self._last_packet

        return StatusPacket(
            agent_id=self.agent_id,
            task_id=self._current_task_id or "",
            status="idle",
            artifacts=TaskArtifacts(
                tokens_input=self._usage_stats.tokens_input,
                tokens_output=self._usage_stats.tokens_output,
                cost_usd=self._usage_stats.total_cost,
            ),
        )

    def is_healthy(self) -> bool:
        """Check if Claude Code CLI is available."""
        return self._claude_available and self._status != AgentStatus.TERMINATED

    async def check_authentication(self) -> bool:
        """Check whether Claude Code CLI is authenticated."""
        if not self._claude_available:
            self._authenticated = False
            self._last_auth_error = "Claude Code CLI not found in PATH"
            return False

        if not self.config.auth_check_prompt:
            self._authenticated = True
            self._last_auth_error = None
            return True

        cwd = self.config.working_dir or self.workspace_path or os.getcwd()
        try:
            cmd = self._build_command(self.config.auth_check_prompt)
            await self._run_command(
                cmd,
                cwd,
                timeout_seconds=self.config.auth_check_timeout_seconds,
            )
            self._authenticated = True
            self._last_auth_error = None
        except Exception as exc:
            self._authenticated = False
            self._last_auth_error = SecretRedactor.redact(str(exc))
        return self._authenticated

    async def cancel(self) -> bool:
        """Cancel the current execution."""
        if self._process:
            try:
                self._process.terminate()
                await asyncio.wait_for(self._process.wait(), timeout=5)
            except asyncio.TimeoutError:
                self._process.kill()
            self._process = None

        self._status = AgentStatus.IDLE
        return True

    def get_name(self) -> str:
        """Get adapter name."""
        return "ClaudeCodeCLI"


def create_claude_code_adapter(
    agent_id: str,
    workspace_path: str,
    timeout_seconds: int = 600,
) -> ClaudeCodeCLIAdapter:
    """
    Factory function to create a Claude Code CLI adapter.

    Args:
        agent_id: Unique identifier for the agent
        workspace_path: Path to the workspace/repository
        timeout_seconds: Maximum execution time

    Returns:
        Configured ClaudeCodeCLIAdapter
    """
    config = ClaudeCodeConfig(timeout_seconds=timeout_seconds)
    return ClaudeCodeCLIAdapter(
        agent_id=agent_id,
        workspace_path=workspace_path,
        config=config,
    )
