from __future__ import annotations

import json
import math
import re
import sqlite3
from dataclasses import fields, is_dataclass
from datetime import datetime
from pathlib import Path
from types import UnionType
from typing import Any, Mapping, get_args, get_origin, get_type_hints

from models import (
    AlertRecord,
    ArtifactRecord,
    BatchDiagnosticRecord,
    EvidenceRecord,
    EventRecord,
    FINAL_RUN_STATUSES,
    HealthScoreRecord,
    LayerSummaryRecord,
    LifecycleEventRecord,
    MetricRecord,
    NoteRecord,
    ParameterDistributionRecord,
    ParameterSummaryRecord,
    PhaseRecord,
    ProjectRecord,
    RunRecord,
    SCHEMA_VERSION,
    SystemTelemetryRecord,
)

CURRENT_SCHEMA_VERSION = SCHEMA_VERSION
LEGACY_SCHEMA_VERSION = 1
SCHEMA_MIGRATION_NAME = "v1_to_v2_versioned_records"

_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
_DB_TABLES_WITH_SCHEMA_VERSION = (
    "projects",
    "runs",
    "run_tags",
    "run_notes",
    "lifecycle_events",
    "events",
    "metric_streams",
    "scalar_metrics",
    "system_telemetry",
    "layer_summaries",
    "parameter_summaries",
    "parameter_distributions",
    "batch_diagnostics",
    "artifacts",
    "alerts",
    "health_scores",
    "phases",
)
_RECORD_TYPES = (
    ProjectRecord,
    NoteRecord,
    LifecycleEventRecord,
    EventRecord,
    MetricRecord,
    SystemTelemetryRecord,
    BatchDiagnosticRecord,
    LayerSummaryRecord,
    ParameterSummaryRecord,
    ParameterDistributionRecord,
    ArtifactRecord,
    EvidenceRecord,
    AlertRecord,
    HealthScoreRecord,
    PhaseRecord,
    RunRecord,
)


class SchemaValidationError(ValueError):
    """Raised when a persisted record fails schema validation."""


class SchemaMigrationError(RuntimeError):
    """Raised when a local store cannot be migrated safely."""


def dump_record(record: Any) -> dict[str, Any]:
    """Validate a persisted record object and return its serialized payload."""

    validate_record(record)
    if not hasattr(record, "to_dict"):
        raise TypeError(f"{type(record).__name__} does not support to_dict serialization.")
    payload = record.to_dict()
    if not isinstance(payload, dict):
        raise TypeError(f"{type(record).__name__}.to_dict() must return a mapping.")
    return payload


def validate_record(record: Any) -> None:
    """Validate a persisted record object against the current schema."""

    if not is_dataclass(record):
        raise TypeError(f"Expected a dataclass record, received {type(record).__name__}.")

    record_type = type(record)
    if record_type not in _RECORD_TYPES:
        raise TypeError(f"Unsupported persisted record type '{record_type.__name__}'.")

    _validate_dataclass_shape(record)
    _validate_schema_version(getattr(record, "schema_version", None), record_type.__name__)
    _VALIDATORS[record_type](record)


def build_store_schema_payload(*, generated_at: str) -> dict[str, Any]:
    _validate_timestamp(generated_at, "generated_at")
    return {
        "schema_version": CURRENT_SCHEMA_VERSION,
        "generated_at": generated_at,
    }


def build_config_payload(*, run_id: str, config: Mapping[str, Any]) -> dict[str, Any]:
    payload = {
        "schema_version": CURRENT_SCHEMA_VERSION,
        "run_id": _validate_non_empty_text(run_id, "run_id"),
        "config": _as_json_object(config, "config"),
    }
    validate_config_payload(payload)
    return payload


def build_environment_payload(*, run_id: str, environment: Mapping[str, Any]) -> dict[str, Any]:
    payload = {
        "schema_version": CURRENT_SCHEMA_VERSION,
        "run_id": _validate_non_empty_text(run_id, "run_id"),
        "environment": _as_json_object(environment, "environment"),
    }
    validate_environment_payload(payload)
    return payload


def build_run_snapshot_payload(
    *,
    project: ProjectRecord,
    run: RunRecord,
    paths: Mapping[str, Any],
) -> dict[str, Any]:
    payload = {
        "schema_version": CURRENT_SCHEMA_VERSION,
        "project": dump_record(project),
        "run": dump_record(run),
        "paths": _as_json_object(paths, "paths"),
    }
    validate_run_snapshot_payload(payload)
    return payload


def build_analysis_snapshot_payload(
    *,
    generated_at: str,
    root_key: str,
    records: tuple[Any, ...] | list[Any],
) -> dict[str, Any]:
    _validate_timestamp(generated_at, "generated_at")
    normalized_root_key = _validate_non_empty_text(root_key, "root_key")
    payload = {
        "schema_version": CURRENT_SCHEMA_VERSION,
        "generated_at": generated_at,
        normalized_root_key: [dump_record(record) for record in records],
    }
    validate_analysis_snapshot_payload(payload, root_key=normalized_root_key)
    return payload


def build_artifact_sidecar_payload(*, artifact: ArtifactRecord) -> dict[str, Any]:
    payload = {
        "schema_version": CURRENT_SCHEMA_VERSION,
        "artifact": dump_record(artifact),
    }
    validate_artifact_sidecar_payload(payload)
    return payload


def build_artifact_layout_payload(*, layout: Mapping[str, Any]) -> dict[str, Any]:
    payload = {
        "schema_version": CURRENT_SCHEMA_VERSION,
        **_as_json_object(layout, "layout"),
    }
    validate_artifact_layout_payload(payload)
    return payload


def validate_config_payload(payload: Mapping[str, Any]) -> None:
    _validate_schema_version(payload.get("schema_version"), "config_snapshot")
    _validate_non_empty_text(payload.get("run_id"), "run_id")
    _as_json_object(payload.get("config"), "config")


def validate_environment_payload(payload: Mapping[str, Any]) -> None:
    _validate_schema_version(payload.get("schema_version"), "environment_snapshot")
    _validate_non_empty_text(payload.get("run_id"), "run_id")
    _as_json_object(payload.get("environment"), "environment")


