from __future__ import annotations

import json
import re
import traceback
import uuid
from collections.abc import Mapping
from datetime import datetime, timezone
from numbers import Integral
from typing import Any, Literal, cast

from models import EventRecord

LogIdentifier = Literal["Info", "Warning", "Error"]

LOG_EVENT_TYPE = "user_log"
RUNTIME_ERROR_EVENT_TYPE = "runtime_error"
RUNTIME_INTERRUPTION_EVENT_TYPE = "runtime_interruption"

_EVENT_TYPE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:[_./-][a-z0-9]+)*$")
_LOG_IDENTIFIER_BY_KEY: dict[str, LogIdentifier] = {
    "info": "Info",
    "warning": "Warning",
    "error": "Error",
}
_SEVERITY_BY_IDENTIFIER: dict[LogIdentifier, str] = {
    "Info": "info",
    "Warning": "warning",
    "Error": "error",
}
_VALID_RUN_STATUSES = {"running", "completed", "failed", "interrupted"}


def utc_now() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def normalize_log_identifier(identifier: str | None = None) -> LogIdentifier:
    if identifier is None:
        return "Info"
    if not isinstance(identifier, str):
        raise TypeError("identifier must be a string when provided.")
    candidate = identifier.strip()
    if not candidate:
        raise ValueError("identifier must not be empty.")
    normalized = _LOG_IDENTIFIER_BY_KEY.get(candidate.lower())
    if normalized is None:
        raise ValueError(
            f"identifier '{identifier}' is not supported. Expected one of {sorted(_LOG_IDENTIFIER_BY_KEY.values())}."
        )
    return normalized


def severity_for_log_identifier(identifier: str | None = None) -> str:
    return _SEVERITY_BY_IDENTIFIER[normalize_log_identifier(identifier)]


def normalize_event_type(event_type: str) -> str:
    candidate = event_type.strip()
    if not candidate:
        raise ValueError("event_type must not be empty.")
    if _EVENT_TYPE_PATTERN.fullmatch(candidate) is None:
        raise ValueError(
            f"event_type '{candidate}' may only contain lowercase letters, digits, '_', '.', '/' or '-'."
        )
    return candidate


def normalize_status(status: str | None) -> str | None:
    if status is None:
        return None
    candidate = status.strip().lower()
    if candidate not in _VALID_RUN_STATUSES:
        raise ValueError(f"status '{status}' is not supported. Expected one of {sorted(_VALID_RUN_STATUSES)}.")
    return candidate


def normalize_index(label: str, value: int | None) -> int | None:
    if value is None:
        return None
    if isinstance(value, bool) or not isinstance(value, Integral):
        raise TypeError(f"{label} must be an integer when provided.")
    normalized_value = int(value)
    if normalized_value < 0:
        raise ValueError(f"{label} must not be negative.")
    return normalized_value


def normalize_json_payload(payload: Mapping[str, Any] | None) -> dict[str, Any]:
    if payload is None:
        return {}
    normalized_payload = dict(payload)
    json.dumps(normalized_payload, sort_keys=True)
    return normalized_payload


def build_log_payload(
    message: str,
    *,
    identifier: str | None = None,
    payload: Mapping[str, Any] | None = None,
) -> tuple[LogIdentifier, dict[str, Any]]:
    if not isinstance(message, str):
        raise TypeError("log message must be a string.")
    normalized_message = message.strip()
    if not normalized_message:
        raise ValueError("log message must not be empty.")

    normalized_identifier = normalize_log_identifier(identifier)
    normalized_payload = normalize_json_payload(payload)
    reserved_keys = {"message", "identifier"}
    duplicates = sorted(reserved_keys.intersection(normalized_payload))
    if duplicates:
        duplicate_list = ", ".join(duplicates)
        raise ValueError(f"log payload must not include reserved keys: {duplicate_list}.")

    log_payload = dict(normalized_payload)
    log_payload["message"] = normalized_message
    log_payload["identifier"] = normalized_identifier
    return normalized_identifier, log_payload


def build_log_event(
    *,
    run_id: str,
    sequence_id: int,
    message: str,
    identifier: str | None = None,
    event_type: str = LOG_EVENT_TYPE,
    step: int | None = None,
    epoch: int | None = None,
    status: str | None = None,
    payload: Mapping[str, Any] | None = None,
    timestamp: str | None = None,
) -> EventRecord:
    normalized_run_id = run_id.strip()
    if not normalized_run_id:
        raise ValueError("run_id must not be empty.")

    normalized_sequence_id = normalize_index("sequence_id", sequence_id)
    if normalized_sequence_id is None:
        raise ValueError("sequence_id must be provided.")

    normalized_identifier, log_payload = build_log_payload(
        message,
        identifier=identifier,
        payload=payload,
    )
    return EventRecord(
        id=uuid.uuid4().hex,
        run_id=normalized_run_id,
        sequence_id=normalized_sequence_id,
        event_type=normalize_event_type(event_type),
        severity=cast(Any, severity_for_log_identifier(normalized_identifier)),
        step=normalize_index("step", step),
        epoch=normalize_index("epoch", epoch),
        timestamp=timestamp or utc_now(),
        status=cast(Any, normalize_status(status)),
        payload=log_payload,
    )


def exception_log_payload(
    exc_type: type[BaseException] | None,
    exc: BaseException | None,
    exc_tb: Any,
    *,
    source: str,
) -> dict[str, Any]:
    payload: dict[str, Any] = {"source": source}
    if exc_type is not None:
        payload["exception_type"] = exc_type.__name__
    if exc is not None:
        payload["exception_message"] = str(exc)
    if exc_type is not None and exc is not None:
        payload["traceback"] = "".join(traceback.format_exception(exc_type, exc, exc_tb))
    return payload
