"""OpenAI compatibility layer.

Translates between OpenAI /v1/chat/completions format and aurora-lens internals.

CRITICAL: All governance outcomes return HTTP 200.
Governance metadata goes in the response body under "aurora", not as error codes.
"""

from __future__ import annotations

import json
import time
import uuid
from dataclasses import dataclass
from typing import Any

from aurora_lens.lens import LensResult
from aurora_lens.govern.decision import InterventionAction
from aurora_lens.verify.flags import Flag, FlagType, flag_from_external


def _content_to_text(content: Any) -> str:
    """Extract text from OpenAI content (string or multimodal list)."""
    if isinstance(content, str):
        return content
    if isinstance(content, list):
        parts: list[str] = []
        for part in content:
            if isinstance(part, str):
                parts.append(part)
            elif isinstance(part, dict):
                if part.get("type") == "text" and isinstance(part.get("text"), str):
                    parts.append(part["text"])
        return "\n".join(p for p in parts if p)
    return ""


# ── Request parsing ─────────────────────────────────────────────────

def _validate_external_flag(raw: Any, index: int) -> None:
    """Validate one external flag. Raises ValueError if malformed (422)."""
    if not isinstance(raw, dict):
        raise ValueError(
            f"aurora.external_flags[{index}]: expected object, got {type(raw).__name__}"
        )
    type_str = raw.get("type")
    if not type_str or not isinstance(type_str, str):
        raise ValueError(
            f"aurora.external_flags[{index}]: missing or invalid 'type' (string required)"
        )
    try:
        FlagType[type_str]
    except KeyError:
        raise ValueError(
            f"aurora.external_flags[{index}]: unknown type {type_str!r}"
        )
    evidence = raw.get("evidence")
    if not isinstance(evidence, list):
        raise ValueError(
            f"aurora.external_flags[{index}]: 'evidence' must be a list"
        )
    if raw.get("source") is not None and not isinstance(raw.get("source"), str):
        raise ValueError(
            f"aurora.external_flags[{index}]: 'source' must be string if present"
        )


def _parse_external_flags(body: dict[str, Any]) -> list[Flag]:
    """Extract aurora.external_flags and convert to Flag objects (Phase 9).

    Validates structure: type (string), evidence (list), source (string, optional).
    Raises ValueError for malformed entries → 422.
    """
    aurora = body.get("aurora") or {}
    raw_list = aurora.get("external_flags")
    if not isinstance(raw_list, list):
        return []
    for i, raw in enumerate(raw_list):
        _validate_external_flag(raw, i)
    flags: list[Flag] = []
    for raw in raw_list:
        f = flag_from_external(raw)
        assert f is not None  # Validation passed
        flags.append(f)
    return flags


@dataclass
class ParsedRequest:
    """Parsed OpenAI chat completion request."""
    user_message: str
    conversation_history: list[dict[str, str]]
    session_id: str
    model: str | None
    stream: bool
    raw: dict[str, Any]
    external_flags: list[Flag]  # From aurora.external_flags (Phase 9)


def parse_chat_request(body: dict[str, Any]) -> ParsedRequest:
    """Parse an OpenAI-format chat completion request body.

    Extracts the last user message and conversation history.
    Session ID comes from a custom field or is generated.
    Handles multimodal content (list of parts with type/text).
    """
    messages = body.get("messages", [])

    stream = bool(body.get("stream", False))

    if not isinstance(messages, list) or not messages:
        for key in ("input", "prompt", "text"):
            v = body.get(key)
            if isinstance(v, str) and v.strip():
                return ParsedRequest(
                    user_message=v.strip(),
                    conversation_history=[],
                    session_id=body.get("aurora_session_id") or f"session-{uuid.uuid4().hex[:12]}",
                    model=body.get("model"),
                    stream=stream,
                    raw=body,
                    external_flags=_parse_external_flags(body),
                )
        raise ValueError("Invalid request: expected 'messages' list (or 'input'/'prompt' string).")

    user_message = ""
    history: list[dict[str, str]] = []

    for i, msg in enumerate(messages):
        if not isinstance(msg, dict):
            continue
        role = msg.get("role", "")
        content = msg.get("content", "")
        text = _content_to_text(content)
        if role == "system":
            continue
        if i == len(messages) - 1 and role == "user":
            user_message = text
        else:
            history.append({"role": role, "content": text})

    if not user_message.strip():
        for msg in reversed(messages):
            if isinstance(msg, dict):
                text = _content_to_text(msg.get("content"))
                if text.strip():
                    user_message = text.strip()
                    break
        if not user_message.strip():
            raise ValueError("Invalid request: could not extract text from messages[].")

    session_id = body.get("aurora_session_id", "")
    if not session_id or not str(session_id).strip():
        session_id = f"session-{uuid.uuid4().hex[:12]}"

    return ParsedRequest(
        user_message=user_message.strip(),
        conversation_history=history,
        session_id=str(session_id).strip(),
        model=body.get("model"),
        stream=stream,
        raw=body,
        external_flags=_parse_external_flags(body),
    )


