"""Kling AI API client for video generation and enhancement."""
from __future__ import annotations

import hashlib
import hmac
import json
import logging
import os
import time
from pathlib import Path
from typing import Any, Dict, List, Optional

import urllib.request
import urllib.error

logger = logging.getLogger(__name__)

KLING_ACCESS_KEY = os.getenv("KLING_ACCESS_KEY", "").strip()
KLING_SECRET_KEY = os.getenv("KLING_SECRET_KEY", "").strip()
KLING_BASE_URL = os.getenv("KLING_BASE_URL", "https://api.klingai.com")
KLING_TIMEOUT = int(os.getenv("KLING_TIMEOUT", "60"))


def _kling_sign(access_key: str, secret_key: str) -> str:
    """Generate Kling AI Bearer token via HMAC-SHA256 JWT-style signing."""
    import base64
    header = base64.urlsafe_b64encode(json.dumps({"alg": "HS256", "typ": "JWT"}).encode()).rstrip(b"=").decode()
    now = int(time.time())
    payload = base64.urlsafe_b64encode(json.dumps({
        "iss": access_key,
        "exp": now + 1800,
        "nbf": now - 5,
    }).encode()).rstrip(b"=").decode()
    msg = f"{header}.{payload}"
    sig = hmac.new(secret_key.encode(), msg.encode(), hashlib.sha256).digest()
    sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b"=").decode()
    return f"{msg}.{sig_b64}"


def _kling_headers() -> Dict[str, str]:
    if not KLING_ACCESS_KEY or not KLING_SECRET_KEY:
        raise RuntimeError(
            "Kling credentials are not configured. Set KLING_ACCESS_KEY and KLING_SECRET_KEY."
        )
    token = _kling_sign(KLING_ACCESS_KEY, KLING_SECRET_KEY)
    return {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    }


def _kling_request(method: str, path: str, body: Optional[Dict] = None, timeout: int = KLING_TIMEOUT) -> Dict[str, Any]:
    url = f"{KLING_BASE_URL}{path}"
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(url, data=data, headers=_kling_headers(), method=method)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8")
            return json.loads(raw)
    except urllib.error.HTTPError as e:
        err_body = e.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"Kling API {method} {path} → {e.code}: {err_body}") from e


def _kling_request_with_fallback(
    method: str,
    paths: List[str],
    body: Optional[Dict] = None,
    timeout: int = KLING_TIMEOUT,
) -> Dict[str, Any]:
    """Try several endpoint variants to tolerate provider path drift/gateway prefixes."""
    last_error: Optional[str] = None
    tried: List[str] = []
    for path in paths:
        tried.append(path)
        try:
            return _kling_request(method, path, body=body, timeout=timeout)
        except RuntimeError as e:
            msg = str(e)
            last_error = msg
            # 404 likely means wrong endpoint path; try next candidate.
            if "→ 404:" in msg:
                continue
            # Non-404 errors are usually actionable immediately.
            raise
    raise RuntimeError(
        f"Kling API endpoint mismatch for {method}. Tried: {tried}. Last error: {last_error or 'unknown'}"
    )


# ── Video Enhancement (Video-to-Video) ──────────────────────────────────────

def kling_video_enhance(
    *,
    video_url: Optional[str] = None,
    video_id: Optional[str] = None,
    prompt: str = "",
    negative_prompt: str = "noise, blur, artifacts",
    mode: str = "pro",
    duration: str = "5",
    cfg_scale: float = 0.5,
    callback_url: Optional[str] = None,
) -> Dict[str, Any]:
    """Submit a video-to-video enhancement task to Kling AI.

    Args:
        video_url: Direct URL to input video.
        video_id: Kling resource ID for previously uploaded video.
        prompt: Text guidance for enhancement.
        negative_prompt: Things to avoid.
        mode: 'std' or 'pro'.
        duration: '5' or '10' seconds.
        cfg_scale: 0.0-1.0, how strongly to follow prompt.
        callback_url: Webhook for completion notification.

    Returns:
        Task response dict with task_id.
    """
    if not video_url and not video_id:
        raise ValueError("Either video_url or video_id must be provided")

    payload: Dict[str, Any] = {
        "model": f"kling-v1-5",
        "mode": mode,
        "duration": duration,
        "cfg_scale": cfg_scale,
        "prompt": prompt,
        "negative_prompt": negative_prompt,
    }
    if video_url:
        payload["video_url"] = video_url
    if video_id:
        payload["video_id"] = video_id
    if callback_url:
        payload["callback_url"] = callback_url

    return _kling_request_with_fallback(
        "POST",
        ["/v1/videos/video2video", "/kling/v1/videos/video2video"],
        body=payload,
    )


