"""F.5a.4: AI image generation + upload chain.

Provides ``wordpress_generate_and_upload_image`` which chains:

1. Resolve the caller's per-user provider API key (or env fallback).
2. Call the chosen provider (OpenAI / Stability / Replicate) and get raw bytes.
3. Reuse the F.5a.1 raw-upload path (optimize → POST /wp/v2/media).
4. Optionally apply metadata (alt/caption/title), attach to a post, or set as featured.
5. Emit an audit-log entry with cost/provider/usage.

The tool is registered on the WordPress plugin because the end result is
a WordPress media item — the ``ai_image`` package is a pure provider
library, not an MCP plugin with its own endpoints.
"""

from __future__ import annotations

import hashlib
import json
import logging
import os
import time
from typing import Any

from plugins.ai_image.providers.base import GenerationRequest, ProviderError
from plugins.ai_image.registry import get_provider, list_providers
from plugins.wordpress.client import WordPressClient
from plugins.wordpress.handlers._media_core import wp_raw_upload
from plugins.wordpress.handlers._media_security import UploadError
from plugins.wordpress.handlers.media import (
    _apply_metadata_and_attach,
    _format_upload_result,
    _maybe_optimize,
)

_logger = logging.getLogger("mcphub.wordpress.ai_media")


def _content_sha(data: bytes) -> str:
    """SHA-256 of the raw generated bytes, truncated for brevity.

    Two independent provider calls for the same prompt produce
    different bytes (diffusion models are non-deterministic), so this
    hash is effectively a per-call fingerprint. Including it in the
    idempotency key means a retry with the *same already-generated*
    bytes dedupes, while a fresh call with a new image gets a new id.
    """
    return hashlib.sha256(data).hexdigest()[:32]


def _idempotency_key_for(
    *,
    provider: str,
    model: str | None,
    prompt: str,
    size: str,
    attach_to_post: int | None,
    set_featured: bool,
    site_url: str,
    user_id: str | None,
    content_sha: str,
) -> str:
    """Build the ``Idempotency-Key`` header value for an AI upload.

    Matches the ``^[A-Za-z0-9_\\-]{1,128}$`` regex the companion
    validates (see airano-mcp-bridge.php handle_upload_and_attach).
    """
    raw = "|".join(
        [
            provider or "",
            model or "",
            prompt or "",
            size or "",
            str(attach_to_post or 0),
            "1" if set_featured else "0",
            site_url or "",
            user_id or "",
            content_sha or "",
        ]
    )
    digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
    return f"mcphub_ai_{digest[:48]}"


def get_tool_specifications() -> list[dict[str, Any]]:
    return [
        {
            "name": "generate_and_upload_image",
            "method_name": "generate_and_upload_image",
            "description": (
                "Generate an image with an AI provider (OpenAI DALL-E, "
                "Stability, or Replicate Flux) and upload it to the WordPress "
                "media library in one call. Optionally attach to a post or set "
                "as featured image. Uses the caller's stored provider API key; "
                "falls back to server env vars if no per-user key is set."
            ),
            "schema": {
                "type": "object",
                "properties": {
                    "provider": {
                        "type": "string",
                        "enum": list_providers(),
                        "description": "AI provider to use.",
                    },
                    "prompt": {
                        "type": "string",
                        "description": "Text prompt describing the image to generate.",
                    },
                    "size": {
                        "type": "string",
                        "default": "1024x1024",
                        "description": "Requested image size (WxH). Providers map to supported sizes.",
                    },
                    "quality": {
                        "type": "string",
                        "default": "standard",
                        "description": "Provider-specific quality hint (e.g. 'standard'/'hd').",
                    },
                    "model": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Optional model override (e.g. 'dall-e-3', 'flux-dev').",
                    },
                    "negative_prompt": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                    },
                    "filename": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Optional filename hint for the WP library.",
                    },
                    "title": {"anyOf": [{"type": "string"}, {"type": "null"}]},
                    "alt_text": {"anyOf": [{"type": "string"}, {"type": "null"}]},
                    "caption": {"anyOf": [{"type": "string"}, {"type": "null"}]},
                    "attach_to_post": {
                        "anyOf": [{"type": "integer", "minimum": 1}, {"type": "null"}],
                    },
                    "set_featured": {"type": "boolean", "default": False},
                    "skip_optimize": {"type": "boolean", "default": False},
                    "convert_to": {
                        "anyOf": [
                            {"type": "string", "enum": ["webp", "avif"]},
                            {"type": "null"},
                        ],
                        "description": (
                            "F.5a.8.1: re-encode the generated image as WebP or "
                            "AVIF before upload (falls back to WebP if AVIF is "
                            "unavailable)."
                        ),
                    },
                },
                "required": ["provider", "prompt"],
            },
            "scope": "write",
        }
    ]


