"""
Codex CLI Adapter - Wrapper for OpenAI's Codex CLI.

Codex CLI features tiered autonomy modes:
- Suggest: Shows proposed changes, requires approval for everything
- Auto-Edit: Automatically applies file edits, requires approval for commands
- Full Auto: Executes everything automatically

This mental model maps directly to our orchestrator's risk gate.

Usage:
    from agent_orchestrator.adapters.codex_cli import CodexCLIAdapter

    adapter = CodexCLIAdapter(
        agent_id="codex-main",
        workspace_path="/path/to/repo",
        autonomy_mode="auto-edit",
    )
    response = await adapter.execute("Write unit tests", context)
"""

import asyncio
import json
import os
import shutil
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
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


class AutonomyMode(Enum):
    """Codex CLI autonomy modes."""

    SUGGEST = "suggest"  # Shows changes, requires approval for everything
    AUTO_EDIT = "auto-edit"  # Auto-applies edits, approves commands
    FULL_AUTO = "full-auto"  # Executes everything automatically


@dataclass
class CodexCLIConfig:
    """Configuration for Codex CLI adapter."""

    # Path to codex executable
    executable: str = "codex"

    # Autonomy mode
    autonomy_mode: AutonomyMode = AutonomyMode.AUTO_EDIT

    # Timeout for execution in seconds
    timeout_seconds: int = 600

    # Working directory
    working_dir: Optional[str] = None

    # Model to use
    model: str = "gpt-5-codex"

    # Additional CLI flags
    additional_flags: list[str] = field(default_factory=list)

    # Allow running outside a trusted git repo
    skip_git_check: bool = True

    # 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

    # Approval mode (maps from autonomy mode)
    @property
    def approval_global_flags(self) -> list[str]:
        """Get global flags for approval (must be before 'exec')."""
        if self.autonomy_mode == AutonomyMode.FULL_AUTO:
            return []
        return ["--ask-for-approval", "untrusted"]

    @property
    def approval_exec_flags(self) -> list[str]:
        """Get exec subcommand flags for approval."""
        if self.autonomy_mode == AutonomyMode.SUGGEST:
            return ["--sandbox", "read-only"]
        elif self.autonomy_mode == AutonomyMode.AUTO_EDIT:
            return ["--sandbox", "workspace-write"]
        elif self.autonomy_mode == AutonomyMode.FULL_AUTO:
            return ["--full-auto"]
        return []


