"""OpenAI LLM provider — GPT-5.4 Vision + Responses API image_generation."""

import base64
import json
import logging

import httpx

from app.config import settings
from app.services.llm.base import (
    BaseLLMProvider,
    GarmentAnalysis,
    StyleAnalysis,
    StyleAnalysisTemp,
)
from app.services.llm.prompts import (
    GARMENT_ANALYSIS_PROMPT,
    GHOST_MANNEQUIN_EDIT_PROMPT,
    STYLE_ANALYSIS_PROMPT,
    STYLE_ANALYSIS_TEMP,
    VTON_PROMPT,
)

logger = logging.getLogger(__name__)

OPENAI_BASE_URL = "https://api.openai.com/v1"
# GPT-5.4 — frontier vision model for garment analysis and image generation
# GPT-5.4 Mini — faster, lighter model for context-aware style analysis
OPENAI_VISION_MODEL = "gpt-5.4-mini"
OPENAI_VISION_MODEL_HIGH = "gpt-5.4"
OPENAI_MINI_MODEL = "gpt-5.4-mini"
# OPENAI_VISION_MODEL = "gpt-5.5"
# OPENAI_VISION_MODEL_HIGH = "gpt-5.5"
# OPENAI_MINI_MODEL = "gpt-5.5"
# Image generation runs through the direct /v1/images/edits endpoint (skips
# Responses API agent overhead). The model is resolved per call:
# explicit ``image_model`` arg → ``settings.OPENAI_IMAGE_MODEL`` fallback.
# Choices: "gpt-image-2" or "gpt-image-1.5" (Dec 2025 — ~4x faster than
# gpt-image-1, better face/logo/lighting preservation).

# Models that accept a custom ``temperature``. GPT-5.5+ reasoning-tier models
# reject it (HTTP 400 ``unsupported_value`` — only the default value 1 is
# allowed); gpt-5.4 and earlier accept it. Models NOT listed here are treated
# as not temperature-capable: omitting ``temperature`` always succeeds, while
# sending an unsupported value is a hard 400.
TEMPERATURE_CAPABLE_MODELS = {"gpt-5.4", "gpt-5.4-mini"}


def _sampling_params(model: str, temperature: float) -> dict:
    """Sampling params for ``model`` — include ``temperature`` only if supported.

    Keeps a low, deterministic ``temperature`` for models that honor it while
    transparently dropping the param for models that don't, instead of
    hard-failing the request.
    """
    if model in TEMPERATURE_CAPABLE_MODELS:
        return {"temperature": temperature}
    return {}


