"""OpenAI integration - implements AICodeGeneratorProtocol.

Uses OpenAI Chat Completions API for plan generation, code generation,
and code review with structured JSON output.
"""
from __future__ import annotations
from typing import Optional

import json

import structlog
from openai import AsyncOpenAI

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

logger = structlog.get_logger()


class OpenAIProvider:
    provider_name = "openai"

    def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
        settings = get_settings()
        self._client = AsyncOpenAI(api_key=api_key or settings.openai_api_key)
        self._model = model or settings.openai_model

    async def generate_plan(
        self,
        system_prompt: str,
        user_prompt: str,
        repo_path: Optional[str] = None,
        line_callback: Optional[object] = None,
        *,
        project_meta_path: Optional[str] = None,
    ) -> CodegenResult:
        response = await self._client.chat.completions.create(
            model=self._model,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt},
            ],
            response_format={"type": "json_object"},
            temperature=0.2,
        )
        content = response.choices[0].message.content
        tokens = response.usage.total_tokens if response.usage else 0
        logger.info("openai_plan_generated", tokens=tokens)

        try:
            plan = json.loads(content)
        except json.JSONDecodeError:
            plan = {"raw_response": content}

        files_detail = self._extract_files_from_plan(plan)
        return CodegenResult(
            plan=plan,
            summary=plan.get("commit_message", ""),
            tokens_used=tokens,
            files_changed_detail=files_detail,
            files_changed=[f["path"] for f in files_detail] if files_detail else None,
            provider=self.provider_name,
        )

    def _extract_files_from_plan(self, plan: dict) -> list[dict]:
        """Extract changed file metadata from plan tasks (files + action)."""
        tasks = plan.get("tasks")
        if not isinstance(tasks, list):
            return []
        seen: set[str] = set()
        result: list[dict] = []
        for t in tasks:
            files = t.get("files")
            action = (t.get("action") or "modify").lower()
            if not isinstance(files, list):
                continue
            for path in files:
                if not isinstance(path, str) or path in seen:
                    continue
                seen.add(path)
                change_type = "added" if action == "create" else "deleted" if action == "delete" else "modified"
                result.append({"path": path, "change_type": change_type})
        return result

    async def apply_changes(
        self,
        repo_path: str,
        plan: dict,
        system_prompt: str,
        line_callback: Optional[object] = None,
        *,
        project_meta_path: Optional[str] = None,
    ) -> CodegenResult:
        """OpenAI doesn't natively apply changes to files.
        The worker must apply the plan's file operations to the repo."""
        return CodegenResult(
            plan=plan,
            summary="Plan generated - changes must be applied by worker",
            provider=self.provider_name,
        )

    async def generate_and_apply(
        self,
        repo_path: str,
        system_prompt: str,
        user_prompt: str,
        line_callback: Optional[object] = None,
        *,
        cursor_resume_session_id: Optional[str] = None,
        cursor_agent_home: Optional[str] = None,
        project_meta_path: Optional[str] = None,
    ) -> CodegenResult:
        result = await self.generate_plan(
            system_prompt,
            user_prompt,
            repo_path=repo_path,
            line_callback=line_callback,
            project_meta_path=project_meta_path,
        )
        return result

    async def generate_code(
        self, system_prompt: str, user_prompt: str,
        context_files: Optional[dict[str, str]] = None,
    ) -> CodegenResult:
        messages = [{"role": "system", "content": system_prompt}]

        if context_files:
            file_context = "\n\n".join(
                f"### {path}\n```\n{content}\n```"
                for path, content in context_files.items()
            )
            messages.append({"role": "user", "content": f"## Current Files\n{file_context}"})

        messages.append({"role": "user", "content": user_prompt})

        response = await self._client.chat.completions.create(
            model=self._model,
            messages=messages,
            response_format={"type": "json_object"},
            temperature=0.1,
        )
        content = response.choices[0].message.content
        tokens = response.usage.total_tokens if response.usage else 0

        try:
            result = json.loads(content)
        except json.JSONDecodeError:
            result = {"raw_response": content}

        return CodegenResult(
            plan=result,
            summary=result.get("commit_message", ""),
            files_changed=list(result.get("files", {}).keys()) if isinstance(result.get("files"), dict) else None,
            tokens_used=tokens,
            provider=self.provider_name,
        )

    async def review_code(self, diff: str, instructions: str) -> dict:
        response = await self._client.chat.completions.create(
            model=self._model,
            messages=[
                {"role": "system", "content": "You are a code reviewer for the P2HS platform. Review the diff against the project instructions and provide feedback."},
                {"role": "user", "content": f"## Instructions\n{instructions}\n\n## Diff\n```\n{diff}\n```"},
            ],
            response_format={"type": "json_object"},
            temperature=0.1,
        )
        content = response.choices[0].message.content
        try:
            return json.loads(content)
        except json.JSONDecodeError:
            return {"raw_response": content}