def validate_run_snapshot_payload(payload: Mapping[str, Any]) -> None:
    _validate_schema_version(payload.get("schema_version"), "run_snapshot")
    project = payload.get("project")
    run = payload.get("run")
    paths = payload.get("paths")
    if not isinstance(project, Mapping):
        raise SchemaValidationError("run_snapshot.project must be a mapping.")
    if not isinstance(run, Mapping):
        raise SchemaValidationError("run_snapshot.run must be a mapping.")
    if not isinstance(paths, Mapping):
        raise SchemaValidationError("run_snapshot.paths must be a mapping.")
    _validate_schema_version(project.get("schema_version"), "project")
    _validate_schema_version(run.get("schema_version"), "run")
    _assert_json_serializable(dict(paths), "paths")


def validate_analysis_snapshot_payload(payload: Mapping[str, Any], *, root_key: str) -> None:
    _validate_schema_version(payload.get("schema_version"), "analysis_snapshot")
    _validate_timestamp(payload.get("generated_at"), "generated_at")
    records = payload.get(root_key)
    if not isinstance(records, list):
        raise SchemaValidationError(
            f"analysis snapshot field '{root_key}' must be a list of serialized records."
        )
    for index, record in enumerate(records):
        if not isinstance(record, Mapping):
            raise SchemaValidationError(
                f"analysis snapshot field '{root_key}' contains a non-mapping record at index {index}."
            )
        _validate_schema_version(record.get("schema_version"), f"{root_key}[{index}]")


def validate_artifact_sidecar_payload(payload: Mapping[str, Any]) -> None:
    _validate_schema_version(payload.get("schema_version"), "artifact_sidecar")
    artifact = payload.get("artifact")
    if not isinstance(artifact, Mapping):
        raise SchemaValidationError("artifact sidecar payload must include an 'artifact' mapping.")
    _validate_schema_version(artifact.get("schema_version"), "artifact")


def validate_artifact_layout_payload(payload: Mapping[str, Any]) -> None:
    _validate_schema_version(payload.get("schema_version"), "artifact_layout")
    _validate_non_empty_text(payload.get("root_directory"), "root_directory")
    _validate_non_empty_text(payload.get("path_template"), "path_template")
    _validate_non_empty_text(payload.get("metadata_sidecar_suffix"), "metadata_sidecar_suffix")
    type_directories = payload.get("type_directories")
    if not isinstance(type_directories, Mapping):
        raise SchemaValidationError("artifact layout must include a 'type_directories' mapping.")
    for artifact_type, directory_name in type_directories.items():
        _validate_non_empty_text(artifact_type, "artifact_type")
        _validate_non_empty_text(directory_name, "directory_name")


def migrate_store(
    storage_dir: str | Path,
    *,
    generated_at: str,
) -> int:
    """Upgrade a local `.quark` store to the current schema version."""

    _validate_timestamp(generated_at, "generated_at")
    resolved_storage_dir = Path(storage_dir).resolve()
    store_root = resolved_storage_dir / ".quark"
    db_path = store_root / "projects.db"
    schema_version_path = store_root / "schema_version.json"

    store_root.mkdir(parents=True, exist_ok=True)
    (store_root / "runs").mkdir(parents=True, exist_ok=True)

    if not db_path.exists():
        _write_json_atomic(schema_version_path, build_store_schema_payload(generated_at=generated_at))
        return CURRENT_SCHEMA_VERSION

    connection = sqlite3.connect(db_path)
    try:
        connection.row_factory = sqlite3.Row
        _ensure_schema_migrations_table(connection)
        current_version = _detect_store_version(connection, schema_version_path)
        if current_version > CURRENT_SCHEMA_VERSION:
            raise SchemaMigrationError(
                f"Store schema version {current_version} is newer than supported version {CURRENT_SCHEMA_VERSION}."
            )
        if current_version < LEGACY_SCHEMA_VERSION:
            raise SchemaMigrationError(
                f"Store schema version {current_version} is too old to migrate automatically."
            )
        if current_version < CURRENT_SCHEMA_VERSION:
            _migrate_v1_to_v2(connection, store_root, generated_at=generated_at)
        connection.commit()
    finally:
        connection.close()

    _write_json_atomic(schema_version_path, build_store_schema_payload(generated_at=generated_at))
    return CURRENT_SCHEMA_VERSION


def _migrate_v1_to_v2(
    connection: sqlite3.Connection,
    store_root: Path,
    *,
    generated_at: str,
) -> None:
    _ensure_schema_version_columns(connection)
    _rewrite_run_files(store_root)
    connection.execute(
        """
        INSERT OR IGNORE INTO schema_migrations (
            migration_name,
            from_version,
            to_version,
            applied_at,
            schema_version
        ) VALUES (?, ?, ?, ?, ?)
        """,
        (
            SCHEMA_MIGRATION_NAME,
            LEGACY_SCHEMA_VERSION,
            CURRENT_SCHEMA_VERSION,
            generated_at,
            CURRENT_SCHEMA_VERSION,
        ),
    )


def _rewrite_run_files(store_root: Path) -> None:
    runs_root = store_root / "runs"
    if not runs_root.exists():
        return

    for project_dir in runs_root.iterdir():
        if not project_dir.is_dir():
            continue
        for run_dir in project_dir.iterdir():
            if not run_dir.is_dir():
                continue
            _rewrite_run_directory(run_dir)