class CodexCLIAdapter(CLIAgentAdapter):
    """
    Adapter for OpenAI's Codex CLI.

    Best suited for:
    - CI/CD automation
    - Test generation
    - Tasks with clear approval modes
    - Quick code generation

    The tiered autonomy modes make it ideal for orchestration:
    - suggest: Safe for review workflows
    - auto-edit: Good balance for automated tasks
    - full-auto: Only for fully trusted pipelines
    """

    def __init__(
        self,
        agent_id: str,
        workspace_path: Optional[str] = None,
        autonomy_mode: str = "auto-edit",
        config: Optional[CodexCLIConfig] = None,
    ) -> None:
        """
        Initialize the Codex CLI adapter.

        Args:
            agent_id: Unique identifier for this agent
            workspace_path: Path to the workspace
            autonomy_mode: suggest, auto-edit, or full-auto
            config: Optional configuration overrides
        """
        super().__init__(agent_id, workspace_path)

        # Parse autonomy mode
        if config:
            self.config = config
        else:
            mode = AutonomyMode(autonomy_mode)
            self.config = CodexCLIConfig(autonomy_mode=mode)

        self._last_packet: Optional[StatusPacket] = None
        self._process: Optional[asyncio.subprocess.Process] = None

        # Verify codex is available
        self._codex_available = self._check_codex_available()

    def _check_codex_available(self) -> bool:
        """Check if codex 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 Codex CLI.

        Args:
            task: The task description/prompt
            context: Project state, constraints, etc.

        Returns:
            AgentResponse with result and usage metrics
        """
        if not self._codex_available:
            return AgentResponse(
                content="",
                success=False,
                error="Codex CLI not found in PATH. Install from: https://github.com/openai/codex-cli",
            )

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

        try:
            # Build the full prompt
            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 output", "Run tests"],
            )
            self._last_packet.artifacts = response.artifacts

            self._status = AgentStatus.IDLE
            return response

        except asyncio.TimeoutError:
            error_msg = f"Codex CLI 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 Codex CLI.

        Args:
            task: The task description
            context: Additional context

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

        self._status = AgentStatus.RUNNING

        try:
            full_prompt = self._build_prompt(task, context)
            cmd = self._build_command(full_prompt)
            cwd = self.config.working_dir or self.workspace_path or os.getcwd()

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

            async for line in process.stdout:
                line_str = line.decode("utf-8")
                if line_str.strip():
                    yield SecretRedactor.redact(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."""
        builder = PromptBuilder.for_cli_agent()

        # Add Codex-specific context about autonomy mode
        additional_sections = {
            "Autonomy Mode": f"Operating in {self.config.autonomy_mode.value} mode."
        }

        code_requirements = [
            "Write clean, well-documented code",
            "Include error handling where appropriate",
            "Follow existing code style",
        ]

        return builder.build(
            task,
            context,
            output_requirements=code_requirements,
            additional_sections=additional_sections,
        )

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

        # Add global flags (e.g. approval policy)
        cmd.extend(self.config.approval_global_flags)

        # Use 'exec' subcommand for non-interactive execution
        cmd.append("exec")

        # Add model
        cmd.extend(["--model", self.config.model])

        # Add exec-specific approval flags (e.g. sandbox)
        cmd.extend(self.config.approval_exec_flags)

        # Skip git repo check if configured
        if self.config.skip_git_check:
            cmd.append("--skip-git-repo-check")

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

        # Add prompt as positional argument
        cmd.append(prompt)

        return cmd

    async def _run_command(
        self,
        cmd: list[str],
        cwd: str,
        timeout_seconds: Optional[int] = None,
    ) -> dict[str, Any]:
        """Run the codex 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"Codex CLI failed: {SecretRedactor.redact(error)}")

        try:
            return json.loads(stdout_str)
        except json.JSONDecodeError:
            return {
                "content": stdout_str,
                "raw_output": True,
            }

    def _parse_response(self, result: dict[str, Any], task: str) -> AgentResponse:
        """Parse the response from Codex CLI."""
        content = result.get("content", result.get("response", ""))

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

        # Estimate cost using centralized pricing
        cost = estimate_cost(self.config.model, tokens_input, tokens_output, provider="openai")

        files_modified = result.get("files_modified", [])

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

        return AgentResponse(
            content=SecretRedactor.redact(content),
            tokens_input=tokens_input,
            tokens_output=tokens_output,
            tokens_used=tokens_input + tokens_output,
            cost=cost,
            model=self.config.model,
            artifacts=artifacts,
            success=True,
            metadata={
                "autonomy_mode": self.config.autonomy_mode.value,
                "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 Codex CLI is available."""
        return self._codex_available and self._status != AgentStatus.TERMINATED

    async def check_authentication(self) -> bool:
        """Check whether Codex CLI is authenticated."""
        if not self._codex_available:
            self._authenticated = False
            self._last_auth_error = "Codex 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 f"CodexCLI({self.config.autonomy_mode.value})"

    def set_autonomy_mode(self, mode: str) -> None:
        """
        Change the autonomy mode.

        Args:
            mode: suggest, auto-edit, or full-auto
        """
        self.config.autonomy_mode = AutonomyMode(mode)


def create_codex_adapter(
    agent_id: str,
    workspace_path: str,
    autonomy_mode: str = "auto-edit",
    timeout_seconds: int = 600,
) -> CodexCLIAdapter:
    """
    Factory function to create a Codex CLI adapter.

    Args:
        agent_id: Unique identifier for the agent
        workspace_path: Path to the workspace
        autonomy_mode: suggest, auto-edit, or full-auto
        timeout_seconds: Maximum execution time

    Returns:
        Configured CodexCLIAdapter
    """
    mode = AutonomyMode(autonomy_mode)
    config = CodexCLIConfig(
        autonomy_mode=mode,
        timeout_seconds=timeout_seconds,
    )
    return CodexCLIAdapter(
        agent_id=agent_id,
        workspace_path=workspace_path,
        config=config,
    )
