"""Config merge engine — deep-merge required_config dicts from multiple frameworks.

Conflict resolution rules (DO NOT CHANGE):
    - Booleans: True wins (more secure)
    - Enums in STRICTNESS_ORDER: most restrictive wins (lowest index)
    - Numerics: max() wins (longer retention, higher threshold)
    - Strings: if identical → keep; if different → raise ConfigMergeConflictError
    - Dicts: recurse
    - Lists: union (deduplicate)
"""

from __future__ import annotations

from typing import Any

from complyform.core.errors import ConfigMergeConflictError

STRICTNESS_ORDER: dict[str, list[str]] = {
    "ssl_mode": [
        "ENCRYPTED_ONLY",
        "TRUSTED_CLIENT_CERTIFICATE_REQUIRED",
        "ALLOW_UNENCRYPTED_AND_ENCRYPTED",
    ],
    "minimum_tls_version": ["TLS_1_3", "TLS_1_2", "TLS_1_1", "TLS_1_0"],
    "public_access_prevention": ["enforced", "inherited"],
    "ingress": [
        "INGRESS_TRAFFIC_INTERNAL_ONLY",
        "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER",
        "INGRESS_TRAFFIC_ALL",
    ],
}

# Reverse index: value → (key, rank)
_ENUM_RANK: dict[str, tuple[str, int]] = {}
for _key, _values in STRICTNESS_ORDER.items():
    for _idx, _val in enumerate(_values):
        _ENUM_RANK[_val] = (_key, _idx)


def merge_required_configs(
    configs: list[tuple[str, str, dict[str, Any]]],
) -> tuple[dict[str, Any], list[str]]:
    """Deep-merge multiple required_config dicts for the same resource type.

    Args:
        configs: List of (framework_id, control_id, required_config) tuples.

    Returns:
        merged: The merged config dict
        trace: List of "framework_id.control_id" strings for comments
    """
    trace: list[str] = []
    merged: dict[str, Any] = {}

    for framework_id, control_id, config in configs:
        trace.append(f"{framework_id}.{control_id}")
        merged = _deep_merge(merged, config, trace_context=trace)

    # Deduplicate trace
    seen: set[str] = set()
    unique_trace: list[str] = []
    for t in trace:
        if t not in seen:
            seen.add(t)
            unique_trace.append(t)

    return merged, unique_trace


def _deep_merge(
    base: dict[str, Any],
    override: dict[str, Any],
    *,
    trace_context: list[str],
) -> dict[str, Any]:
    """Recursively merge override into base."""
    result = dict(base)
    for key, new_val in override.items():
        if key not in result:
            result[key] = new_val
            continue

        old_val = result[key]
        result[key] = _merge_value(key, old_val, new_val, trace_context=trace_context)

    return result


def _merge_value(
    key: str,
    old: Any,
    new: Any,
    *,
    trace_context: list[str],
) -> Any:
    """Merge two values using conflict resolution rules."""
    # Both dicts → recurse
    if isinstance(old, dict) and isinstance(new, dict):
        return _deep_merge(old, new, trace_context=trace_context)

    # Both lists → union (deduplicate)
    if isinstance(old, list) and isinstance(new, list):
        seen: set[str] = set()
        result: list[Any] = []
        for item in [*old, *new]:
            item_key = str(item)
            if item_key not in seen:
                seen.add(item_key)
                result.append(item)
        return result

    # Both booleans → True wins
    if isinstance(old, bool) and isinstance(new, bool):
        return old or new

    # Check STRICTNESS_ORDER enums (before numeric check since bool is int subclass)
    if isinstance(old, str) and isinstance(new, str):
        if old in _ENUM_RANK and new in _ENUM_RANK:
            old_key, old_rank = _ENUM_RANK[old]
            new_key, new_rank = _ENUM_RANK[new]
            if old_key == new_key:
                return old if old_rank <= new_rank else new

        # Same string → keep
        if old == new:
            return old

        # Different strings → conflict
        raise ConfigMergeConflictError(
            attribute=key,
            frameworks=_trace_frameworks(trace_context),
            values=[str(old), str(new)],
        )

    # Both numeric → max wins
    if isinstance(old, (int, float)) and isinstance(new, (int, float)):
        return max(old, new)

    # Identical values
    if old == new:
        return old

    # Type mismatch — conflict
    raise ConfigMergeConflictError(
        attribute=key,
        frameworks=_trace_frameworks(trace_context),
        values=[str(old), str(new)],
    )


def _trace_frameworks(trace_context: list[str]) -> list[str]:
    """Extract first and last framework refs from trace context."""
    if len(trace_context) >= 2:
        return [trace_context[0], trace_context[-1]]
    return list(trace_context)