class AIMediaHandler:
    """Handles AI-powered image generation + WP upload chain."""

    def __init__(
        self,
        client: WordPressClient,
        user_id: str | None = None,
        site_id: str | None = None,
        wc_client: WordPressClient | None = None,
    ):
        self.client = client
        # ``user_id`` is injected by the user-endpoint plumbing (F.5a.4).
        # Admin / system endpoints leave it None.
        self.user_id = user_id
        # ``site_id`` is injected by the user-endpoint plumbing (F.5a.9.x)
        # and is the primary input for resolving per-site provider API
        # keys. Admin / env-driven endpoints leave it None, in which case
        # the resolver falls back to the legacy env-var path.
        self.site_id = site_id
        # F.X.fix-pass6 — optional separate client for /wc/v3/* (the
        # WC consumer-key/secret pair on a WC site). Used by
        # _apply_metadata_and_attach to detect "attach_to_post is a
        # WC product" and route the featured-image set through
        # /wc/v3/products/{id}/images instead of /wp/v2/posts/{id}
        # which 404s for the product CPT. None = same client serves
        # both REST roots (WP plugin or legacy single-credential WC).
        self.wc_client = wc_client

    async def generate_and_upload_image(
        self,
        provider: str,
        prompt: str,
        size: str = "1024x1024",
        quality: str = "standard",
        model: str | None = None,
        negative_prompt: str | None = None,
        filename: str | None = None,
        title: str | None = None,
        alt_text: str | None = None,
        caption: str | None = None,
        attach_to_post: int | None = None,
        set_featured: bool = False,
        skip_optimize: bool = False,
        convert_to: str | None = None,
    ) -> str:
        started = time.time()
        try:
            from core.tool_rate_limiter import ToolRateLimitError, get_tool_rate_limiter

            try:
                get_tool_rate_limiter().check("wordpress_generate_and_upload_image", self.user_id)
            except ToolRateLimitError as e:
                return json.dumps(e.to_dict(), indent=2)

            provider_impl = get_provider(provider)
            api_key = await self._resolve_api_key(provider)
            # F.X.fix-pass3 — when caller omitted ``model``, fall back
            # to the per-site default the operator pinned in the
            # dashboard. Keeps MCP call sites short and lets the user
            # rotate models without code changes.
            if model is None and self.site_id:
                try:
                    from core.database import get_database

                    db = get_database()
                    model = await db.get_site_provider_default_model(self.site_id, provider)
                except Exception as exc:  # noqa: BLE001
                    _logger.debug(
                        "default_model lookup skipped site=%s provider=%s: %s",
                        self.site_id,
                        provider,
                        exc,
                    )
            if not api_key:
                if self.site_id:
                    msg = (
                        f"No API key configured for provider '{provider}' on "
                        f"this site. Open the site in the dashboard and add the "
                        f"key under 'AI Image Generation' in Connection Settings."
                    )
                    dashboard_url = f"/dashboard/sites/{self.site_id}"
                else:
                    msg = (
                        f"No API key configured for provider '{provider}'. "
                        f"Set the {provider.upper()}_API_KEY env var on the server."
                    )
                    dashboard_url = None
                payload = {
                    "error_code": "NO_PROVIDER_KEY",
                    "message": msg,
                    "provider": provider,
                }
                if dashboard_url:
                    payload["dashboard_url"] = dashboard_url
                return json.dumps(payload, indent=2)

            request = GenerationRequest(
                prompt=prompt,
                size=size,
                quality=quality,
                model=model,
                negative_prompt=negative_prompt,
            )
            result = await provider_impl.generate(api_key, request)

            data, mime_hint = _maybe_optimize(
                result.data, result.mime, skip=skip_optimize, convert_to=convert_to
            )
            # F.X.fix #7: stable idempotency key per logical call. A
            # client-side retry (after timeout) produces the same
            # digest, so the companion dedupes instead of creating an
            # "-2.webp" orphan. Key covers every input that would
            # change the intended outcome.
            idempotency_key = _idempotency_key_for(
                provider=provider,
                model=result.meta.get("model") or model,
                prompt=prompt,
                size=size,
                attach_to_post=attach_to_post,
                set_featured=set_featured,
                site_url=self.client.site_url,
                user_id=self.user_id,
                content_sha=_content_sha(data),
            )
            media = await wp_raw_upload(
                self.client,
                data,
                filename=filename or result.filename,
                mime_hint=mime_hint or result.mime,
                # F.5a.8.5: single-call upload+metadata+attach+featured
                # when the companion's upload-and-attach route is
                # advertised; _apply_metadata_and_attach becomes a no-op.
                attach_to_post=attach_to_post,
                set_featured=set_featured,
                title=title,
                alt_text=alt_text,
                caption=caption,
                idempotency_key=idempotency_key,
            )
            attach_status = await _apply_metadata_and_attach(
                self.client,
                media,
                title=title,
                alt_text=alt_text,
                caption=caption,
                attach_to_post=attach_to_post,
                set_featured=set_featured,
                wc_client=self.wc_client,
            )

            duration_ms = int((time.time() - started) * 1000)
            self._audit(
                provider=provider,
                model=result.meta.get("model") or model,
                size=size,
                duration_ms=duration_ms,
                bytes_=len(result.data),
                cost_usd=result.cost_usd,
                media_id=media.get("id"),
                error=None,
            )
            from core.media_audit import log_media_upload

            log_media_upload(
                site=self.client.site_url,
                user_id=self.user_id,
                mime=media.get("mime_type") or result.mime,
                size_bytes=len(data),
                source=f"ai:{provider}",
                media_id=media.get("id"),
                cost_usd=result.cost_usd,
            )

            # F.X.fix-pass6 — surface partial-success: media uploaded
            # but featured-set / metadata-apply may have warnings. Old
            # behaviour bubbled those exceptions up as
            # GENERATION_FAILED, hiding the freshly-created media id
            # and leaving an orphan in the library.
            payload = _format_upload_result(media, source=f"ai:{provider}")
            payload["provider"] = provider
            payload["provider_meta"] = result.meta
            payload["cost_usd"] = result.cost_usd
            payload["duration_ms"] = duration_ms
            payload["attach"] = {
                "metadata_applied": attach_status.get("metadata_applied", False),
                "featured_set": attach_status.get("featured_set", False),
                "featured_context": attach_status.get("featured_context"),
            }
            warnings = attach_status.get("warnings") or []
            if warnings:
                payload["warnings"] = warnings
            return json.dumps(payload, indent=2)

        except ProviderError as e:
            self._audit(
                provider=provider,
                model=model,
                size=size,
                duration_ms=int((time.time() - started) * 1000),
                bytes_=0,
                cost_usd=None,
                media_id=None,
                error=e.code,
            )
            return json.dumps(e.to_dict(), indent=2)
        except UploadError as e:
            self._audit(
                provider=provider,
                model=model,
                size=size,
                duration_ms=int((time.time() - started) * 1000),
                bytes_=0,
                cost_usd=None,
                media_id=None,
                error=e.code,
            )
            return json.dumps(e.to_dict(), indent=2)
        except Exception as e:
            _logger.exception("generate_and_upload_image failed")
            return json.dumps(
                {
                    "error_code": "GENERATION_FAILED",
                    "message": f"generate_and_upload_image failed: {e}",
                },
                indent=2,
            )

    async def _resolve_api_key(self, provider: str) -> str | None:
        """Resolve the provider key for this call.

        F.5a.9.x policy (single-source, per-site):

        1. ``site_id`` set (per-user endpoint, the only UI-reachable path):
           read the key from the per-site ``site_provider_keys`` row.
           Env fallback is deliberately NOT applied here so a single
           ``OPENAI_API_KEY=...`` on the server cannot silently paper
           over a missing per-site configuration — callers instead get
           ``NO_PROVIDER_KEY`` pointing at the site's dashboard page.

        2. ``site_id is None`` (admin / master-key endpoint, no site
           context): fall back to the ``<PROVIDER>_API_KEY`` env var.
           This preserves the legacy self-hosted / admin workflow.

        The per-user provider-keys store from F.18.8 has been removed
        in favour of the per-site model; there is no hybrid fallback.
        """
        # 1. Per-site key (the primary path)
        if self.site_id:
            try:
                from core.site_api import get_site_provider_key

                return await get_site_provider_key(self.site_id, provider)
            except Exception as exc:
                _logger.error(
                    "Site provider-key lookup failed site=%s provider=%s: %s",
                    self.site_id,
                    provider,
                    exc,
                )
                return None

        # 2. Admin / env-var fallback (no site context)
        env_var = f"{provider.upper()}_API_KEY"
        return os.environ.get(env_var)

    def _audit(
        self,
        *,
        provider: str,
        model: str | None,
        size: str,
        duration_ms: int,
        bytes_: int,
        cost_usd: float | None,
        media_id: int | None,
        error: str | None,
    ) -> None:
        try:
            from core.audit_log import get_audit_logger

            logger_ = get_audit_logger()
        except Exception:
            return
        try:
            logger_.log_tool_call(
                tool_name="wordpress_generate_and_upload_image",
                params={
                    "provider": provider,
                    "model": model,
                    "size": size,
                    "bytes": bytes_,
                    "cost_usd": cost_usd,
                    "media_id": media_id,
                },
                duration_ms=duration_ms,
                user_id=self.user_id,
                error=error,
                result_summary=(
                    f"{provider}/{model or '-'} {bytes_}B "
                    f"${cost_usd or 0:.4f} -> media_id={media_id}"
                    if not error
                    else f"failed: {error}"
                ),
            )
        except Exception:
            _logger.debug("audit log emit failed", exc_info=True)