def _rewrite_run_directory(run_dir: Path) -> None:
    meta_dir = run_dir / "meta"
    run_snapshot_path = meta_dir / "run.json"
    config_path = meta_dir / "config.json"
    environment_path = meta_dir / "environment.json"
    analysis_dir = run_dir / "analysis"
    events_path = run_dir / "events" / "events.ndjson"
    artifacts_dir = run_dir / "artifacts"

    run_id = run_dir.name
    if run_snapshot_path.exists():
        run_snapshot = json.loads(run_snapshot_path.read_text(encoding="utf-8"))
        migrated_snapshot = _migrate_run_snapshot_payload(run_snapshot)
        run_id = str(migrated_snapshot["run"]["id"])
        _write_json_atomic(run_snapshot_path, migrated_snapshot)

    if config_path.exists():
        config_payload = json.loads(config_path.read_text(encoding="utf-8"))
        _write_json_atomic(config_path, _migrate_config_payload(config_payload, run_id=run_id))

    if environment_path.exists():
        environment_payload = json.loads(environment_path.read_text(encoding="utf-8"))
        _write_json_atomic(
            environment_path,
            _migrate_environment_payload(environment_payload, run_id=run_id),
        )

    if events_path.exists():
        _rewrite_event_history(events_path)

    if analysis_dir.exists():
        for file_name, root_key in (
            ("alerts.json", "alerts"),
            ("health_scores.json", "health_scores"),
            ("phases.json", "phases"),
        ):
            path = analysis_dir / file_name
            if not path.exists():
                continue
            payload = json.loads(path.read_text(encoding="utf-8"))
            _write_json_atomic(path, _migrate_analysis_snapshot_payload(payload, root_key=root_key))

    if artifacts_dir.exists():
        layout_path = artifacts_dir / "layout.json"
        if layout_path.exists():
            layout_payload = json.loads(layout_path.read_text(encoding="utf-8"))
            _write_json_atomic(layout_path, _migrate_artifact_layout_payload(layout_payload))

        for sidecar_path in artifacts_dir.rglob("*.meta.json"):
            sidecar_payload = json.loads(sidecar_path.read_text(encoding="utf-8"))
            _write_json_atomic(sidecar_path, _migrate_artifact_sidecar_payload(sidecar_payload))


def _rewrite_event_history(path: Path) -> None:
    lines = path.read_text(encoding="utf-8").splitlines()
    migrated_lines: list[str] = []
    for line in lines:
        if not line.strip():
            continue
        payload = json.loads(line)
        payload["schema_version"] = CURRENT_SCHEMA_VERSION
        migrated_lines.append(json.dumps(payload, sort_keys=True))
    path.write_text("\n".join(migrated_lines) + ("\n" if migrated_lines else ""), encoding="utf-8")


def _migrate_run_snapshot_payload(payload: Mapping[str, Any]) -> dict[str, Any]:
    project_payload = dict(_as_json_object(payload.get("project"), "project"))
    run_payload = dict(_as_json_object(payload.get("run"), "run"))
    project_payload["schema_version"] = CURRENT_SCHEMA_VERSION
    run_payload["schema_version"] = CURRENT_SCHEMA_VERSION
    run_payload["notes"] = [
        _migrate_nested_record(item) for item in run_payload.get("notes", [])
    ]
    run_payload["lifecycle_events"] = [
        _migrate_nested_record(item) for item in run_payload.get("lifecycle_events", [])
    ]
    migrated_payload = {
        "schema_version": CURRENT_SCHEMA_VERSION,
        "project": project_payload,
        "run": run_payload,
        "paths": _as_json_object(payload.get("paths"), "paths"),
    }
    validate_run_snapshot_payload(migrated_payload)
    return migrated_payload


def _migrate_config_payload(payload: Mapping[str, Any], *, run_id: str) -> dict[str, Any]:
    if "config" in payload and "run_id" in payload:
        migrated_payload = {
            "schema_version": CURRENT_SCHEMA_VERSION,
            "run_id": _validate_non_empty_text(payload.get("run_id"), "run_id"),
            "config": _as_json_object(payload.get("config"), "config"),
        }
    else:
        migrated_payload = build_config_payload(run_id=run_id, config=payload)
    validate_config_payload(migrated_payload)
    return migrated_payload


def _migrate_environment_payload(payload: Mapping[str, Any], *, run_id: str) -> dict[str, Any]:
    if "environment" in payload and "run_id" in payload:
        migrated_payload = {
            "schema_version": CURRENT_SCHEMA_VERSION,
            "run_id": _validate_non_empty_text(payload.get("run_id"), "run_id"),
            "environment": _as_json_object(payload.get("environment"), "environment"),
        }
    else:
        migrated_payload = build_environment_payload(run_id=run_id, environment=payload)
    validate_environment_payload(migrated_payload)
    return migrated_payload


def _migrate_analysis_snapshot_payload(
    payload: Mapping[str, Any],
    *,
    root_key: str,
) -> dict[str, Any]:
    records = payload.get(root_key, [])
    if not isinstance(records, list):
        raise SchemaMigrationError(
            f"Analysis snapshot '{root_key}' must contain a list of records."
        )
    migrated_records = []
    for record in records:
        migrated_record = _migrate_nested_record(record)
        if isinstance(migrated_record.get("evidence"), list):
            migrated_record["evidence"] = [
                _migrate_nested_record(item) for item in migrated_record["evidence"]
            ]
        migrated_records.append(migrated_record)

    generated_at = payload.get("generated_at")
    if not isinstance(generated_at, str) or not generated_at.strip():
        generated_at = datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
    migrated_payload = {
        "schema_version": CURRENT_SCHEMA_VERSION,
        "generated_at": generated_at,
        root_key: migrated_records,
    }
    validate_analysis_snapshot_payload(migrated_payload, root_key=root_key)
    return migrated_payload


def _migrate_artifact_sidecar_payload(payload: Mapping[str, Any]) -> dict[str, Any]:
    artifact_payload = _migrate_nested_record(payload.get("artifact"))
    migrated_payload = {
        "schema_version": CURRENT_SCHEMA_VERSION,
        "artifact": artifact_payload,
    }
    validate_artifact_sidecar_payload(migrated_payload)
    return migrated_payload


def _migrate_artifact_layout_payload(payload: Mapping[str, Any]) -> dict[str, Any]:
    migrated_payload = {
        "schema_version": CURRENT_SCHEMA_VERSION,
        **_as_json_object(payload, "artifact layout"),
    }
    migrated_payload["schema_version"] = CURRENT_SCHEMA_VERSION
    validate_artifact_layout_payload(migrated_payload)
    return migrated_payload


