"""Multimodal file processing utilities for MultimodalAgent.

Handles downloading, content type detection, text extraction, office doc
conversion, and base64 encoding of files for multimodal LLM messages.
"""

from __future__ import annotations

import asyncio
import base64
import mimetypes
import os
import urllib.request
from pathlib import Path
from typing import Any

from fluxibly.logging import Logger

logger = Logger(component="multimodal")

# ── Content type classification ──────────────────────────────────

# File extensions → MIME types (supplement stdlib mimetypes)
_EXTRA_MIME_TYPES: dict[str, str] = {
    ".md": "text/markdown",
    ".yaml": "application/x-yaml",
    ".yml": "application/x-yaml",
    ".tsx": "text/x-typescript",
    ".ts": "text/x-typescript",
    ".jsx": "text/javascript",
}

TEXT_TYPES: frozenset[str] = frozenset(
    {
        "text/plain",
        "text/markdown",
        "text/csv",
        "application/json",
        "text/xml",
        "text/html",
        "text/x-python",
        "text/javascript",
        "text/x-typescript",
        "application/x-yaml",
    }
)

OFFICE_TYPES: frozenset[str] = frozenset(
    {
        # .docx
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        # .pptx
        "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    }
)

VISUAL_TYPES: frozenset[str] = frozenset(
    {
        "image/png",
        "image/jpeg",
        "image/gif",
        "image/webp",
        "application/pdf",
    }
)

# Maximum file size for base64 encoding (20 MB)
MAX_VISUAL_FILE_BYTES = 20 * 1024 * 1024


# ── Content type detection ───────────────────────────────────────


def detect_content_type(
    file_name: str | None = None,
    content_type: str | None = None,
) -> str:
    """Detect content type from explicit value or file extension.

    Args:
        file_name: Original file name (used for extension-based guessing).
        content_type: Explicit content type (takes priority if provided).

    Returns:
        MIME type string, or ``"application/octet-stream"`` if unknown.
    """
    if content_type:
        return content_type

    if file_name:
        ext = os.path.splitext(file_name)[1].lower()
        if ext in _EXTRA_MIME_TYPES:
            return _EXTRA_MIME_TYPES[ext]
        guessed, _ = mimetypes.guess_type(file_name)
        if guessed:
            return guessed

    return "application/octet-stream"


def is_text_type(content_type: str) -> bool:
    return content_type in TEXT_TYPES


def is_office_type(content_type: str) -> bool:
    return content_type in OFFICE_TYPES


def is_visual_type(content_type: str) -> bool:
    return content_type in VISUAL_TYPES


# ── File downloading ─────────────────────────────────────────────


def _download_sync(url: str, dest_path: str) -> str:
    """Synchronous download using stdlib (no extra dependencies)."""
    urllib.request.urlretrieve(url, dest_path)  # noqa: S310
    return dest_path


async def download_file(url: str, dest_dir: str, file_name: str = "") -> str:
    """Download a file from an HTTP(S) URL to a local directory.

    Args:
        url: HTTP(S) URL to download.
        dest_dir: Directory to save the file in.
        file_name: Suggested file name. If empty, derived from URL.

    Returns:
        Absolute path to the downloaded file.
    """
    if not file_name:
        # Extract filename from URL (strip query params)
        url_path = url.split("?")[0]
        file_name = os.path.basename(url_path) or "downloaded_file"

    os.makedirs(dest_dir, exist_ok=True)
    dest_path = os.path.join(dest_dir, file_name)

    await asyncio.to_thread(_download_sync, url, dest_path)
    logger.info(
        "Downloaded {url} → {dest}",
        url=url[:100],
        dest=dest_path,
    )
    return dest_path


# ── Text reading ─────────────────────────────────────────────────


def read_text_file(path: str) -> str:
    """Read a file as UTF-8 text."""
    return Path(path).read_text(encoding="utf-8", errors="replace")


# ── Office document conversion ───────────────────────────────────


def convert_office_to_text(path: str, content_type: str) -> str:
    """Convert .docx or .pptx to plain text.

    Requires ``python-docx`` (for .docx) or ``python-pptx`` (for .pptx)
    to be installed. Raises ``ImportError`` with a clear message if missing.
    """
    if "wordprocessingml" in content_type:
        return _convert_docx(path)
    elif "presentationml" in content_type:
        return _convert_pptx(path)
    else:
        raise ValueError(f"Unsupported office content type: {content_type}")


def _convert_docx(path: str) -> str:
    try:
        import docx
    except ImportError as e:
        raise ImportError(
            "python-docx is required for .docx conversion. "
            "Install it with: pip install python-docx"
        ) from e

    doc = docx.Document(path)
    return "\n".join(p.text for p in doc.paragraphs)


def _convert_pptx(path: str) -> str:
    try:
        from pptx import Presentation
    except ImportError as e:
        raise ImportError(
            "python-pptx is required for .pptx conversion. "
            "Install it with: pip install python-pptx"
        ) from e

    prs = Presentation(path)
    return "\n".join(
        shape.text
        for slide in prs.slides
        for shape in slide.shapes
        if shape.has_text_frame
    )


# ── Visual file encoding ────────────────────────────────────────


def encode_visual_content(path: str, content_type: str) -> dict[str, Any]:
    """Base64-encode a visual file into a multimodal content part.

    Args:
        path: Local file path.
        content_type: MIME type (e.g. ``"image/png"``, ``"application/pdf"``).

    Returns:
        Unified multimodal content dict::

            {"type": "image"|"document", "source": {"type": "base64", ...}}
    """
    raw = Path(path).read_bytes()

    if len(raw) > MAX_VISUAL_FILE_BYTES:
        logger.warning(
            "File too large for visual encoding: {path} ({size} bytes)",
            path=path,
            size=len(raw),
        )
        raise ValueError(
            f"File {path} is {len(raw)} bytes, exceeds "
            f"{MAX_VISUAL_FILE_BYTES} byte limit"
        )

    mm_type = "image" if content_type.startswith("image/") else "document"

    return {
        "type": mm_type,
        "source": {
            "type": "base64",
            "media_type": content_type,
            "data": base64.b64encode(raw).decode("ascii"),
        },
    }
