"""Vector loader and runner for financial math test vectors."""

import json
from decimal import Decimal
from pathlib import Path
from typing import Any, Callable

_MONOREPO = Path(__file__).parent.parent.parent / "vectors"
_INSTALLED = Path(__file__).parent / "vectors"
VECTORS_DIR = _MONOREPO if _MONOREPO.exists() else _INSTALLED


class VectorError(Exception):
    """Raised when a vector file cannot be loaded."""


class VectorFailure(Exception):
    """Raised when a vector execution produces incorrect results."""


SEARCH_DIRS = [
    "arithmetic", "serialization", "budgeting", "reconciliation",
    "household", "debt", "interest", "investing", "loans", "tax",
    "pipeline",
]


def load_all_suites() -> list[dict[str, Any]]:
    """Load all vector suites from all categories.

    Returns a list of suite dicts, each with 'suite', 'version', and 'vectors' keys.
    """
    suites = []
    for subdir in SEARCH_DIRS:
        subdir_path = VECTORS_DIR / subdir
        if not subdir_path.exists():
            continue
        for path in sorted(subdir_path.glob("*.json")):
            with open(path) as f:
                data = json.load(f)
            suites.append(data)
    return suites


def load_suite(name: str) -> list[dict[str, Any]]:
    """Load all vectors from a named suite.

    Searches all category directories for a suite file matching the given name.
    """
    for subdir in SEARCH_DIRS:
        for path in (VECTORS_DIR / subdir).glob("*.json"):
            with open(path) as f:
                data = json.load(f)
            if data.get("suite") == name:
                return data["vectors"]

    raise VectorError(f"Suite '{name}' not found in {VECTORS_DIR}")


def parse_decimal_values(obj: Any) -> Any:
    """Recursively parse string values as Decimal where possible."""
    if isinstance(obj, str):
        try:
            return Decimal(obj)
        except Exception:
            return obj
    if isinstance(obj, list):
        return [parse_decimal_values(item) for item in obj]
    if isinstance(obj, dict):
        return {k: parse_decimal_values(v) for k, v in obj.items()}
    return obj


def run_vector(
    vector: dict[str, Any],
    implementation: Callable[..., Any],
) -> dict[str, Any]:
    """Run a single vector against an implementation function.

    The implementation receives parsed inputs (strings converted to Decimal)
    and should return a dict matching the expected output structure.

    Returns the result dict on success, raises VectorFailure on mismatch.
    """
    inputs = parse_decimal_values(vector["inputs"])
    expected = parse_decimal_values(vector["expected"])
    precision = Decimal(vector.get("precision", "0.01"))

    result = implementation(**inputs) if isinstance(inputs, dict) else implementation(inputs)

    _compare(vector["id"], expected, result, precision)
    return result


def _compare(
    vector_id: str,
    expected: Any,
    actual: Any,
    precision: Decimal,
    path: str = "",
) -> None:
    """Recursively compare expected vs actual within precision tolerance."""
    if isinstance(expected, dict):
        if not isinstance(actual, dict):
            raise VectorFailure(
                f"[{vector_id}] {path}: expected dict, got {type(actual).__name__}"
            )
        for key in expected:
            if key not in actual:
                raise VectorFailure(
                    f"[{vector_id}] {path}.{key}: missing from result"
                )
            _compare(vector_id, expected[key], actual[key], precision, f"{path}.{key}")
    elif isinstance(expected, list):
        if not isinstance(actual, list):
            raise VectorFailure(
                f"[{vector_id}] {path}: expected list, got {type(actual).__name__}"
            )
        if len(expected) != len(actual):
            raise VectorFailure(
                f"[{vector_id}] {path}: expected {len(expected)} items, got {len(actual)}"
            )
        for i, (e, a) in enumerate(zip(expected, actual)):
            _compare(vector_id, e, a, precision, f"{path}[{i}]")
    elif isinstance(expected, Decimal):
        if not isinstance(actual, Decimal):
            try:
                actual = Decimal(str(actual))
            except Exception:
                raise VectorFailure(
                    f"[{vector_id}] {path}: expected Decimal, got {type(actual).__name__}: {actual}"
                )
        if abs(expected - actual) > precision:
            raise VectorFailure(
                f"[{vector_id}] {path}: expected {expected}, got {actual} "
                f"(diff {abs(expected - actual)} > precision {precision})"
            )
    elif isinstance(expected, bool):
        if actual != expected:
            raise VectorFailure(
                f"[{vector_id}] {path}: expected {expected}, got {actual}"
            )
    else:
        if actual != expected:
            raise VectorFailure(
                f"[{vector_id}] {path}: expected {expected!r}, got {actual!r}"
            )


def run_suite(
    name: str,
    implementation: Callable[..., Any],
) -> list[dict[str, Any]]:
    """Load and run all vectors in a suite against an implementation.

    Returns list of results. Raises VectorFailure on first mismatch.
    """
    vectors = load_suite(name)
    results = []
    for vector in vectors:
        result = run_vector(vector, implementation)
        results.append(result)
    return results
