"""Analysis recommendation engine for automated signal processing.

Provides the ``recommend_analyses`` and ``get_optimal_domain_order`` public
APIs along with supporting helpers for building prioritized recommendations
based on signal characteristics.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

import numpy as np
from numpy.typing import NDArray

from oscura.inference.signal_intelligence.classification import classify_signal

if TYPE_CHECKING:
    from oscura.reporting.config import AnalysisDomain


@dataclass
class AnalysisRecommendation:
    """Recommendation for an analysis to run.

    Attributes:
        domain: Analysis domain to run.
        priority: Priority ranking (1=highest).
        confidence: Expected confidence if run (0.0-1.0).
        reasoning: Human-readable explanation.
        estimated_runtime_ms: Estimated runtime in milliseconds.
        prerequisites_met: Whether all prerequisites are satisfied.
    """

    domain: AnalysisDomain
    priority: int  # 1=highest priority
    confidence: float  # Expected confidence if run
    reasoning: str
    estimated_runtime_ms: int = 100
    prerequisites_met: bool = True


def _add_foundational_recommendations(
    recommendations: list[AnalysisRecommendation],
    exclude: set[AnalysisDomain],
) -> None:
    """Add foundational analysis recommendations (waveform, statistics).

    Args:
        recommendations: List to append recommendations to
        exclude: Domains to exclude
    """
    from oscura.reporting.config import AnalysisDomain

    if AnalysisDomain.WAVEFORM not in exclude:
        recommendations.append(
            AnalysisRecommendation(
                domain=AnalysisDomain.WAVEFORM,
                priority=1,
                confidence=0.95,
                reasoning="Basic waveform measurements are always applicable",
                estimated_runtime_ms=50,
            )
        )

    if AnalysisDomain.STATISTICS not in exclude:
        recommendations.append(
            AnalysisRecommendation(
                domain=AnalysisDomain.STATISTICS,
                priority=1,
                confidence=0.95,
                reasoning="Statistical analysis provides foundational metrics",
                estimated_runtime_ms=30,
            )
        )


def _add_spectral_recommendation(
    recommendations: list[AnalysisRecommendation],
    exclude: set[AnalysisDomain],
    is_periodic: bool,
) -> None:
    """Add spectral analysis recommendation.

    Args:
        recommendations: List to append recommendations to
        exclude: Domains to exclude
        is_periodic: Whether signal is periodic
    """
    from oscura.reporting.config import AnalysisDomain

    if AnalysisDomain.SPECTRAL not in exclude:
        spectral_conf = 0.85 if is_periodic else 0.70
        recommendations.append(
            AnalysisRecommendation(
                domain=AnalysisDomain.SPECTRAL,
                priority=2 if is_periodic else 3,
                confidence=spectral_conf,
                reasoning="Spectral analysis reveals frequency content"
                + (" - signal appears periodic" if is_periodic else ""),
                estimated_runtime_ms=100,
            )
        )


def _add_digital_recommendations(
    recommendations: list[AnalysisRecommendation],
    exclude: set[AnalysisDomain],
    dominant_freq: float | None,
) -> None:
    """Add digital signal analysis recommendations.

    Args:
        recommendations: List to append recommendations to
        exclude: Domains to exclude
        dominant_freq: Dominant frequency in Hz
    """
    from oscura.reporting.config import AnalysisDomain

    if AnalysisDomain.DIGITAL not in exclude:
        recommendations.append(
            AnalysisRecommendation(
                domain=AnalysisDomain.DIGITAL,
                priority=1,
                confidence=0.90,
                reasoning="Digital signal detected - edge and timing analysis recommended",
                estimated_runtime_ms=80,
            )
        )

    if AnalysisDomain.TIMING not in exclude:
        recommendations.append(
            AnalysisRecommendation(
                domain=AnalysisDomain.TIMING,
                priority=2,
                confidence=0.85,
                reasoning="Timing analysis valuable for digital signals",
                estimated_runtime_ms=60,
            )
        )

    if AnalysisDomain.PROTOCOLS not in exclude and dominant_freq:
        # Check if frequency matches common baud rates
        common_bauds = [9600, 19200, 38400, 57600, 115200]
        if any(abs(dominant_freq * 2 - b) / b < 0.1 for b in common_bauds):
            recommendations.append(
                AnalysisRecommendation(
                    domain=AnalysisDomain.PROTOCOLS,
                    priority=3,
                    confidence=0.70,
                    reasoning=f"Frequency {dominant_freq:.0f} Hz suggests serial protocol",
                    estimated_runtime_ms=150,
                )
            )


def _add_periodic_recommendations(
    recommendations: list[AnalysisRecommendation],
    exclude: set[AnalysisDomain],
    is_digital: bool,
) -> None:
    """Add periodic signal analysis recommendations.

    Args:
        recommendations: List to append recommendations to
        exclude: Domains to exclude
        is_digital: Whether signal is digital
    """
    from oscura.reporting.config import AnalysisDomain

    if AnalysisDomain.JITTER not in exclude and is_digital:
        recommendations.append(
            AnalysisRecommendation(
                domain=AnalysisDomain.JITTER,
                priority=3,
                confidence=0.80,
                reasoning="Periodic digital signal - jitter analysis applicable",
                estimated_runtime_ms=120,
            )
        )

    if AnalysisDomain.EYE not in exclude and is_digital:
        recommendations.append(
            AnalysisRecommendation(
                domain=AnalysisDomain.EYE,
                priority=3,
                confidence=0.75,
                reasoning="Eye diagram analysis for signal integrity assessment",
                estimated_runtime_ms=200,
            )
        )


def _add_pattern_and_entropy_recommendations(
    recommendations: list[AnalysisRecommendation],
    exclude: set[AnalysisDomain],
    data_length: int,
    is_periodic: bool,
) -> None:
    """Add pattern and entropy analysis recommendations.

    Args:
        recommendations: List to append recommendations to
        exclude: Domains to exclude
        data_length: Length of signal data
        is_periodic: Whether signal is periodic
    """
    from oscura.reporting.config import AnalysisDomain

    # Pattern analysis - good for complex signals
    if AnalysisDomain.PATTERNS not in exclude and data_length > 1000:
        pattern_conf = 0.70 if is_periodic else 0.50
        recommendations.append(
            AnalysisRecommendation(
                domain=AnalysisDomain.PATTERNS,
                priority=4,
                confidence=pattern_conf,
                reasoning="Pattern analysis can reveal repeating structures",
                estimated_runtime_ms=500,
            )
        )

    # Entropy analysis - useful for random/encrypted data
    if AnalysisDomain.ENTROPY not in exclude:
        recommendations.append(
            AnalysisRecommendation(
                domain=AnalysisDomain.ENTROPY,
                priority=5,
                confidence=0.80,
                reasoning="Entropy analysis characterizes randomness and complexity",
                estimated_runtime_ms=100,
            )
        )


def _filter_by_confidence(
    recommendations: list[AnalysisRecommendation],
    confidence_target: float,
) -> list[AnalysisRecommendation]:
    """Filter recommendations by confidence threshold.

    Args:
        recommendations: List of recommendations
        confidence_target: Minimum confidence threshold

    Returns:
        Filtered recommendations
    """
    return [r for r in recommendations if r.confidence >= confidence_target]


def _filter_by_time_budget(
    recommendations: list[AnalysisRecommendation],
    time_budget_seconds: float,
) -> list[AnalysisRecommendation]:
    """Filter recommendations by time budget.

    Args:
        recommendations: List of recommendations
        time_budget_seconds: Time budget in seconds

    Returns:
        Filtered recommendations within budget
    """
    budget_ms = time_budget_seconds * 1000
    cumulative = 0
    filtered = []

    # Sort by priority and confidence for selection
    for rec in sorted(recommendations, key=lambda x: (x.priority, -x.confidence)):
        if cumulative + rec.estimated_runtime_ms <= budget_ms:
            filtered.append(rec)
            cumulative += rec.estimated_runtime_ms

    return filtered


def recommend_analyses(
    data: NDArray[np.floating[Any]],
    sample_rate: float = 1.0,
    *,
    time_budget_seconds: float | None = None,
    confidence_target: float = 0.7,
    exclude_domains: list[AnalysisDomain] | None = None,
) -> list[AnalysisRecommendation]:
    """Recommend which analyses to run based on signal characteristics.

    Uses signal classification, quality metrics, and heuristics to
    recommend the most valuable analyses for a given signal.

    Args:
        data: Input signal data.
        sample_rate: Sample rate in Hz.
        time_budget_seconds: Optional time budget (prioritizes faster analyses).
        confidence_target: Minimum expected confidence threshold.
        exclude_domains: Domains to exclude from recommendations.

    Returns:
        List of AnalysisRecommendation sorted by priority.

    Example:
        >>> import numpy as np
        >>> import oscura as osc
        >>> # Generate test signal
        >>> t = np.linspace(0, 1, 10000)
        >>> signal = np.sin(2 * np.pi * 100 * t)
        >>> recommendations = osc.recommend_analyses(signal, sample_rate=10000)
        >>> for rec in recommendations[:3]:
        ...     print(f"{rec.domain.value}: {rec.reasoning}")
        waveform: Basic waveform measurements are always applicable
        statistics: Statistical analysis provides foundational metrics
        spectral: Spectral analysis reveals frequency content - signal appears periodic
    """
    recommendations: list[AnalysisRecommendation] = []
    exclude = set(exclude_domains or [])

    # Extract signal features via classification
    classification = classify_signal(data, sample_rate)
    is_digital = classification.get("is_digital", False)
    is_periodic = classification.get("is_periodic", False)
    dominant_freq = classification.get("dominant_frequency")

    # Build recommendations based on signal characteristics
    _add_foundational_recommendations(recommendations, exclude)
    _add_spectral_recommendation(recommendations, exclude, is_periodic)

    if is_digital:
        _add_digital_recommendations(recommendations, exclude, dominant_freq)

    if is_periodic:
        _add_periodic_recommendations(recommendations, exclude, is_digital)

    _add_pattern_and_entropy_recommendations(recommendations, exclude, len(data), is_periodic)

    # Apply filtering and ranking
    recommendations = _filter_by_confidence(recommendations, confidence_target)

    if time_budget_seconds is not None:
        recommendations = _filter_by_time_budget(recommendations, time_budget_seconds)

    # Final ranking by priority, then confidence
    recommendations.sort(key=lambda x: (x.priority, -x.confidence))

    return recommendations


def get_optimal_domain_order(
    recommendations: list[AnalysisRecommendation],
) -> list[AnalysisDomain]:
    """Get optimal order for running analyses.

    Considers dependencies and priorities to determine best order.

    Args:
        recommendations: List of analysis recommendations.

    Returns:
        Ordered list of domains to analyze.

    Example:
        >>> import numpy as np
        >>> import oscura as osc
        >>> # Generate test signal
        >>> t = np.linspace(0, 1, 10000)
        >>> signal = np.sin(2 * np.pi * 100 * t)
        >>> recommendations = osc.recommend_analyses(signal, sample_rate=10000)
        >>> order = osc.get_optimal_domain_order(recommendations)
        >>> print([d.value for d in order])
        ['waveform', 'statistics', 'spectral', 'patterns', 'entropy']
    """
    # Avoid circular import
    from oscura.reporting.config import AnalysisDomain

    # Define dependencies
    dependencies = {
        AnalysisDomain.JITTER: [AnalysisDomain.TIMING],
        AnalysisDomain.EYE: [AnalysisDomain.DIGITAL],
        AnalysisDomain.PROTOCOLS: [AnalysisDomain.DIGITAL],
        AnalysisDomain.INFERENCE: [AnalysisDomain.PATTERNS],
    }

    # Build order respecting dependencies
    ordered: list[AnalysisDomain] = []
    remaining = {r.domain for r in recommendations}

    while remaining:
        # Find domains with satisfied dependencies
        ready = []
        for domain in remaining:
            deps = dependencies.get(domain, [])
            if all(d not in remaining or d in ordered for d in deps):
                ready.append(domain)

        if not ready:
            # No ready domains - just add remaining (circular deps)
            ready = list(remaining)

        # Add highest priority ready domain
        for rec in sorted(recommendations, key=lambda x: (x.priority, -x.confidence)):
            if rec.domain in ready:
                ordered.append(rec.domain)
                remaining.discard(rec.domain)
                break

    return ordered
