"""Media Handler - manages WordPress media library operations"""

import base64 as _b64
import binascii
import json
from typing import Any

from core.media_audit import log_media_upload
from plugins.wordpress.client import WordPressClient
from plugins.wordpress.handlers._media_core import (
    fetch_url_bytes,
    wp_raw_upload,
    wp_set_featured_media,
    wp_update_media_metadata,
)
from plugins.wordpress.handlers._media_security import (
    DEFAULT_MAX_BYTES,
    UploadError,
    ssrf_check,
)


def get_tool_specifications() -> list[dict[str, Any]]:
    """Return tool specifications for ToolGenerator"""
    return [
        # === MEDIA ===
        {
            "name": "list_media",
            "method_name": "list_media",
            "description": "List media library items. Returns images, videos, documents with URLs and metadata.",
            "schema": {
                "type": "object",
                "properties": {
                    "per_page": {
                        "type": "integer",
                        "description": "Number of media items per page",
                        "default": 20,
                        "minimum": 1,
                        "maximum": 100,
                    },
                    "page": {
                        "type": "integer",
                        "description": "Page number for pagination",
                        "default": 1,
                        "minimum": 1,
                    },
                    "media_type": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Filter by media type (image, video, audio, or application)",
                        "enum": ["image", "video", "audio", "application"],
                    },
                },
            },
            "scope": "read",
        },
        {
            "name": "get_media",
            "method_name": "get_media",
            "description": "Get detailed information about a media item. Returns full metadata including URLs, dimensions, and MIME type.",
            "schema": {
                "type": "object",
                "properties": {
                    "media_id": {
                        "type": "integer",
                        "description": "Media ID to retrieve",
                        "minimum": 1,
                    }
                },
                "required": ["media_id"],
            },
            "scope": "read",
        },
        {
            "name": "upload_media_from_url",
            "method_name": "upload_media_from_url",
            "description": "Upload media from a public URL to the WordPress media library (sideload). Downloads the file with SSRF protection, sniffs MIME, and uploads via raw-binary POST.",
            "schema": {
                "type": "object",
                "properties": {
                    "url": {
                        "type": "string",
                        "description": "Public HTTPS URL of the media file",
                    },
                    "filename": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Override filename (default: derived from URL)",
                    },
                    "title": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Media title (used in media library)",
                    },
                    "alt_text": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Alternative text for accessibility",
                    },
                    "caption": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Media caption",
                    },
                    "attach_to_post": {
                        "anyOf": [{"type": "integer"}, {"type": "null"}],
                        "description": "Attach uploaded media to this post/page ID",
                    },
                    "set_featured": {
                        "type": "boolean",
                        "default": False,
                        "description": "If true and attach_to_post is set, also set as the post's featured image",
                    },
                    "skip_optimize": {
                        "type": "boolean",
                        "default": False,
                        "description": "Skip server-side image optimization (F.5a.2)",
                    },
                    "convert_to": {
                        "anyOf": [{"type": "string", "enum": ["webp", "avif"]}, {"type": "null"}],
                        "description": (
                            "F.5a.8.1: re-encode the image in a modern format before upload. "
                            "'webp' or 'avif'. Falls back to WebP if AVIF is unavailable. "
                            "Leave null to keep source format (or use WP_MEDIA_CONVERT_TO env default)."
                        ),
                    },
                },
                "required": ["url"],
            },
            "scope": "write",
        },
        {
            "name": "upload_media_from_base64",
            "method_name": "upload_media_from_base64",
            "description": "Upload a base64-encoded file directly to the WordPress media library. For chat-attached images/files smaller than ~10 MB. Use upload_media_from_url for larger files or chunked path later.",
            "schema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "string",
                        "description": "Base64-encoded file bytes (no data: URL prefix required; prefix will be stripped if present)",
                    },
                    "filename": {
                        "type": "string",
                        "description": "Filename including extension (e.g. 'cover.jpg')",
                    },
                    "mime": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Client-supplied MIME hint; ignored if magic-byte sniff says otherwise",
                    },
                    "title": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Media title",
                    },
                    "alt_text": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Alternative text",
                    },
                    "caption": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Caption",
                    },
                    "attach_to_post": {
                        "anyOf": [{"type": "integer"}, {"type": "null"}],
                        "description": "Attach to this post/page ID",
                    },
                    "set_featured": {
                        "type": "boolean",
                        "default": False,
                        "description": "Also set as the post's featured image",
                    },
                    "skip_optimize": {
                        "type": "boolean",
                        "default": False,
                        "description": "Skip server-side image optimization",
                    },
                    "convert_to": {
                        "anyOf": [{"type": "string", "enum": ["webp", "avif"]}, {"type": "null"}],
                        "description": (
                            "F.5a.8.1: re-encode the image in a modern format before upload. "
                            "'webp' or 'avif'. Falls back to WebP if AVIF is unavailable."
                        ),
                    },
                },
                "required": ["data", "filename"],
            },
            "scope": "write",
        },
        {
            "name": "update_media",
            "method_name": "update_media",
            "description": "Update media metadata. Supports title, description, slug, alt text, caption, status, and associated post.",
            "schema": {
                "type": "object",
                "properties": {
                    "media_id": {
                        "type": "integer",
                        "description": "Media ID to update",
                        "minimum": 1,
                    },
                    "title": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Media title (displayed in media library)",
                    },
                    "description": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Media description (full text content, displayed in attachment page)",
                    },
                    "slug": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Media URL slug (e.g., 'my-image-name')",
                    },
                    "alt_text": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Alternative text for accessibility (important for images)",
                    },
                    "caption": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Media caption (shown below image in content)",
                    },
                    "status": {
                        "anyOf": [{"type": "string"}, {"type": "null"}],
                        "description": "Publication status of the media",
                        "enum": ["publish", "draft", "private"],
                    },
                    "post": {
                        "anyOf": [{"type": "integer"}, {"type": "null"}],
                        "description": "ID of the post/page to attach this media to",
                    },
                },
                "required": ["media_id"],
            },
            "scope": "write",
        },
        {
            "name": "delete_media",
            "method_name": "delete_media",
            "description": "Delete media from library. Can permanently delete or move to trash.",
            "schema": {
                "type": "object",
                "properties": {
                    "media_id": {
                        "type": "integer",
                        "description": "Media ID to delete",
                        "minimum": 1,
                    },
                    "force": {
                        "type": "boolean",
                        "description": "Permanently delete (true) or move to trash (false)",
                        "default": False,
                    },
                },
                "required": ["media_id"],
            },
            "scope": "write",
        },
    ]


