"""Central schema registry for mesh protocol events."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable

from services.mesh.mesh_protocol import normalize_payload, PROTOCOL_VERSION, NETWORK_ID


def _safe_int(val, default=0):
    try:
        return int(val)
    except (TypeError, ValueError):
        return default


@dataclass(frozen=True)
class EventSchema:
    event_type: str
    required_fields: tuple[str, ...]
    optional_fields: tuple[str, ...]
    validate: Callable[[dict[str, Any]], tuple[bool, str]]

    def validate_payload(self, payload: dict[str, Any]) -> tuple[bool, str]:
        return self.validate(payload)


def _require_fields(payload: dict[str, Any], fields: tuple[str, ...]) -> tuple[bool, str]:
    for key in fields:
        if key not in payload:
            return False, f"Missing field: {key}"
    return True, "ok"


def _validate_message(payload: dict[str, Any]) -> tuple[bool, str]:
    ok, reason = _require_fields(
        payload, ("message", "destination", "channel", "priority", "ephemeral")
    )
    if not ok:
        return ok, reason
    if payload.get("priority") not in ("normal", "high", "emergency", "low"):
        return False, "Invalid priority"
    if not isinstance(payload.get("ephemeral"), bool):
        return False, "ephemeral must be boolean"
    return True, "ok"


def _validate_gate_message(payload: dict[str, Any]) -> tuple[bool, str]:
    ok, reason = _require_fields(payload, ("gate", "ciphertext", "nonce", "sender_ref"))
    if not ok:
        return ok, reason
    if "message" in payload:
        return False, "plaintext gate message field is not allowed"
    gate = str(payload.get("gate", "")).strip().lower()
    if not gate:
        return False, "gate cannot be empty"
    if "epoch" in payload:
        epoch = _safe_int(payload.get("epoch", 0) or 0, 0)
        if epoch <= 0:
            return False, "epoch must be a positive integer"
    elif (
        not str(payload.get("ciphertext", "")).strip()
        and not str(payload.get("nonce", "")).strip()
        and not str(payload.get("sender_ref", "")).strip()
    ):
        return False, "epoch must be a positive integer"
    if not str(payload.get("ciphertext", "")).strip():
        return False, "ciphertext cannot be empty"
    if not str(payload.get("nonce", "")).strip():
        return False, "nonce cannot be empty"
    if not str(payload.get("sender_ref", "")).strip():
        return False, "sender_ref cannot be empty"
    payload_format = str(payload.get("format", "mls1") or "mls1").strip().lower()
    if payload_format != "mls1":
        return False, "Unsupported gate message format"
    return True, "ok"


def _validate_vote(payload: dict[str, Any]) -> tuple[bool, str]:
    ok, reason = _require_fields(payload, ("target_id", "vote", "gate"))
    if not ok:
        return ok, reason
    if payload.get("vote") not in (-1, 1):
        return False, "Invalid vote"
    return True, "ok"


def _validate_gate_create(payload: dict[str, Any]) -> tuple[bool, str]:
    ok, reason = _require_fields(payload, ("gate_id", "display_name", "rules"))
    if not ok:
        return ok, reason
    if not isinstance(payload.get("rules"), dict):
        return False, "rules must be an object"
    return True, "ok"


def _validate_prediction(payload: dict[str, Any]) -> tuple[bool, str]:
    return _require_fields(payload, ("market_title", "side", "stake_amount"))


def _validate_stake(payload: dict[str, Any]) -> tuple[bool, str]:
    return _require_fields(payload, ("message_id", "poster_id", "side", "amount", "duration_days"))


def _validate_dm_block(payload: dict[str, Any]) -> tuple[bool, str]:
    ok, reason = _require_fields(payload, ("blocked_id", "action"))
    if not ok:
        return ok, reason
    if payload.get("action") not in ("block", "unblock"):
        return False, "Invalid action"
    return True, "ok"


def _validate_dm_key(payload: dict[str, Any]) -> tuple[bool, str]:
    ok, reason = _require_fields(payload, ("dh_pub_key", "dh_algo", "timestamp"))
    if not ok:
        return ok, reason
    if payload.get("dh_algo") not in ("X25519", "ECDH", "ECDH_P256"):
        return False, "Invalid dh_algo"
    return True, "ok"


def _validate_dm_message(payload: dict[str, Any]) -> tuple[bool, str]:
    ok, reason = _require_fields(
        payload, ("recipient_id", "delivery_class", "recipient_token", "ciphertext", "msg_id", "timestamp")
    )
    if not ok:
        return ok, reason
    delivery_class = str(payload.get("delivery_class", "")).lower()
    if delivery_class not in ("request", "shared"):
        return False, "Invalid delivery_class"
    if delivery_class == "shared" and not str(payload.get("recipient_token", "")).strip():
        return False, "recipient_token required for shared delivery"
    dm_format = str(payload.get("format", "mls1") or "mls1").strip().lower()
    if dm_format not in ("mls1", "dm1"):
        return False, f"Unknown DM format: {dm_format}"
    return True, "ok"


def _validate_mailbox_claims(claims: Any) -> tuple[bool, str]:
    if not isinstance(claims, list) or not claims:
        return False, "mailbox_claims must be a non-empty list"
    for claim in claims:
        if not isinstance(claim, dict):
            return False, "mailbox_claims entries must be objects"
        claim_type = str(claim.get("type", "")).lower()
        if claim_type not in ("self", "requests", "shared"):
            return False, "Invalid mailbox claim type"
        if not str(claim.get("token", "")).strip():
            return False, f"{claim_type} mailbox claims require token"
    return True, "ok"


def _validate_dm_poll(payload: dict[str, Any]) -> tuple[bool, str]:
    ok, reason = _require_fields(payload, ("mailbox_claims", "timestamp", "nonce"))
    if not ok:
        return ok, reason
    return _validate_mailbox_claims(payload.get("mailbox_claims"))


def _validate_dm_count(payload: dict[str, Any]) -> tuple[bool, str]:
    return _validate_dm_poll(payload)


def _validate_key_rotate(payload: dict[str, Any]) -> tuple[bool, str]:
    ok, reason = _require_fields(
        payload,
        (
            "old_node_id",
            "old_public_key",
            "old_public_key_algo",
            "new_public_key",
            "new_public_key_algo",
            "timestamp",
            "old_signature",
        ),
    )
    if not ok:
        return ok, reason
    return True, "ok"


def _validate_key_revoke(payload: dict[str, Any]) -> tuple[bool, str]:
    ok, reason = _require_fields(
        payload,
        (
            "revoked_public_key",
            "revoked_public_key_algo",
            "revoked_at",
            "grace_until",
            "reason",
        ),
    )
    if not ok:
        return ok, reason
    revoked_at = _safe_int(payload.get("revoked_at", 0) or 0, 0)
    grace_until = _safe_int(payload.get("grace_until", 0) or 0, 0)
    if revoked_at <= 0:
        return False, "revoked_at must be a positive timestamp"
    if grace_until < revoked_at:
        return False, "grace_until must be >= revoked_at"
    return True, "ok"


def _validate_abuse_report(payload: dict[str, Any]) -> tuple[bool, str]:
    ok, reason = _require_fields(payload, ("target_id", "reason"))
    if not ok:
        return ok, reason
    if not str(payload.get("reason", "")).strip():
        return False, "reason cannot be empty"
    return True, "ok"


SCHEMA_REGISTRY: dict[str, EventSchema] = {
    "message": EventSchema(
        event_type="message",
        required_fields=("message", "destination", "channel", "priority", "ephemeral"),
        optional_fields=(),
        validate=_validate_message,
    ),
    "gate_message": EventSchema(
        event_type="gate_message",
        required_fields=("gate", "ciphertext", "nonce", "sender_ref"),
        optional_fields=("format",),
        validate=_validate_gate_message,
    ),
    "vote": EventSchema(
        event_type="vote",
        required_fields=("target_id", "vote", "gate"),
        optional_fields=(),
        validate=_validate_vote,
    ),
    "gate_create": EventSchema(
        event_type="gate_create",
        required_fields=("gate_id", "display_name", "rules"),
        optional_fields=(),
        validate=_validate_gate_create,
    ),
    "prediction": EventSchema(
        event_type="prediction",
        required_fields=("market_title", "side", "stake_amount"),
        optional_fields=(),
        validate=_validate_prediction,
    ),
    "stake": EventSchema(
        event_type="stake",
        required_fields=("message_id", "poster_id", "side", "amount", "duration_days"),
        optional_fields=(),
        validate=_validate_stake,
    ),
    "dm_block": EventSchema(
        event_type="dm_block",
        required_fields=("blocked_id", "action"),
        optional_fields=(),
        validate=_validate_dm_block,
    ),
    "dm_key": EventSchema(
        event_type="dm_key",
        required_fields=("dh_pub_key", "dh_algo", "timestamp"),
        optional_fields=(),
        validate=_validate_dm_key,
    ),
    "dm_message": EventSchema(
        event_type="dm_message",
        required_fields=("recipient_id", "delivery_class", "recipient_token", "ciphertext", "msg_id", "timestamp"),
        optional_fields=(),
        validate=_validate_dm_message,
    ),
    "dm_poll": EventSchema(
        event_type="dm_poll",
        required_fields=("mailbox_claims", "timestamp", "nonce"),
        optional_fields=(),
        validate=_validate_dm_poll,
    ),
    "dm_count": EventSchema(
        event_type="dm_count",
        required_fields=("mailbox_claims", "timestamp", "nonce"),
        optional_fields=(),
        validate=_validate_dm_count,
    ),
    "key_rotate": EventSchema(
        event_type="key_rotate",
        required_fields=(
            "old_node_id",
            "old_public_key",
            "old_public_key_algo",
            "new_public_key",
            "new_public_key_algo",
            "timestamp",
            "old_signature",
        ),
        optional_fields=(),
        validate=_validate_key_rotate,
    ),
    "key_revoke": EventSchema(
        event_type="key_revoke",
        required_fields=(
            "revoked_public_key",
            "revoked_public_key_algo",
            "revoked_at",
            "grace_until",
            "reason",
        ),
        optional_fields=(),
        validate=_validate_key_revoke,
    ),
    "abuse_report": EventSchema(
        event_type="abuse_report",
        required_fields=("target_id", "reason"),
        optional_fields=("gate", "evidence"),
        validate=_validate_abuse_report,
    ),
}


ACTIVE_PUBLIC_LEDGER_EVENT_TYPES: frozenset[str] = frozenset(
    {
        "message",
        "vote",
        "gate_create",
        "prediction",
        "stake",
        "key_rotate",
        "key_revoke",
        "abuse_report",
    }
)
"""Event types that may be newly appended to the public infonet chain."""

LEGACY_PUBLIC_LEDGER_EVENT_TYPES: frozenset[str] = frozenset(
    {
        "gate_message",
    }
)
"""Event types that exist historically on the public chain and must remain
ingestable for sync/restart compatibility, but may NOT be newly appended."""

PUBLIC_LEDGER_EVENT_TYPES: frozenset[str] = (
    ACTIVE_PUBLIC_LEDGER_EVENT_TYPES | LEGACY_PUBLIC_LEDGER_EVENT_TYPES
)
"""Union of active + legacy — the full set accepted during ingest."""

_PUBLIC_LEDGER_FORBIDDEN_FIELDS: frozenset[str] = frozenset(
    {
        "ip",
        "ip_address",
        "origin_ip",
        "source_ip",
        "client_ip",
        "host",
        "hostname",
        "origin",
        "originator",
        "originator_hint",
        "routing_hint",
        "route",
        "route_hint",
        "route_reason",
        "routed_via",
        "transport",
        "transport_handle",
        "transport_lock",
        "recipient_id",
        "recipient_token",
        "delivery_class",
        "mailbox_claims",
        "dh_pub_key",
        "sender_token",
    }
)


def get_schema(event_type: str) -> EventSchema | None:
    return SCHEMA_REGISTRY.get(event_type)


# ─── Extension registry (Sprint 8+ chain cutover, 2026-04-28) ────────────
# The infonet economy layer registers its event-type validators here at
# import time via ``services/infonet/_chain_cutover.py``. mesh_schema does
# NOT import from services.infonet (would create a cycle); the direction
# stays one-way (infonet → mesh).
#
# Extensions opt out of the legacy normalize_payload + ephemeral-check
# pipeline because their payloads have their own normalization rules.
# The legacy flow stays byte-identical for legacy event types.

_EXTENSION_VALIDATORS: dict[str, Callable[[dict[str, Any]], tuple[bool, str]]] = {}


def register_extension_validator(
    event_type: str,
    validator: Callable[[dict[str, Any]], tuple[bool, str]],
) -> None:
    """Register an extension event-type validator.

    Idempotent — calling twice with the same ``event_type`` overwrites
    the prior validator (no-op when called with the same function).
    Used by ``services/infonet/_chain_cutover.py``.
    """
    if not isinstance(event_type, str) or not event_type:
        raise ValueError("event_type must be a non-empty string")
    _EXTENSION_VALIDATORS[event_type] = validator


def is_extension_event_type(event_type: str) -> bool:
    return event_type in _EXTENSION_VALIDATORS


def validate_event_payload(event_type: str, payload: dict[str, Any]) -> tuple[bool, str]:
    schema = get_schema(event_type)
    if schema is None:
        # Fall through to extension validators (registered by infonet
        # economy layer at import time).
        ext = _EXTENSION_VALIDATORS.get(event_type)
        if ext is not None:
            return ext(payload)
        return False, "Unknown event_type"
    normalized = normalize_payload(event_type, payload)
    if normalized != payload:
        return False, "Payload is not normalized"
    if event_type not in ("message", "gate_message") and "ephemeral" in payload:
        return False, "ephemeral not allowed for this event type"
    return schema.validate_payload(payload)


def validate_public_ledger_payload(event_type: str, payload: dict[str, Any]) -> tuple[bool, str]:
    if event_type not in PUBLIC_LEDGER_EVENT_TYPES and event_type not in _EXTENSION_VALIDATORS:
        return False, f"{event_type} is not allowed on the public ledger"
    forbidden = sorted(
        key
        for key in payload.keys()
        if str(key or "").strip().lower() in _PUBLIC_LEDGER_FORBIDDEN_FIELDS
    )
    if forbidden:
        return False, f"public ledger payload contains forbidden fields: {', '.join(forbidden)}"
    if event_type == "message":
        destination = str(payload.get("destination", "") or "").strip().lower()
        if destination and destination != "broadcast":
            return False, "public ledger message destination must be broadcast"
    return True, "ok"


def validate_protocol_fields(protocol_version: str, network_id: str) -> tuple[bool, str]:
    if protocol_version != PROTOCOL_VERSION:
        return False, "Unsupported protocol_version"
    if network_id != NETWORK_ID:
        return False, "network_id mismatch"
    return True, "ok"
