"""Centralized OpenAI client for the platform layer."""

from __future__ import annotations

import json
import os
import re
from pathlib import Path
from typing import Any

from openai import OpenAI
from pdf2image import convert_from_bytes

from bridge_platform.logging.platform_logger import get_platform_logger, log_structured
from bridge_platform.quotas.service import consume, record_observed_usage


logger = get_platform_logger("ai")
_PROJECT_ROOT = Path(__file__).resolve().parents[2]
_KEYS_DIR = _PROJECT_ROOT / "platform" / "secrets" / "openai_keys"


def run_llm(context, app_id, prompt, model="gpt-4o-mini", options=None):
    """Execute an OpenAI request and return normalized output plus usage."""
    options = options or {}
    tenant_id = ((context or {}).get("tenant") or {}).get("tenant_id") if isinstance((context or {}).get("tenant"), dict) else None
    reservation = _estimated_capacity(options) if tenant_id else 0
    if tenant_id:
        consume(
            tenant_id=tenant_id, app_id=app_id, metric="ai_tokens",
            amount=reservation,
            metadata={"kind": "provider_reservation", "request_id": (context or {}).get("request_id")},
        )
    api_key = _resolve_api_key(app_id, tenant_id=tenant_id)
    client = OpenAI(api_key=api_key)

    request_kwargs = _build_request_kwargs(prompt=prompt, model=model, options=options)
    try:
        response = client.chat.completions.create(**request_kwargs)
    except Exception as exc:
        log_structured(
            logger,
            "ai_provider_request_failed",
            tenant_id=tenant_id,
            app_id=app_id,
            model=model,
            request_id=(context or {}).get("request_id") if isinstance(context, dict) else None,
            exception_type=type(exc).__name__,
            http_status=getattr(exc, "status_code", None),
            provider_code=getattr(exc, "code", None),
            provider_param=getattr(exc, "param", None),
        )
        if tenant_id and reservation:
            record_observed_usage(
                tenant_id=tenant_id, app_id=app_id, metric="ai_tokens", amount=-reservation,
                metadata={"kind": "provider_reservation_release", "request_id": (context or {}).get("request_id")},
            )
        raise

    content = ""
    if response.choices and response.choices[0].message:
        content = response.choices[0].message.content or ""

    output = _normalize_output(content=content, options=options)
    usage = _extract_usage(response)
    if tenant_id:
        record_observed_usage(
            tenant_id=tenant_id,
            app_id=app_id,
            metric="ai_tokens",
            amount=usage.get("total_tokens", 0) - reservation,
            metadata={
                "kind": "provider_reservation_reconciliation",
                "model": model,
                "request_id": (context or {}).get("request_id") if isinstance(context, dict) else None,
                "input_tokens": usage.get("input_tokens", 0),
                "output_tokens": usage.get("output_tokens", 0),
            },
        )

    log_structured(
        logger,
        "ai_usage",
        tenant_id=tenant_id,
        app_id=app_id,
        model=model,
        tokens=usage.get("total_tokens"),
        request_id=(context or {}).get("request_id") if isinstance(context, dict) else None,
    )

    return {
        "output": output,
        "usage": usage,
        # Restricted pipeline telemetry. Callers must not expose raw_output in
        # public envelopes or logs.
        "raw_output": content,
        "provider_request_id": getattr(response, "_request_id", None),
        "provider_response_id": getattr(response, "id", None),
    }


def _estimated_capacity(options: dict[str, Any]) -> int:
    """Conservative reservation; actual usage is recorded after the response."""
    if options.get("file_bytes"):
        return max(1, int(options.get("capacity_reservation_tokens") or 50000))
    return max(1, int(options.get("capacity_reservation_tokens") or 5000))