# ── Response formatting ─────────────────────────────────────────────

def _usage_from_result(result: LensResult) -> dict[str, int]:
    """Build OpenAI-format usage from LensResult.usage (adapter token counts)."""
    u = getattr(result, "usage", None)
    if u and isinstance(u, dict):
        pt = u.get("input_tokens") or u.get("prompt_tokens", 0)
        ct = u.get("output_tokens") or u.get("completion_tokens", 0)
        return {
            "prompt_tokens": int(pt),
            "completion_tokens": int(ct),
            "total_tokens": int(pt) + int(ct),
        }
    return {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}


def format_chat_response(
    result: LensResult,
    model: str = "aurora-lens",
    request_model: str | None = None,
    include_operator_detail: bool = False,
    session_id: str | None = None,
) -> dict[str, Any]:
    """Format a LensResult as an OpenAI-compatible chat completion response.

    Always produces a valid response body. Governance metadata goes under the
    "aurora" key — never as an error.

    Two-plane output contract:
      User plane (always returned):
        aurora.governance  — action enum: PASS | SOFT_CORRECT | FORCE_REVISE | HARD_STOP
        aurora.turn        — turn counter
        aurora.session_id  — routing handle (when provided)
        aurora.unverified  — true when action is SOFT_CORRECT (UI hint; no internal detail)
        aurora.audit_id    — opaque audit handle when available

      Operator plane (audit log by default; returned in response only when
      include_operator_detail=True):
        aurora.flags, aurora.rationale, aurora.policy,
        aurora.governance_note, aurora.original_response
    """
    response_id = f"chatcmpl-aurora-{uuid.uuid4().hex[:12]}"
    timestamp = int(time.time())

    response: dict[str, Any] = {
        "id": response_id,
        "object": "chat.completion",
        "created": timestamp,
        "model": request_model or result.model or model,
        "choices": [
            {
                "index": 0,
                "message": {
                    "role": "assistant",
                    "content": result.response,
                },
                "finish_reason": "stop",
            }
        ],
        "usage": _usage_from_result(result),
    }

    # User-plane aurora — always present, never contains internal diagnostics
    aurora: dict[str, Any] = {
        "governance": result.action.name,
        "turn": result.turn,
    }

    # Routing handle — not a diagnostic, belongs in user-plane
    if session_id:
        aurora["session_id"] = session_id

    # Neutral epistemic hint: claim is based on general knowledge, not conversation state
    if result.action == InterventionAction.SOFT_CORRECT:
        aurora["unverified"] = True

    # Opaque audit handle — pointer for operator lookup, no semantics for clients
    if result.decision and result.decision.cid:
        aurora["audit_id"] = result.decision.cid

    # Canonical forensic event envelope — non-ADMIT only (FORCE_REVISE, HARD_STOP, pre-LLM blocks)
    if (
        result.action not in (InterventionAction.PASS, InterventionAction.SOFT_CORRECT)
        and result.decision
        and result.decision.forensic_event is not None
    ):
        aurora["forensic_event"] = result.decision.forensic_event

    # Operator-plane fields — audit log only by default
    if include_operator_detail and result.decision and result.action != InterventionAction.PASS:
        aurora["flags"] = list(dict.fromkeys(f.flag_type.name for f in result.flags))
        aurora["rationale"] = result.decision.rationale
        aurora["policy"] = result.decision.policy
        if result.original_response is not None:
            aurora["original_response"] = result.original_response
        if result.decision.governance_note:
            aurora["governance_note"] = result.decision.governance_note

    response["aurora"] = aurora
    return response


def format_stream_metadata_event(aurora: dict[str, Any]) -> str:
    """Format aurora metadata as SSE event. Emitted as final event after stream completes."""
    return f"data: {json.dumps({'aurora': aurora}, separators=(',', ':'))}\n\n"
