from __future__ import annotations

import json
import random
import time
from typing import Any, Dict, Optional, Tuple

try:
    import requests
except ModuleNotFoundError:  # pragma: no cover - exercised only in minimal local env
    requests = None  # type: ignore[assignment]

from .exceptions import GrsAIError
from .logger import Logger
from .models import ClientConfig, DrawConfig, PollConfig


def clamp_backoff(value: float, cap: float) -> float:
    return min(value, cap)


def fmt_s(value: float) -> str:
    if value < 0:
        value = 0
    if value < 60:
        return f"{value:.0f}s"
    minutes = int(value) // 60
    seconds = int(value) % 60
    return f"{minutes}m{seconds:02d}s"


class GrsAIClient:
    def __init__(self, cfg: ClientConfig, logger: Logger, session: Optional[Any] = None) -> None:
        if requests is None:
            raise GrsAIError("缺少依赖 requests，请先安装：pip install requests")
        self.cfg = cfg
        self.log = logger
        self.base_url = cfg.base_url.rstrip("/")
        self.s = session or requests.Session()
        self.s.headers.update({"Authorization": f"Bearer {cfg.api_key}"})

    def _post(self, path: str, json_body: Dict[str, Any]) -> Dict[str, Any]:
        url = f"{self.base_url}{path}"
        last_err: Optional[Exception] = None

        for attempt in range(self.cfg.retries + 1):
            try:
                if self.log.debug_enabled:
                    body = json.dumps(json_body, ensure_ascii=False)
                    self.log.debug(
                        f"POST {url} attempt={attempt + 1}/{self.cfg.retries + 1} body={body}"
                    )

                resp = self.s.post(
                    url,
                    json=json_body,
                    timeout=(self.cfg.connect_timeout, self.cfg.timeout),
                )

                if resp.status_code >= 400:
                    text = (resp.text or "")[:500]
                    if resp.status_code in (429, 500, 502, 503, 504) and attempt < self.cfg.retries:
                        raise requests.HTTPError(f"HTTP {resp.status_code}: {text}", response=resp)
                    raise GrsAIError(f"HTTP {resp.status_code}: {text}")

                try:
                    payload = resp.json()
                except ValueError as exc:
                    raise GrsAIError(f"响应不是合法 JSON: {(resp.text or '')[:200]}") from exc

                if not isinstance(payload, dict):
                    raise GrsAIError(f"响应不是 JSON 对象: {payload!r}")

                code = payload.get("code")
                msg = payload.get("msg")
                data = payload.get("data")

                if data is None and msg:
                    raise GrsAIError(f"API 业务失败: msg={msg} (code={code})")
                if code not in (None, 0):
                    raise GrsAIError(f"API 业务失败: code={code}, msg={msg}, data={data}")

                return payload

            except (requests.RequestException, GrsAIError) as exc:
                last_err = exc
                if attempt >= self.cfg.retries:
                    raise GrsAIError(f"请求失败: {url} | err={exc}") from exc

                base = self.cfg.backoff_base * (2**attempt)
                sleep_s = clamp_backoff(base, self.cfg.backoff_cap) * (0.7 + random.random() * 0.6)
                self.log.warn(f"请求异常，将重试：{exc} | backoff={sleep_s:.2f}s")
                time.sleep(sleep_s)

        raise GrsAIError(f"请求失败: {url} | err={last_err}")

    def submit_draw(self, payload: Dict[str, Any]) -> str:
        res = self._post("/v1/draw/nano-banana", json_body=payload)

        data = res.get("data") or {}
        task_id = data.get("id") if isinstance(data, dict) else None
        if not task_id:
            raise GrsAIError(f"提交任务未返回有效 id: {res}")
        return task_id

    def fetch_result(self, task_id: str) -> Dict[str, Any]:
        res = self._post("/v1/draw/result", json_body={"id": task_id, "task_id": task_id})
        data = res.get("data")
        if not isinstance(data, dict):
            raise GrsAIError(f"查询结果返回结构异常: {res}")
        return data

    def draw_and_wait(
        self,
        *,
        prompt: str,
        draw_cfg: DrawConfig,
        poll_cfg: PollConfig,
        verbose: bool = True,
    ) -> Tuple[str, Optional[str], Dict[str, Any]]:
        payload = draw_cfg.to_payload(prompt)

        if verbose:
            submit_msg = (
                f"提交任务: model={draw_cfg.model} aspectRatio={draw_cfg.aspect_ratio} "
                f"imageSize={draw_cfg.image_size} webHook={draw_cfg.web_hook}"
            )
            self.log.info(
                submit_msg
            )

        task_id = self.submit_draw(payload)
        if verbose:
            self.log.info(f"任务ID: {task_id}")

        start = time.monotonic()
        deadline = start + poll_cfg.max_wait

        last_sig: Optional[Tuple[Any, Any, Any, Any, Any]] = None
        last_log_t = 0.0
        last_data: Dict[str, Any] = {}

        while True:
            now = time.monotonic()
            if now >= deadline:
                elapsed = now - start
                status = last_data.get("status")
                progress = last_data.get("progress")
                stage = last_data.get("stage") or last_data.get("phase")
                self.log.warn(
                    f"超时: elapsed={fmt_s(elapsed)} last_status={status} "
                    f"progress={progress} stage={stage}"
                )
                return task_id, None, {"status": "timeout", "last": last_data}

            time.sleep(poll_cfg.interval)

            data = self.fetch_result(task_id)
            last_data = data

            status = data.get("status")
            progress = data.get("progress")
            stage = data.get("stage") or data.get("phase")
            queue_pos = data.get("queuePosition") or data.get("queue_pos")
            eta = data.get("eta") or data.get("estimated_time")
            reason = data.get("failure_reason") or data.get("error") or ""

            sig = (status, progress, stage, queue_pos, eta)
            changed = sig != last_sig

            now2 = time.monotonic()
            heartbeat_due = (now2 - last_log_t) >= poll_cfg.heartbeat

            if verbose and (changed or heartbeat_due):
                elapsed = now2 - start
                remain = deadline - now2

                parts = [f"status={status}", f"progress={progress}"]
                if stage is not None:
                    parts.append(f"stage={stage}")
                if queue_pos is not None:
                    parts.append(f"queue={queue_pos}")
                if eta is not None:
                    parts.append(f"eta={eta}")
                if reason:
                    parts.append(f"reason={reason}")

                self.log.info(f"[{fmt_s(elapsed)}/{fmt_s(remain)}] " + " ".join(parts))
                last_log_t = now2
                last_sig = sig

            if status == "succeeded":
                results = data.get("results") or []
                url = results[0].get("url") if (isinstance(results, list) and results) else None
                if verbose:
                    self.log.info("生成成功")
                    self.log.info(f"图片URL: {url}")
                return task_id, url, data

            if status == "failed":
                if verbose:
                    self.log.error("生成失败")
                    self.log.error(
                        "失败详情: "
                        + json.dumps(
                            {
                                "status": status,
                                "progress": progress,
                                "stage": stage,
                                "queue": queue_pos,
                                "eta": eta,
                                "reason": reason,
                                "raw": data,
                            },
                            ensure_ascii=False,
                        )
                    )
                return task_id, None, data