def _resolve_api_key(app_id: str, *, tenant_id: str | None = None) -> str:
    normalized = re.sub(r"[^A-Z0-9]+", "_", str(app_id or "").upper()).strip("_")
    safe_app = re.sub(r"[^a-z0-9_]+", "_", str(app_id or "").lower()).strip("_")
    safe_tenant = re.sub(r"[^a-z0-9_-]+", "_", str(tenant_id or "").lower()).strip("_-")
    tenant_key = _read_key_file(_KEYS_DIR / "tenants" / safe_tenant / f"{safe_app}.key") if safe_tenant and safe_app else None
    app_key = _read_key_file(_KEYS_DIR / f"{normalized.lower()}.key")
    dev_key = _read_key_file(_KEYS_DIR / "dev.key")

    env_app_key = os.getenv(f"OPENAI_API_KEY_{normalized}")
    env_dev_key = os.getenv("OPENAI_API_KEY_DEV") or os.getenv("OPENAI_API_KEY")

    api_key = tenant_key or app_key or dev_key or env_app_key or env_dev_key
    if not api_key:
        raise RuntimeError(f"Missing OpenAI API key for app_id={app_id!r}")
    return api_key


def _read_key_file(path: Path) -> str | None:
    try:
        value = path.read_text(encoding="utf-8").strip()
    except FileNotFoundError:
        return None
    except OSError:
        return None
    return value or None


def _build_request_kwargs(*, prompt: str, model: str, options: dict[str, Any]) -> dict[str, Any]:
    request_kwargs: dict[str, Any] = {
        "model": model,
        "messages": [
            {
                "role": "user",
                "content": _build_content(prompt=prompt, options=options),
            }
        ],
    }

    response_format = options.get("response_format")
    if response_format == "json_object":
        request_kwargs["response_format"] = {"type": "json_object"}
    elif response_format == "json_schema":
        schema = options.get("json_schema")
        if not isinstance(schema, dict):
            raise ValueError("json_schema response format requires a schema object")
        request_kwargs["response_format"] = {
            "type": "json_schema",
            "json_schema": {
                "name": str(options.get("schema_name") or "structured_output"),
                "strict": True,
                "schema": schema,
            },
        }
    return request_kwargs


def _build_content(*, prompt: str, options: dict[str, Any]) -> list[dict[str, Any]]:
    content: list[dict[str, Any]] = [{"type": "text", "text": prompt}]
    file_bytes = options.get("file_bytes")
    if file_bytes:
        file_bytes, mimetype = _prepare_visual_input(
            file_bytes=file_bytes,
            mimetype=options.get("mimetype") or "application/octet-stream",
        )
        import base64

        b64 = base64.b64encode(file_bytes).decode("utf-8")
        content.append(
            {
                "type": "image_url",
                "image_url": {"url": f"data:{mimetype};base64,{b64}"},
            }
        )
    return content


def _prepare_visual_input(*, file_bytes: bytes, mimetype: str) -> tuple[bytes, str]:
    if mimetype == "application/pdf":
        return _pdf_first_page_to_png(file_bytes), "image/png"
    return file_bytes, mimetype


def _pdf_first_page_to_png(pdf_bytes: bytes) -> bytes:
    pages = convert_from_bytes(pdf_bytes, dpi=200, fmt="png", first_page=1, last_page=1)
    if not pages:
        raise RuntimeError("PDF conversion failed: no pages rendered")

    from io import BytesIO

    buf = BytesIO()
    pages[0].save(buf, format="PNG")
    return buf.getvalue()


def _normalize_output(*, content: str, options: dict[str, Any]):
    if options.get("response_format") in {"json_object", "json_schema"}:
        try:
            return json.loads(content)
        except Exception:
            return {"_raw": content}
    return content


def _extract_usage(response) -> dict[str, int]:
    usage = getattr(response, "usage", None)
    if usage is None:
        return {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
    return {
        "input_tokens": int(getattr(usage, "prompt_tokens", 0) or 0),
        "output_tokens": int(getattr(usage, "completion_tokens", 0) or 0),
        "total_tokens": int(getattr(usage, "total_tokens", 0) or 0),
    }
