"""Random OHR sampler for testing and benchmarking.

Generates random OHR trees with configurable depth, breadth, and content.
Supports pitchspace types, generic values, and chord-table-based generation.
"""

from __future__ import annotations

import logging
import random
from dataclasses import dataclass
from typing import Any

from flexohr.core.component import AbsentComponent, Component, UndefinedComponent
from flexohr.core.ohr import OHR
from flexohr.core.validators import Validator
from flexohr.harmony.enums import EMPTY_PROPS, FrozenProperties, props

logger = logging.getLogger(__name__)


@dataclass
class SamplerConfig:
    """Configuration for random OHR generation."""

    max_depth: int = 3
    max_breadth: int = 5
    min_breadth: int = 1
    use_pitchspace: bool = True
    use_generic: bool = True
    property_density: float = 0.7
    absent_probability: float = 0.1
    undefined_probability: float = 0.05
    nesting_probability: float = 0.3
    reference_probability: float = 0.6
    reference_ohr_probability: float = 0.2
    ordered_probability: float = 0.3
    horizontal_probability: float = 0.2


class OHRSampler:
    """Random OHR generator for testing and benchmarking."""

    def __init__(
        self,
        config: SamplerConfig | None = None,
        seed: int | None = None,
    ) -> None:
        self.config = config or SamplerConfig()
        self.rng = random.Random(seed)

    def sample(self) -> OHR:
        """Generate a single random OHR."""
        return self._sample_ohr(depth=0)

    def sample_batch(self, n: int) -> list[OHR]:
        """Generate *n* random OHRs."""
        return [self.sample() for _ in range(n)]

    def sample_valid(self, validator: Validator, max_attempts: int = 100) -> OHR:
        """Generate an OHR that passes *validator*.

        Raises ``RuntimeError`` if no valid OHR is found within *max_attempts*.
        """
        for _ in range(max_attempts):
            ohr = self.sample()
            if validator(ohr).is_valid:
                return ohr
        raise RuntimeError(f"Could not generate a valid OHR in {max_attempts} attempts")

    def sample_invalid(self, validator: Validator, max_attempts: int = 100) -> OHR:
        """Generate an OHR that fails *validator*.

        Raises ``RuntimeError`` if no invalid OHR is found within *max_attempts*.
        """
        for _ in range(max_attempts):
            ohr = self._sample_intentionally_invalid()
            if not validator(ohr).is_valid:
                return ohr
        raise RuntimeError(f"Could not generate an invalid OHR in {max_attempts} attempts")

    def sample_from_chord_table(self) -> OHR:
        """Generate an OHR from a random ChordQuality with a random root.

        Picks a random chord quality from ``CHORD_QUALITY_INTERVALS``, a
        random root SPC, and assembles the corresponding OHR.
        """
        from flexohr.harmony.chord_tables import CHORD_QUALITY_INTERVALS
        from flexohr.paradigms.pitchspace import SPC

        qualities = list(CHORD_QUALITY_INTERVALS.keys())
        quality = self.rng.choice(qualities)
        intervals = CHORD_QUALITY_INTERVALS[quality]

        # Random root: fifths in [-7, 7] gives common note names
        root = SPC(self.rng.randint(-7, 7))

        first_sic, first_tfs = intervals[0]
        body_components = []
        for sic, tfs in intervals[1:]:
            body_components.append(Component(sic, properties=props(*tfs)))

        return OHR(
            reference_component=Component(root, properties=props(*first_tfs)),
            body=tuple(body_components),
            properties=props(quality),
        )

    # ── Internal helpers ──

    def _sample_ohr(self, depth: int) -> OHR:
        """Recursively generate a random OHR tree."""
        cfg = self.config

        # Reference component
        ref_comp: Component | AbsentComponent | UndefinedComponent | None = None
        if self.rng.random() < cfg.reference_probability:
            ref_comp = self._sample_component()

        # Reference OHR (only if not at max depth)
        ref_ohr: OHR | None = None
        if depth + 1 < cfg.max_depth and self.rng.random() < cfg.reference_ohr_probability:
            ref_ohr = self._sample_ohr(depth + 1)

        # Body
        breadth = self.rng.randint(cfg.min_breadth, cfg.max_breadth)
        body: list[Component | OHR] = []
        for _ in range(breadth):
            # Nesting probability decreases with depth
            effective_nesting_p = cfg.nesting_probability * (1.0 - depth / max(cfg.max_depth, 1))
            if depth + 1 < cfg.max_depth and self.rng.random() < effective_nesting_p:
                body.append(self._sample_ohr(depth + 1))
            else:
                body.append(self._sample_component())

        # Flags
        ordered = self.rng.random() < cfg.ordered_probability
        horizontal = self.rng.random() < cfg.horizontal_probability

        # OHR-level properties
        ohr_props = self._sample_ohr_properties()

        return OHR(
            reference_component=ref_comp,
            reference_ohr=ref_ohr,
            body=tuple(body),
            ordered=ordered,
            horizontal=horizontal,
            properties=ohr_props,
        )

    def _sample_component(self) -> Component | AbsentComponent | UndefinedComponent:
        """Generate a random Component, AbsentComponent, or UndefinedComponent."""
        cfg = self.config
        roll = self.rng.random()

        if roll < cfg.undefined_probability:
            return UndefinedComponent(properties=self._sample_properties())

        if roll < cfg.undefined_probability + cfg.absent_probability:
            return AbsentComponent(
                value=self._sample_value(),
                properties=self._sample_properties(),
                reason=self.rng.choice(["omitted", "silent", "editorial", ""]),
            )

        return Component(
            value=self._sample_value(),
            properties=self._sample_properties(),
        )

    def _sample_value(self) -> Any:
        """Generate a random component value.

        When ``use_pitchspace`` is True, chooses from pitchspace class and
        pair types.  When ``use_generic`` is True, also includes plain ints
        and strings.
        """
        cfg = self.config
        options: list[str] = []
        if cfg.use_pitchspace:
            options.extend(
                [
                    "epc",
                    "spc",
                    "gpc",
                    "eic",
                    "sic",
                    "gic",
                    "ep",
                    "sp",
                    "gp",
                    "ei",
                    "si",
                    "gi",
                ]
            )
        if cfg.use_generic:
            options.extend(["int", "str"])

        if not options:
            return self.rng.randint(0, 127)

        choice = self.rng.choice(options)
        return self._value_from_choice(choice)

    def _value_from_choice(self, choice: str) -> Any:
        """Generate a concrete value from a type choice string."""
        if choice == "int":
            return self.rng.randint(-100, 100)
        if choice == "str":
            labels = ["C", "D", "E", "F", "G", "A", "B", "root", "bass", "x"]
            return self.rng.choice(labels)

        # Pitchspace types — import lazily
        from flexohr.paradigms.pitchspace import (
            EI,
            EIC,
            EP,
            EPC,
            GI,
            GIC,
            GP,
            GPC,
            SI,
            SIC,
            SP,
            SPC,
        )

        # Class types (IntType subclasses)
        if choice == "epc":
            return EPC(self.rng.randint(0, 11))
        if choice == "spc":
            return SPC(self.rng.randint(-7, 7))
        if choice == "gpc":
            return GPC(self.rng.randint(0, 6))
        if choice == "eic":
            return EIC(self.rng.randint(0, 11))
        if choice == "sic":
            return SIC(self.rng.randint(-7, 7))
        if choice == "gic":
            return GIC(self.rng.randint(0, 6))

        # Pair types (PairOfInts subclasses)
        if choice == "ep":
            return EP((self.rng.randint(0, 11), self.rng.randint(0, 8)))
        if choice == "sp":
            return SP((self.rng.randint(-7, 7), self.rng.randint(0, 8)))
        if choice == "gp":
            return GP((self.rng.randint(0, 6), self.rng.randint(0, 8)))
        if choice == "ei":
            return EI((self.rng.randint(0, 11), self.rng.randint(0, 3)))
        if choice == "si":
            return SI((self.rng.randint(-7, 7), self.rng.randint(0, 3)))
        if choice == "gi":
            return GI((self.rng.randint(0, 6), self.rng.randint(0, 3)))

        return self.rng.randint(0, 127)  # fallback

    def _sample_properties(self) -> FrozenProperties:
        """Generate random component-level properties."""
        if self.rng.random() > self.config.property_density:
            return EMPTY_PROPS
        return self._make_random_props()

    def _sample_ohr_properties(self) -> FrozenProperties:
        """Generate random OHR-level properties."""
        if self.rng.random() > self.config.property_density:
            return EMPTY_PROPS

        from flexohr.harmony.harmony_enums import ChordQuality, Inversion

        items: list[Any] = []

        # Optionally add a chord quality
        if self.rng.random() < 0.5:
            qualities = [m for m in ChordQuality if m.name == m.value]
            items.append(self.rng.choice(qualities))

        # Optionally add an inversion
        if self.rng.random() < 0.3:
            inversions = [m for m in Inversion if m.name == m.value]
            items.append(self.rng.choice(inversions))

        if not items:
            return EMPTY_PROPS
        return props(*items)

    def _make_random_props(self) -> FrozenProperties:
        """Build a small random property bag for a component."""
        from flexohr.harmony.enums import ComponentStatus
        from flexohr.harmony.harmony_enums import ToneFunction

        items: list[Any] = []

        # Pick a tone function
        functions = [m for m in ToneFunction if m.name == m.value]
        items.append(self.rng.choice(functions))

        # Optionally add a ComponentStatus
        if self.rng.random() < 0.3:
            statuses = [m for m in ComponentStatus if m.name == m.value]
            items.append(self.rng.choice(statuses))

        return props(*items)

    def _sample_intentionally_invalid(self) -> OHR:
        """Generate an OHR likely to violate common validators.

        Strategies: empty body, missing reference, contradicting tone
        functions on the same component.
        """
        from flexohr.harmony.harmony_enums import ToneFunction

        strategy = self.rng.choice(
            [
                "no_reference",
                "empty_body",
                "contradicting_functions",
            ]
        )

        if strategy == "no_reference":
            # No reference_component at all
            return OHR(
                reference_component=None,
                body=(self._sample_component(),),
            )

        if strategy == "empty_body":
            return OHR(
                reference_component=self._sample_component(),
                body=(),
            )

        # contradicting_functions: put two exclusive ToneFunctions on one component
        exclusive = [
            ToneFunction.root,
            ToneFunction.third,
            ToneFunction.fifth,
            ToneFunction.seventh,
        ]
        tf1, tf2 = self.rng.sample(exclusive, 2)
        bad_comp = Component(
            value=self._sample_value(),
            properties=props(tf1, tf2),
        )
        return OHR(
            reference_component=bad_comp,
            body=(self._sample_component(),),
        )
