from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional, Union, overload

from mockmesh.core.logger import get_logger

_log = get_logger("interceptors.loader")

_PROVIDER_FILES: Dict[str, tuple] = {
    "aws":      (Path(__file__).parent.parent / "defaults" / "aws.json",      "map"),
    "azure":    (Path(__file__).parent.parent / "defaults" / "azure.json",    "map"),
    "gcp":      (Path(__file__).parent.parent / "defaults" / "gcp.json",      "map"),
    "streaming": (Path(__file__).parent.parent / "defaults" / "streaming.json", "map"),
    "nosql":    (Path(__file__).parent.parent / "defaults" / "nosql.json",    "map"),
    "sql":      (Path(__file__).parent.parent / "defaults" / "sql.json",      "map"),
}


def load_all_provider_defaults(
    responses_folder: Optional[Path] = None,
) -> Dict[str, Any]:
    result: Dict[str, Any] = {}
    for provider, (default_path, mode) in _PROVIDER_FILES.items():
        base = load_json(default_path, mode)  # type: ignore[arg-type]
        override_path: Optional[Path] = None
        if responses_folder:
            candidate = responses_folder / f"{provider}.json"
            if candidate.is_file():
                override_path = candidate
        merged = merge_override(base, override_path, mode)  # type: ignore[arg-type]
        result[provider] = merged
        _log.debug(
            "Provider defaults loaded",
            extra={
                "mm_provider": provider,
                "mm_override": str(override_path) if override_path else "none",
            },
        )
    if "streaming" in result:
        streaming = result.pop("streaming")
        if "kafka" in streaming:
            kafka_data = streaming["kafka"]
            result["kafka"] = kafka_data.get("rules", [])
        if "rabbitmq" in streaming:
            rabbitmq_data = streaming["rabbitmq"]
            result["rabbitmq"] = rabbitmq_data.get("rules", [])
    if "nosql" in result:
        nosql = result.pop("nosql")
        if "mongodb" in nosql:
            result["mongodb"] = nosql["mongodb"]
        if "redis" in nosql:
            result["redis"] = nosql["redis"]
    _log.info(
        "All provider defaults loaded",
        extra={"mm_providers": list(result.keys()),
               "mm_folder": str(responses_folder or "built-in only")},
    )
    return result


def find_service_override(folder: Optional[Path], name: str) -> Optional[str]:
    if not folder:
        return None
    p = folder / f"{name}.json"
    return str(p) if p.is_file() else None

LoadMode = Literal["map", "rules"]


def _read_json(path: Path) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        _log.warning("JSON file not found", extra={"mm_path": str(path)})
    except json.JSONDecodeError as exc:
        _log.error("Malformed JSON file", extra={"mm_path": str(path), "mm_error": str(exc)})
    except OSError as exc:
        _log.error("Cannot read JSON file", extra={"mm_path": str(path), "mm_error": str(exc)})
    return None


def _strip_comments(raw: dict) -> dict:
    return {k: v for k, v in raw.items() if not k.startswith("_") or k == "_config"}


_CONFIG_PLACEHOLDER_RE = re.compile(r"\{_config\.(\w+)\}")


def _resolve_config_placeholders(data: Any, config: dict) -> Any:
    """Recursively replace {_config.xxx} placeholders in string values.

    Only top-level string-typed keys of *config* are resolvable.
    Nested dicts (like config["rds"] or config["auth"]) are left alone
    so they don't collide with runtime templates.
    """
    if isinstance(data, str):
        def _replacer(match: re.Match) -> str:
            key = match.group(1)
            val = config.get(key)
            if val is not None and isinstance(val, str):
                return val
            return match.group(0)
        return _CONFIG_PLACEHOLDER_RE.sub(_replacer, data)
    if isinstance(data, dict):
        return {k: _resolve_config_placeholders(v, config) for k, v in data.items()}
    if isinstance(data, list):
        return [_resolve_config_placeholders(item, config) for item in data]
    return data


@overload
def load_json(path: Path, mode: Literal["map"]) -> Dict[str, Any]: ...
@overload
def load_json(path: Path, mode: Literal["rules"]) -> List[dict]: ...

def load_json(path: Path, mode: LoadMode) -> Union[Dict[str, Any], List[dict]]:
    raw = _read_json(path)
    if raw is None:
        return {} if mode == "map" else []
    if mode == "map":
        if not isinstance(raw, dict):
            _log.error("Expected a JSON object for map mode",
                       extra={"mm_path": str(path), "mm_type": type(raw).__name__})
            return {}
        result = _strip_comments(raw)
        config = result.get("_config", {})
        if config:
            result = _resolve_config_placeholders(result, config)
        _log.debug("Loaded JSON map", extra={"mm_path": str(path), "mm_keys": list(result.keys())})
        return result
    if not isinstance(raw, dict) or "rules" not in raw:
        _log.error('Expected {"rules": [...]} for rules mode', extra={"mm_path": str(path)})
        return []
    rules = raw["rules"]
    if not isinstance(rules, list):
        _log.error('"rules" key is not a list',
                   extra={"mm_path": str(path), "mm_type": type(rules).__name__})
        return []
    _log.debug("Loaded JSON rules", extra={"mm_path": str(path), "mm_count": len(rules)})
    return rules


def merge_override(
    base: Union[Dict[str, Any], List[dict]],
    override_path: "str | Path | None",
    mode: LoadMode,
) -> Union[Dict[str, Any], List[dict]]:
    if not override_path:
        return base
    p        = Path(override_path)
    override = load_json(p, mode)  # type: ignore[arg-type]
    if mode == "map":
        merged: Dict[str, Any] = dict(base)  # type: ignore[arg-type]
        for key, value in override.items():  # type: ignore[union-attr]
            if isinstance(value, dict) and isinstance(merged.get(key), dict):
                merged[key] = {**merged[key], **value}
            else:
                merged[key] = value
        _log.info("Merged map override",
                  extra={"mm_path": str(p), "mm_override_keys": list(override.keys())})  # type: ignore[union-attr]
        return merged
    merged_rules: List[dict] = list(override) + list(base)  # type: ignore[arg-type]
    _log.info("Merged rules override",
              extra={"mm_path": str(p), "mm_user_rules": len(override),  # type: ignore[arg-type]
                     "mm_total": len(merged_rules)})
    return merged_rules
