"""Finite versioned inventory of license-selectable application surfaces."""

from __future__ import annotations

from dataclasses import dataclass
import hashlib
from types import MappingProxyType
from typing import Iterable, Mapping

from .canonical_json import canonicalize_json
from .constants import validate_identifier


PRODUCT_SURFACE_INVENTORY_REVISION = 1
PRODUCT_SURFACE_KIND_ANALYSIS = "analysis"
PRODUCT_SURFACE_KIND_MAIN_TAB = "main_tab"
PRODUCT_SURFACE_KINDS = frozenset(
    (PRODUCT_SURFACE_KIND_ANALYSIS, PRODUCT_SURFACE_KIND_MAIN_TAB)
)


@dataclass(frozen=True)
class ProductSurfaceDefinition:
    """One selectable analysis or primary workspace."""

    surface_id: str
    kind: str
    label: str

    def __post_init__(self) -> None:
        object.__setattr__(
            self,
            "surface_id",
            validate_identifier(self.surface_id, "surface_id"),
        )
        if self.kind not in PRODUCT_SURFACE_KINDS:
            raise ValueError("kind must identify a supported product surface kind")
        if not isinstance(self.label, str) or not self.label.strip():
            raise ValueError("label must be non-empty text")
        if len(self.label.strip()) > 256:
            raise ValueError("label is too long")
        object.__setattr__(self, "label", self.label.strip())

    def to_mapping(self) -> dict[str, str]:
        return {
            "id": self.surface_id,
            "kind": self.kind,
            "label": self.label,
        }


_ANALYSIS_SURFACE_IDS = (
    "aclr_acpr",
    "adaptive_filtering",
    "am_fm_pm_demod",
    "basic_waveform_features",
    "bathtub_ber_extrapolation",
    "ber_prbs",
    "ber_trend",
    "ccdf_papr",
    "cepstrum",
    "channel_power_obw",
    "chirp_sweep_fra",
    "clock_eye_diagram",
    "clock_recovery_pll",
    "coil_session_saturation_maps",
    "cross_spectrum_coherence",
    "cyclostationary_spectral_analysis",
    "ddc_iq_demod",
    "deconvolution",
    "delay_phase_extraction",
    "digital_lockin",
    "digital_phase_noise_lf",
    "edge_time_extraction",
    "envelope_extraction",
    "evm_vector",
    "eye_diagram",
    "eye_mask_margin",
    "fft_spectrum",
    "frf_estimator_comparison",
    "glitch_runt_detection",
    "harmonic_distortion",
    "higher_order_spectral_analysis",
    "hilbert_analytic_signal",
    "impedance_voltage_current",
    "intermodulation_distortion",
    "iq_correction",
    "jitter_analysis",
    "jitter_fft_asd",
    "jitter_persistence_density",
    "jitter_persistence_mode",
    "jitter_rj_dj_tj",
    "loop_gain_processing",
    "matched_filtering",
    "mechanical_envelope_cepstrum_kurtosis",
    "mechanical_orbit_bearing_orders",
    "mechanical_order_analysis",
    "mimo_system_identification",
    "multi_session_statistics_gage_rr",
    "multichannel_coherence_matrix",
    "multichannel_frf_matrix",
    "music_spectral_estimation",
    "ncycle_jitter",
    "noise_excitation_fra",
    "parametric_spectral_estimation",
    "pca_ica",
    "phase_noise_allan",
    "power_efficiency",
    "power_harmonics",
    "power_quality_waveform_events",
    "preconditioning_resample_align",
    "prony_matrix_pencil",
    "protocol_can_lin",
    "protocol_i2c",
    "protocol_i2s",
    "protocol_spi",
    "protocol_transaction_analytics",
    "protocol_uart",
    "protocol_usb_ethernet_style",
    "rf_nonlinear",
    "rf_spectrum",
    "ringdown_fit",
    "setup_hold",
    "si_deembedding_equalization",
    "sine_fit",
    "sparameters_from_waveforms",
    "spectral_emission_mask",
    "spectral_persistence_max_hold",
    "statistical_timing",
    "step_impulse_response",
    "stepped_sine_fra",
    "stft_spectrogram",
    "tdr_tdt",
    "threshold_logic",
    "time_gating",
    "wavelet_analysis",
    "welch_psd",
)

_MAIN_TAB_SURFACES = (
    ("acquisition", "Acquisition"),
    ("preprocessing", "Preprocessing"),
    ("coil_measurement", "Coil Measurement"),
    ("touchstone_analysis", "Touchstone Analysis"),
    ("rlc_z", "RLC Z"),
    ("postprocessing", "Postprocessing"),
    ("data_logger", "Data Logger"),
    ("workspace", "Workspace"),
    ("analysis", "Analysis"),
    ("guided_signal_investigator", "Guided Signal Investigator"),
    ("time_frequency", "Time-Frequency"),
    ("rf_lab", "RF Lab"),
    ("sequencer", "Sequencer"),
    ("report", "Report"),
)


def _analysis_label(surface_id: str) -> str:
    return surface_id.replace("_", " ").title()