class OpenAIProvider(BaseLLMProvider):
    """OpenAI API provider using httpx for async requests."""

    def __init__(self) -> None:
        self.api_key = settings.OPENAI_API_KEY
        if not self.api_key:
            logger.warning("OPENAI_API_KEY is not set — OpenAI calls will fail")

    def _auth_header(self) -> dict[str, str]:
        return {"Authorization": f"Bearer {self.api_key}"}

    def _json_headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }

    async def _chat_completion(self, payload: dict) -> dict:
        """POST to /chat/completions, surfacing OpenAI's error body on failure.

        ``raise_for_status()`` alone discards the response body, but that body
        carries the actionable detail (unknown model, unsupported parameter
        value, etc.). Logging it mirrors what ``generate_ghost_mannequin``
        already does for the images endpoint.
        """
        url = f"{OPENAI_BASE_URL}/chat/completions"
        async with httpx.AsyncClient(timeout=60.0) as client:
            resp = await client.post(url, json=payload, headers=self._json_headers())
            if not resp.is_success:
                logger.error(
                    "OpenAI chat/completions failed (model=%s): HTTP %d — %s",
                    payload.get("model"),
                    resp.status_code,
                    resp.text,
                )
                resp.raise_for_status()
            return resp.json()

    async def analyze_garment(self, image: bytes) -> list[GarmentAnalysis]:
        """Analyze garments in image using GPT-5.4 Vision."""
        b64_image = base64.b64encode(image).decode()

        payload = {
            "model": OPENAI_VISION_MODEL,
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": GARMENT_ANALYSIS_PROMPT},
                        {
                            "type": "image_url",
                            "image_url": {
                                "url": f"data:image/jpeg;base64,{b64_image}",
                                "detail": "high",
                            },
                        },
                    ],
                }
            ],
            **_sampling_params(OPENAI_VISION_MODEL, 0.2),
            "max_completion_tokens": 4096,
            "response_format": {"type": "json_object"},
        }

        data = await self._chat_completion(payload)

        text = data["choices"][0]["message"]["content"]
        parsed = json.loads(text)

        # GPT json_object mode may wrap in {"garments": [...]}
        if isinstance(parsed, dict):
            for v in parsed.values():
                if isinstance(v, list):
                    parsed = v
                    break

        if not isinstance(parsed, list):
            logger.error("Unexpected GPT response format: %s", type(parsed))
            return []

        return [GarmentAnalysis(**item) for item in parsed]

    async def generate_ghost_mannequin(
        self,
        image: bytes,
        item: GarmentAnalysis,
        mode: str,
        image_model: str | None = None,
    ) -> bytes | None:
        """Generate ghost mannequin via /v1/images/edits.

        Skips the Responses-API agent reasoning overhead by calling the direct
        edits endpoint; ``input_fidelity=high`` preserves logos, stitching, and
        color patterns more faithfully than the previous setup. The ``mode``
        parameter is accepted for signature compatibility with Gemini but is
        currently ignored (single "edit" path). ``image_model`` selects the
        image-generation model; ``None`` falls back to ``OPENAI_IMAGE_MODEL``.
        """
        model = image_model or settings.OPENAI_IMAGE_MODEL
        prompt = GHOST_MANNEQUIN_EDIT_PROMPT.format(
            category_main=item.category_main,
            category_sub=item.category_sub or item.category_main,
        )

        files: list[tuple[str, tuple[str, bytes, str]]] = [
            ("image[]", ("source.jpg", image, "image/jpeg")),
        ]

        data = {
            "model": model,
            "prompt": prompt,
            "n": "1",
            "size": "1024x1024",
            "quality": "low",
            "output_format": "png",
        }
        # ``input_fidelity`` (logo/stitching preservation) is only supported by
        # gpt-image-1 / gpt-image-1.5 — gpt-image-2 rejects it with HTTP 400.
        # Transparent ``background`` is intentionally NOT requested: the ghost
        # lands on a plain white backdrop (see GHOST_MANNEQUIN_EDIT_PROMPT) that
        # GarmentService strips with rembg, so every image model takes the same
        # path and gpt-image-2's lack of transparent-background support is moot.
        if model in ("gpt-image-1", "gpt-image-1.5"):
            data["input_fidelity"] = "high"

        url = f"{OPENAI_BASE_URL}/images/edits"

        async with httpx.AsyncClient(timeout=180.0) as client:
            resp = await client.post(
                url,
                data=data,
                files=files,
                headers=self._auth_header(),
            )
            if not resp.is_success:
                error_body = resp.text
                logger.error(
                    "OpenAI ghost mannequin failed: HTTP %d — %s",
                    resp.status_code, error_body,
                )
                resp.raise_for_status()
            body = resp.json()

        items = body.get("data") or []
        if not items:
            logger.warning("OpenAI images/edits returned no image")
            return None
        b64 = items[0].get("b64_json", "")
        if not b64:
            logger.warning("OpenAI images/edits returned empty b64_json")
            return None
        return base64.b64decode(b64)

    async def generate_vton(
        self,
        person_image: bytes,
        ghost_images: list[bytes],
        categories: list[str],
        image_model: str | None = None,
    ) -> tuple[bytes | None, str | None]:
        """Composite ghost garments onto a person via /v1/images/edits.

        Sends the person photo and each ghost image as multipart ``image[]``
        parts. The direct edits endpoint skips the Responses-API agent
        reasoning, so it is notably faster but does not return any free-text
        remark — the second tuple slot is always None. ``image_model`` selects
        the image-generation model; ``None`` falls back to ``OPENAI_IMAGE_MODEL``.
        """
        model = image_model or settings.OPENAI_IMAGE_MODEL
        catalog = ", ".join(f"#{i + 1}={cat}" for i, cat in enumerate(categories))
        prompt = VTON_PROMPT.format(
            garment_count=len(ghost_images),
            catalog=catalog,
        )

        files: list[tuple[str, tuple[str, bytes, str]]] = [
            ("image[]", ("person.jpg", person_image, "image/jpeg")),
        ]
        for i, g in enumerate(ghost_images):
            files.append(("image[]", (f"garment_{i}.png", g, "image/png")))

        data = {
            "model": model,
            "prompt": prompt,
            "n": "1",
            "size": "1024x1536",
            "quality": "low",
            "output_format": "png",
        }

        url = f"{OPENAI_BASE_URL}/images/edits"

        async with httpx.AsyncClient(timeout=240.0) as client:
            resp = await client.post(
                url,
                data=data,
                files=files,
                headers=self._auth_header(),
            )
            if not resp.is_success:
                error_body = resp.text
                logger.error(
                    "OpenAI VTON failed: HTTP %d — %s", resp.status_code, error_body
                )
                resp.raise_for_status()
            body = resp.json()

        items = body.get("data") or []
        if not items:
            logger.warning("OpenAI VTON response contained no image")
            return None, None
        b64 = items[0].get("b64_json", "")
        image_bytes = base64.b64decode(b64) if b64 else None
        if image_bytes is None:
            logger.warning("OpenAI VTON response contained no image")
        return image_bytes, None

    async def analyze_style(self, image: bytes) -> StyleAnalysis:
        """Analyze overall outfit style using GPT-5.4 Vision."""
        b64_image = base64.b64encode(image).decode()

        payload = {
            "model": OPENAI_VISION_MODEL,
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": STYLE_ANALYSIS_PROMPT},
                        {
                            "type": "image_url",
                            "image_url": {
                                "url": f"data:image/jpeg;base64,{b64_image}",
                                "detail": "high",
                            },
                        },
                    ],
                }
            ],
            **_sampling_params(OPENAI_VISION_MODEL, 0.3),
            "max_completion_tokens": 4096,
            "response_format": {"type": "json_object"},
        }

        data = await self._chat_completion(payload)

        text = data["choices"][0]["message"]["content"]
        parsed = json.loads(text)

        return StyleAnalysis(**parsed)

    async def analyze_style_temp(self, image: bytes) -> StyleAnalysisTemp:
        """Context-aware outfit style analysis using GPT-5.4 Mini."""
        b64_image = base64.b64encode(image).decode()

        payload = {
            "model": OPENAI_MINI_MODEL,
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": STYLE_ANALYSIS_TEMP},
                        {
                            "type": "image_url",
                            "image_url": {
                                "url": f"data:image/jpeg;base64,{b64_image}",
                                "detail": "high",
                            },
                        },
                    ],
                }
            ],
            **_sampling_params(OPENAI_MINI_MODEL, 0.3),
            "max_completion_tokens": 4096,
            "response_format": {"type": "json_object"},
        }

        data = await self._chat_completion(payload)

        text = data["choices"][0]["message"]["content"]
        parsed = json.loads(text)

        return StyleAnalysisTemp(**parsed)
