"""Claude Code (Anthropic) integration - implements AICodeGeneratorProtocol.

Claude Code is Anthropic's agentic coding CLI that can:
  - Understand full repo context
  - Plan and execute multi-file changes
  - Create git commits
  - Follow CLAUDE.md project instructions

Two modes of operation:
  1. API mode: Use Anthropic API directly for plan generation
  2. CLI mode: Use `claude` CLI for repo-aware agentic coding

Usage flow (CLI mode):
  1. Clone/checkout the project repo
  2. Write system prompt to CLAUDE.md in repo root
  3. Invoke `claude --dir <path> --message "<prompt>" --yes`
  4. Read git diff / commit SHA
  5. Push changes back to GitHub

Usage flow (API mode):
  1. Use Anthropic Messages API with streaming for plan generation
  2. Return plan as Markdown document
  3. Worker stores and presents plan to user
"""
from __future__ import annotations
import asyncio
import os
import shutil
from typing import Callable, Coroutine, Optional

import structlog

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

logger = structlog.get_logger()

LineCallback = Optional[Callable[[str], Coroutine]]


class ClaudeCodeProvider:
    provider_name = "claude"

    def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
        settings = get_settings()
        self._api_key = api_key or settings.anthropic_api_key
        self._model = model or settings.claude_model
        self._cli_available = self._check_cli()

    def _check_cli(self) -> bool:
        import shutil
        return shutil.which("claude") is not None

    async def _run_cli_streaming(
        self,
        repo_path: str,
        prompt: str,
        system_prompt: Optional[str] = None,
        line_callback: LineCallback = None,
        *,
        project_meta_path: Optional[str] = None,
    ) -> tuple[str, str, int]:
        if system_prompt:
            if project_meta_path:
                os.makedirs(project_meta_path, exist_ok=True)
                canon = os.path.join(project_meta_path, "CLAUDE.md")
                with open(canon, "w", encoding="utf-8") as f:
                    f.write(system_prompt)
                shutil.copyfile(canon, os.path.join(repo_path, "CLAUDE.md"))
            else:
                claude_md = os.path.join(repo_path, "CLAUDE.md")
                with open(claude_md, "w", encoding="utf-8") as f:
                    f.write(system_prompt)

        cmd = ["claude", "--dir", repo_path, "--message", prompt, "--yes"]
        if self._model:
            cmd += ["--model", self._model]

        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            stdin=asyncio.subprocess.DEVNULL,
            cwd=repo_path,
            env={**os.environ, "ANTHROPIC_API_KEY": self._api_key},
        )

        stdout_lines: list[str] = []
        stderr_lines: list[str] = []

        async def _read_stdout() -> None:
            assert proc.stdout is not None
            async for raw_line in proc.stdout:
                line = raw_line.decode("utf-8", errors="replace").rstrip()
                stdout_lines.append(line)
                if line_callback:
                    try:
                        await line_callback(line)
                    except Exception:
                        pass

        async def _read_stderr() -> None:
            assert proc.stderr is not None
            async for raw_line in proc.stderr:
                stderr_lines.append(raw_line.decode("utf-8", errors="replace").rstrip())

        try:
            await asyncio.wait_for(
                asyncio.gather(_read_stdout(), _read_stderr()),
                timeout=300,
            )
        except asyncio.TimeoutError:
            try:
                proc.kill()
            except ProcessLookupError:
                pass
            raise

        await proc.wait()
        rc = int(proc.returncode if proc.returncode is not None else -1)
        return "\n".join(stdout_lines), "\n".join(stderr_lines), rc

    async def _get_latest_commit(self, repo_path: str) -> tuple[str, str]:
        proc = await asyncio.create_subprocess_exec(
            "git", "log", "-1", "--format=%H%n%s",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=repo_path,
        )
        stdout, _ = await proc.communicate()
        lines = stdout.decode().strip().split("\n")
        sha = lines[0] if lines else ""
        message = lines[1] if len(lines) > 1 else ""
        return sha, message

    async def _get_changed_files(self, repo_path: str) -> list[str]:
        proc = await asyncio.create_subprocess_exec(
            "git", "diff", "--name-only", "HEAD~1",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=repo_path,
        )
        stdout, _ = await proc.communicate()
        return [f for f in stdout.decode().strip().split("\n") if f]

    async def _get_changed_files_detail(self, repo_path: str) -> list[dict]:
        proc = await asyncio.create_subprocess_exec(
            "git", "diff", "--numstat", "--diff-filter=AMCRD", "HEAD~1",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=repo_path,
        )
        stdout, _ = await proc.communicate()
        proc_status = await asyncio.create_subprocess_exec(
            "git", "diff", "--name-status", "--diff-filter=AMCRD", "HEAD~1",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=repo_path,
        )
        stdout_status, _ = await proc_status.communicate()
        status_lines = [l for l in stdout_status.decode().strip().split("\n") if l]
        status_map: dict[str, str] = {}
        for line in status_lines:
            parts = line.split("\t", 1)
            if len(parts) == 2:
                status_map[parts[1]] = parts[0][0]
        result = []
        for line in stdout.decode().strip().split("\n"):
            if not line:
                continue
            parts = line.split("\t")
            if len(parts) >= 3:
                add_s, del_s, path = parts[0], parts[1], parts[2]
                code = status_map.get(path, "M")
                change_type = "added" if code == "A" else "deleted" if code == "D" else "modified"
                result.append({
                    "path": path,
                    "change_type": change_type,
                    "lines_added": int(add_s) if add_s != "-" else 0,
                    "lines_removed": int(del_s) if del_s != "-" else 0,
                })
        return result

    async def _api_generate_plan(
        self,
        system_prompt: str,
        user_prompt: str,
        line_callback: LineCallback = None,
    ) -> CodegenResult:
        """Generate a markdown plan via Anthropic streaming API (no CLI needed)."""
        try:
            import anthropic
        except ImportError:
            return CodegenResult(
                plan={"document": ""},
                summary="anthropic package not installed. Add to requirements.txt.",
                provider=self.provider_name,
            )

        client = anthropic.AsyncAnthropic(api_key=self._api_key)

        plan_chunks: list[str] = []
        tokens = 0

        async with client.messages.stream(
            model=self._model,
            max_tokens=4096,
            system=system_prompt,
            messages=[{"role": "user", "content": user_prompt}],
        ) as stream:
            async for text_delta in stream.text_stream:
                plan_chunks.append(text_delta)
                if line_callback and "\n" in text_delta:
                    # Emit complete lines as they arrive
                    for line in text_delta.split("\n"):
                        if line:
                            try:
                                await line_callback(line)
                            except Exception:
                                pass

            final_msg = await stream.get_final_message()
            tokens = (
                (final_msg.usage.input_tokens + final_msg.usage.output_tokens)
                if final_msg.usage
                else 0
            )

        plan_document = "".join(plan_chunks).strip()

        return CodegenResult(
            plan={"document": plan_document},
            summary=plan_document[:300] if plan_document else "Plan generated.",
            tokens_used=tokens,
            provider=self.provider_name,
        )

    async def generate_plan(
        self,
        system_prompt: str,
        user_prompt: str,
        repo_path: Optional[str] = None,
        line_callback: LineCallback = None,
        *,
        project_meta_path: Optional[str] = None,
    ) -> CodegenResult:
        """Generate a markdown implementation plan.

        Uses Anthropic streaming API. repo_path is accepted for protocol
        compatibility but Claude plan generation is always API-based.
        """
        return await self._api_generate_plan(system_prompt, user_prompt, line_callback=line_callback)

    async def apply_changes(
        self,
        repo_path: str,
        plan: dict,
        system_prompt: str,
        line_callback: LineCallback = None,
        *,
        project_meta_path: Optional[str] = None,
    ) -> CodegenResult:
        user_prompt = plan.get("user_prompt", plan.get("original_user_prompt", ""))
        return await self.generate_and_apply(
            repo_path,
            system_prompt,
            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: LineCallback = None,
        *,
        cursor_resume_session_id: Optional[str] = None,
        cursor_agent_home: Optional[str] = None,
        project_meta_path: Optional[str] = None,
    ) -> CodegenResult:
        """Primary flow: if CLI available, use it for agentic coding.
        Otherwise fall back to API-only plan generation."""
        if not self._cli_available:
            logger.info("claude_cli_not_available_using_api")
            return await self._api_generate_plan(system_prompt, user_prompt, line_callback=line_callback)

        try:
            stdout, stderr, rc = await self._run_cli_streaming(
                repo_path=repo_path,
                prompt=user_prompt,
                system_prompt=system_prompt,
                line_callback=line_callback,
                project_meta_path=project_meta_path,
            )
            sha, message = await self._get_latest_commit(repo_path)
            changed = await self._get_changed_files(repo_path)
            changed_detail = await self._get_changed_files_detail(repo_path)

            logger.info(
                "claude_code_completed",
                repo=repo_path, sha=sha[:8] if sha else "none",
                files_changed=len(changed),
                returncode=rc,
            )

            summary = message or stdout.strip() or stderr.strip()
            if rc != 0 and not summary:
                summary = f"Claude Code exited with code {rc}."

            return CodegenResult(
                commit_sha=sha,
                summary=summary,
                files_changed=changed,
                files_changed_detail=changed_detail if changed_detail else None,
                provider=self.provider_name,
                provider_invocation_failed=rc != 0,
            )
        except asyncio.TimeoutError:
            logger.error("claude_code_timeout", repo=repo_path)
            return CodegenResult(
                summary="Claude Code timed out after 300s",
                provider=self.provider_name,
                provider_invocation_failed=True,
            )
        except Exception as e:
            logger.error("claude_code_error", error=str(e))
            return CodegenResult(
                summary=f"Claude Code error: {e}",
                provider=self.provider_name,
                provider_invocation_failed=True,
            )
