"""ComfyUI client — HD generation with resilient progress tracking."""
from __future__ import annotations

import asyncio
import json
import logging
import os
import random
import uuid
from typing import Any, Optional
from urllib.parse import urlencode

import httpx

log = logging.getLogger("tools-api.comfyui")

COMFYUI_URL = os.getenv("COMFYUI_URL", "http://10.4.7.18:8188").rstrip("/")
CHECKPOINT = os.getenv("COMFYUI_CHECKPOINT", "v1-5-pruned-emaonly.safetensors")
DEFAULT_STEPS = int(os.getenv("COMFYUI_STEPS", "15"))
POLL_INTERVAL = float(os.getenv("COMFYUI_POLL_INTERVAL", "2.0"))
MAX_WAIT = float(os.getenv("COMFYUI_MAX_WAIT", "1800"))
WS_IDLE_TIMEOUT = float(os.getenv("COMFYUI_WS_IDLE", "90"))

QUALITY_PRESETS: dict[str, dict[str, Any]] = {
    "fast": {"width": 512, "height": 512, "steps": 15, "label": "Snel (512px)"},
    "hd": {"width": 1024, "height": 1024, "steps": 28, "label": "HD (1024px)"},
    "ultra": {"width": 1024, "height": 1024, "steps": 35, "label": "Ultra HD (1024px, 35 steps)"},
}

_jobs: dict[str, dict[str, Any]] = {}


def resolve_quality(
    quality: str | None = None,
    width: int | None = None,
    height: int | None = None,
    steps: int | None = None,
) -> tuple[int, int, int, str]:
    q = (quality or "hd").lower()
    preset = QUALITY_PRESETS.get(q, QUALITY_PRESETS["hd"])
    w = width or preset["width"]
    h = height or preset["height"]
    s = steps or preset["steps"]
    label = preset["label"]
    return w, h, s, label


def get_job(prompt_id: str) -> dict[str, Any] | None:
    return _jobs.get(prompt_id)


def build_workflow(
    prompt: str,
    negative: str = "blurry, low quality, watermark, text, ugly, deformed",
    width: int = 1024,
    height: int = 1024,
    steps: int = DEFAULT_STEPS,
    seed: Optional[int] = None,
) -> dict[str, Any]:
    seed = seed if seed is not None else random.randint(1, 2**31 - 1)
    return {
        "3": {
            "class_type": "KSampler",
            "inputs": {
                "seed": seed,
                "steps": steps,
                "cfg": 7.5,
                "sampler_name": "euler",
                "scheduler": "normal",
                "denoise": 1.0,
                "model": ["4", 0],
                "positive": ["6", 0],
                "negative": ["7", 0],
                "latent_image": ["5", 0],
            },
        },
        "4": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": CHECKPOINT}},
        "5": {
            "class_type": "EmptyLatentImage",
            "inputs": {"width": width, "height": height, "batch_size": 1},
        },
        "6": {
            "class_type": "CLIPTextEncode",
            "inputs": {"text": prompt, "clip": ["4", 1]},
        },
        "7": {
            "class_type": "CLIPTextEncode",
            "inputs": {"text": negative, "clip": ["4", 1]},
        },
        "8": {
            "class_type": "VAEDecode",
            "inputs": {"samples": ["3", 0], "vae": ["4", 2]},
        },
        "9": {
            "class_type": "SaveImage",
            "inputs": {"filename_prefix": "foodlinkk", "images": ["8", 0]},
        },
    }


def view_url(filename: str, subfolder: str = "", img_type: str = "output") -> str:
    params = urlencode({"filename": filename, "type": img_type, "subfolder": subfolder})
    return f"{COMFYUI_URL}/view?{params}"


def _new_job(prompt_id: str, prompt: str, width: int, height: int, steps: int, quality: str) -> None:
    _jobs[prompt_id] = {
        "prompt_id": prompt_id,
        "status": "queued",
        "percent": 0,
        "step": 0,
        "max_step": steps,
        "node": None,
        "message": "In wachtrij bij ComfyUI…",
        "prompt": prompt[:500],
        "width": width,
        "height": height,
        "steps": steps,
        "quality": quality,
        "events": [],
        "result": None,
        "error": None,
    }


def _append_event(prompt_id: str, message: str) -> None:
    job = _jobs.get(prompt_id)
    if not job:
        return
    job["message"] = message
    events: list[str] = job.setdefault("events", [])
    if not events or events[-1] != message:
        events.append(message)
    if len(events) > 50:
        del events[: len(events) - 50]


