"""Trace Event Interface (TEI) v0 validation helpers.

This module provides a stable, lightweight contract validator for framework
emitters that want to send ingestible events to HiveOS Trace.
"""

from __future__ import annotations

from typing import Any

TEI_VERSION = "0.1"

_REQUIRED_FIELDS = (
    "tei_version",
    "event_id",
    "event_type",
    "occurred_at_ms",
    "run_id",
    "source",
    "payload",
)

_ALLOWED_OUTCOMES = {"success", "failed", "denied", "running", "unknown"}
_ALLOWED_STATUSES = {"success", "failed", "denied", "partial", "unknown"}

_CANONICAL_EVENT_TYPES = {
    "turn.start",
    "model.request",
    "model.response",
    "tool.request",
    "tool.result",
    "decision",
    "checkpoint",
    "response.commit",
    "turn.finish",
    "error",
}


def _clean_text(value: Any) -> str:
    return str(value or "").strip()


def _to_positive_int(value: Any, *, field: str, allow_zero: bool = False) -> tuple[int | None, str | None]:
    try:
        if isinstance(value, bool):
            raise ValueError("bool is not a valid integer value")
        number = int(value)
    except Exception:
        return None, f"{field} must be an integer"
    minimum = 0 if allow_zero else 1
    if number < minimum:
        return None, f"{field} must be >= {minimum}"
    return number, None


def _canonical_event_type(raw_event_type: str) -> str:
    raw = _clean_text(raw_event_type).lower()
    if not raw:
        return ""
    if raw in _CANONICAL_EVENT_TYPES:
        return raw
    if raw in {"observe_run_started", "run.started", "run.start", "turn.started"}:
        return "turn.start"
    if raw in {"observe_run_finished", "run.finished", "run.finish", "turn.finished"}:
        return "turn.finish"
    if raw in {"observe_checkpoint", "checkpoint.created"}:
        return "checkpoint"
    if raw in {"observe_output", "output.commit", "response.commit"}:
        return "response.commit"
    if raw in {"observe_branch_decision", "branch.decision", "decision.branch"}:
        return "decision"
    if raw in {"observe_tool_call_start", "tool.call.start"}:
        return "tool.request"
    if raw in {"observe_tool_call_end", "tool.call.end"}:
        return "tool.result"
    if "tool" in raw:
        if any(token in raw for token in ("start", "request", "requested")):
            return "tool.request"
        if any(token in raw for token in ("end", "result", "response", "finished")):
            return "tool.result"
        return "tool.result"
    if "model" in raw or "llm" in raw:
        if any(token in raw for token in ("start", "request", "prompt", "input")):
            return "model.request"
        if any(token in raw for token in ("end", "response", "output", "completion")):
            return "model.response"
    if any(token in raw for token in ("error", "exception", "failed")):
        return "error"
    return ""


def _normalize_status(outcome: str) -> str:
    text = _clean_text(outcome).lower()
    if text in _ALLOWED_STATUSES:
        return text
    if text in {"running"}:
        return "unknown"
    return "unknown"


def validate_tei_event(event: Any, *, strict_version: bool = True) -> dict[str, Any]:
    """Validate one TEI event envelope.

    Returns:
      {
        "ok": bool,
        "errors": list[str],
        "normalized": dict,
      }
    """

    errors: list[str] = []
    normalized: dict[str, Any] = {}

    if not isinstance(event, dict):
        return {"ok": False, "errors": ["event must be an object"], "normalized": {}}

    for key in _REQUIRED_FIELDS:
        if key not in event:
            errors.append(f"missing required field: {key}")

    tei_version = _clean_text(event.get("tei_version"))
    if tei_version:
        normalized["tei_version"] = tei_version
        if strict_version and tei_version != TEI_VERSION:
            errors.append(f"unsupported tei_version: {tei_version} (expected {TEI_VERSION})")
    else:
        errors.append("tei_version must be a non-empty string")

    event_id = _clean_text(event.get("event_id"))
    if event_id:
        normalized["event_id"] = event_id
    else:
        errors.append("event_id must be a non-empty string")

    event_type = _clean_text(event.get("event_type"))
    if event_type:
        normalized["event_type"] = event_type
        canonical = _canonical_event_type(event_type)
        normalized["canonical_event_type"] = canonical or event_type
        normalized["event_type_is_canonical"] = bool(canonical and canonical == event_type.lower())
    else:
        errors.append("event_type must be a non-empty string")

    run_id = _clean_text(event.get("run_id"))
    if run_id:
        normalized["run_id"] = run_id
    else:
        errors.append("run_id must be a non-empty string")

    occurred_at_ms, occurred_err = _to_positive_int(event.get("occurred_at_ms"), field="occurred_at_ms")
    if occurred_err:
        errors.append(occurred_err)
    else:
        normalized["occurred_at_ms"] = occurred_at_ms

    source = event.get("source")
    if not isinstance(source, dict):
        errors.append("source must be an object")
    else:
        framework = _clean_text(source.get("framework"))
        if not framework:
            errors.append("source.framework must be a non-empty string")
        normalized_source = {"framework": framework}
        emitter = _clean_text(source.get("emitter"))
        if emitter:
            normalized_source["emitter"] = emitter
        normalized["source"] = normalized_source

    payload = event.get("payload")
    if not isinstance(payload, dict):
        errors.append("payload must be an object")
    else:
        normalized["payload"] = payload

    # Optional lineage fields
    for opt_field in ("flow_id", "step_id", "parent_step_id", "tool_call_id", "checkpoint_id", "step_type"):
        value = _clean_text(event.get(opt_field))
        if value:
            normalized[opt_field] = value

    outcome = _clean_text(event.get("outcome"))
    if outcome:
        if outcome not in _ALLOWED_OUTCOMES:
            errors.append(f"outcome must be one of: {', '.join(sorted(_ALLOWED_OUTCOMES))}")
        else:
            normalized["outcome"] = outcome
    normalized["status"] = _normalize_status(outcome)

    if "attempt" in event and event.get("attempt") is not None:
        attempt, attempt_err = _to_positive_int(event.get("attempt"), field="attempt")
        if attempt_err:
            errors.append(attempt_err)
        else:
            normalized["attempt"] = attempt

    metadata = event.get("metadata")
    if metadata is not None:
        if not isinstance(metadata, dict):
            errors.append("metadata must be an object when provided")
        else:
            normalized["metadata"] = metadata

    tags = event.get("tags")
    if tags is not None:
        if not isinstance(tags, list) or any(not _clean_text(item) for item in tags):
            errors.append("tags must be a list of non-empty strings when provided")
        else:
            normalized["tags"] = [_clean_text(item) for item in tags]

    return {"ok": not errors, "errors": errors, "normalized": normalized}


def validate_tei_batch(events: Any, *, strict_version: bool = True) -> dict[str, Any]:
    """Validate a list of TEI events and return aggregate diagnostics."""

    if not isinstance(events, list):
        return {
            "ok": False,
            "errors": ["events must be an array"],
            "total": 0,
            "valid": 0,
            "invalid": 0,
            "items": [],
        }
    items = []
    valid = 0
    for index, raw_event in enumerate(events):
        result = validate_tei_event(raw_event, strict_version=strict_version)
        items.append({"index": index, **result})
        if result["ok"]:
            valid += 1
    total = len(items)
    invalid = max(0, total - valid)
    return {
        "ok": invalid == 0,
        "errors": [],
        "total": total,
        "valid": valid,
        "invalid": invalid,
        "items": items,
    }