PRODUCT_SURFACE_DEFINITIONS = tuple(
    ProductSurfaceDefinition(
        surface_id=value,
        kind=PRODUCT_SURFACE_KIND_ANALYSIS,
        label=_analysis_label(value),
    )
    for value in _ANALYSIS_SURFACE_IDS
) + tuple(
    ProductSurfaceDefinition(
        surface_id=surface_id,
        kind=PRODUCT_SURFACE_KIND_MAIN_TAB,
        label=label,
    )
    for surface_id, label in _MAIN_TAB_SURFACES
)

_DEFINITIONS_BY_KIND: Mapping[str, tuple[ProductSurfaceDefinition, ...]] = (
    MappingProxyType(
        {
            kind: tuple(
                definition
                for definition in PRODUCT_SURFACE_DEFINITIONS
                if definition.kind == kind
            )
            for kind in sorted(PRODUCT_SURFACE_KINDS)
        }
    )
)
_DEFINITIONS_BY_KEY = MappingProxyType(
    {
        (definition.kind, definition.surface_id): definition
        for definition in PRODUCT_SURFACE_DEFINITIONS
    }
)

if len(_DEFINITIONS_BY_KEY) != len(PRODUCT_SURFACE_DEFINITIONS):
    raise RuntimeError("product surface inventory contains duplicate identifiers")


def product_surface_definitions(
    kind: str | None = None,
) -> tuple[ProductSurfaceDefinition, ...]:
    if kind is None:
        return PRODUCT_SURFACE_DEFINITIONS
    if not isinstance(kind, str):
        raise TypeError("kind must be a string or None")
    if kind not in PRODUCT_SURFACE_KINDS:
        raise ValueError("kind does not identify a supported product surface kind")
    return _DEFINITIONS_BY_KIND[kind]


def product_surface_ids(kind: str) -> tuple[str, ...]:
    return tuple(
        definition.surface_id
        for definition in product_surface_definitions(kind)
    )


def product_surface_definition(
    kind: str,
    surface_id: str,
) -> ProductSurfaceDefinition:
    if not isinstance(kind, str):
        raise TypeError("kind must be a string")
    normalized_id = validate_identifier(surface_id, "surface_id")
    definition = _DEFINITIONS_BY_KEY.get((kind, normalized_id))
    if definition is None:
        raise KeyError(f"unknown {kind} product surface: {normalized_id}")
    return definition


def product_surface_id_for_label(kind: str, label: str) -> str:
    if not isinstance(kind, str):
        raise TypeError("kind must be a string")
    if not isinstance(label, str) or not label.strip():
        raise ValueError("label must be non-empty text")
    wanted = label.strip().casefold()
    matches = tuple(
        definition.surface_id
        for definition in product_surface_definitions(kind)
        if definition.label.casefold() == wanted
    )
    if len(matches) != 1:
        raise KeyError(f"unknown or ambiguous {kind} product surface label: {label}")
    return matches[0]


def validate_product_surface_ids(
    kind: str,
    values: Iterable[str],
    *,
    require_nonempty: bool = False,
) -> tuple[str, ...]:
    if not isinstance(kind, str):
        raise TypeError("kind must be a string")
    if kind not in PRODUCT_SURFACE_KINDS:
        raise ValueError("kind does not identify a supported product surface kind")
    if isinstance(values, (str, bytes, bytearray)):
        raise TypeError("values must be an iterable of identifiers")
    normalized = tuple(validate_identifier(value, "surface_id") for value in values)
    if tuple(sorted(set(normalized))) != normalized:
        raise ValueError("product surface identifiers must be sorted and unique")
    if require_nonempty and not normalized:
        raise ValueError("at least one product surface identifier is required")
    unknown = tuple(
        value
        for value in normalized
        if (kind, value) not in _DEFINITIONS_BY_KEY
    )
    if unknown:
        raise ValueError(f"unknown {kind} product surfaces: {', '.join(unknown)}")
    return normalized


def product_surface_inventory_mapping() -> dict[str, object]:
    return {
        "revision": PRODUCT_SURFACE_INVENTORY_REVISION,
        "surfaces": [
            definition.to_mapping()
            for definition in PRODUCT_SURFACE_DEFINITIONS
        ],
    }


PRODUCT_SURFACE_INVENTORY_SHA256 = hashlib.sha256(
    canonicalize_json(product_surface_inventory_mapping())
).hexdigest()


__all__ = [
    "PRODUCT_SURFACE_DEFINITIONS",
    "PRODUCT_SURFACE_INVENTORY_REVISION",
    "PRODUCT_SURFACE_INVENTORY_SHA256",
    "PRODUCT_SURFACE_KIND_ANALYSIS",
    "PRODUCT_SURFACE_KIND_MAIN_TAB",
    "ProductSurfaceDefinition",
    "product_surface_definition",
    "product_surface_definitions",
    "product_surface_ids",
    "product_surface_id_for_label",
    "product_surface_inventory_mapping",
    "validate_product_surface_ids",
]
