"""Experiment manifest loading, config merging, and run-ID generation."""
from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any

from libs.backtest.domain import BacktestConfig, ExperimentManifest
from libs.backtest.snapshots import resolve_snapshot
from libs.common.ids import sha256_checksum_str
from libs.common.logging import get_logger
from libs.common.time_utils import utc_now

logger = get_logger(__name__)


def load_base_config(config_path: str | Path) -> dict[str, Any]:
    """Load a base config JSON file."""
    p = Path(config_path)
    if not p.exists():
        raise FileNotFoundError(f"Base config not found: {p}")
    return json.loads(p.read_text())


def deep_merge(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
    """Recursively merge overrides into base (overrides win on conflict)."""
    result = dict(base)
    for key, value in overrides.items():
        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
            result[key] = deep_merge(result[key], value)
        else:
            result[key] = value
    return result


def load_manifest(manifest_path: str | Path) -> ExperimentManifest:
    """Load and validate an experiment manifest JSON file."""
    p = Path(manifest_path)
    if not p.exists():
        raise FileNotFoundError(f"Manifest not found: {p}")
    raw = json.loads(p.read_text())
    return ExperimentManifest.model_validate(raw)


def resolve_config(
    manifest: ExperimentManifest,
    config_root: str | Path | None = None,
    snapshot_id_override: str | None = None,
) -> BacktestConfig:
    """Load base config, apply manifest overrides, validate into BacktestConfig.

    Args:
        manifest: The experiment manifest.
        config_root: Root directory for resolving relative config paths.
        snapshot_id_override: If provided, overrides the dataset_snapshot_id.
    """
    base_config_path = manifest.base_config
    if config_root is not None:
        resolved_path = Path(config_root) / base_config_path
        if resolved_path.exists():
            base_config_path = str(resolved_path)

    base = load_base_config(base_config_path)
    merged = deep_merge(base, manifest.overrides)

    requested_snapshot_id = snapshot_id_override or manifest.dataset_snapshot_id
    resolution = resolve_snapshot(requested_snapshot_id)
    merged["dataset_snapshot_id"] = resolution.canonical_snapshot_id
    merged["requested_snapshot_id"] = requested_snapshot_id
    merged["canonical_snapshot_id"] = resolution.canonical_snapshot_id
    if manifest.strategy_engines:
        merged["strategy_engines"] = [
            engine.model_dump(mode="json")
            for engine in manifest.strategy_engines
        ]

    return BacktestConfig.model_validate(merged)


def _safe_slug(text: str, max_len: int = 20) -> str:
    """Convert text to safe alphanumeric slug."""
    slug = re.sub(r"[^a-zA-Z0-9_-]", "_", text.strip())
    return slug[:max_len]


def generate_run_id(
    config: BacktestConfig,
    strategy_override: str | None = None,
) -> str:
    """Generate a unique, deterministic run ID.

    Format: bt_{safe_strategy}_{safe_snapshot[:12]}_{timestamp_us}_{config_hash[:8]}
    """
    strategy = _safe_slug(strategy_override or config.strategy_name)
    snapshot = _safe_slug(config.dataset_snapshot_id, max_len=12)
    timestamp = utc_now().strftime("%Y%m%d%H%M%S%f")
    config_json = config.model_dump_json(indent=None)
    config_hash = sha256_checksum_str(config_json)[:8]
    return f"bt_{strategy}_{snapshot}_{timestamp}_{config_hash}"


def save_resolved_config(
    config: BacktestConfig,
    run_dir: Path,
) -> Path:
    """Write resolved_config.json to the run directory."""
    out = run_dir / "resolved_config.json"
    out.write_text(config.model_dump_json(indent=2))
    return out


def save_manifest(
    manifest: ExperimentManifest,
    run_dir: Path,
) -> Path:
    """Write manifest.json (copy of experiment manifest) to the run directory."""
    out = run_dir / "manifest.json"
    out.write_text(manifest.model_dump_json(indent=2))
    return out
