#!/usr/bin/env python3
"""Private GPT Image broker for isolated Hermes chat tenants."""

from __future__ import annotations

import base64
import hmac
import importlib.util
import json
import os
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


HOST = os.environ.get("HERMES_IMAGE_BROKER_LISTEN_HOST", "0.0.0.0")
# Kubernetes reserves <SERVICE>_PORT for service-link discovery and injects a
# tcp://... value.  Keep listener configuration out of that namespace.
PORT = int(os.environ.get("HERMES_IMAGE_BROKER_LISTEN_PORT", "9002"))
TOKEN = os.environ.get("HERMES_IMAGE_BROKER_KEY", "").strip()
PROVIDER_PATH = Path(
    os.environ.get(
        "HERMES_IMAGE_PROVIDER_PATH",
        "/opt/hermes/plugins/image_gen/openai-codex/__init__.py",
    )
)
DEFAULT_MODEL = os.environ.get(
    "HERMES_IMAGE_BROKER_DEFAULT_MODEL", "atlas-image-auto-high"
)
LOCAL_URL = os.environ.get(
    "HERMES_LOCAL_IMAGE_URL",
    "http://hermes-local-image.hermes.svc.cluster.local:9004",
).rstrip("/")
POLICY_PATH = Path(
    os.environ.get("HERMES_IMAGE_POLICY_PATH", "/etc/hermes-image-policy/policy.json")
)
MAX_BODY_BYTES = int(os.environ.get("HERMES_IMAGE_BROKER_MAX_BODY", str(96 << 20)))
QUEUE_TIMEOUT_SECONDS = float(
    os.environ.get("HERMES_IMAGE_BROKER_QUEUE_TIMEOUT", "900")
)
ALLOWED_MODELS = {
    "atlas-image-auto-high",
    "gpt-image-2-low",
    "gpt-image-2-medium",
    "gpt-image-2-high",
    "flux-2-klein-4b-local",
}
ALLOWED_ASPECTS = {"landscape", "square", "portrait"}
_generation_lock = threading.Lock()

# This is intentionally a tiny deterministic hard boundary, not a hosted-style
# appearance/race classifier. Operators can add site-specific phrases through
# the ConfigMap, but benign restoration and appearance correction remain valid.
_MINOR_TERMS = {
    "baby",
    "child",
    "children",
    "infant",
    "kid",
    "minor",
    "preteen",
    "toddler",
    "underage",
}
_SEXUAL_TERMS = {
    "erotic",
    "explicit",
    "genital",
    "naked",
    "nude",
    "porn",
    "sexual",
}


def _load_provider() -> Any:
    """Load the pinned Hermes provider without duplicating its API client."""
    spec = importlib.util.spec_from_file_location(
        "atlas_openai_codex_image_provider", PROVIDER_PATH
    )
    if spec is None or spec.loader is None:
        raise RuntimeError(f"cannot load image provider from {PROVIDER_PATH}")
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module.OpenAICodexImageGenProvider()


_PROVIDER: Any | None = None


def _provider() -> Any:
    """Load the provider once after the sidecar environment is ready."""
    global _PROVIDER
    if _PROVIDER is None:
        _PROVIDER = _load_provider()
    return _PROVIDER


def _authorized(header: str | None) -> bool:
    """Compare the bearer token without leaking timing information."""
    if not TOKEN or not header or not header.startswith("Bearer "):
        return False
    return hmac.compare_digest(header[7:].strip(), TOKEN)


def _policy_error(prompt: str) -> str | None:
    """Return the narrow hard-boundary reason, otherwise allow the request."""
    lowered = prompt.casefold()
    words = {"".join(char for char in word if char.isalnum()) for word in lowered.split()}
    if words & _MINOR_TERMS and words & _SEXUAL_TERMS:
        return "sexual content involving minors is not permitted"
    try:
        configured = json.loads(POLICY_PATH.read_text(encoding="utf-8"))
    except (OSError, ValueError, json.JSONDecodeError):
        configured = {}
    for phrase in configured.get("additional_blocked_phrases") or []:
        if isinstance(phrase, str) and phrase.strip().casefold() in lowered:
            return "request is blocked by the operator image policy"
    return None


def _local_request(payload: dict[str, Any], timeout: float = 1800.0) -> dict[str, Any]:
    """Call the fixed in-cluster FLUX endpoint without forwarding credentials."""
    request = Request(
        f"{LOCAL_URL}/v1/images/generations",
        data=json.dumps(payload).encode("utf-8"),
        method="POST",
        headers={"Content-Type": "application/json", "Accept": "application/json"},
    )
    try:
        with urlopen(request, timeout=timeout) as response:  # noqa: S310 - fixed internal URL
            return json.loads(response.read())
    except HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")[:1000]
        raise RuntimeError(f"local image service returned HTTP {exc.code}: {body}") from exc
    except URLError as exc:
        raise RuntimeError(f"local image service is unavailable: {exc.reason}") from exc


def _local_health() -> dict[str, Any]:
    """Read local availability for the broker health and AUTO router."""
    request = Request(f"{LOCAL_URL}/health", method="GET")
    try:
        with urlopen(request, timeout=5) as response:  # noqa: S310 - fixed internal URL
            return json.loads(response.read())
    except Exception as exc:
        return {"success": False, "available": False, "error": str(exc)}


