"""VTON (Virtual Try-On) pipeline: person + ghost garments → composited image.

Stateless — no DB record is created for the result. The composite image is
saved under ``storage/vton/{user_id}/{uuid}.png`` and the URL is returned.

Flow:
1. Validate ``garment_ids`` (non-empty).
2. Detect a face in the person image — if none, fail fast with ``ValueError``
   so the router can surface a 400.
3. Fetch the selected garments from DB, preserving request order.
4. Require each garment to have a ``ghost_image_path`` (source_image is a
   full-body person photo, not an isolated garment — unsuitable for VTON).
5. Resize person + load each ghost image bytes from disk.
6. Call ``llm.generate_vton`` (multi-image) → composite bytes + optional note.
7. Remove background from the LLM output (it renders the person on the same
   white backdrop we fed it) so the saved composite has a transparent PNG
   background matching the original RGBA input.
8. Save composite image; parse "#N" references in the note to split
   ``used_garment_ids`` / ``skipped_garment_ids``.

Upper-body handling is delegated to the LLM via prompt instruction: when the
person image is an upper-body crop, the LLM skips bottom-category garments
and reports the skip in its text response (see ``VTON_PROMPT``).
"""

import asyncio
import logging
import uuid
from dataclasses import dataclass
from pathlib import Path

import cv2
import numpy as np
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.config import GLOBAL_USER_ID, settings
from app.models.garment import Garment
from app.services.llm.base import BaseLLMProvider
from app.services.segmentation_service import SegmentationService
from app.utils.face_analyzer import FaceAnalyzer
from app.utils.image import (
    resize_image_if_needed,
    resize_png_if_needed,
    save_vton_image,
)

logger = logging.getLogger(__name__)


@dataclass
class VtonResult:
    """Internal result object returned by ``VtonService.generate``."""

    path: str
    used_ids: list[uuid.UUID]
    skipped_ids: list[uuid.UUID]
    note: str | None


class VtonService:
    def __init__(
        self,
        llm_provider: BaseLLMProvider,
        face_analyzer: FaceAnalyzer,
        segmentation_service: SegmentationService,
    ):
        self.llm = llm_provider
        self.face = face_analyzer
        self.segmentation = segmentation_service

    async def generate(
        self,
        db: AsyncSession,
        person_image: bytes,
        garment_ids: list[uuid.UUID],
        user_id: uuid.UUID | None,
        image_model: str | None = None,
    ) -> VtonResult:
        if not garment_ids:
            raise ValueError("garment_ids는 최소 1개 이상이어야 합니다.")

        resolved_user_id = user_id or GLOBAL_USER_ID

        # 1. Face detection — fail fast if no face (or image can't be decoded).
        arr = np.frombuffer(person_image, dtype=np.uint8)
        decoded = cv2.imdecode(arr, cv2.IMREAD_COLOR)
        if decoded is None:
            raise ValueError("이미지를 디코딩할 수 없습니다.")
        embeddings = self.face.get_embeddings(decoded)
        if not embeddings:
            raise ValueError("얼굴을 감지할 수 없습니다. 인물 사진이 필요합니다.")

        # 2. Fetch garments in request order.
        result = await db.execute(select(Garment).where(Garment.id.in_(garment_ids)))
        garments = list(result.scalars().all())
        by_id = {g.id: g for g in garments}

        missing = [gid for gid in garment_ids if gid not in by_id]
        if missing:
            raise LookupError(f"의류를 찾을 수 없습니다: {missing[0]}")

        ordered = [by_id[gid] for gid in garment_ids]

        # 3. Ghost validity — source_image is a full-body photo and is not
        # acceptable here; a generated ghost is required.
        for g in ordered:
            if not g.ghost_image_path:
                raise ValueError(
                    f"고스트 마네킹이 생성되지 않은 의류입니다: {g.id}. "
                    "먼저 POST /api/v1/garments/{id}/ghost 로 고스트를 생성하세요."
                )

        # 4. Prepare LLM inputs.
        person_resized = resize_image_if_needed(person_image, max_size=1024)

        ghost_bytes_list: list[bytes] = []
        for g in ordered:
            ghost_full = Path(settings.UPLOAD_DIR) / g.ghost_image_path
            if not ghost_full.exists():
                raise ValueError(f"고스트 이미지 파일이 없습니다: {g.ghost_image_path}")
            ghost_bytes_list.append(
                resize_png_if_needed(ghost_full.read_bytes(), max_size=1024)
            )

        categories = [g.category_main for g in ordered]

        # 5. Call LLM.
        try:
            image_bytes, _ = await self.llm.generate_vton(
                person_resized, ghost_bytes_list, categories, image_model
            )
        except Exception as e:
            raise RuntimeError(f"LLM VTON 생성 실패: {e}") from e
        if image_bytes is None:
            raise RuntimeError("LLM이 VTON 이미지를 반환하지 않았습니다")

        # 6. Strip the white backdrop the LLM preserved from our input.
        # resize_image_if_needed composites RGBA → white before the JPEG encode
        # we hand to the model, so the returned PNG has a solid white
        # background even when the caller's original image was transparent.
        # rembg runs on CPU/GPU synchronously, so offload to a worker thread
        # to avoid blocking the event loop. On failure, fall back to the raw
        # LLM bytes so the caller still gets a usable result.
        try:
            final_bytes = await asyncio.to_thread(
                self.segmentation.remove_background, image_bytes
            )
        except Exception:
            logger.warning(
                "VTON background removal failed; saving raw LLM output",
                exc_info=True,
            )
            final_bytes = image_bytes

        # 7. Persist composite image (stateless — no DB row).
        path = save_vton_image(final_bytes, resolved_user_id)

        # The /v1/images/edits endpoint does not surface which garments the
        # model silently dropped (e.g., bottoms on an upper-body crop). Treat
        # every requested garment as "used"; clients rely on the rendered image.
        used_ids = list(garment_ids)
        skipped_ids: list[uuid.UUID] = []

        logger.info(
            "VTON done user=%s garments=%d path=%s",
            resolved_user_id, len(used_ids), path,
        )
        return VtonResult(path=path, used_ids=used_ids, skipped_ids=skipped_ids, note=None)
