"""verify_db_data implementation with permutation matching."""

from __future__ import annotations

from pathlib import Path
from typing import Any

from db_test_helpers.core._errors import UnsupportedError
from db_test_helpers.core._matchers import is_dsl_string, parse_matcher
from db_test_helpers.core._parse import ParsedGroup, ParsedRecord, parse_yaml
from db_test_helpers.core._types import Config, DatabaseAdapter, HierarchicalAdapter


class MatchError(Exception):
    """Internal: field comparison mismatch."""


def verify_db_data(adapter: DatabaseAdapter, filepath: str | Path, config: Config | None = None) -> None:
    """Verify that database state matches the expected YAML fixture.

    Args:
        adapter: Database adapter used to read records.
        filepath: Path to the YAML fixture file describing expected state.
        config: Optional configuration (e.g. variables for ``$var()``).

    Raises:
        AssertionError: If record counts or field values do not match.
        UnsupportedError: If ``__children__`` is used but the adapter does not
            implement ``HierarchicalAdapter``.
    """
    groups = parse_yaml(filepath)
    variables = config.variables if config else {}
    _verify_groups(adapter, groups, "", variables)


def _verify_groups(
    adapter: DatabaseAdapter,
    groups: list[ParsedGroup],
    parent_path: str,
    variables: dict[str, object],
) -> None:
    for group in groups:
        actuals = adapter.read_records(group.name, parent_path)
        expected_records = group.records

        if len(actuals) != len(expected_records):
            raise AssertionError(
                f"Group '{group.name}': expected {len(expected_records)} records, got {len(actuals)}"
            )

        pairs = _find_matching(expected_records, actuals, variables)

        for expected_record, actual_data in pairs:
            if expected_record.children:
                if not isinstance(adapter, HierarchicalAdapter):
                    raise UnsupportedError(
                        f"{type(adapter).__name__} does not support __children__"
                    )
                child_path = adapter.build_child_path(parent_path, group.name, actual_data)
                child_groups = [
                    ParsedGroup(name=name, records=records)
                    for name, records in expected_record.children.items()
                ]
                _verify_groups(adapter, child_groups, child_path, variables)


_DETAIL_RECORD_LIMIT = 5


def _find_matching(
    expected: list[ParsedRecord],
    actuals: list[dict[str, Any]],
    variables: dict[str, object],
) -> list[tuple[ParsedRecord, dict[str, Any]]]:
    n = len(expected)
    used = [False] * n
    pairs: list[tuple[int, int]] = []
    errors: list[tuple[int, int, MatchError]] = []

    def backtrack(ei: int) -> bool:
        if ei == n:
            return True
        for ai in range(n):
            if used[ai]:
                continue
            try:
                _compare_fields(expected[ei].fields, actuals[ai], variables, "")
                used[ai] = True
                pairs.append((ei, ai))
                if backtrack(ei + 1):
                    return True
                pairs.pop()
                used[ai] = False
            except MatchError as e:
                errors.append((ei, ai, e))
                continue
        return False

    if not backtrack(0):
        first_error = errors[0][2] if errors else None
        raise AssertionError(_build_permutation_error(expected, actuals, errors)) from first_error

    return [(expected[ei], actuals[ai]) for ei, ai in pairs]


def _build_permutation_error(
    expected: list[ParsedRecord],
    actuals: list[dict[str, Any]],
    errors: list[tuple[int, int, MatchError]],
) -> str:
    lines = ["No matching permutation found."]

    if len(expected) <= _DETAIL_RECORD_LIMIT:
        lines.append("")
        lines.append("Expected records:")
        for i, rec in enumerate(expected):
            lines.append(f"  [{i}] {rec.fields}")
        lines.append("Actual records:")
        for i, rec in enumerate(actuals):
            lines.append(f"  [{i}] {rec}")

    if errors:
        lines.append("")
        lines.append("Mismatches:")
        seen: set[tuple[int, int]] = set()
        for ei, ai, err in errors:
            if (ei, ai) in seen:
                continue
            seen.add((ei, ai))
            lines.append(f"  expected[{ei}] vs actual[{ai}]: {err}")

    return "\n".join(lines)


def _compare_fields(
    expected: dict[str, Any],
    actual: dict[str, Any],
    variables: dict[str, object],
    path: str,
) -> None:
    expected_keys = set(expected.keys())
    actual_keys = set(actual.keys())

    display_path = path or "<root>"

    missing = expected_keys - actual_keys
    if missing:
        raise MatchError(f"Missing fields at '{display_path}': {missing}")

    extra = actual_keys - expected_keys
    if extra:
        raise MatchError(f"Extra fields at '{display_path}': {extra}")

    for key in expected_keys:
        field_path = f"{path}.{key}" if path else key
        _compare_value(expected[key], actual[key], variables, field_path)


def _compare_value(
    expected: Any,
    actual: Any,
    variables: dict[str, object],
    path: str,
) -> None:
    if isinstance(expected, str) and is_dsl_string(expected):
        matcher = parse_matcher(expected, variables)
        if not matcher.matches(actual):
            raise MatchError(f"Field '{path}': {actual!r} did not match {expected!r}")
        return

    if isinstance(expected, dict):
        if not isinstance(actual, dict):
            raise MatchError(f"Field '{path}': expected dict, got {type(actual).__name__}")
        _compare_fields(expected, actual, variables, path)
        return

    if isinstance(expected, list):
        _compare_list(expected, actual, variables, path)
        return

    _strict_eq(actual, expected, path)


def _compare_list(
    expected: list[Any],
    actual: Any,
    variables: dict[str, object],
    path: str,
) -> None:
    if not isinstance(actual, list):
        raise MatchError(f"Field '{path}': expected list, got {type(actual).__name__}")
    if len(actual) != len(expected):
        raise MatchError(f"Field '{path}': expected length {len(expected)}, got {len(actual)}")
    for i, (e, a) in enumerate(zip(expected, actual, strict=True)):
        _compare_value(e, a, variables, f"{path}[{i}]")


def _strict_eq(actual: object, expected: object, path: str) -> None:
    if type(actual) is not type(expected):
        raise MatchError(
            f"Field '{path}': type mismatch - "
            f"expected type {type(expected).__name__} with value {expected!r}, "
            f"got type {type(actual).__name__} with value {actual!r}"
        )
    if actual != expected:
        raise MatchError(f"Field '{path}': expected {expected!r}, got {actual!r}")