def _generate_hosted(
    payload: dict[str, Any], model: str, prompt: str, aspect: str
) -> dict[str, Any]:
    """Generate through the owner's hosted GPT Image provider."""
    old_model = os.environ.get("OPENAI_IMAGE_MODEL")
    try:
        os.environ["OPENAI_IMAGE_MODEL"] = model
        result = _provider().generate(
            prompt,
            aspect,
            image_url=payload.get("image_url"),
            reference_image_urls=payload.get("reference_image_urls"),
        )
    finally:
        if old_model is None:
            os.environ.pop("OPENAI_IMAGE_MODEL", None)
        else:
            os.environ["OPENAI_IMAGE_MODEL"] = old_model
    if not result.get("success"):
        return result
    image_path = Path(str(result["image"]))
    try:
        raw = image_path.read_bytes()
    finally:
        image_path.unlink(missing_ok=True)
    response = {key: value for key, value in result.items() if key != "image"}
    response.update(
        {
            "image_b64": base64.b64encode(raw).decode("ascii"),
            "mime_type": "image/png",
            "route": "openai",
        }
    )
    return response


def _generate(payload: dict[str, Any]) -> dict[str, Any]:
    """Route one request and keep generated bytes inside the tenant boundary."""
    prompt = str(payload.get("prompt") or "").strip()
    if not prompt:
        raise ValueError("prompt is required")
    policy_error = _policy_error(prompt)
    if policy_error:
        raise ValueError(policy_error)
    aspect = str(payload.get("aspect_ratio") or "landscape").lower()
    if aspect not in ALLOWED_ASPECTS:
        raise ValueError("aspect_ratio must be landscape, square, or portrait")
    model = str(payload.get("model") or DEFAULT_MODEL)
    if model not in ALLOWED_MODELS:
        raise ValueError("unsupported image quality tier")
    image_url = payload.get("image_url")
    references = payload.get("reference_image_urls")
    if image_url is not None and not isinstance(image_url, str):
        raise ValueError("image_url must be a string")
    if references is not None and not (
        isinstance(references, list)
        and all(isinstance(item, str) for item in references)
    ):
        raise ValueError("reference_image_urls must be a list of strings")

    acquired = _generation_lock.acquire(timeout=QUEUE_TIMEOUT_SECONDS)
    if not acquired:
        raise TimeoutError("image generation queue is busy; retry shortly")
    try:
        if model == "flux-2-klein-4b-local":
            return _local_request(payload)
        if model != "atlas-image-auto-high":
            return _generate_hosted(payload, model, prompt, aspect)

        hosted_error = "hosted image generation was unavailable"
        try:
            result = _generate_hosted(payload, "gpt-image-2-high", prompt, aspect)
            if result.get("success"):
                result["requested_model"] = model
                return result
            hosted_error = str(result.get("error") or hosted_error)
        except Exception as exc:
            hosted_error = f"{type(exc).__name__}: {exc}"
        try:
            result = _local_request({**payload, "model": "flux-2-klein-4b-local"})
        except Exception as exc:
            return {
                "success": False,
                "error": (
                    f"OpenAI image route failed ({hosted_error}); local FLUX fallback "
                    f"also failed ({type(exc).__name__}: {exc})"
                ),
                "error_type": "all_image_routes_failed",
            }
        result["requested_model"] = model
        result["hosted_fallback_reason"] = hosted_error
        return result
    finally:
        _generation_lock.release()


class Handler(BaseHTTPRequestHandler):
    """Small authenticated JSON API; prompts and images are never logged."""

    server_version = "HermesImageBroker/1"

    def _json(self, status: int, value: dict[str, Any]) -> None:
        body = json.dumps(value, separators=(",", ":")).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)

    def _check_auth(self) -> bool:
        if _authorized(self.headers.get("Authorization")):
            return True
        self._json(401, {"success": False, "error": "unauthorized"})
        return False

    def do_GET(self) -> None:  # noqa: N802 - BaseHTTPRequestHandler API
        if self.path != "/health":
            self._json(404, {"success": False, "error": "not found"})
            return
        if not self._check_auth():
            return
        local = _local_health()
        try:
            openai_available = bool(_provider().is_available())
        except Exception:
            openai_available = False
        self._json(
            200,
            {
                "success": True,
                "provider": "atlas-image-router",
                "available": openai_available or bool(local.get("available")),
                "openai_available": openai_available,
                "local": local,
                "busy": _generation_lock.locked(),
            },
        )

    def do_POST(self) -> None:  # noqa: N802 - BaseHTTPRequestHandler API
        if self.path != "/v1/images/generations":
            self._json(404, {"success": False, "error": "not found"})
            return
        if not self._check_auth():
            return
        try:
            length = int(self.headers.get("Content-Length", "0"))
        except ValueError:
            length = 0
        if length <= 0 or length > MAX_BODY_BYTES:
            self._json(413, {"success": False, "error": "invalid request size"})
            return
        try:
            payload = json.loads(self.rfile.read(length))
            if not isinstance(payload, dict):
                raise ValueError("JSON object required")
            result = _generate(payload)
            self._json(200 if result.get("success") else 502, result)
        except ValueError as exc:
            self._json(400, {"success": False, "error": str(exc)})
        except TimeoutError as exc:
            self._json(503, {"success": False, "error": str(exc)})
        except Exception as exc:  # keep provider details useful but omit prompts
            self._json(
                502,
                {
                    "success": False,
                    "error": f"image provider failed: {type(exc).__name__}: {exc}",
                },
            )

    def log_message(self, format: str, *args: Any) -> None:
        """Log only method/path/status metadata, never request bodies."""
        print(f"image-broker {self.address_string()} {format % args}", flush=True)


def main() -> None:
    """Serve until Kubernetes terminates the sidecar."""
    if not TOKEN:
        raise SystemExit("HERMES_IMAGE_BROKER_KEY is required")
    server = ThreadingHTTPServer((HOST, PORT), Handler)
    server.serve_forever()


if __name__ == "__main__":
    main()