def _migrate_nested_record(payload: Any) -> dict[str, Any]:
    if not isinstance(payload, Mapping):
        raise SchemaMigrationError("Expected a mapping while migrating a nested record payload.")
    migrated_payload = dict(payload)
    migrated_payload["schema_version"] = CURRENT_SCHEMA_VERSION
    return migrated_payload


def _detect_store_version(
    connection: sqlite3.Connection,
    schema_version_path: Path,
) -> int:
    if schema_version_path.exists():
        payload = json.loads(schema_version_path.read_text(encoding="utf-8"))
        version = payload.get("schema_version")
        if isinstance(version, int):
            if version >= CURRENT_SCHEMA_VERSION and not _all_tables_have_schema_version(connection):
                return LEGACY_SCHEMA_VERSION
            return version
        raise SchemaMigrationError("schema_version.json does not contain a valid integer schema_version.")

    if _all_tables_have_schema_version(connection):
        return CURRENT_SCHEMA_VERSION
    return LEGACY_SCHEMA_VERSION


def _ensure_schema_version_columns(connection: sqlite3.Connection) -> None:
    for table_name in _DB_TABLES_WITH_SCHEMA_VERSION:
        if not _table_exists(connection, table_name):
            continue
        if not _table_has_column(connection, table_name, "schema_version"):
            connection.execute(
                f"ALTER TABLE {table_name} ADD COLUMN schema_version INTEGER NOT NULL DEFAULT {CURRENT_SCHEMA_VERSION}"
            )
        connection.execute(
            f"UPDATE {table_name} SET schema_version = ? WHERE schema_version IS NULL OR schema_version = 0",
            (CURRENT_SCHEMA_VERSION,),
        )


def _ensure_schema_migrations_table(connection: sqlite3.Connection) -> None:
    connection.execute(
        """
        CREATE TABLE IF NOT EXISTS schema_migrations (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            migration_name TEXT NOT NULL UNIQUE,
            from_version INTEGER NOT NULL,
            to_version INTEGER NOT NULL,
            applied_at TEXT NOT NULL,
            schema_version INTEGER NOT NULL
        )
        """
    )


def _all_tables_have_schema_version(connection: sqlite3.Connection) -> bool:
    for table_name in _DB_TABLES_WITH_SCHEMA_VERSION:
        if not _table_exists(connection, table_name):
            continue
        if not _table_has_column(connection, table_name, "schema_version"):
            return False
    return True


def _table_exists(connection: sqlite3.Connection, table_name: str) -> bool:
    row = connection.execute(
        "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
        (table_name,),
    ).fetchone()
    return row is not None


def _table_has_column(connection: sqlite3.Connection, table_name: str, column_name: str) -> bool:
    rows = connection.execute(f"PRAGMA table_info({table_name})").fetchall()
    return any(str(row[1]) == column_name for row in rows)


