"""fal.ai 앵글 helper — Phase 3b.5에서 image_service.py에서 이관.

세 개의 module-level 함수를 별도 모듈로 분리해 `scene_image_service.py` →
`image_service.py` 역의존을 제거한다. ImageService에서는 backward compat 용도로
동일 이름의 shim을 유지한다.

- `select_and_recommend_angle` (구 `_select_and_recommend_angle`)
- `select_final_best`         (구 `_select_final_best`)
- `apply_fal_angle`           (구 `_apply_fal_angle`)
"""
from __future__ import annotations

import base64
import io
import json
import logging
import time
import urllib.error
import urllib.request
from typing import Optional

from app.core.config import settings
from app.core.image_call_budget import (
    ImageCallBudgetExceeded,
    reserve_current_call,
)
from app.services.image_capture.sink import capture_generated_image

logger = logging.getLogger(__name__)


__all__ = [
    "select_and_recommend_angle",
    "select_final_best",
    "apply_fal_angle",
]


def select_and_recommend_angle(
    image_bytes_list: list,
    beat_title: str,
    t2i_prompt: str,
    prev_scene_title: str = "",
    prev_scene_description: str = "",
) -> Optional[dict]:
    """GPT Vision: N개 이미지 중 앵글 적용할 이미지 선택 + 앵글 추천 (최소 20도).

    LiteLLM Router 경유 — Opik 자동 추적.
    prev_scene_title/description: 앞쪽 씬 텍스트 맥락 (이미지 아닌 텍스트만)
    """
    from app.modules.llm.llm_client import router_completion

    if not image_bytes_list:
        return None

    n = len(image_bytes_list)
    prev_context = ""
    if prev_scene_title:
        prev_context = f"\n\nPrevious scene (for visual continuity): {prev_scene_title}"
        if prev_scene_description:
            prev_context += f"\n{prev_scene_description}"

    prompt = f"""You are a cinematographer. You have {n} candidate images for the current scene.
Scene: {beat_title}
Prompt: {t2i_prompt}{prev_context}

Task:
1. Consider visual continuity with the previous scene description.
2. Choose which image would benefit MOST from a camera angle adjustment.
3. Recommend the camera angle adjustment for that image.

Rules:
- horizontal_angle: 0-360 (0=front, 90=right side, 180=back, 270=left side)
- vertical_angle: -30 to 90 (-30=low angle looking up, 0=eye level, 90=bird's eye)
- zoom: 0-10 (0=wide shot, 5=normal, 10=extreme closeup)
- IMPORTANT: horizontal_angle >= 20 OR abs(vertical_angle) >= 20. No trivial adjustments.

Return JSON:
{{"best_for_angle": N, "horizontal_angle": N, "vertical_angle": N, "zoom": N, "reason": "Korean explanation"}}
best_for_angle is 0-based index (0 to {n-1})."""

    try:
        content = [{"type": "text", "text": prompt}]
        for i, img_bytes in enumerate(image_bytes_list):
            img_b64 = base64.b64encode(img_bytes).decode()
            content.append({"type": "text", "text": f"Image {i+1}:"})
            content.append({"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}})

        response = router_completion(
            model="gpt",
            messages=[{"role": "user", "content": content}],
            response_format={"type": "json_object"},
            metadata={"opik": {"tags": ["angle_recommend"]}},
        )

        result = json.loads(response.choices[0].message.content)

        idx = max(0, min(n - 1, int(result.get("best_for_angle", 0))))
        h = max(0, min(360, float(result.get("horizontal_angle", 0))))
        v = max(-30, min(90, float(result.get("vertical_angle", 0))))
        z = max(0, min(10, float(result.get("zoom", 5))))

        if h < 20 and abs(v) < 20:
            if h >= abs(v):
                h = 20.0
            else:
                v = 20.0 if v >= 0 else -20.0

        return {"best_for_angle": idx, "horizontal_angle": h, "vertical_angle": v, "zoom": z, "reason": result.get("reason", "")}
    except Exception as exc:
        logger.warning("Angle selection+recommendation failed: %s", exc)
        return None


def select_final_best(
    image_bytes_list: list,
    beat_title: str,
) -> int:
    """GPT Vision: N+1개 이미지 중 최종 대표 이미지 선택.

    LiteLLM Router 경유 — Opik 자동 추적.
    """
    from app.modules.llm.llm_client import router_completion

    if not image_bytes_list:
        return 0
    n = len(image_bytes_list)
    if n == 1:
        return 0

    prompt = f"""You are a film director selecting the final hero image for a scene.
You have {n} images. The last image may be an angle-adjusted variant.
Scene: {beat_title}

Select the BEST image that:
1. Has the most cinematic composition and visual impact
2. Best represents the scene's mood and narrative moment
3. Has good lighting, focus, and overall quality
4. Feels like a professional film still frame

Return JSON: {{"selected_index": N, "reason": "Korean explanation"}}
selected_index is 1-based (1 to {n})."""

    try:
        content = [{"type": "text", "text": prompt}]
        for i, img_bytes in enumerate(image_bytes_list):
            img_b64 = base64.b64encode(img_bytes).decode()
            content.append({"type": "text", "text": f"Image {i+1}:"})
            content.append({"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}})

        response = router_completion(
            model="gpt",
            messages=[{"role": "user", "content": content}],
            response_format={"type": "json_object"},
            metadata={"opik": {"tags": ["final_select"]}},
        )

        result = json.loads(response.choices[0].message.content)
        selected = int(result.get("selected_index", 1))
        idx = max(0, min(selected - 1, n - 1))
        logger.info("Final best selection: image %d/%d — %s", selected, n, result.get("reason", ""))
        return idx
    except Exception as exc:
        logger.warning("Final best selection failed, defaulting to first: %s", exc)
        return 0


def apply_fal_angle(
    img_bytes: bytes,
    horizontal: float,
    vertical: float,
    zoom: float,
) -> tuple:
    """fal.ai로 이미지에 앵글 적용. Returns (result_bytes, elapsed_ms) or (None, 0)."""
    # 4.5MB 이상이면 리사이즈 (fal.ai base64 제한 5MB)
    if len(img_bytes) > 4_500_000:
        try:
            from PIL import Image
            pil_img = Image.open(io.BytesIO(img_bytes))
            max_side = 1536
            ratio = min(max_side / pil_img.width, max_side / pil_img.height)
            if ratio < 1:
                new_size = (int(pil_img.width * ratio), int(pil_img.height * ratio))
                pil_img = pil_img.resize(new_size, Image.LANCZOS)
            buf = io.BytesIO()
            pil_img.save(buf, format="PNG")
            img_bytes = buf.getvalue()
        except Exception as exc:
            logger.warning("fal image resize optimization failed: %s — 원본 바이트 사용", exc)

    img_b64 = base64.b64encode(img_bytes).decode()

    body = {
        "image_urls": [f"data:image/png;base64,{img_b64}"],
        "horizontal_angle": horizontal,
        "vertical_angle": vertical,
        "zoom": zoom,
        "output_format": "png",
        "num_images": 1,
    }

    req = urllib.request.Request(
        "https://fal.run/fal-ai/qwen-image-edit-2511-multiple-angles",
        data=json.dumps(body).encode(),
        headers={
            "Authorization": f"Key {settings.fal_key}",
            "Content-Type": "application/json",
        },
    )

    # Reserve OUTSIDE the broad except so budget exhaustion propagates
    # rather than being silenced as a generic ``(None, 0)`` failure. Only
    # the fal.run generation call counts against the cap — the download
    # of the produced image URL (below) is not counted.
    reserve_current_call(source="fal_angle_helpers.fal_run")

    t0 = time.time()
    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            result = json.loads(resp.read())
        elapsed = int((time.time() - t0) * 1000)

        images = result.get("images", [])
        if not images:
            return None, 0

        result_url = images[0].get("url", "")
        if not result_url:
            return None, 0

        with urllib.request.urlopen(result_url, timeout=30) as dl:
            data = dl.read()
        from app.modules.llm.image_tracer import get_image_tracer
        get_image_tracer().log(
            step="fal_angle", model="qwen-image-edit-2511",
            prompt=f"H={horizontal} V={vertical} Z={zoom}",
            duration_ms=elapsed,
            params={"horizontal": horizontal, "vertical": vertical, "zoom": zoom},
        )
        # Phase B: fal 결과 이미지(result_url 다운로드 bytes) capture(scope 미배선이면
        # no-op). resize 된 input 은 output 이 아니므로 capture 대상 아님.
        capture_generated_image(
            data,
            role="fal_angle",
            prompt=f"H={horizontal} V={vertical} Z={zoom}",
            pipeline_metadata={
                "horizontal": horizontal,
                "vertical": vertical,
                "zoom": zoom,
                "budget_source": "fal_angle_helpers.fal_run",
            },
        )
        return data, elapsed
    except ImageCallBudgetExceeded:
        raise
    except Exception as exc:
        logger.warning("fal.ai angle apply failed: %s", exc)
        from app.modules.llm.image_tracer import get_image_tracer
        get_image_tracer().log(
            step="fal_angle", model="qwen-image-edit-2511",
            prompt=f"H={horizontal} V={vertical} Z={zoom}",
            status="error", error=str(exc),
            params={"horizontal": horizontal, "vertical": vertical, "zoom": zoom},
        )
        return None, 0