def _decode_base64(data: str) -> bytes:
    """Accept raw base64 or data: URL prefix; return decoded bytes."""
    s = (data or "").strip()
    if s.startswith("data:") and "," in s:
        s = s.split(",", 1)[1]
    s = s.replace("\n", "").replace("\r", "").replace(" ", "")
    try:
        return _b64.b64decode(s, validate=True)
    except (binascii.Error, ValueError) as e:
        raise UploadError("BAD_BASE64", f"Invalid base64 payload: {e}") from e


def _maybe_optimize(
    data: bytes,
    mime_hint: str | None,
    *,
    skip: bool,
    convert_to: str | None = None,
) -> tuple[bytes, str | None]:
    """Route image bytes through the F.5a.2 optimize pipeline.

    F.5a.8.1: ``convert_to`` is forwarded to the optimizer so callers can
    force WebP/AVIF output. A falsy value defers to the ``WP_MEDIA_CONVERT_TO``
    env var.
    """
    if skip:
        return data, mime_hint
    try:
        from plugins.wordpress.handlers._media_optimize import optimize  # type: ignore
    except ImportError:
        return data, mime_hint
    return optimize(data, mime_hint, convert_to=convert_to)


async def _apply_metadata_and_attach(
    client: WordPressClient,
    media: dict[str, Any],
    *,
    title: str | None,
    alt_text: str | None,
    caption: str | None,
    attach_to_post: int | None,
    set_featured: bool,
    wc_client: WordPressClient | None = None,
) -> dict[str, Any]:
    """Apply metadata + attach to post / featured-image.

    F.5a.8.5 — when the companion's single-call ``upload-and-attach``
    route already applied these fields, skip everything (saves 1-2
    redundant round-trips).

    F.X.fix-pass6 — return a status dict so callers can surface
    "media uploaded but featured failed" as partial success instead
    of a misleading GENERATION_FAILED. When ``set_featured`` targets
    a WooCommerce product (CPT, not addressable via /wp/v2/posts/{id}),
    route through the WC products endpoint via ``wc_client``. Both
    "metadata applied" and "featured set" steps are reported
    independently in the result dict.
    """
    status: dict[str, Any] = {
        "metadata_applied": False,
        "featured_set": False,
        "featured_context": None,
        "warnings": [],
    }
    if media.get("_upload_route") == "companion_unified":
        # Companion did metadata + attach + featured atomically.
        status["metadata_applied"] = True
        status["featured_set"] = bool(set_featured and attach_to_post)
        status["featured_context"] = "companion_unified"
        return status

    if any(v is not None for v in (title, alt_text, caption)) or attach_to_post is not None:
        try:
            await wp_update_media_metadata(
                client,
                media["id"],
                title=title,
                alt_text=alt_text,
                caption=caption,
                post=attach_to_post,
            )
            status["metadata_applied"] = True
        except Exception as exc:  # noqa: BLE001
            status["warnings"].append(f"metadata_failed: {exc}")

    if set_featured and attach_to_post is not None:
        # 1. WC product first (CPT). Falls through to /wp/v2/posts on miss.
        wc = wc_client or client
        wc_product = None
        try:
            wc_product = await wc.get(f"products/{attach_to_post}", use_woocommerce=True)
        except Exception:
            wc_product = None

        if isinstance(wc_product, dict) and wc_product.get("id"):
            try:
                existing_images = list(wc_product.get("images") or [])
                # Featured = images[0]; preserve gallery via the same
                # merge primitive used by attach_media_to_product.
                from plugins.wordpress.handlers.media_attach import _merge_product_images

                new_images = _merge_product_images(
                    existing=existing_images,
                    new_ids=[media["id"]],
                    role="main",
                    mode="replace",
                )
                await wc.put(
                    f"products/{attach_to_post}",
                    json_data={"images": new_images},
                    use_woocommerce=True,
                )
                status["featured_set"] = True
                status["featured_context"] = "product"
            except Exception as exc:  # noqa: BLE001
                status["warnings"].append(f"featured_set_failed (product): {exc}")
        else:
            # Issue #90 — set_featured used to hit /wp/v2/posts/{id}
            # only, which 404s for pages and other CPTs. Now: try
            # `posts` first (the common case), and on miss, walk the
            # thumbnail-supporting rest_bases (pages, then any other
            # CPT that opted in via add_theme_support('post-thumbnails')
            # / declares ``thumbnail`` in its supports list).
            ctx = await _set_featured_with_fallback(client, attach_to_post, media["id"])
            if ctx is not None:
                status["featured_set"] = True
                status["featured_context"] = ctx
            else:
                status["warnings"].append(
                    f"featured_set_failed: post {attach_to_post} not found "
                    "in any thumbnail-supporting post type"
                )

    return status


