"""Composable validators for OHR objects.

Validators combine via ``&`` (and), ``|`` (or), ``~`` (not).
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from flexohr.core.protocol import FlexohrProtocol


@dataclass
class ValidationResult:
    """Outcome of validating an OHR."""

    is_valid: bool
    messages: list[str] = field(default_factory=list)

    def __bool__(self) -> bool:
        return self.is_valid

    def __and__(self, other: ValidationResult) -> ValidationResult:
        return ValidationResult(
            is_valid=self.is_valid and other.is_valid,
            messages=self.messages + other.messages,
        )

    def __or__(self, other: ValidationResult) -> ValidationResult:
        if self.is_valid:
            return ValidationResult(is_valid=True, messages=self.messages)
        if other.is_valid:
            return ValidationResult(is_valid=True, messages=other.messages)
        return ValidationResult(
            is_valid=False,
            messages=self.messages + other.messages,
        )

    @classmethod
    def ok(cls) -> ValidationResult:
        return cls(is_valid=True)

    @classmethod
    def fail(cls, message: str) -> ValidationResult:
        return cls(is_valid=False, messages=[message])


class Validator(ABC):
    """Base class for OHR validators.  Compose via ``&``, ``|``, ``~``."""

    @abstractmethod
    def __call__(self, ohr: FlexohrProtocol) -> ValidationResult: ...

    def __and__(self, other: Validator) -> Validator:
        return _AndValidator(self, other)

    def __or__(self, other: Validator) -> Validator:
        return _OrValidator(self, other)

    def __invert__(self) -> Validator:
        return _NotValidator(self)


class _AndValidator(Validator):
    __slots__ = ("_left", "_right")

    def __init__(self, left: Validator, right: Validator) -> None:
        self._left = left
        self._right = right

    def __call__(self, ohr: FlexohrProtocol) -> ValidationResult:
        return self._left(ohr) & self._right(ohr)


class _OrValidator(Validator):
    __slots__ = ("_left", "_right")

    def __init__(self, left: Validator, right: Validator) -> None:
        self._left = left
        self._right = right

    def __call__(self, ohr: FlexohrProtocol) -> ValidationResult:
        left_result = self._left(ohr)
        if left_result:
            return left_result
        return self._right(ohr)


class _NotValidator(Validator):
    __slots__ = ("_inner",)

    def __init__(self, inner: Validator) -> None:
        self._inner = inner

    def __call__(self, ohr: FlexohrProtocol) -> ValidationResult:
        result = self._inner(ohr)
        if result:
            return ValidationResult.fail("Expected validation to fail but it passed")
        return ValidationResult.ok()


# ── Built-in structural validators ──


class HasReference(Validator):
    """Validates that the OHR has a ``reference_component``."""

    def __call__(self, ohr: FlexohrProtocol) -> ValidationResult:
        if ohr.reference_component is not None:
            return ValidationResult.ok()
        return ValidationResult.fail("OHR has no reference_component")


class BodyNotEmpty(Validator):
    """Validates that the OHR body is not empty."""

    def __call__(self, ohr: FlexohrProtocol) -> ValidationResult:
        if len(ohr.body) > 0:
            return ValidationResult.ok()
        return ValidationResult.fail("OHR body is empty")


class MaxNestingDepth(Validator):
    """Validates that OHR nesting does not exceed *max_depth*."""

    __slots__ = ("_max_depth",)

    def __init__(self, max_depth: int) -> None:
        self._max_depth = max_depth

    def __call__(self, ohr: FlexohrProtocol) -> ValidationResult:
        depth = self._measure_depth(ohr)
        if depth <= self._max_depth:
            return ValidationResult.ok()
        return ValidationResult.fail(f"Nesting depth {depth} exceeds maximum {self._max_depth}")

    def _measure_depth(self, ohr: Any, current: int = 1) -> int:
        max_child = current
        for item in ohr.body:
            if hasattr(item, "body"):
                child_depth = self._measure_depth(item, current + 1)
                max_child = max(max_child, child_depth)
        return max_child


class NoContradictingToneFunctions(Validator):
    """Validates that no component has contradicting ToneFunction assignments.

    Mutually exclusive groups: a component cannot simultaneously be assigned
    two different members from the same exclusive group. By default, the
    exclusive group is ``{root, third, fifth, seventh}`` — a single pitch
    cannot be both the root and the third of a chord.

    Non-exclusive functions like ``leading_tone``, ``altered``, ``bass``, and
    ``downward_leading_tone`` may coexist with any function.
    """

    __slots__ = ("_exclusive",)

    def __init__(
        self,
        exclusive: frozenset | None = None,
    ) -> None:
        if exclusive is None:
            from flexohr.harmony.harmony_enums import ToneFunction

            self._exclusive = frozenset(
                {
                    ToneFunction.root,
                    ToneFunction.third,
                    ToneFunction.fifth,
                    ToneFunction.seventh,
                    ToneFunction.ninth,
                    ToneFunction.eleventh,
                    ToneFunction.thirteenth,
                }
            )
        else:
            self._exclusive = exclusive

    def __call__(self, ohr: FlexohrProtocol) -> ValidationResult:
        from flexohr.core.component import Component
        from flexohr.harmony.harmony_enums import ToneFunction

        messages: list[str] = []
        all_components = [item for item in ohr.body if isinstance(item, Component)]
        if ohr.reference_component is not None:
            all_components.insert(0, ohr.reference_component)

        for comp in all_components:
            exclusive_found: list[ToneFunction] = []
            for item in comp.properties:
                if isinstance(item, ToneFunction) and item in self._exclusive:
                    exclusive_found.append(item)
            if len(exclusive_found) > 1:
                names = ", ".join(tf.name for tf in exclusive_found)
                messages.append(f"Component {comp.value!r} has contradicting " f"tone functions: {names}")

        if messages:
            return ValidationResult(is_valid=False, messages=messages)
        return ValidationResult.ok()


class InversionBassConsistency(Validator):
    """Validates consistency of inversion, bass, and chord-type intervals.

    Checks (in order):

    1. **Single bass**: At most one body component should carry
       ``ToneFunction.bass``.
    2. **Bass ↔ inversion agreement**: If both an ``Inversion`` property
       and a ``ToneFunction.bass`` component exist, the bass component's
       primary ToneFunction must match the inversion mapping (0→root,
       1→third, 2→fifth, 3→seventh).
    3. **Interval consistency** (the essential check): If the OHR has a
       ``ChordQuality`` property and its body components carry
       ``Point`` values (i.e. the OHR is resolved), the pairwise
       intervals between components **must** match the intervals
       prescribed by ``CHORD_QUALITY_INTERVALS``.  Components are
       matched by ``ToneFunction``; any component whose ToneFunction
       has no entry in the quality table is skipped.

       This catches errors where ``.with_()`` sets a wrong bass SPC or
       root SPC on a resolved OHR.

    .. note::

       This validator assumes the Western tertian harmony paradigm.
       In other paradigms, arbitrary property combinations may be valid.
    """

    def __call__(self, ohr: FlexohrProtocol) -> ValidationResult:
        from flexohr.core.abstract import Vector
        from flexohr.core.component import Component
        from flexohr.harmony.chord_tables import (
            CHORD_QUALITY_INTERVALS,
            INVERSION_INDEX_TO_TONE_FUNCTION,
            INVERSION_TO_INDEX,
        )
        from flexohr.harmony.harmony_enums import (
            ChordQuality,
            Inversion,
            ToneFunction,
        )

        messages: list[str] = []

        components = [item for item in ohr.body if isinstance(item, Component)]

        # ── Structural checks ──

        # Find Inversion and bass-marked components
        inv_enum = None
        for p in ohr.properties:
            if isinstance(p, Inversion):
                inv_enum = p
                break

        bass_components = [c for c in components if any(p is ToneFunction.bass for p in c.properties)]

        # Check 1: Single bass
        if len(bass_components) > 1:
            messages.append(
                f"Multiple components marked as bass " f"({len(bass_components)} found); expected at most 1"
            )

        # Check 2: Bass ↔ inversion agreement
        if inv_enum is not None and len(bass_components) == 1:
            inv_index = INVERSION_TO_INDEX.get(inv_enum, 0)
            expected_tf = INVERSION_INDEX_TO_TONE_FUNCTION.get(inv_index)
            if expected_tf is not None:
                bass_comp = bass_components[0]
                primary_tfs = [
                    p for p in bass_comp.properties if isinstance(p, ToneFunction) and p is not ToneFunction.bass
                ]
                if primary_tfs and expected_tf not in primary_tfs:
                    actual_names = ", ".join(tf.name for tf in primary_tfs)
                    messages.append(
                        f"Inversion {inv_enum.name} expects bass on "
                        f"{expected_tf.name}, but bass is on component "
                        f"with ToneFunction(s): {actual_names}"
                    )

        # ── Interval consistency check ──

        # Find ChordQuality in OHR properties
        quality = None
        for p in ohr.properties:
            if isinstance(p, ChordQuality):
                quality = p
                break

        if quality is not None and quality in CHORD_QUALITY_INTERVALS:
            # Build expected interval map: ToneFunction -> SIC
            expected = CHORD_QUALITY_INTERVALS[quality]
            tf_to_expected_sic: dict[ToneFunction, Any] = {}
            for sic, tfs in expected:
                for tf in tfs:
                    # Only map primary (non-bass) tone functions
                    if tf is not ToneFunction.bass:
                        tf_to_expected_sic[tf] = sic

            # Build actual map: ToneFunction -> component value
            # Only for resolved (Point) values
            tf_to_actual_value: dict[ToneFunction, tuple[Any, Component]] = {}
            for comp in components:
                if comp.value is None or isinstance(comp.value, Vector):
                    continue  # unresolved component, skip
                for p in comp.properties:
                    if isinstance(p, ToneFunction) and p is not ToneFunction.bass:
                        if p in tf_to_expected_sic:
                            tf_to_actual_value[p] = (comp.value, comp)

            # We need at least two resolved components to compare intervals
            if len(tf_to_actual_value) >= 2:
                # Use root as anchor if available; otherwise use the first available
                anchor_tf = None
                for candidate in (ToneFunction.root, *tf_to_actual_value):
                    if candidate in tf_to_actual_value:
                        anchor_tf = candidate
                        break

                if anchor_tf is not None:
                    anchor_val = tf_to_actual_value[anchor_tf][0]
                    anchor_expected_sic = tf_to_expected_sic[anchor_tf]

                    for tf, (val, _comp) in tf_to_actual_value.items():
                        if tf is anchor_tf:
                            continue
                        expected_sic = tf_to_expected_sic[tf]
                        # Expected interval from anchor to this tf
                        expected_interval = expected_sic - anchor_expected_sic
                        try:
                            actual_interval = val - anchor_val
                        except Exception:
                            continue  # incompatible types, skip
                        if actual_interval != expected_interval:
                            messages.append(
                                f"Interval from {anchor_tf.name} to "
                                f"{tf.name}: expected {expected_interval}, "
                                f"got {actual_interval} "
                                f"(values: {anchor_val}, {val})"
                            )

        if messages:
            return ValidationResult(is_valid=False, messages=messages)
        return ValidationResult.ok()
