"""Gemini i2i 앵글/색감 편집 모듈 — 원본 이미지 기반 변형 생성."""

import base64
import io
import json
import logging
import math
import socket
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Optional

from PIL import Image, ImageDraw

logger = logging.getLogger(__name__)

GEMINI_API_URL_TEMPLATE = (
    "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
)
from app.core.config import settings as _settings
from app.services.image_capture.sink import (
    capture_artifact,
    capture_generated_image,
)

I2I_PROMPT_DIR = (
    Path(__file__).resolve().parent.parent.parent.parent
    / "prompts" / "_base" / "i2i_editor" / "v1"
)

def _load_i2i_prompt(filename: str) -> str:
    return (I2I_PROMPT_DIR / filename).read_text(encoding="utf-8").strip()


def _create_camera_diagram(
    horizontal_deg: int,
    vertical_deg: int = 0,
    zoom: float = 1.0,
    size: int = 512,
) -> bytes:
    """카메라 앵글 다이어그램 이미지 생성 (PIL).

    Args:
        horizontal_deg: 수평 회전 각도 (0=front, 90=right, 180=behind, 270=left).
        vertical_deg: 수직 각도 (-30~30, positive=looking down, negative=looking up).
        zoom: 줌 배율 (0.8~1.5, 1.0=normal).
        size: 이미지 크기 (px).

    Returns:
        PNG image bytes.
    """
    img = Image.new("RGB", (size, size), (30, 30, 40))
    draw = ImageDraw.Draw(img)

    cx, cy = size // 2, size // 2
    r = size // 3

    # Draw the circle
    draw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=(200, 200, 200), width=2)

    # Degree labels
    draw.text((cx + r + 10, cy - 10), "0\u00b0", fill=(255, 100, 100))
    draw.text((cx - 15, cy + r + 10), "90\u00b0", fill=(255, 100, 100))

    # IMAGE label (center)
    draw.rectangle(
        [cx - 30, cy - 8, cx + 30, cy + 8],
        fill=(80, 80, 80),
        outline=(200, 200, 200),
    )
    draw.text((cx - 20, cy - 6), "IMAGE", fill=(255, 255, 255))

    # Camera position
    rad = math.radians(horizontal_deg)
    cam_x = cx + int(r * 0.7 * math.cos(rad))
    cam_y = cy + int(r * 0.7 * math.sin(rad))

    # Camera -> center arrow
    draw.line([(cam_x, cam_y), (cx, cy)], fill=(255, 200, 0), width=3)

    # Camera icon
    draw.rectangle(
        [cam_x - 12, cam_y - 10, cam_x + 12, cam_y + 10],
        fill=(180, 180, 180),
        outline=(255, 255, 255),
    )
    draw.text((cam_x - 25, cam_y + 15), "CAMERA", fill=(200, 200, 200))

    # Angle text
    draw.text((10, 10), f"Angle: {horizontal_deg}\u00b0", fill=(100, 200, 255))
    if vertical_deg != 0:
        draw.text((10, 30), f"Elevation: {vertical_deg}\u00b0", fill=(100, 200, 255))
    if zoom != 1.0:
        draw.text((10, 50), f"Zoom: {zoom:.1f}x", fill=(100, 200, 255))

    buf = io.BytesIO()
    img.save(buf, format="PNG")
    png = buf.getvalue()
    # Phase B: i2i 입력 카메라 다이어그램 capture(비모델 아티팩트, scope 미배선이면 no-op).
    capture_artifact(
        png,
        role="angle_camera_diagram",
        disposition="diagnostic",
        pipeline_metadata={
            "horizontal_deg": horizontal_deg,
            "vertical_deg": vertical_deg,
            "zoom": zoom,
        },
    )
    return png