def _write_json_atomic(path: Path, payload: Mapping[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    temp_path = path.with_suffix(f"{path.suffix}.tmp")
    temp_path.write_text(json.dumps(dict(payload), indent=2, sort_keys=True), encoding="utf-8")
    temp_path.replace(path)


def _validate_dataclass_shape(record: Any) -> None:
    hints = get_type_hints(type(record))
    for field in fields(record):
        if field.name not in hints:
            continue
        value = getattr(record, field.name)
        annotation = hints[field.name]
        if not _matches_annotation(value, annotation):
            raise SchemaValidationError(
                f"{type(record).__name__}.{field.name} expected {annotation!r}, received {value!r}."
            )


def _matches_annotation(value: Any, annotation: Any) -> bool:
    if annotation is Any:
        return True

    origin = get_origin(annotation)
    if origin is None:
        if isinstance(annotation, type):
            if annotation is float:
                return isinstance(value, (int, float)) and not isinstance(value, bool)
            if annotation is int:
                return isinstance(value, int) and not isinstance(value, bool)
            return isinstance(value, annotation)
        return True

    if origin in (UnionType,):
        return any(_matches_annotation(value, candidate) for candidate in get_args(annotation))
    if str(origin) == "typing.Union":
        return any(_matches_annotation(value, candidate) for candidate in get_args(annotation))
    if str(origin) == "typing.Literal":
        return value in get_args(annotation)
    if origin is tuple:
        if not isinstance(value, tuple):
            return False
        args = get_args(annotation)
        if len(args) == 2 and args[1] is Ellipsis:
            return all(_matches_annotation(item, args[0]) for item in value)
        return len(value) == len(args) and all(
            _matches_annotation(item, item_type)
            for item, item_type in zip(value, args, strict=True)
        )
    if origin is list:
        if not isinstance(value, list):
            return False
        (item_type,) = get_args(annotation)
        return all(_matches_annotation(item, item_type) for item in value)
    if origin is dict:
        if not isinstance(value, dict):
            return False
        key_type, value_type = get_args(annotation)
        return all(
            _matches_annotation(key, key_type) and _matches_annotation(item, value_type)
            for key, item in value.items()
        )
    return True


def _validate_schema_version(value: Any, label: str) -> int:
    if not isinstance(value, int):
        raise SchemaValidationError(f"{label} must include an integer schema_version.")
    if value != CURRENT_SCHEMA_VERSION:
        raise SchemaValidationError(
            f"{label} schema_version {value} is not compatible with current version {CURRENT_SCHEMA_VERSION}."
        )
    return value


def _validate_non_empty_text(value: Any, label: str) -> str:
    if not isinstance(value, str):
        raise SchemaValidationError(f"{label} must be a string.")
    normalized = value.strip()
    if not normalized:
        raise SchemaValidationError(f"{label} must not be empty.")
    return normalized


def _validate_optional_text(value: Any, label: str) -> str | None:
    if value is None:
        return None
    return _validate_non_empty_text(value, label)


def _validate_timestamp(value: Any, label: str) -> str:
    candidate = _validate_non_empty_text(value, label)
    try:
        datetime.fromisoformat(candidate.replace("Z", "+00:00"))
    except ValueError as exc:
        raise SchemaValidationError(f"{label} must be an ISO-8601 timestamp.") from exc
    return candidate


def _validate_non_negative_int(value: Any, label: str) -> int:
    if not isinstance(value, int) or isinstance(value, bool):
        raise SchemaValidationError(f"{label} must be an integer.")
    if value < 0:
        raise SchemaValidationError(f"{label} must not be negative.")
    return value


def _validate_optional_non_negative_int(value: Any, label: str) -> int | None:
    if value is None:
        return None
    return _validate_non_negative_int(value, label)


def _validate_finite_number(value: Any, label: str) -> float:
    if not isinstance(value, (int, float)) or isinstance(value, bool):
        raise SchemaValidationError(f"{label} must be numeric.")
    normalized = float(value)
    if not math.isfinite(normalized):
        raise SchemaValidationError(f"{label} must be finite.")
    return normalized


def _validate_optional_finite_number(value: Any, label: str) -> float | None:
    if value is None:
        return None
    return _validate_finite_number(value, label)


def _validate_percentage(value: Any, label: str) -> float | None:
    normalized = _validate_optional_finite_number(value, label)
    if normalized is None:
        return None
    if not 0.0 <= normalized <= 100.0:
        raise SchemaValidationError(f"{label} must be between 0 and 100.")
    return normalized


def _validate_ratio(value: Any, label: str) -> float | None:
    normalized = _validate_optional_finite_number(value, label)
    if normalized is None:
        return None
    if not 0.0 <= normalized <= 1.0:
        raise SchemaValidationError(f"{label} must be between 0 and 1.")
    return normalized


def _assert_json_serializable(payload: Any, label: str) -> None:
    try:
        json.dumps(payload, sort_keys=True)
    except TypeError as exc:
        raise SchemaValidationError(f"{label} must be JSON-serializable.") from exc


def _as_json_object(value: Any, label: str) -> dict[str, Any]:
    if not isinstance(value, Mapping):
        raise SchemaValidationError(f"{label} must be a mapping.")
    payload = dict(value)
    _assert_json_serializable(payload, label)
    return payload


def _validate_project(record: ProjectRecord) -> None:
    _validate_non_empty_text(record.id, "project.id")
    _validate_non_empty_text(record.name, "project.name")
    _validate_non_empty_text(record.root_path, "project.root_path")
    _validate_timestamp(record.created_at, "project.created_at")
    _validate_timestamp(record.updated_at, "project.updated_at")


def _validate_note(record: NoteRecord) -> None:
    _validate_non_empty_text(record.id, "note.id")
    _validate_non_empty_text(record.run_id, "note.run_id")
    _validate_non_empty_text(record.note, "note.note")
    _validate_timestamp(record.created_at, "note.created_at")


def _validate_lifecycle_event(record: LifecycleEventRecord) -> None:
    _validate_non_empty_text(record.id, "lifecycle_event.id")
    _validate_non_empty_text(record.run_id, "lifecycle_event.run_id")
    _validate_non_empty_text(record.event_type, "lifecycle_event.event_type")
    _validate_timestamp(record.timestamp, "lifecycle_event.timestamp")
    if record.status not in FINAL_RUN_STATUSES | {"running"}:
        raise SchemaValidationError("lifecycle_event.status must be a supported run status.")
    _assert_json_serializable(record.payload, "lifecycle_event.payload")


def _validate_event(record: EventRecord) -> None:
    _validate_non_empty_text(record.id, "event.id")
    _validate_non_empty_text(record.run_id, "event.run_id")
    _validate_non_negative_int(record.sequence_id, "event.sequence_id")
    _validate_non_empty_text(record.event_type, "event.event_type")
    _validate_timestamp(record.timestamp, "event.timestamp")
    _validate_optional_non_negative_int(record.step, "event.step")
    _validate_optional_non_negative_int(record.epoch, "event.epoch")
    _assert_json_serializable(record.payload, "event.payload")


def _validate_metric(record: MetricRecord) -> None:
    _validate_non_empty_text(record.id, "metric.id")
    _validate_non_empty_text(record.run_id, "metric.run_id")
    _validate_non_negative_int(record.sequence_id, "metric.sequence_id")
    if record.namespace:
        _validate_non_empty_text(record.namespace, "metric.namespace")
    _validate_non_empty_text(record.metric_name, "metric.metric_name")
    _validate_non_empty_text(record.metric_key, "metric.metric_key")
    _validate_finite_number(record.value, "metric.value")
    _validate_optional_non_negative_int(record.step, "metric.step")
    _validate_optional_non_negative_int(record.epoch, "metric.epoch")
    _validate_timestamp(record.timestamp, "metric.timestamp")
    _validate_optional_text(record.description, "metric.description")
    if record.metadata is not None:
        _assert_json_serializable(record.metadata, "metric.metadata")


def _validate_system_telemetry(record: SystemTelemetryRecord) -> None:
    _validate_non_empty_text(record.id, "telemetry.id")
    _validate_non_empty_text(record.run_id, "telemetry.run_id")
    _validate_non_negative_int(record.sequence_id, "telemetry.sequence_id")
    _validate_timestamp(record.timestamp, "telemetry.timestamp")
    _validate_non_empty_text(record.cpu_backend, "telemetry.cpu_backend")
    _validate_non_empty_text(record.gpu_backend, "telemetry.gpu_backend")
    _validate_non_negative_int(record.overflow_count, "telemetry.overflow_count")
    _validate_non_negative_int(record.skipped_step_count, "telemetry.skipped_step_count")
    _validate_optional_non_negative_int(record.step, "telemetry.step")
    _validate_optional_non_negative_int(record.epoch, "telemetry.epoch")
    _validate_optional_finite_number(record.step_time_ms, "telemetry.step_time_ms")
    _validate_optional_finite_number(record.data_time_ms, "telemetry.data_time_ms")
    _validate_optional_finite_number(record.compute_time_ms, "telemetry.compute_time_ms")
    _validate_percentage(record.cpu_usage_percent, "telemetry.cpu_usage_percent")
    _validate_optional_text(record.cpu_fallback_reason, "telemetry.cpu_fallback_reason")
    _validate_optional_non_negative_int(record.ram_used_bytes, "telemetry.ram_used_bytes")
    _validate_optional_non_negative_int(record.ram_total_bytes, "telemetry.ram_total_bytes")
    _validate_optional_non_negative_int(record.ram_available_bytes, "telemetry.ram_available_bytes")
    _validate_percentage(record.ram_usage_percent, "telemetry.ram_usage_percent")
    _validate_optional_non_negative_int(record.process_rss_bytes, "telemetry.process_rss_bytes")
    _validate_optional_non_negative_int(record.gpu_index, "telemetry.gpu_index")
    _validate_optional_text(record.gpu_name, "telemetry.gpu_name")
    _validate_percentage(record.gpu_utilization_percent, "telemetry.gpu_utilization_percent")
    _validate_optional_non_negative_int(record.gpu_memory_used_bytes, "telemetry.gpu_memory_used_bytes")
    _validate_optional_non_negative_int(record.gpu_memory_total_bytes, "telemetry.gpu_memory_total_bytes")
    _validate_percentage(record.gpu_memory_usage_percent, "telemetry.gpu_memory_usage_percent")
    _validate_optional_text(record.gpu_fallback_reason, "telemetry.gpu_fallback_reason")
    if record.metadata is not None:
        _assert_json_serializable(record.metadata, "telemetry.metadata")


def _validate_batch_diagnostic(record: BatchDiagnosticRecord) -> None:
    _validate_non_empty_text(record.id, "batch_diagnostic.id")
    _validate_non_empty_text(record.run_id, "batch_diagnostic.run_id")
    _validate_non_negative_int(record.sequence_id, "batch_diagnostic.sequence_id")
    _validate_finite_number(record.loss, "batch_diagnostic.loss")
    _validate_timestamp(record.timestamp, "batch_diagnostic.timestamp")
    _validate_optional_non_negative_int(record.step, "batch_diagnostic.step")
    _validate_optional_non_negative_int(record.epoch, "batch_diagnostic.epoch")
    _validate_optional_non_negative_int(record.batch_size, "batch_diagnostic.batch_size")
    for index, sample_id in enumerate(record.sample_ids):
        _validate_non_empty_text(sample_id, f"batch_diagnostic.sample_ids[{index}]")
    for label, count in record.class_counts.items():
        _validate_non_empty_text(label, "batch_diagnostic.class_counts key")
        _validate_non_negative_int(count, f"batch_diagnostic.class_counts[{label}]")
    for index, length in enumerate(record.sequence_lengths):
        _validate_non_negative_int(length, f"batch_diagnostic.sequence_lengths[{index}]")
    if record.metadata is not None:
        _assert_json_serializable(record.metadata, "batch_diagnostic.metadata")


def _validate_layer_summary(record: LayerSummaryRecord) -> None:
    _validate_non_empty_text(record.id, "layer_summary.id")
    _validate_non_empty_text(record.run_id, "layer_summary.run_id")
    _validate_non_negative_int(record.sequence_id, "layer_summary.sequence_id")
    _validate_non_negative_int(record.step, "layer_summary.step")
    _validate_optional_non_negative_int(record.epoch, "layer_summary.epoch")
    _validate_non_empty_text(record.module_name, "layer_summary.module_name")
    _validate_non_empty_text(record.module_type, "layer_summary.module_type")
    _validate_non_negative_int(record.call_index, "layer_summary.call_index")
    _validate_non_negative_int(record.element_count, "layer_summary.element_count")
    _validate_timestamp(record.timestamp, "layer_summary.timestamp")
    if record.tensor_shape is not None:
        for index, dimension in enumerate(record.tensor_shape):
            _validate_non_negative_int(dimension, f"layer_summary.tensor_shape[{index}]")
    _validate_optional_finite_number(record.mean, "layer_summary.mean")
    _validate_optional_finite_number(record.std, "layer_summary.std")
    _validate_optional_finite_number(record.norm, "layer_summary.norm")
    _validate_optional_finite_number(record.variance, "layer_summary.variance")
    _validate_ratio(record.dead_unit_ratio, "layer_summary.dead_unit_ratio")
    _validate_ratio(record.saturation_ratio, "layer_summary.saturation_ratio")
    if record.metadata is not None:
        _assert_json_serializable(record.metadata, "layer_summary.metadata")


_RAW_TENSOR_METADATA_KEYS = {
    "gradient_tensor",
    "parameter_tensor",
    "raw_tensor",
    "raw_tensor_payload",
    "tensor",
    "tensor_payload",
    "update_tensor",
}


def _assert_no_raw_tensor_metadata_keys(payload: Any, label: str) -> None:
    if isinstance(payload, Mapping):
        for key, value in payload.items():
            if str(key) in _RAW_TENSOR_METADATA_KEYS:
                raise SchemaValidationError(
                    f"{label} must not include raw tensor payload key '{key}'."
                )
            _assert_no_raw_tensor_metadata_keys(value, label)
    elif isinstance(payload, (list, tuple)):
        for item in payload:
            _assert_no_raw_tensor_metadata_keys(item, label)


def _validate_positive_int(value: Any, label: str) -> int:
    normalized = _validate_non_negative_int(value, label)
    if normalized <= 0:
        raise SchemaValidationError(f"{label} must be positive.")
    return normalized


def _validate_optional_ratio(value: Any, label: str) -> float | None:
    return _validate_ratio(value, label)


def _validate_parameter_summary(record: ParameterSummaryRecord) -> None:
    _validate_non_empty_text(record.id, "parameter_summary.id")
    _validate_non_empty_text(record.run_id, "parameter_summary.run_id")
    _validate_non_negative_int(record.sequence_id, "parameter_summary.sequence_id")
    _validate_non_negative_int(record.step, "parameter_summary.step")
    _validate_optional_non_negative_int(record.epoch, "parameter_summary.epoch")
    _validate_timestamp(record.timestamp, "parameter_summary.timestamp")
    _validate_non_empty_text(record.module_name, "parameter_summary.module_name")
    _validate_optional_text(record.module_type, "parameter_summary.module_type")
    _validate_non_empty_text(record.parameter_name, "parameter_summary.parameter_name")
    _validate_non_empty_text(record.parameter_key, "parameter_summary.parameter_key")
    if record.parameter_role not in {"weight", "bias", "other"}:
        raise SchemaValidationError("parameter_summary.parameter_role must be weight, bias, or other.")
    _validate_positive_int(record.element_count, "parameter_summary.element_count")
    if record.tensor_shape is not None:
        for index, dimension in enumerate(record.tensor_shape):
            _validate_non_negative_int(dimension, f"parameter_summary.tensor_shape[{index}]")
    _validate_optional_text(record.dtype, "parameter_summary.dtype")
    for field_name in (
        "mean",
        "std",
        "variance",
        "norm",
        "min",
        "max",
        "abs_mean",
        "abs_max",
        "grad_mean",
        "grad_std",
        "grad_norm",
        "grad_abs_max",
        "update_norm",
        "update_mean",
        "update_std",
    ):
        _validate_optional_finite_number(getattr(record, field_name), f"parameter_summary.{field_name}")
    for field_name in ("zero_ratio", "near_zero_ratio", "finite_ratio", "update_to_parameter_norm_ratio"):
        _validate_optional_ratio(getattr(record, field_name), f"parameter_summary.{field_name}")
    for field_name in ("nan_count", "inf_count", "optimizer_group_index"):
        _validate_optional_non_negative_int(getattr(record, field_name), f"parameter_summary.{field_name}")
    _validate_optional_text(record.optimizer_name, "parameter_summary.optimizer_name")
    if record.metadata is not None:
        _assert_json_serializable(record.metadata, "parameter_summary.metadata")
        _assert_no_raw_tensor_metadata_keys(record.metadata, "parameter_summary.metadata")


def _validate_parameter_distribution(record: ParameterDistributionRecord) -> None:
    _validate_non_empty_text(record.id, "parameter_distribution.id")
    _validate_non_empty_text(record.run_id, "parameter_distribution.run_id")
    _validate_non_negative_int(record.sequence_id, "parameter_distribution.sequence_id")
    _validate_non_negative_int(record.step, "parameter_distribution.step")
    _validate_optional_non_negative_int(record.epoch, "parameter_distribution.epoch")
    _validate_timestamp(record.timestamp, "parameter_distribution.timestamp")
    _validate_non_empty_text(record.module_name, "parameter_distribution.module_name")
    _validate_optional_text(record.module_type, "parameter_distribution.module_type")
    _validate_non_empty_text(record.parameter_name, "parameter_distribution.parameter_name")
    _validate_non_empty_text(record.parameter_key, "parameter_distribution.parameter_key")
    if record.parameter_role not in {"weight", "bias", "other"}:
        raise SchemaValidationError("parameter_distribution.parameter_role must be weight, bias, or other.")
    if record.distribution_type not in {"value", "gradient", "update"}:
        raise SchemaValidationError("parameter_distribution.distribution_type must be value, gradient, or update.")
    if len(record.bin_edges) != len(record.counts) + 1:
        raise SchemaValidationError("parameter_distribution.bin_edges length must equal len(counts) + 1.")
    previous_edge: float | None = None
    for index, edge in enumerate(record.bin_edges):
        normalized_edge = _validate_finite_number(edge, f"parameter_distribution.bin_edges[{index}]")
        if previous_edge is not None and normalized_edge <= previous_edge:
            raise SchemaValidationError("parameter_distribution.bin_edges must be strictly increasing.")
        previous_edge = normalized_edge
    for index, count in enumerate(record.counts):
        _validate_non_negative_int(count, f"parameter_distribution.counts[{index}]")
    _validate_non_negative_int(record.sample_count, "parameter_distribution.sample_count")
    _validate_optional_non_negative_int(record.total_count, "parameter_distribution.total_count")
    _validate_optional_finite_number(record.min, "parameter_distribution.min")
    _validate_optional_finite_number(record.max, "parameter_distribution.max")
    for field_name in ("nan_count", "inf_count", "clipped_low_count", "clipped_high_count"):
        _validate_optional_non_negative_int(getattr(record, field_name), f"parameter_distribution.{field_name}")
    _validate_optional_text(record.summary_id, "parameter_distribution.summary_id")
    if record.metadata is not None:
        _assert_json_serializable(record.metadata, "parameter_distribution.metadata")
        _assert_no_raw_tensor_metadata_keys(record.metadata, "parameter_distribution.metadata")


def _validate_artifact(record: ArtifactRecord) -> None:
    _validate_non_empty_text(record.id, "artifact.id")
    _validate_non_empty_text(record.run_id, "artifact.run_id")
    _validate_non_negative_int(record.sequence_id, "artifact.sequence_id")
    _validate_non_empty_text(record.artifact_type, "artifact.artifact_type")
    if record.namespace:
        _validate_non_empty_text(record.namespace, "artifact.namespace")
    _validate_non_empty_text(record.artifact_name, "artifact.artifact_name")
    _validate_non_empty_text(record.artifact_key, "artifact.artifact_key")
    _validate_non_empty_text(record.content_type, "artifact.content_type")
    _validate_non_empty_text(record.payload_path, "artifact.payload_path")
    _validate_non_empty_text(record.payload_relative_path, "artifact.payload_relative_path")
    _validate_non_empty_text(record.metadata_path, "artifact.metadata_path")
    _validate_non_empty_text(record.metadata_relative_path, "artifact.metadata_relative_path")
    _validate_optional_non_negative_int(record.step, "artifact.step")
    _validate_optional_non_negative_int(record.epoch, "artifact.epoch")
    _validate_timestamp(record.timestamp, "artifact.timestamp")
    _validate_non_negative_int(record.size_bytes, "artifact.size_bytes")
    if _SHA256_PATTERN.fullmatch(record.sha256) is None:
        raise SchemaValidationError("artifact.sha256 must be a lowercase hexadecimal SHA-256 digest.")
    if record.metadata is not None:
        _assert_json_serializable(record.metadata, "artifact.metadata")


def _validate_evidence(record: EvidenceRecord) -> None:
    _validate_non_empty_text(record.kind, "evidence.kind")
    _validate_non_empty_text(record.source, "evidence.source")
    _validate_non_empty_text(record.summary, "evidence.summary")
    _validate_optional_non_negative_int(record.step, "evidence.step")
    _validate_optional_non_negative_int(record.step_start, "evidence.step_start")
    _validate_optional_non_negative_int(record.step_end, "evidence.step_end")
    if (
        record.step_start is not None
        and record.step_end is not None
        and record.step_start > record.step_end
    ):
        raise SchemaValidationError("evidence.step_start must be <= evidence.step_end.")
    if record.metadata is not None:
        _assert_json_serializable(record.metadata, "evidence.metadata")


def _validate_alert(record: AlertRecord) -> None:
    _validate_non_empty_text(record.id, "alert.id")
    _validate_non_empty_text(record.run_id, "alert.run_id")
    _validate_non_negative_int(record.sequence_id, "alert.sequence_id")
    _validate_non_empty_text(record.rule_id, "alert.rule_id")
    _validate_non_empty_text(record.title, "alert.title")
    _validate_non_empty_text(record.summary, "alert.summary")
    _validate_non_empty_text(record.fact, "alert.fact")
    _validate_non_empty_text(record.inference, "alert.inference")
    _validate_timestamp(record.timestamp, "alert.timestamp")
    _validate_optional_non_negative_int(record.step, "alert.step")
    _validate_optional_non_negative_int(record.step_start, "alert.step_start")
    _validate_optional_non_negative_int(record.step_end, "alert.step_end")
    if record.step_start is not None and record.step_end is not None and record.step_start > record.step_end:
        raise SchemaValidationError("alert.step_start must be <= alert.step_end.")
    if record.metadata is not None:
        _assert_json_serializable(record.metadata, "alert.metadata")


def _validate_health_score(record: HealthScoreRecord) -> None:
    _validate_non_empty_text(record.id, "health_score.id")
    _validate_non_empty_text(record.run_id, "health_score.run_id")
    _validate_non_negative_int(record.sequence_id, "health_score.sequence_id")
    _validate_non_empty_text(record.category, "health_score.category")
    _validate_percentage(record.score, "health_score.score")
    _validate_non_empty_text(record.status, "health_score.status")
    _validate_non_empty_text(record.summary, "health_score.summary")
    _validate_timestamp(record.timestamp, "health_score.timestamp")
    _validate_optional_non_negative_int(record.step_start, "health_score.step_start")
    _validate_optional_non_negative_int(record.step_end, "health_score.step_end")
    if record.step_start is not None and record.step_end is not None and record.step_start > record.step_end:
        raise SchemaValidationError("health_score.step_start must be <= health_score.step_end.")
    for index, alert_id in enumerate(record.contributing_alert_ids):
        _validate_non_empty_text(alert_id, f"health_score.contributing_alert_ids[{index}]")
    if record.metadata is not None:
        _assert_json_serializable(record.metadata, "health_score.metadata")


def _validate_phase(record: PhaseRecord) -> None:
    _validate_non_empty_text(record.id, "phase.id")
    _validate_non_empty_text(record.run_id, "phase.run_id")
    _validate_non_negative_int(record.sequence_id, "phase.sequence_id")
    _validate_non_empty_text(record.display_name, "phase.display_name")
    _validate_non_empty_text(record.summary, "phase.summary")
    _validate_non_negative_int(record.step_start, "phase.step_start")
    _validate_non_negative_int(record.step_end, "phase.step_end")
    if record.step_start > record.step_end:
        raise SchemaValidationError("phase.step_start must be <= phase.step_end.")
    _validate_timestamp(record.timestamp, "phase.timestamp")
    if record.metadata is not None:
        _assert_json_serializable(record.metadata, "phase.metadata")


def _validate_run(record: RunRecord) -> None:
    _validate_non_empty_text(record.id, "run.id")
    _validate_non_empty_text(record.project_id, "run.project_id")
    _validate_non_empty_text(record.project_name, "run.project_name")
    _validate_timestamp(record.created_at, "run.created_at")
    _validate_timestamp(record.started_at, "run.started_at")
    _validate_timestamp(record.updated_at, "run.updated_at")
    if record.ended_at is not None:
        _validate_timestamp(record.ended_at, "run.ended_at")
    if record.parent_run_id is not None:
        _validate_non_empty_text(record.parent_run_id, "run.parent_run_id")
    _validate_non_empty_text(record.lineage_root_run_id, "run.lineage_root_run_id")
    _validate_non_negative_int(record.resume_count, "run.resume_count")
    if record.latest_resume_at is not None:
        _validate_timestamp(record.latest_resume_at, "run.latest_resume_at")
    _validate_non_empty_text(record.config_path, "run.config_path")
    _validate_non_empty_text(record.environment_path, "run.environment_path")
    _validate_non_empty_text(record.run_path, "run.run_path")
    if record.git_commit is not None:
        _validate_non_empty_text(record.git_commit, "run.git_commit")
    seen_tags: set[str] = set()
    for index, tag in enumerate(record.tags):
        normalized_tag = _validate_non_empty_text(tag, f"run.tags[{index}]")
        if normalized_tag in seen_tags:
            raise SchemaValidationError(f"run.tags contains a duplicate value '{normalized_tag}'.")
        seen_tags.add(normalized_tag)


_VALIDATORS = {
    ProjectRecord: _validate_project,
    NoteRecord: _validate_note,
    LifecycleEventRecord: _validate_lifecycle_event,
    EventRecord: _validate_event,
    MetricRecord: _validate_metric,
    SystemTelemetryRecord: _validate_system_telemetry,
    BatchDiagnosticRecord: _validate_batch_diagnostic,
    LayerSummaryRecord: _validate_layer_summary,
    ParameterSummaryRecord: _validate_parameter_summary,
    ParameterDistributionRecord: _validate_parameter_distribution,
    ArtifactRecord: _validate_artifact,
    EvidenceRecord: _validate_evidence,
    AlertRecord: _validate_alert,
    HealthScoreRecord: _validate_health_score,
    PhaseRecord: _validate_phase,
    RunRecord: _validate_run,
}
