"""OpenAI Codex CLI integration for opt-in repo-based codegen runs."""
from __future__ import annotations

import asyncio
import os
import shutil
import sys
from pathlib import Path
from typing import Optional

import structlog

from src.config import get_settings
from src.integrations.ai_provider import CodegenResult

logger = structlog.get_logger()


def resolve_codex_cli_path(configured: str) -> str | None:
    """Resolve Codex from PATH and common headless/server install locations."""
    found = shutil.which(configured)
    if found:
        return found
    if configured.lower() not in {"codex", "codex.exe"}:
        return None

    candidates: list[Path] = []
    install_dir = os.environ.get("CODEX_INSTALL_DIR")
    if install_dir:
        candidates.extend([Path(install_dir) / "codex", Path(install_dir) / "codex.exe"])
    codex_home = os.environ.get("CODEX_HOME")
    if codex_home:
        candidates.extend([Path(codex_home) / "bin" / "codex", Path(codex_home) / "bin" / "codex.exe"])
    home = Path.home()
    candidates.extend([
        home / ".local" / "bin" / "codex",
        home / ".local" / "bin" / "codex.exe",
        Path("/usr/local/bin/codex"),
        Path("/usr/bin/codex"),
    ])
    if sys.platform == "win32":
        local_app_data = os.environ.get("LOCALAPPDATA")
        if local_app_data:
            candidates.extend(Path(local_app_data, "OpenAI", "Codex", "bin").glob("*/codex.exe"))
    existing = [path for path in candidates if path.is_file()]
    if existing:
        return str(max(existing, key=lambda path: path.stat().st_mtime))
    return None


class CodexCLIProvider:
    provider_name = "codex"

    def __init__(self, cli_path: Optional[str] = None, model: Optional[str] = None, api_key: Optional[str] = None):
        settings = get_settings()
        self._cli = resolve_codex_cli_path(cli_path or settings.codex_cli_path) or (
            cli_path or settings.codex_cli_path
        )
        self._model = model or settings.codex_model
        self._timeout = settings.codex_cli_timeout
        self._api_key = api_key or settings.codex_api_key or settings.openai_api_key

    async def generate_plan(self, system_prompt, user_prompt, repo_path=None, line_callback=None, *, project_meta_path=None):
        return CodegenResult(
            plan={"document": "Codex CLI runs in apply mode for this integration test."},
            summary="Codex CLI is an apply-only provider.",
            provider=self.provider_name,
        )

    async def apply_changes(self, repo_path, plan, system_prompt, line_callback=None, *, project_meta_path=None):
        return await self.generate_and_apply(
            repo_path, system_prompt, plan.get("user_prompt", ""), line_callback=line_callback,
            project_meta_path=project_meta_path,
        )

    async def generate_and_apply(
        self, repo_path: str, system_prompt: str, user_prompt: str,
        line_callback=None, *, cursor_resume_session_id=None, cursor_agent_home=None,
        project_meta_path=None,
    ) -> CodegenResult:
        prompt = (
            "You are the P2HS codegen agent. Follow the system instructions below.\n\n"
            f"SYSTEM INSTRUCTIONS:\n{system_prompt}\n\n"
            f"USER REQUEST:\n{user_prompt}\n\n"
            "Implement the request directly in this repository. Do not merely describe changes. "
            "Keep platform-managed files unchanged."
        )
        # The hosting worker/VM is the isolation boundary. Avoid Codex's
        # bubblewrap workspace-write mode on hosts that block network namespaces.
        cmd = [self._cli, "exec", "--sandbox", "danger-full-access", "--skip-git-repo-check"]
        if self._model:
            cmd += ["--model", self._model]
        cmd.append("-")
        env = dict(os.environ)
        if self._api_key:
            env["CODEX_API_KEY"] = self._api_key
        logger.info("codex_cli_invoke", cli=self._cli, model_set=bool(self._model), timeout_s=self._timeout)
        try:
            proc = await asyncio.create_subprocess_exec(
                *cmd, cwd=repo_path, env=env, stdin=asyncio.subprocess.PIPE,
                stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
            )
            stdout, stderr = await asyncio.wait_for(proc.communicate(prompt.encode()), timeout=self._timeout)
            out = stdout.decode("utf-8", errors="replace")
            err = stderr.decode("utf-8", errors="replace")
            for line in (out + "\n" + err).splitlines():
                if line.strip() and line_callback:
                    try:
                        await line_callback(line, "stdout" if line in out.splitlines() else "stderr")
                    except TypeError:
                        await line_callback(line)
            changed = await self._changed_files(repo_path)
            summary = out.strip() or err.strip() or f"Codex CLI exited with code {proc.returncode}."
            return CodegenResult(
                summary=summary[-4000:], files_changed=changed or None,
                provider=self.provider_name, provider_invocation_failed=proc.returncode != 0,
            )
        except asyncio.TimeoutError:
            try:
                proc.kill()
            except (ProcessLookupError, UnboundLocalError):
                pass
            return CodegenResult(summary=f"Codex CLI timed out after {self._timeout}s", provider=self.provider_name, provider_invocation_failed=True)
        except Exception as exc:
            return CodegenResult(summary=f"Codex CLI error: {exc}", provider=self.provider_name, provider_invocation_failed=True)

    async def _changed_files(self, repo_path: str) -> list[str]:
        proc = await asyncio.create_subprocess_exec(
            "git", "status", "--porcelain", cwd=repo_path,
            stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
        )
        stdout, _ = await proc.communicate()
        return [line[3:] for line in stdout.decode("utf-8", errors="replace").splitlines() if len(line) > 3]

    @property
    def available(self) -> bool:
        return resolve_codex_cli_path(self._cli) is not None
