"""OpenAI DALL-E image generation service for recipes.

Downloads generated images to `backend/static/images/` and returns
relative paths suitable for `Recipe.image_url`.
"""
from __future__ import annotations

import base64
import hashlib
import logging
import os
import time
from pathlib import Path
from typing import Optional

import httpx
from sqlalchemy.orm import Session

from app.config import settings
from app.models import Recipe

logger = logging.getLogger(__name__)

# Directory where generated images are persisted.
STATIC_IMAGES_DIR = Path(__file__).resolve().parent.parent.parent / "static" / "images"
PUBLIC_URL_PREFIX = "/static/images/"


def _build_prompt(recipe: Recipe) -> str:
    """Create a vivid, appetising DALL-E prompt from a recipe."""
    name = recipe.name or "A delicious dish"
    ingredients = recipe.ingredients or []
    ingredient_names = [str(ing.get("name", "")) for ing in ingredients if ing.get("name")]
    ingredient_str = ", ".join(ingredient_names[:6]) if ingredient_names else "fresh ingredients"
    tags = recipe.cuisine_tags or []
    tag_str = f", {', '.join(tags)} style" if tags else ""
    return (
        f"Professional food photography of {name}{tag_str}. "
        f"Served on a clean white ceramic plate with natural lighting, "
        f"shallow depth of field. Vibrant colours, appetising. "
        f"Ingredients visible: {ingredient_str}. "
        f"No text, no watermark."
    )


def _sanitise_filename(recipe_name: str) -> str:
    """Return a filesystem-safe lowercase slug."""
    keep = recipe_name.lower()
    for ch in " /'\"?!:\\|;#":
        keep = keep.replace(ch, "_")
    return keep.strip("_")


def _openai_api_key() -> Optional[str]:
    if settings.AI_IMAGE_PROVIDER and settings.AI_IMAGE_PROVIDER.lower() != "openai":
        return None
    return settings.AI_IMAGE_API_KEY or os.environ.get("OPENAI_API_KEY")


def generate_image_for_recipe(
    recipe: Recipe,
    db: Optional[Session] = None,
    size: str = "1024x1024",
    quality: str = "standard",  # or "hd"
    force: bool = False,
) -> Optional[str]:
    """Generate an image for *recipe* using DALL-E 3.

    Returns the public static path (e.g. ``/static/images/caprese_pasta_a1b2.png``)
    or ``None`` if generation is disabled / fails.

    If *force* is ``False`` and the recipe already has an ``image_url`` that
    looks like a local static path, the existing image is returned immediately.
    """
    if not settings.AI_IMAGE_ENABLED:
        logger.info("AI_IMAGE_ENABLED=false; skipping image generation for %s", recipe.name)
        return None

    api_key = _openai_api_key()
    if not api_key:
        logger.warning("No OpenAI API key configured; skipping image generation.")
        return None

    slug = _sanitise_filename(recipe.name)
    digest_id = hashlib.sha256(str(recipe.id).encode()).hexdigest()[:8]
    out_name = f"{slug}_{digest_id}.png"
    out_path = STATIC_IMAGES_DIR / out_name

    # Reuse existing image unless forced.
    if not force and out_path.exists():
        logger.debug("Image already exists for %s", recipe.name)
        return f"{PUBLIC_URL_PREFIX}{out_name}"

    if not force and recipe.image_url and recipe.image_url.startswith("/static/images/"):
        return recipe.image_url

    prompt = _build_prompt(recipe)

    model = os.environ.get("AI_IMAGE_MODEL", "gpt-image-1")
    logger.info("Generating image for '%s' via %s …", recipe.name, model)

    try:
        payload = {
            "model": model,
            "prompt": prompt,
            "size": size,
            "n": 1,
        }
        # gpt-image-* uses low/medium/high/auto; dall-e uses standard/hd
        if not model.startswith("gpt-image"):
            payload["quality"] = quality
        else:
            # Map dall-e quality names to gpt-image quality names
            quality_map = {"standard": "medium", "hd": "high"}
            payload["quality"] = quality_map.get(quality, "auto")
        resp = httpx.post(
            "https://api.openai.com/v1/images/generations",
            headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
            json=payload,
            timeout=120.0,
        )
    except httpx.HTTPError as exc:
        logger.error("Network error generating image for '%s': %s", recipe.name, exc)
        return None

    if resp.status_code != 200:
        logger.error(
            "DALL-E API error for '%s': HTTP %s – %s",
            recipe.name,
            resp.status_code,
            resp.text,
        )
        return None

    try:
        data = resp.json()["data"][0]
        if "b64_json" in data:
            image_bytes = base64.b64decode(data["b64_json"])
        elif "url" in data:
            image_url = data["url"]
            img_resp = httpx.get(image_url, timeout=30.0)
            if img_resp.status_code != 200:
                logger.error("Failed downloading image for '%s': HTTP %s", recipe.name, img_resp.status_code)
                return None
            image_bytes = img_resp.content
        else:
            raise KeyError("no image data (url or b64_json)")
    except (KeyError, IndexError) as exc:
        logger.error("Unexpected DALL-E response for '%s': %s", recipe.name, exc)
        return None

    STATIC_IMAGES_DIR.mkdir(parents=True, exist_ok=True)
    out_path.write_bytes(image_bytes)
    public_path = f"{PUBLIC_URL_PREFIX}{out_name}"

    # Persist on recipe row if a DB session was provided.
    if db is not None:
        recipe.image_url = public_path
        db.add(recipe)
        db.commit()
        logger.info("Image saved for '%s' -> %s", recipe.name, public_path)

    return public_path


def generate_images_batch(
    db: Session,
    recipe_ids: Optional[list[str]] = None,
    missing_only: bool = False,
    force: bool = False,
    limit: int = 10,
) -> dict:
    """Batch-generate images for recipes.

    Returns a dict::
        {
            "total": <int>,
            "succeeded": <int>,
            "failed": <int>,
            "skipped": <int>,
            "results": {"recipe_id": "image_url | null", …},
        }
    """
    from sqlalchemy import or_

    query = db.query(Recipe)

    if recipe_ids:
        query = query.filter(Recipe.id.in_(recipe_ids))

    if missing_only:
        query = query.filter(
            or_(
                Recipe.image_url.is_(None),
                Recipe.image_url == "",
            )
        )

    recipes = query.limit(limit).all()

    total = len(recipes)
    succeeded = 0
    failed = 0
    skipped = 0
    results = {}

    for recipe in recipes:
        recipe_id = str(recipe.id)
        # Skip if already has local image unless forced.
        if not force and recipe.image_url and recipe.image_url.startswith("/static/images/"):
            skipped += 1
            results[recipe_id] = {"status": "skipped", "url": recipe.image_url}
            continue

        url = generate_image_for_recipe(recipe, db=db, force=force)
        if url:
            succeeded += 1
            results[recipe_id] = {"status": "ok", "url": url}
        else:
            failed += 1
            results[recipe_id] = {"status": "failed", "url": None}

        time.sleep(2)  # rate-limiting courtesy between requests

    return {
        "total": total,
        "succeeded": succeeded,
        "failed": failed,
        "skipped": skipped,
        "results": results,
    }