async def submit_prompt(workflow: dict[str, Any]) -> tuple[str, str]:
    client_id = str(uuid.uuid4())
    async with httpx.AsyncClient(timeout=30.0) as client:
        resp = await client.post(
            f"{COMFYUI_URL}/prompt",
            json={"prompt": workflow, "client_id": client_id},
        )
        resp.raise_for_status()
        data = resp.json()
    if data.get("node_errors"):
        raise RuntimeError(f"ComfyUI node errors: {data['node_errors']}")
    return data["prompt_id"], client_id


async def _prompt_in_history(prompt_id: str) -> bool:
    async with httpx.AsyncClient(timeout=20.0) as client:
        resp = await client.get(f"{COMFYUI_URL}/history/{prompt_id}")
        if resp.status_code == 200 and prompt_id in resp.json():
            return True
    return False


async def _prompt_in_queue(prompt_id: str) -> bool:
    async with httpx.AsyncClient(timeout=20.0) as client:
        resp = await client.get(f"{COMFYUI_URL}/queue")
        if resp.status_code != 200:
            return False
        data = resp.json()
        for bucket in ("queue_running", "queue_pending"):
            for item in data.get(bucket) or []:
                if isinstance(item, (list, tuple)) and len(item) > 1 and item[1] == prompt_id:
                    return True
    return False


async def _is_still_running(prompt_id: str) -> bool:
    if await _prompt_in_history(prompt_id):
        return False
    return await _prompt_in_queue(prompt_id)


async def wait_for_output(prompt_id: str) -> dict[str, Any]:
    deadline = asyncio.get_event_loop().time() + MAX_WAIT
    tick = 0
    async with httpx.AsyncClient(timeout=30.0) as client:
        while asyncio.get_event_loop().time() < deadline:
            resp = await client.get(f"{COMFYUI_URL}/history/{prompt_id}")
            if resp.status_code == 200:
                hist = resp.json()
                if prompt_id in hist:
                    outputs = hist[prompt_id].get("outputs") or {}
                    for node_out in outputs.values():
                        images = node_out.get("images") or []
                        if images:
                            img = images[0]
                            return {
                                "filename": img["filename"],
                                "subfolder": img.get("subfolder", ""),
                                "type": img.get("type", "output"),
                            }
            tick += 1
            if tick % 15 == 0:
                _append_event(prompt_id, "ComfyUI CPU render duurt even — nog bezig…")
            await asyncio.sleep(POLL_INTERVAL)
    raise TimeoutError(f"ComfyUI generation timed out after {int(MAX_WAIT)}s")


async def _track_ws(client_id: str, prompt_id: str) -> None:
    try:
        import websockets
    except ImportError:
        _append_event(prompt_id, "Polling modus (geen websocket)")
        return

    ws_url = COMFYUI_URL.replace("https://", "wss://").replace("http://", "ws://") + f"/ws?clientId={client_id}"
    try:
        async with websockets.connect(ws_url, ping_interval=30, ping_timeout=60, close_timeout=10) as ws:
            finished = False
            while not finished:
                try:
                    raw = await asyncio.wait_for(ws.recv(), timeout=WS_IDLE_TIMEOUT)
                except asyncio.TimeoutError:
                    if await _prompt_in_history(prompt_id):
                        finished = True
                        break
                    if await _is_still_running(prompt_id):
                        _append_event(prompt_id, "Nog bezig op CPU (geen WS update)…")
                        continue
                    break

                data = json.loads(raw)
                msg_type = data.get("type")
                payload = data.get("data") or {}
                pid = payload.get("prompt_id")
                if pid not in (None, prompt_id):
                    continue

                if msg_type == "execution_start":
                    _jobs[prompt_id]["status"] = "running"
                    _append_event(prompt_id, "ComfyUI gestart")

                elif msg_type == "progress":
                    val = int(payload.get("value") or 0)
                    mx = int(payload.get("max") or 1)
                    pct = int(100 * val / mx) if mx else 0
                    _jobs[prompt_id].update(
                        status="running",
                        percent=pct,
                        step=val,
                        max_step=mx,
                        node=payload.get("node"),
                    )
                    _append_event(prompt_id, f"KSampler {val}/{mx} ({pct}%)")

                elif msg_type == "executing":
                    node = payload.get("node")
                    if node is None:
                        _jobs[prompt_id]["status"] = "finishing"
                        _append_event(prompt_id, "Render klaar — opslaan…")
                        finished = True
                    else:
                        _jobs[prompt_id]["node"] = node
                        _append_event(prompt_id, f"Node {node}")

                elif msg_type == "execution_error":
                    err = payload.get("exception_message") or "ComfyUI execution error"
                    raise RuntimeError(str(err))
    except Exception as exc:
        log.warning("WS tracking ended for %s: %s — falling back to poll", prompt_id, exc)
        if await _prompt_in_history(prompt_id):
            return
        if await _is_still_running(prompt_id):
            _append_event(prompt_id, "Voortgang via polling (WS verbroken)")
            return
        raise


