"""Comparison strategies for eval assertions.

Supports:
1. schema_validate — validate JSON against a named schema (stub, checks structure)
2. golden_exact — byte-exact comparison
3. golden_structural — schema-aware, order-insensitive comparison
4. numeric_tolerance — per-field numeric tolerance
5. regex_assert — regex pattern matching in text
6. exact — strict equality
7. structural — structural comparison
8. presence_assert — assert expected artifacts exist in evidence pack
"""
from __future__ import annotations

import json
import math
import re
from typing import Any


def compare_exact(actual: Any, expected: Any) -> tuple[bool, str]:
    """Byte-exact comparison."""
    if actual == expected:
        return True, "Exact match"
    return False, f"Values differ: actual={_short(actual)}, expected={_short(expected)}"


def compare_golden_exact(actual: Any, expected: Any) -> tuple[bool, str]:
    """Golden byte-exact: serialize both to canonical JSON and compare."""
    a_str = json.dumps(actual, sort_keys=True, ensure_ascii=False)
    e_str = json.dumps(expected, sort_keys=True, ensure_ascii=False)
    if a_str == e_str:
        return True, "Golden exact match"
    # Generate diff
    diff_lines = []
    for i, (ac, ec) in enumerate(zip(a_str, e_str, strict=False)):
        if ac != ec:
            diff_lines.append(f"First difference at position {i}: actual='{ac}', expected='{ec}'")
            break
    if len(a_str) != len(e_str):
        diff_lines.append(f"Length differs: actual={len(a_str)}, expected={len(e_str)}")
    return False, "; ".join(diff_lines) if diff_lines else "Content differs"


def compare_golden_structural(actual: Any, expected: Any) -> tuple[bool, str]:
    """Schema-aware structural comparison — order-insensitive for dicts and sets."""
    return _structural_compare(actual, expected, path="$")


def compare_numeric_tolerance(
    actual: Any, expected: Any, tolerance: float = 1e-6
) -> tuple[bool, str]:
    """Recursive numeric comparison with tolerance."""
    return _numeric_compare(actual, expected, tolerance, path="$")


def compare_regex_assert(actual: Any, expected: Any) -> tuple[bool, str]:
    """Assert regex patterns match in text.

    expected should be a string (single pattern) or list of patterns.
    actual should be a string to search in.
    """
    if not isinstance(actual, str):
        actual = json.dumps(actual, ensure_ascii=False)

    patterns = expected if isinstance(expected, list) else [expected]
    failures = []
    for pattern in patterns:
        if not re.search(pattern, actual):
            failures.append(f"Pattern not found: {pattern}")

    if failures:
        return False, "; ".join(failures)
    return True, f"All {len(patterns)} pattern(s) matched"


def compare_presence_assert(actual: Any, expected: Any) -> tuple[bool, str]:
    """Assert that expected artifacts are present.

    expected: list of artifact names/paths that should exist
    actual: list of actual artifact names/paths or a dict with keys
    """
    if isinstance(actual, dict):
        actual_set = set(actual.keys())
    elif isinstance(actual, list):
        actual_set = set(str(x) for x in actual)
    else:
        return False, f"Cannot check presence in type {type(actual).__name__}"

    expected_items = expected if isinstance(expected, list) else [expected]
    missing = [item for item in expected_items if item not in actual_set]

    if missing:
        return False, f"Missing artifacts: {missing}"
    return True, f"All {len(expected_items)} artifact(s) present"


def compare_schema_validate(actual: Any, expected: Any) -> tuple[bool, str]:
    """Validate actual data against expected schema structure.

    Since we can't import jsonschema without adding a dependency,
    we do a structural validation: expected defines required keys and types.
    If expected is a dict with "$required_keys", validate those keys exist.
    Otherwise, do structural comparison.
    """
    if isinstance(expected, dict) and "$required_keys" in expected:
        required = expected["$required_keys"]
        if not isinstance(actual, dict):
            return False, f"Expected dict, got {type(actual).__name__}"
        missing = [k for k in required if k not in actual]
        if missing:
            return False, f"Missing required keys: {missing}"
        return True, "Schema validation passed"

    # Fallback: structural comparison
    return _structural_compare(actual, expected, path="$")