def _gemini_generate_content(
    api_key: str,
    model: str,
    prompt: str,
    input_images: list,
    aspect_ratio: str = "16:9",
) -> bytes:
    """Gemini generateContent API 호출 — i2i 편집.

    Args:
        api_key: Gemini API key.
        model: Gemini model name.
        prompt: Text prompt for editing.
        input_images: List of (label, image_bytes) tuples.
        aspect_ratio: Aspect ratio for the output.

    Returns:
        Edited image bytes (PNG).

    Raises:
        RuntimeError: If the API call fails or no image is returned.
    """
    parts = [{"text": prompt}]

    for label, img_bytes in input_images:
        if label:
            parts.append({"text": label})
        parts.append({
            "inline_data": {
                "mime_type": "image/png",
                "data": base64.b64encode(img_bytes).decode("ascii"),
            }
        })

    body = {
        "contents": [{"parts": parts}],
        "generationConfig": {
            "responseModalities": ["TEXT", "IMAGE"],
            "imageConfig": {
                "aspectRatio": aspect_ratio,
                "imageSize": "2K",
            },
        },
    }

    url = GEMINI_API_URL_TEMPLATE.format(model=model, api_key=api_key)
    req = urllib.request.Request(
        url,
        data=json.dumps(body).encode("utf-8"),
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    timeout = _settings.llm_timeout_image_gen
    max_retries = _settings.llm_max_retries
    last_error: Exception | None = None

    # ★이 경로는 GeminiImageClient 를 우회하는 REST 직접 호출이라, 기록을 따로
    #  붙이지 않으면 **성공도 실패도 어디에도 안 남는다**(Codex 리뷰 2026-08-07).
    #  최종 스틸 경로(scene_image_service / scene_variation_service)가 여기를
    #  타므로, 여기 기록이 없으면 그 단계가 도는 동안 살아 있는지 볼 근거가 없다.
    from app.modules.llm.image_tracer import (
        ambient_call_meta, record_provider_call, resolve_step_name)

    meta = ambient_call_meta()
    step = resolve_step_name("i2i_edit", meta)
    started = time.monotonic()

    def _elapsed_ms() -> int:
        return int((time.monotonic() - started) * 1000)

    def _fail(message: str, status: str = "error") -> None:
        record_provider_call(
            step=step, model=model, prompt=prompt, status=status,
            duration_ms=_elapsed_ms(), meta=meta, ref_count=len(input_images),
            operation="i2i_edit", error=message[:500])

    try:
        for attempt in range(1, max_retries + 2):  # 첫 시도 + max_retries 재시도
            try:
                with urllib.request.urlopen(req, timeout=timeout) as response:
                    payload = json.loads(response.read().decode("utf-8"))
                break
            except urllib.error.HTTPError as exc:
                error_text = exc.read().decode("utf-8", errors="replace")
                last_error = RuntimeError(
                    f"Gemini i2i API error {exc.code}: {error_text}")
                if exc.code in {429, 500, 502, 503, 504} and attempt <= max_retries:
                    time.sleep(2 * attempt)
                    continue
                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
                raise RuntimeError(
                    f"Gemini i2i API failed after retries: {exc}") from exc
        else:
            raise RuntimeError(f"Gemini i2i API failed: {last_error}")

        # Check for moderation blocks
        prompt_feedback = payload.get("promptFeedback", {})
        block_reason = prompt_feedback.get("blockReason")
        if block_reason:
            _fail(f"Content moderation blocked: {block_reason}",
                  status="moderation_blocked")
            raise RuntimeError(f"Content moderation blocked: {block_reason}")

        candidates = payload.get("candidates", [])
        if candidates:
            finish_reason = candidates[0].get("finishReason", "")
            if finish_reason == "SAFETY":
                _fail("Content blocked by safety filter",
                      status="moderation_blocked")
                raise RuntimeError("Content blocked by safety filter")

        # Extract image bytes
        for candidate in candidates:
            content = candidate.get("content", {})
            for part in content.get("parts", []):
                inline_data = part.get("inlineData") or part.get("inline_data")
                if isinstance(inline_data, dict):
                    b64 = inline_data.get("data")
                    if b64:
                        img_bytes = base64.b64decode(b64)
                        call_id = record_provider_call(
                            step=step, model=model, prompt=prompt,
                            status="success", duration_ms=_elapsed_ms(),
                            meta=meta, ref_count=len(input_images),
                            operation="i2i_edit",
                            output_text="[image generated]")
                        # Phase B: i2i 생성 이미지 capture(scope 미배선이면 no-op).
                        # 이 단일점이 edit_angle/edit_color/edit_combined 모든
                        # 경로를 커버.
                        capture_generated_image(
                            img_bytes,
                            role="i2i_edit",
                            generation_call_id=call_id,
                            prompt=prompt,
                            pipeline_metadata={"model": model,
                                               "aspect_ratio": aspect_ratio},
                        )
                        return img_bytes
    except Exception as exc:
        # 위에서 이미 남긴 차단 사유는 두 번 세지 않는다.
        if not str(exc).startswith(("Content moderation blocked",
                                    "Content blocked by safety filter")):
            _fail(str(exc))
        raise

    _fail("Gemini i2i returned no image", status="empty_response")
    raise RuntimeError(f"Gemini i2i returned no image: {json.dumps(payload)[:500]}")


class GeminiI2IEditor:
    """Gemini i2i로 앵글/색감 편집.

    원본 이미지를 받아 카메라 앵글 변경, 색감/조명 변경, 또는
    둘 다 적용한 변형 이미지를 생성한다.
    """

    def __init__(self, api_key: str, model: str) -> None:
        self._api_key = api_key
        self._model = model

    def edit_angle(
        self,
        image_bytes: bytes,
        horizontal: int,
        vertical: int,
        zoom: float,
        prompt: str = "",
    ) -> bytes:
        """카메라 다이어그램 + 프롬프트로 앵글 변경.

        Args:
            image_bytes: Original image PNG bytes.
            horizontal: Horizontal rotation degrees (0-360).
            vertical: Vertical angle degrees (-30 to 30).
            zoom: Zoom factor (0.8 to 1.5).
            prompt: Optional additional prompt for the edit.

        Returns:
            Edited image bytes (PNG).
        """
        diagram_bytes = _create_camera_diagram(horizontal, vertical, zoom)

        # 앵글은 다이어그램으로만 전달 — 텍스트에는 렌즈/구도 정보만
        angle_prompt = _load_i2i_prompt("angle.md")
        if prompt:
            angle_prompt += " " + _load_i2i_prompt("composition.md").format(composition=prompt)

        return _gemini_generate_content(
            api_key=self._api_key,
            model=self._model,
            prompt=angle_prompt,
            input_images=[
                ("Original scene image:", image_bytes),
                ("Camera angle diagram:", diagram_bytes),
            ],
        )

    def edit_color(
        self,
        image_bytes: bytes,
        color_prompt: str,
    ) -> bytes:
        """색감/조명 변경.

        Args:
            image_bytes: Original image PNG bytes.
            color_prompt: Description of the desired lighting/color change.

        Returns:
            Edited image bytes (PNG).
        """
        full_prompt = _load_i2i_prompt("color.md").format(color_prompt=color_prompt)

        return _gemini_generate_content(
            api_key=self._api_key,
            model=self._model,
            prompt=full_prompt,
            input_images=[
                ("Original scene image:", image_bytes),
            ],
        )

    def edit_combined(
        self,
        original_bytes: bytes,
        horizontal: int,
        vertical: int,
        zoom: float,
        color_prompt: str,
        composition: str = "",
    ) -> bytes:
        """Apply angle + color in a single I2I call.

        Creates a camera angle diagram AND includes color prompt in a single
        generation request. Falls back to sequential (angle then color) if
        single call fails.

        Args:
            original_bytes: Original image PNG bytes.
            horizontal: Horizontal rotation degrees (0-360).
            vertical: Vertical angle degrees (-30 to 30).
            zoom: Zoom factor (0.8 to 1.5).
            color_prompt: Color/lighting change description.
            composition: Optional composition note.

        Returns:
            Edited image bytes (PNG) with both angle and color applied.
        """
        diagram_bytes = _create_camera_diagram(horizontal, vertical, zoom)

        combined_prompt = _load_i2i_prompt("combined.md").format(
            color_prompt=color_prompt,
        )
        if composition:
            combined_prompt += " " + _load_i2i_prompt("composition.md").format(
                composition=composition,
            )

        try:
            return _gemini_generate_content(
                api_key=self._api_key,
                model=self._model,
                prompt=combined_prompt,
                input_images=[
                    ("Original scene image:", original_bytes),
                    ("Camera angle diagram:", diagram_bytes),
                ],
            )
        except Exception as exc:
            logger.warning(
                "edit_combined single-call failed (%s), falling back to sequential",
                exc,
            )
            angle_result = self.edit_angle(
                original_bytes, horizontal, vertical, zoom, prompt=composition,
            )
            return self.edit_color(angle_result, color_prompt)

    def edit_angle_and_color(
        self,
        image_bytes: bytes,
        horizontal: int,
        vertical: int,
        zoom: float,
        color_prompt: str,
    ) -> bytes:
        """앵글 먼저 적용 후 색감 적용 (순차).

        Args:
            image_bytes: Original image PNG bytes.
            horizontal: Horizontal rotation degrees.
            vertical: Vertical angle degrees.
            zoom: Zoom factor.
            color_prompt: Color/lighting change description.

        Returns:
            Edited image bytes (PNG) with both angle and color applied.
        """
        angle_result = self.edit_angle(image_bytes, horizontal, vertical, zoom)
        return self.edit_color(angle_result, color_prompt)