async def _set_featured_with_fallback(
    client: WordPressClient,
    target_id: int,
    media_id: int,
) -> str | None:
    """Set featured image on ``target_id`` across post / page / CPT.

    Tries ``posts`` first (the 99% case — and the path covered by
    ``wp_set_featured_media`` to keep existing test seams stable),
    then falls back to ``pages`` and any other rest_base that declares
    ``thumbnail`` support via ``/wp/v2/types``. Returns the rest_base on
    success, ``None`` when nothing accepted the write.
    """
    try:
        await wp_set_featured_media(client, target_id, media_id)
        return "post"
    except Exception:
        pass

    body = {"featured_media": media_id}

    try:
        await client.post(f"pages/{target_id}", json_data=body)
        return "page"
    except Exception:
        pass

    try:
        types_data = await client.get("types")
    except Exception:
        return None
    if not isinstance(types_data, dict):
        return None

    for slug, data in types_data.items():
        if not isinstance(data, dict):
            continue
        rest_base = data.get("rest_base") or slug
        if rest_base in ("posts", "pages", "media", "attachment"):
            continue
        supports = data.get("supports") or {}
        # `supports` is sometimes a list (older WP / some plugins) and
        # sometimes a dict keyed by feature. Treat both as truthy when
        # ``thumbnail`` is present.
        if isinstance(supports, dict):
            if not supports.get("thumbnail"):
                continue
        elif isinstance(supports, list):
            if "thumbnail" not in supports:
                continue
        else:
            continue
        try:
            await client.post(f"{rest_base}/{target_id}", json_data=body)
            return rest_base
        except Exception:
            continue

    return None


