"""
Baseline serialization and file management.

Writes :class:`~test_runner.models.BaselineData` instances to JSON files
following the convention::

    {baseline_dir}/{YYYYMMDD}/{safe_proc_name}/case_{NNN}_{hash}.json

Values that are not natively JSON-serializable (``datetime``, ``Decimal``,
``bytes``, ...) are converted to JSON-safe representations.  The actual
cross-platform normalization is handled by the ``snowflake-data-validation``
framework's validators at comparison time.
"""

from __future__ import annotations

import hashlib
import json
from datetime import date, datetime, timezone
from decimal import Decimal
from pathlib import Path
from typing import Any, Optional

from .models import BaselineData


# ---------------------------------------------------------------------------
# JSON serialization (minimal -- just make values JSON-safe)
# ---------------------------------------------------------------------------

def _serialize_value(value: Any) -> Any:
    """Convert a single value to a JSON-safe representation."""
    if value is None:
        return None
    if isinstance(value, datetime):
        return value.isoformat()
    if isinstance(value, date):
        return value.isoformat()
    if isinstance(value, Decimal):
        return str(value)
    if isinstance(value, bytes):
        return value.hex()
    if isinstance(value, bytearray):
        return bytes(value).hex()
    return value


def _serialize_row(row: dict[str, Any]) -> dict[str, Any]:
    """Serialize all values in a row dict."""
    return {k: _serialize_value(v) for k, v in row.items()}


def serialize_result_sets(
    result_sets: list[list[dict[str, Any]]],
) -> list[list[dict[str, Any]]]:
    """Make an entire list of result sets JSON-safe."""
    return [
        [_serialize_row(row) for row in rs]
        for rs in result_sets
    ]


def _serialize_table_deltas(
    table_deltas: Optional[dict[str, Any]],
) -> Optional[dict[str, Any]]:
    """Make table delta dicts JSON-safe.

    Delta dicts contain ``inserts``, ``deletes``, and ``updates`` lists
    whose row values may include ``datetime``, ``Decimal``, or ``bytes``
    objects that need conversion before ``json.dump``.
    """
    if not table_deltas:
        return None
    safe: dict[str, Any] = {}
    for table_name, delta in table_deltas.items():
        safe_delta = dict(delta)
        for key in ("inserts", "deletes", "updates"):
            if key in safe_delta and isinstance(safe_delta[key], list):
                safe_delta[key] = [_serialize_row(row) for row in safe_delta[key]]
        safe[table_name] = safe_delta
    return safe


def compute_params_hash(parameters: dict[str, Any]) -> str:
    """Compute the 8-character MD5 hash used to identify a parameter set.

    Values are serialized to JSON-safe representations first so the hash
    matches what was stored during capture.
    """
    serialized = {k: _serialize_value(v) for k, v in parameters.items()}
    param_str = json.dumps(serialized, sort_keys=True, default=str)
    return hashlib.md5(param_str.encode()).hexdigest()[:8]


# ---------------------------------------------------------------------------
# File paths
# ---------------------------------------------------------------------------

def get_baseline_dir(
    base_dir: str | Path,
    code_unit_name: str,
    capture_date: datetime | None = None,
) -> Path:
    """Return the directory where baselines for *code_unit_name* are stored.

    Layout::

        {base_dir}/{YYYYMMDD}/{safe_proc_name}/
    """
    if capture_date is None:
        capture_date = datetime.now(timezone.utc)
    date_folder = capture_date.strftime("%Y%m%d")
    safe_name = code_unit_name.replace(".", "_").replace(" ", "_")
    return Path(base_dir) / date_folder / safe_name


def get_baseline_path(
    base_dir: str | Path,
    code_unit_name: str,
    case_index: int,
    params_hash: str,
    capture_date: datetime | None = None,
) -> Path:
    """Return the full file path for a single baseline JSON.

    Layout::

        {base_dir}/{YYYYMMDD}/{safe_proc_name}/case_{NNN}_{hash}.json
    """
    proc_dir = get_baseline_dir(base_dir, code_unit_name, capture_date)
    return proc_dir / f"case_{case_index:03d}_{params_hash}.json"


# ---------------------------------------------------------------------------
# Write
# ---------------------------------------------------------------------------

def write_baseline(
    baseline: BaselineData,
    base_dir: str | Path,
    case_index: int,
    capture_date: datetime | None = None,
) -> Path:
    """Serialize and write *baseline* to disk.

    Values are made JSON-safe but **not** cross-platform-normalized.
    Normalization is deferred to comparison time where the
    ``snowflake-data-validation`` framework handles it.

    Creates parent directories as needed.  Returns the path of the
    written file.
    """
    safe_baseline = BaselineData(
        procedure=baseline.procedure,
        parameters={k: _serialize_value(v) for k, v in baseline.parameters.items()},
        captured_at=baseline.captured_at,
        result_sets=serialize_result_sets(baseline.result_sets),
        row_counts=baseline.row_counts,
        success=baseline.success,
        error=baseline.error,
        column_types=baseline.column_types,
        output_params={k: _serialize_value(v) for k, v in baseline.output_params.items()}
        if baseline.output_params else None,
        output_param_types=baseline.output_param_types,
        table_deltas=_serialize_table_deltas(baseline.table_deltas),
        column_metadata=baseline.column_metadata,
    )

    file_path = get_baseline_path(
        base_dir,
        baseline.procedure,
        case_index,
        safe_baseline.params_hash,
        capture_date,
    )
    file_path.parent.mkdir(parents=True, exist_ok=True)

    with open(file_path, "w", encoding="utf-8") as fh:
        json.dump(safe_baseline.to_dict(), fh, indent=2, default=str)

    return file_path


# ---------------------------------------------------------------------------
# Read
# ---------------------------------------------------------------------------

def load_baseline(file_path: str | Path) -> Optional[BaselineData]:
    """Load a baseline JSON file from disk into a :class:`BaselineData`.

    Returns ``None`` if the file does not exist or cannot be parsed.
    """
    path = Path(file_path)
    if not path.exists():
        return None

    try:
        with open(path, "r", encoding="utf-8") as fh:
            data: dict[str, Any] = json.load(fh)

        return BaselineData(
            procedure=data.get("procedure", ""),
            parameters=data.get("parameters", {}),
            captured_at=data.get("captured_at", ""),
            result_sets=data.get("result_sets", []),
            row_counts=data.get("row_counts", []),
            success=data.get("success", True),
            error=data.get("error"),
            column_types=data.get("column_types", []),
            output_params=data.get("output_params"),
            output_param_types=data.get("output_param_types"),
            table_deltas=data.get("table_deltas"),
            column_metadata=data.get("column_metadata"),
        )
    except (json.JSONDecodeError, KeyError):
        return None


def discover_baselines(baseline_dir: str | Path) -> list[Path]:
    """Find all baseline JSON files under *baseline_dir*.

    Returns a sorted list of paths.
    """
    base = Path(baseline_dir)
    if not base.exists():
        return []
    return sorted(base.rglob("*.json"))
