from __future__ import annotations

import base64
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


_VALID_MSG_KEYS = {"role", "content", "name", "tool_calls", "tool_call_id"}
_API_CONTENT_PART_TYPES = frozenset({"text", "image_url", "video_url", "input_audio"})
_ATTACHMENT_TOOL_HINT = (
    "Use the attachment_id values above when calling tools that take file paths. "
    "The system will resolve them automatically."
)


def _url_to_base64_data_url(url: str, timeout: int = 10) -> str | None:
    """Fetch an image URL and return a data:image/...;base64,... URL."""
    if not url or not isinstance(url, str):
        return None
    u = url.strip()
    lower = u.lower()
    if lower.startswith("data:"):
        return u
    if not (lower.startswith("http://") or lower.startswith("https://")):
        return None
    try:
        req = Request(u, headers={"User-Agent": "Mugi/1.0"})
        with urlopen(req, timeout=timeout) as resp:
            data = resp.read()
            ct = resp.headers.get_content_type() or "application/octet-stream"
            if not ct.startswith("image/"):
                ct = "image/png"
            b64 = base64.standard_b64encode(data).decode("ascii")
            return f"data:{ct};base64,{b64}"
    except (URLError, HTTPError, OSError):
        return None


def _file_attachment_line(part: dict) -> str:
    """One ``FILE ATTACHED`` line for a stored UI ``file_attachment`` part."""
    filename = str(part.get("filename") or "").strip() or "attachment"
    url = str(part.get("url") or "").strip()
    attachment_id = None
    try:
        import attachment_registry as _ar

        attachment_id = _ar.find_attachment_id(filename=filename, url=url)
    except Exception:
        attachment_id = None
    if attachment_id:
        return f"FILE ATTACHED: {filename} (attachment_id={attachment_id})"
    return f"FILE ATTACHED: {filename}"


def _attached_files_block(file_parts: list[dict]) -> str:
    """Rebuild the first-turn ``=== ATTACHED FILES ===`` section for LLM replay."""
    lines = [_file_attachment_line(part) for part in file_parts]
    block = "\n".join(lines)
    if any("attachment_id=" in line for line in lines):
        block = f"{block}\n{_ATTACHMENT_TOOL_HINT}"
    return f"=== ATTACHED FILES ===\n{block}"


def _api_safe_content_part(part) -> dict | None:
    """Return one OpenAI-compatible content part, or ``None`` to drop."""
    if not isinstance(part, dict):
        return None
    ptype = part.get("type")
    if ptype == "text":
        return {"type": "text", "text": part.get("text") or ""}
    if ptype == "file_attachment":
        return None
    if ptype == "image_url" or "image_url" in part:
        if ptype != "image_url":
            return {"type": "image_url", "image_url": part.get("image_url") or {}}
        return part
    if ptype in _API_CONTENT_PART_TYPES:
        return part
    return None


def _normalize_message_content_for_api(content) -> str | list:
    """Strip UI-only parts (e.g. ``file_attachment``) before LLM API calls."""
    if content is None:
        return ""
    if isinstance(content, str):
        return content
    if not isinstance(content, list):
        return str(content)

    text_bits: list[str] = []
    media_parts: list[dict] = []
    file_parts: list[dict] = []
    for part in content:
        if isinstance(part, dict) and part.get("type") == "file_attachment":
            file_parts.append(part)
            continue
        safe = _api_safe_content_part(part)
        if safe is None:
            continue
        if safe.get("type") == "text":
            text = (safe.get("text") or "").strip()
            if text:
                text_bits.append(text)
        else:
            media_parts.append(safe)

    if file_parts:
        attachment_section = _attached_files_block(file_parts)
        if text_bits:
            text_bits.append(attachment_section)
        else:
            text_bits = [attachment_section]

    if media_parts:
        out: list[dict] = []
        if text_bits:
            out.append({"type": "text", "text": "\n\n".join(text_bits)})
        out.extend(media_parts)
        return out
    return "\n\n".join(text_bits) if text_bits else ""


def _sanitize_messages_for_api(messages: list, on_log=None) -> list:
    """Return a new list of message dicts safe for the OpenAI-compatible API."""
    out = []
    for m in messages:
        clean = {k: v for k, v in m.items() if k in _VALID_MSG_KEYS}
        clean["content"] = _normalize_message_content_for_api(clean.get("content"))
        out.append(clean)
    return out


def _prepare_messages_for_vision_api(messages: list, on_log=None) -> None:
    """Convert http(s) image URLs in vision parts to base64 data URLs in place."""
    for m in messages:
        content = m.get("content")
        if not isinstance(content, list):
            continue
        for part in content:
            if not isinstance(part, dict):
                continue
            if part.get("type") != "image_url":
                continue
            img = part.get("image_url")
            if not isinstance(img, dict):
                continue
            url = img.get("url")
            if not url or not isinstance(url, str):
                continue
            u = url.strip().lower()
            if not (u.startswith("http://") or u.startswith("https://")):
                continue
            data_url = _url_to_base64_data_url(url)
            if data_url:
                img["url"] = data_url
            elif on_log:
                on_log("WARN", f"Could not fetch image for base64: {url[:80]}...")