def _format_upload_result(media: dict[str, Any], *, source: str) -> dict[str, Any]:
    title = media.get("title")
    rendered_title = title.get("rendered") if isinstance(title, dict) else title
    return {
        "id": media["id"],
        "title": rendered_title or "",
        "url": media.get("source_url", ""),
        "mime_type": media.get("mime_type", ""),
        "media_type": media.get("media_type", ""),
        "size_bytes": media.get("media_details", {}).get("filesize"),
        "source": source,
        "message": f"Media uploaded successfully (id={media['id']}).",
    }


class MediaHandler:
    """Handle media-related operations for WordPress"""

    def __init__(self, client: WordPressClient, *, user_id: str | None = None):
        """
        Initialize media handler.

        Args:
            client: WordPress API client instance
            user_id: Calling user id, used for audit logging (None = admin/env)
        """
        self.client = client
        self.user_id = user_id

    # === MEDIA ===

    async def list_media(
        self, per_page: int = 20, page: int = 1, media_type: str | None = None
    ) -> str:
        """
        List media library items.

        Args:
            per_page: Number of media items per page (1-100)
            page: Page number
            media_type: Filter by media type (image, video, audio, application)

        Returns:
            JSON string with media list
        """
        try:
            params = {"per_page": per_page, "page": page}
            if media_type:
                params["media_type"] = media_type

            media = await self.client.get("media", params=params)

            # Format response
            result = {
                "total": len(media),
                "page": page,
                "per_page": per_page,
                "media": [
                    {
                        "id": m["id"],
                        "title": m["title"]["rendered"],
                        "mime_type": m["mime_type"],
                        "media_type": m.get("media_type", ""),
                        "url": m["source_url"],
                        "date": m["date"],
                        "alt_text": m.get("alt_text", ""),
                        "link": m.get("link", ""),
                        # F.X.fix #6: expose post_parent so callers can
                        # verify "is media X attached to post Y" without
                        # a second WP REST round trip. WP returns 0 for
                        # unattached media; we preserve that.
                        "post_parent": m.get("post") or 0,
                    }
                    for m in media
                ],
            }

            return json.dumps(result, indent=2)
        except Exception as e:
            return json.dumps(
                {"error": str(e), "message": f"Failed to list media: {str(e)}"}, indent=2
            )

    async def get_media(self, media_id: int) -> str:
        """
        Get detailed information about a specific media item.

        Args:
            media_id: Media ID to retrieve

        Returns:
            JSON string with media data
        """
        try:
            media = await self.client.get(f"media/{media_id}")

            result = {
                "id": media["id"],
                "title": media["title"]["rendered"],
                "mime_type": media["mime_type"],
                "media_type": media.get("media_type", ""),
                "url": media["source_url"],
                "alt_text": media.get("alt_text", ""),
                "caption": media.get("caption", {}).get("rendered", ""),
                "description": media.get("description", {}).get("rendered", ""),
                "date": media["date"],
                "modified": media.get("modified", ""),
                "link": media.get("link", ""),
                "media_details": media.get("media_details", {}),
                # F.X.fix #6: expose post_parent so attach-verification
                # doesn't need a second round trip via posts/{id}.
                "post_parent": media.get("post") or 0,
            }

            return json.dumps(result, indent=2)
        except Exception as e:
            return json.dumps(
                {"error": str(e), "message": f"Failed to get media {media_id}: {str(e)}"}, indent=2
            )

    async def upload_media_from_url(
        self,
        url: str,
        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:
        """Upload media from a public URL to the WordPress media library."""
        try:
            ssrf = ssrf_check(url)
            if not ssrf.allowed:
                raise UploadError(
                    "SSRF", ssrf.reason or "URL rejected by SSRF guard.", {"url": url}
                )

            data, declared_ct, fname_guess = await fetch_url_bytes(url, max_bytes=DEFAULT_MAX_BYTES)
            data, mime_hint = _maybe_optimize(
                data, declared_ct, skip=skip_optimize, convert_to=convert_to
            )

            media = await wp_raw_upload(
                self.client,
                data,
                filename=filename or fname_guess,
                mime_hint=mime_hint or declared_ct,
                # F.5a.8.5: forward metadata so the companion's single-
                # call route (when advertised) bundles upload + attach +
                # featured in one PHP request. ``_apply_metadata_and_attach``
                # below is a no-op in that case.
                attach_to_post=attach_to_post,
                set_featured=set_featured,
                title=title,
                alt_text=alt_text,
                caption=caption,
            )
            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,
            )

            log_media_upload(
                site=self.client.site_url,
                user_id=self.user_id,
                mime=media.get("mime_type") or mime_hint or declared_ct,
                size_bytes=len(data),
                source="url",
                media_id=media.get("id"),
            )
            return json.dumps(_format_upload_result(media, source=url), indent=2)
        except UploadError as e:
            return json.dumps(e.to_dict(), indent=2)
        except Exception as e:
            return json.dumps(
                {"error_code": "INTERNAL", "message": f"Upload from URL failed: {e}"}, indent=2
            )

    async def upload_media_from_base64(
        self,
        data: str,
        filename: str,
        mime: 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:
        """Upload a base64-encoded file to the WordPress media library."""
        try:
            raw = _decode_base64(data)
            raw, mime_hint = _maybe_optimize(raw, mime, skip=skip_optimize, convert_to=convert_to)

            media = await wp_raw_upload(
                self.client,
                raw,
                filename=filename,
                mime_hint=mime_hint or mime,
                # F.5a.8.5: single-call path when companion advertises it.
                attach_to_post=attach_to_post,
                set_featured=set_featured,
                title=title,
                alt_text=alt_text,
                caption=caption,
            )
            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,
            )

            log_media_upload(
                site=self.client.site_url,
                user_id=self.user_id,
                mime=media.get("mime_type") or mime_hint or mime,
                size_bytes=len(raw),
                source="base64",
                media_id=media.get("id"),
            )
            return json.dumps(_format_upload_result(media, source="base64"), indent=2)
        except UploadError as e:
            return json.dumps(e.to_dict(), indent=2)
        except Exception as e:
            return json.dumps(
                {"error_code": "INTERNAL", "message": f"Base64 upload failed: {e}"}, indent=2
            )

    async def update_media(
        self,
        media_id: int,
        title: str | None = None,
        description: str | None = None,
        slug: str | None = None,
        alt_text: str | None = None,
        caption: str | None = None,
        status: str | None = None,
        post: int | None = None,
    ) -> str:
        """
        Update media metadata.

        Args:
            media_id: Media ID to update
            title: Media title
            description: Media description
            slug: Media URL slug
            alt_text: Alternative text for accessibility
            caption: Media caption
            status: Publication status (publish, draft, private)
            post: ID of post/page to attach media to

        Returns:
            JSON string with updated media data
        """
        try:
            # Build data dict with only provided values
            data = {}
            if title is not None:
                data["title"] = title
            if description is not None:
                data["description"] = description
            if slug is not None:
                data["slug"] = slug
            if alt_text is not None:
                data["alt_text"] = alt_text
            if caption is not None:
                data["caption"] = caption
            if status is not None:
                data["status"] = status
            if post is not None:
                data["post"] = post

            media = await self.client.post(f"media/{media_id}", json_data=data)

            result = {
                "id": media["id"],
                "title": media["title"]["rendered"],
                "alt_text": media.get("alt_text", ""),
                "caption": media.get("caption", {}).get("rendered", ""),
                "url": media["source_url"],
                "message": f"Media {media_id} updated successfully",
            }

            return json.dumps(result, indent=2)
        except Exception as e:
            return json.dumps(
                {"error": str(e), "message": f"Failed to update media {media_id}: {str(e)}"},
                indent=2,
            )

    async def delete_media(self, media_id: int, force: bool = False) -> str:
        """
        Delete or trash media from library.

        Args:
            media_id: Media ID to delete
            force: Permanently delete (True) or move to trash (False)

        Returns:
            JSON string with deletion result
        """
        try:
            params = {"force": "true" if force else "false"}
            result = await self.client.delete(f"media/{media_id}", params=params)

            message = f"Media {media_id} {'permanently deleted' if force else 'moved to trash'}"
            return json.dumps({"success": True, "message": message, "result": result}, indent=2)
        except Exception as e:
            return json.dumps(
                {"error": str(e), "message": f"Failed to delete media {media_id}: {str(e)}"},
                indent=2,
            )
