import asyncio
import re
import time
from pathlib import Path

from google import genai
from google.genai import types
from pydantic import BaseModel

from . import config

_client: genai.Client | None = None


class RateLimiter:
    """Paces call starts to stay under a requests-per-minute budget, instead
    of firing everything at once and reacting to 429s after the fact."""

    def __init__(self, rpm: int):
        self._min_interval = 60.0 / rpm
        self._lock = asyncio.Lock()
        self._next_slot = 0.0

    async def wait(self):
        async with self._lock:
            now = time.monotonic()
            start_at = max(now, self._next_slot)
            self._next_slot = start_at + self._min_interval
            delay = start_at - now
        if delay > 0:
            await asyncio.sleep(delay)


_rate_limiter = RateLimiter(config.GEMINI_RPM)


def get_client() -> genai.Client:
    global _client
    if _client is None:
        if not config.GEMINI_API_KEY:
            raise RuntimeError("GEMINI_API_KEY is not set (put it in .env)")
        _client = genai.Client(api_key=config.GEMINI_API_KEY)
    return _client


# --- Structured output schema -----------------------------------------------
# Instead of asking Gemini for direct 0-100 scores (unreliable / inconsistent
# across runs), we ask a set of yes/no + short-text questions and convert the
# answers into scores ourselves in scoring.py. This is the approach we agreed
# on: decompose subjective judgment into small, low-ambiguity questions, keep
# the scoring formula deterministic and under our own control.
#
# Technical quality (focus/exposure) is NOT asked here - it's computed locally
# via OpenCV in image_processing.py, which is faster, cheaper and more
# consistent than asking a vision model to judge blur/exposure.

class SubjectAnalysis(BaseModel):
    face_detected: bool
    eyes_open: bool  # if no face detected, answer true (not applicable)
    expression_natural: bool  # if no face detected, answer true (not applicable)
    subject_clear_at_first_glance: bool


class CompositionAnalysis(BaseModel):
    follows_thirds_or_intentional_framing: bool
    background_distracting: bool
    subject_well_positioned: bool


class EmotionAnalysis(BaseModel):
    stranger_would_pause: bool
    captures_a_moment: bool


class PhotoAnalysis(BaseModel):
    subject: SubjectAnalysis
    composition: CompositionAnalysis
    emotion_story: EmotionAnalysis
    issues: list[str]
    strengths: list[str]
    reason: str


PROMPT = """你是一位專業攝影師的助理，任務是幫忙做照片初步篩選的客觀判斷。
請針對這張照片，誠實回答以下 yes/no 問題（不要猜測式討好，觀察不到就給保守答案）：

subject（主體）：
- face_detected: 照片中是否有清楚可辨識的人臉
- eyes_open: 若有人臉，眼睛是否張開／狀態自然可用；若沒有人臉，填 true
- expression_natural: 若有人臉，表情是否自然、非過渡表情；若沒有人臉，填 true
- subject_clear_at_first_glance: 第一眼是否能看出照片的主體/重點

composition（構圖）：
- follows_thirds_or_intentional_framing: 是否符合三分法、置中或其他明顯有意圖的構圖
- background_distracting: 背景是否雜亂或搶了主體注意力
- subject_well_positioned: 主體在畫面中的位置是否恰當、留白/裁切是否合理

emotion_story（情緒/故事性）：
- stranger_would_pause: 一個不認識拍攝者、不認識照片中人物的陌生人，是否會因為這張照片而多看兩眼
- captures_a_moment: 是否捕捉到一個值得記錄的瞬間（動作、互動、表情），而非純紀錄照

另外提供：
- issues: 這張照片的具體問題（簡短片語陣列，可為空）
- strengths: 這張照片的優點（簡短片語陣列，可為空）
- reason: 一到兩句話，用中文說明整體判斷理由
"""

_RETRY_DELAY_RE = re.compile(r"'retryDelay':\s*'(\d+(?:\.\d+)?)s'")


def _retry_wait_seconds(error: Exception, attempt: int) -> float:
    """429s carry the server's own suggested wait (e.g. "retry in 38s") -
    honor that instead of guessing with a short exponential backoff, which
    just burns through retries without ever waiting long enough."""
    match = _RETRY_DELAY_RE.search(str(error))
    if match:
        return float(match.group(1)) + 1  # small buffer
    return min(2 ** attempt, 30)


async def analyze_image(original_path: Path) -> dict:
    from . import image_processing

    client = get_client()
    # Downscale in memory before upload — originals stay untouched on disk.
    image_bytes = await asyncio.to_thread(
        image_processing.encode_for_analysis, original_path
    )
    part = types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg")

    last_error = None
    for attempt in range(config.ANALYZE_MAX_RETRIES):
        await _rate_limiter.wait()
        try:
            response = await client.aio.models.generate_content(
                model=config.GEMINI_MODEL,
                contents=[PROMPT, part],
                config=types.GenerateContentConfig(
                    response_mime_type="application/json",
                    response_schema=PhotoAnalysis,
                ),
            )
            parsed: PhotoAnalysis = response.parsed
            if parsed is None:
                raise ValueError(f"Gemini returned no parsed JSON: {response.text!r}")
            return parsed.model_dump()
        except Exception as e:  # noqa: BLE001 - want to retry broadly, then surface last error
            last_error = e
            if attempt < config.ANALYZE_MAX_RETRIES - 1:
                await asyncio.sleep(_retry_wait_seconds(e, attempt))
                continue
            raise last_error
    raise last_error  # unreachable, keeps type-checkers happy