async def _run_job(
    prompt_id: str,
    client_id: str,
    prompt: str,
    width: int,
    height: int,
    steps: int,
    quality: str,
    seed: Optional[int],
) -> None:
    try:
        try:
            await _track_ws(client_id, prompt_id)
        except Exception as ws_exc:
            log.warning("WS phase issue %s: %s", prompt_id, ws_exc)
            if not await _is_still_running(prompt_id) and not await _prompt_in_history(prompt_id):
                raise

        img = await wait_for_output(prompt_id)
        result = {
            "prompt_id": prompt_id,
            "filename": img["filename"],
            "subfolder": img.get("subfolder", ""),
            "type": img.get("type", "output"),
            "image_url": view_url(img["filename"], img.get("subfolder", ""), img.get("type", "output")),
            "prompt": prompt,
            "width": width,
            "height": height,
            "steps": steps,
            "quality": quality,
        }
        _jobs[prompt_id].update(status="done", percent=100, result=result, message="Klaar!")
        _append_event(prompt_id, "Afbeelding klaar")
        log.info("ComfyUI done %s (%dx%d)", prompt_id, width, height)
    except Exception as exc:
        log.exception("ComfyUI job failed %s", prompt_id)
        if await _prompt_in_history(prompt_id):
            try:
                img = await wait_for_output(prompt_id)
                result = {
                    "prompt_id": prompt_id,
                    "filename": img["filename"],
                    "subfolder": img.get("subfolder", ""),
                    "type": img.get("type", "output"),
                    "image_url": view_url(img["filename"], img.get("subfolder", ""), img.get("type", "output")),
                    "prompt": prompt,
                    "width": width,
                    "height": height,
                    "steps": steps,
                    "quality": quality,
                }
                _jobs[prompt_id].update(status="done", percent=100, result=result, message="Klaar!")
                return
            except Exception:
                pass
        _jobs[prompt_id].update(status="error", error=str(exc), message=str(exc))
        _append_event(prompt_id, f"Fout: {exc}")


async def start_generation(
    prompt: str,
    *,
    negative: str = "blurry, low quality, watermark, text, ugly, deformed",
    quality: str = "hd",
    width: int | None = None,
    height: int | None = None,
    steps: int | None = None,
    seed: Optional[int] = None,
) -> dict[str, Any]:
    w, h, s, label = resolve_quality(quality, width, height, steps)
    workflow = build_workflow(prompt, negative=negative, width=w, height=h, steps=s, seed=seed)
    prompt_id, client_id = await submit_prompt(workflow)
    _new_job(prompt_id, prompt, w, h, s, quality)
    _append_event(prompt_id, f"Gestart — {label}")
    asyncio.create_task(_run_job(prompt_id, client_id, prompt, w, h, s, quality, seed))
    return {
        "prompt_id": prompt_id,
        "client_id": client_id,
        "quality": quality,
        "width": w,
        "height": h,
        "steps": s,
        "quality_label": label,
    }


async def generate_image(
    prompt: str,
    width: int = 1024,
    height: int = 1024,
    steps: int = DEFAULT_STEPS,
    seed: Optional[int] = None,
    quality: str = "hd",
) -> dict[str, Any]:
    if quality and quality != "custom":
        width, height, steps, _ = resolve_quality(quality, width, height, steps)
    started = await start_generation(
        prompt,
        quality="custom",
        width=width,
        height=height,
        steps=steps,
        seed=seed,
    )
    prompt_id = started["prompt_id"]
    deadline = asyncio.get_event_loop().time() + MAX_WAIT
    while asyncio.get_event_loop().time() < deadline:
        job = _jobs.get(prompt_id) or {}
        if job.get("status") == "done" and job.get("result"):
            return job["result"]
        if job.get("status") == "error":
            raise RuntimeError(job.get("error") or "Generation failed")
        await asyncio.sleep(POLL_INTERVAL)
    raise TimeoutError(f"ComfyUI generation timed out after {int(MAX_WAIT)}s")


async def fetch_image_bytes(filename: str, subfolder: str = "", img_type: str = "output") -> bytes:
    params = {"filename": filename, "type": img_type, "subfolder": subfolder}
    async with httpx.AsyncClient(timeout=120.0) as client:
        resp = await client.get(f"{COMFYUI_URL}/view", params=params)
        resp.raise_for_status()
        return resp.content
