"""OpenAI API 클라이언트 — urllib 기반 구현 + 자동 DB 로깅."""

import json
import socket
import time
import urllib.error
import urllib.request
from typing import Any, Dict, List

from app.core.config import settings
from app.modules.llm.base import BaseLLMClient
from app.modules.llm.llm_logger import log_llm_call

OPENAI_API_URL = "https://api.openai.com/v1/responses"


def _response_text_from_payload(payload: Dict[str, Any]) -> str:
    """Extract text from OpenAI Responses API payload."""
    output_text = payload.get("output_text")
    if isinstance(output_text, str) and output_text.strip():
        return output_text

    output = payload.get("output")
    if isinstance(output, list):
        for item in output:
            if not isinstance(item, dict):
                continue
            content = item.get("content")
            if not isinstance(content, list):
                continue
            for part in content:
                if isinstance(part, dict) and part.get("type") == "output_text":
                    text = part.get("text")
                    if isinstance(text, str) and text.strip():
                        return text

    raise RuntimeError("OpenAI response did not include output_text.")


class OpenAIClient(BaseLLMClient):
    """OpenAI Responses API client using urllib.request + 자동 DB 로깅."""

    def __init__(
        self,
        api_key: str | None = None,
        model: str | None = None,
    ) -> None:
        self._api_key = api_key or settings.openai_api_key
        self._model = model or settings.openai_model
        self._ctx: Dict[str, Any] = {}

    def set_context(self, **kwargs) -> "OpenAIClient":
        """로깅 컨텍스트 설정: project_id, episode_id, operation_type, step_name."""
        self._ctx.update(kwargs)
        return self

    def generate_structured(
        self,
        system_prompt: str,
        user_prompt: str,
        response_schema: Dict[str, Any],
        schema_name: str = "response",
        max_tokens: int = None,
    ) -> dict:
        if max_tokens is None:
            max_tokens = settings.llm_max_output_tokens
        if not self._api_key:
            raise RuntimeError("OpenAI API key is not configured.")

        body: Dict[str, Any] = {
            "model": self._model,
            "instructions": system_prompt,
            "input": user_prompt,
            "temperature": 0.2,
            "store": False,
            "text": {
                "format": {
                    "type": "json_schema",
                    "name": schema_name,
                    "strict": True,
                    "schema": response_schema,
                }
            },
        }
        if max_tokens is not None:
            body["max_output_tokens"] = max_tokens

        request = urllib.request.Request(
            OPENAI_API_URL,
            data=json.dumps(body).encode("utf-8"),
            headers={
                "Authorization": f"Bearer {self._api_key}",
                "Content-Type": "application/json",
            },
            method="POST",
        )

        timeout = settings.llm_timeout_text
        max_retries = settings.llm_max_retries
        last_error: Exception | None = None
        start_ms = time.monotonic()

        for attempt in range(1, max_retries + 2):
            try:
                with urllib.request.urlopen(request, timeout=timeout) as resp:
                    payload = json.loads(resp.read().decode("utf-8"))
                break
            except urllib.error.HTTPError as exc:
                error_text = exc.read().decode("utf-8", errors="replace")
                last_error = RuntimeError(f"OpenAI API error {exc.code}: {error_text}")
                if exc.code in {429, 500, 502, 503, 504} and attempt <= max_retries:
                    time.sleep(2 * attempt)
                    continue
                duration = int((time.monotonic() - start_ms) * 1000)
                log_llm_call(
                    model_name=self._model, user_prompt=user_prompt,
                    system_prompt=system_prompt, status="error",
                    error_message=str(last_error), duration_ms=duration,
                    **self._ctx,
                )
                raise last_error from exc
            except (urllib.error.URLError, socket.timeout) as exc:
                last_error = exc
                if attempt <= max_retries:
                    time.sleep(2 * attempt)
                    continue
                duration = int((time.monotonic() - start_ms) * 1000)
                log_llm_call(
                    model_name=self._model, user_prompt=user_prompt,
                    system_prompt=system_prompt, status="error",
                    error_message=str(last_error), duration_ms=duration,
                    **self._ctx,
                )
                raise RuntimeError(f"OpenAI API failed after {max_retries} retries: {exc}") from exc
        else:
            duration = int((time.monotonic() - start_ms) * 1000)
            log_llm_call(
                model_name=self._model, user_prompt=user_prompt,
                system_prompt=system_prompt, status="error",
                error_message=str(last_error), duration_ms=duration,
                **self._ctx,
            )
            raise RuntimeError(f"OpenAI API failed: {last_error}")

        duration = int((time.monotonic() - start_ms) * 1000)
        text = _response_text_from_payload(payload)

        # 토큰 사용량
        usage = payload.get("usage", {})
        input_tokens = usage.get("input_tokens")
        output_tokens = usage.get("output_tokens")

        # 자동 DB 로깅
        log_llm_call(
            model_name=self._model,
            system_prompt=system_prompt,
            user_prompt=user_prompt,
            output_text=text,
            duration_ms=duration,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            status="success",
            **self._ctx,
        )

        try:
            return json.loads(text)
        except json.JSONDecodeError as exc:
            raise RuntimeError(f"Structured output was not valid JSON:\n{text}") from exc