COMPARATORS = {
    "exact": compare_exact,
    "golden_exact": compare_golden_exact,
    "golden_structural": compare_golden_structural,
    "numeric_tolerance": compare_numeric_tolerance,
    "regex_assert": compare_regex_assert,
    "presence_assert": compare_presence_assert,
    "schema_validate": compare_schema_validate,
    "structural": compare_golden_structural,
}


def _structural_compare(actual: Any, expected: Any, path: str) -> tuple[bool, str]:
    """Recursive structural comparison."""
    if isinstance(expected, dict):
        if not isinstance(actual, dict):
            return False, f"At {path}: expected dict, got {type(actual).__name__}"
        for key in sorted(expected.keys()):
            if key not in actual:
                return False, f"At {path}: missing key '{key}'"
            ok, msg = _structural_compare(actual[key], expected[key], f"{path}.{key}")
            if not ok:
                return False, msg
        return True, "Structural match"

    if isinstance(expected, list):
        if not isinstance(actual, list):
            return False, f"At {path}: expected list, got {type(actual).__name__}"
        if len(actual) != len(expected):
            return False, (
                f"At {path}: length differs: actual={len(actual)}, expected={len(expected)}"
            )
        for i, (a, e) in enumerate(zip(actual, expected, strict=True)):
            ok, msg = _structural_compare(a, e, f"{path}[{i}]")
            if not ok:
                return False, msg
        return True, "Structural match"

    if type(actual) is not type(expected):
        return False, (
            f"At {path}: type differs: "
            f"actual={type(actual).__name__}, expected={type(expected).__name__}"
        )

    if actual != expected:
        return False, (
            f"At {path}: values differ: actual={_short(actual)}, expected={_short(expected)}"
        )

    return True, "Match"


def _numeric_compare(
    actual: Any, expected: Any, tolerance: float, path: str
) -> tuple[bool, str]:
    """Recursive numeric comparison with tolerance."""
    if isinstance(expected, dict):
        if not isinstance(actual, dict):
            return False, f"At {path}: expected dict, got {type(actual).__name__}"
        for key in sorted(expected.keys()):
            if key not in actual:
                return False, f"At {path}: missing key '{key}'"
            ok, msg = _numeric_compare(actual[key], expected[key], tolerance, f"{path}.{key}")
            if not ok:
                return False, msg
        return True, "Numeric match within tolerance"

    if isinstance(expected, list):
        if not isinstance(actual, list):
            return False, f"At {path}: expected list, got {type(actual).__name__}"
        if len(actual) != len(expected):
            return False, f"At {path}: length differs"
        for i, (a, e) in enumerate(zip(actual, expected, strict=True)):
            ok, msg = _numeric_compare(a, e, tolerance, f"{path}[{i}]")
            if not ok:
                return False, msg
        return True, "Numeric match within tolerance"

    if isinstance(expected, (int, float)) and isinstance(actual, (int, float)):
        if math.isnan(expected) and math.isnan(actual):
            return True, "Both NaN"
        if math.isnan(expected) or math.isnan(actual):
            return False, f"At {path}: NaN mismatch"
        if abs(actual - expected) > tolerance:
            return False, (
                f"At {path}: {actual} differs from {expected} "
                f"by {abs(actual - expected)} (tolerance={tolerance})"
            )
        return True, "Within tolerance"

    if actual != expected:
        return False, f"At {path}: {_short(actual)} != {_short(expected)}"
    return True, "Match"


def _short(val: Any, maxlen: int = 80) -> str:
    s = repr(val)
    return s if len(s) <= maxlen else s[:maxlen] + "..."