def kling_video_generate(
    *,
    image_b64: Optional[str] = None,
    image_url: Optional[str] = None,
    image_id: Optional[str] = None,
    prompt: str,
    negative_prompt: str = "noise, blur, artifacts, distortion",
    model: str = "kling-v1-5",
    mode: str = "pro",
    duration: str = "5",
    cfg_scale: float = 0.5,
    aspect_ratio: str = "16:9",
    callback_url: Optional[str] = None,
) -> Dict[str, Any]:
    """Generate video from image + prompt (image-to-video).

    Args:
        image_url: Source still frame URL.
        image_id: Kling resource ID for previously uploaded image.
        prompt: Animation guidance.
        model: 'kling-v1', 'kling-v1-5', 'kling-v1-6'.
        mode: 'std' or 'pro'.
        duration: '5' or '10'.
        aspect_ratio: '16:9', '9:16', '1:1'.
    """
    if not image_b64 and not image_url and not image_id:
        raise ValueError("One of image_b64 / image_url / image_id must be provided")

    payload: Dict[str, Any] = {
        "model": model,
        "mode": mode,
        "duration": duration,
        "cfg_scale": cfg_scale,
        "prompt": prompt,
        "negative_prompt": negative_prompt,
        "aspect_ratio": aspect_ratio,
    }
    # Current Kling endpoint expects "image" as base64 payload string.
    # Keep url/id compatibility as a best-effort fallback for older gateways.
    if image_b64:
        payload["image"] = image_b64
    elif image_url:
        payload["image"] = image_url
    elif image_id:
        payload["image"] = image_id
    if callback_url:
        payload["callback_url"] = callback_url

    return _kling_request_with_fallback(
        "POST",
        ["/v1/videos/image2video", "/kling/v1/videos/image2video"],
        body=payload,
    )


def kling_video_generate_from_file(
    *,
    image_path: Path,
    prompt: str,
    negative_prompt: str = "noise, blur, artifacts, distortion",
    model: str = "kling-v1-5",
    mode: str = "pro",
    duration: str = "5",
    cfg_scale: float = 0.5,
    aspect_ratio: str = "16:9",
    callback_url: Optional[str] = None,
) -> Dict[str, Any]:
    """Generate video from a local image file by sending base64 payload."""
    import base64

    with image_path.open("rb") as fh:
        image_b64 = base64.b64encode(fh.read()).decode()

    return kling_video_generate(
        image_b64=image_b64,
        prompt=prompt,
        negative_prompt=negative_prompt,
        model=model,
        mode=mode,
        duration=duration,
        cfg_scale=cfg_scale,
        aspect_ratio=aspect_ratio,
        callback_url=callback_url,
    )


def kling_task_status(task_id: str) -> Dict[str, Any]:
    """Get status of any Kling task by ID."""
    return _kling_request_with_fallback(
        "GET",
        [f"/v1/tasks/{task_id}", f"/kling/v1/tasks/{task_id}"],
    )


def kling_video_task_status(task_id: str, endpoint: str = "video2video") -> Dict[str, Any]:
    """Get status of a video task."""
    return _kling_request_with_fallback(
        "GET",
        [f"/v1/videos/{endpoint}/{task_id}", f"/kling/v1/videos/{endpoint}/{task_id}"],
    )


def kling_list_models() -> Dict[str, Any]:
    """List available Kling models."""
    return _kling_request_with_fallback(
        "GET",
        ["/v1/models", "/kling/v1/models"],
    )


def kling_upload_file(file_path: Path) -> Dict[str, Any]:
    """Upload a local file to Kling storage and return resource_id."""
    import base64
    with open(file_path, "rb") as f:
        data = f.read()
    b64 = base64.b64encode(data).decode()
    suffix = file_path.suffix.lstrip(".").lower()
    mime_map = {
        "mp4": "video/mp4", "mov": "video/quicktime", "avi": "video/x-msvideo",
        "jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png",
    }
    mime = mime_map.get(suffix, "application/octet-stream")
    payload = {
        "file": b64,
        "file_name": file_path.name,
        "content_type": mime,
    }
    return _kling_request_with_fallback(
        "POST",
        ["/v1/files/upload", "/v1/files", "/kling/v1/files/upload", "/kling/v1/files"],
        body=payload,
        timeout=120,
    )


def kling_poll_until_done(
    task_id: str,
    endpoint: str = "video2video",
    max_wait_sec: int = 600,
    poll_interval: int = 5,
) -> Dict[str, Any]:
    """Poll Kling task until completed/failed or timeout."""
    start = time.time()
    while True:
        status_resp = kling_video_task_status(task_id, endpoint)
        task = status_resp.get("data", {})
        state = task.get("task_status") or task.get("status") or "processing"

        if state in ("succeed", "completed", "failed", "error"):
            return status_resp

        elapsed = time.time() - start
        if elapsed >= max_wait_sec:
            raise TimeoutError(f"Kling task {task_id} timed out after {max_wait_sec}s (last status: {state})")

        logger.debug("Kling task %s status=%s elapsed=%.0fs", task_id, state, elapsed)
        time.sleep(poll_interval)


def kling_health_check() -> Dict[str, Any]:
    """Quick connectivity check — returns status dict."""
    try:
        # `/v1/models` may be disabled in some accounts/regions.
        # `/v1/videos/image2video` reliably returns code=0 when auth+endpoint are valid.
        resp = _kling_request("GET", "/v1/videos/image2video", timeout=10)
        code = resp.get("code") if isinstance(resp, dict) else None
        if code not in (None, 0, "0"):
            return {"ok": False, "error": f"Kling probe returned non-zero code: {code}", "probe": resp}
        return {"ok": True, "probe_path": "/v1/videos/image2video", "probe": resp}
    except Exception as exc:
        return {"ok": False, "error": str(exc)}
