"""Suppression logic for low-quality data or low confidence.

Evaluates the quality of the underlying data feeding a trend summary
and suppresses automated trade eligibility when data quality is poor.
Suppressed recommendations are marked as informational only.

This layer runs *before* the eligibility engine and acts as a pre-filter
on data quality. The eligibility engine handles signal-level thresholds
(confidence, strength, contradiction); this module handles data-level
quality concerns (stale evidence, low extraction quality, poor source
diversity, insufficient valid documents).

Requirements: 7.4
"""
from __future__ import annotations

import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum

from services.shared.schemas import TrendSummary

logger = logging.getLogger(__name__)


class SuppressionReason(str, Enum):
    """Why a recommendation was suppressed due to data quality."""

    LOW_DATA_CONFIDENCE = "low_data_confidence"
    STALE_EVIDENCE = "stale_evidence"
    LOW_SOURCE_DIVERSITY = "low_source_diversity"
    HIGH_EXTRACTION_FAILURE_RATE = "high_extraction_failure_rate"
    INSUFFICIENT_VALID_DOCUMENTS = "insufficient_valid_documents"
    MACRO_ONLY_SIGNAL = "macro_only_signal"
    PATTERN_ONLY_SIGNAL = "pattern_only_signal"


@dataclass(frozen=True)
class SuppressionConfig:
    """Tunable thresholds for data quality suppression.

    These thresholds focus on the quality of the *input data* rather
    than the trend signal itself (which is handled by EligibilityConfig).
    """

    # Minimum average extraction confidence across evidence documents.
    # Below this, the underlying data is too unreliable for trade decisions.
    min_avg_extraction_confidence: float = 0.40

    # Maximum age (hours) of the most recent evidence document.
    # If the freshest evidence is older than this, the trend is stale.
    max_evidence_staleness_hours: float = 168.0  # 7 days

    # Minimum number of distinct source types (e.g. news, filings, market)
    # represented in the evidence. Low diversity means the signal may be
    # driven by a single unreliable source class.
    min_source_types: int = 1

    # Maximum tolerable extraction failure rate (0-1).
    # If more than this fraction of documents failed extraction,
    # the data pipeline is unreliable for this ticker.
    max_extraction_failure_rate: float = 0.50

    # Minimum number of valid (non-failed) documents that contributed
    # to the trend. Below this, there isn't enough data to act on.
    min_valid_documents: int = 2

    # Overall data quality confidence threshold.
    # The computed data quality score must exceed this for the
    # recommendation to be eligible for automated trading.
    min_data_quality_score: float = 0.30


DEFAULT_SUPPRESSION_CONFIG = SuppressionConfig()


@dataclass
class DataQualityContext:
    """Quality metrics about the data underlying a trend summary.

    Populated by querying document and extraction metadata for the
    ticker and window. When not available from the database, callers
    can construct this from the trend summary itself.
    """

    total_documents: int = 0
    valid_documents: int = 0
    failed_documents: int = 0
    avg_extraction_confidence: float = 0.0
    newest_evidence_at: datetime | None = None
    source_types: set[str] = field(default_factory=set)


@dataclass
class SuppressionResult:
    """Output of the suppression evaluation."""

    suppressed: bool
    reasons: list[SuppressionReason] = field(default_factory=list)
    data_quality_score: float = 0.0
    context: DataQualityContext | None = None


def build_quality_context_from_summary(
    summary: TrendSummary,
) -> DataQualityContext:
    """Build a minimal DataQualityContext from a TrendSummary.

    This is a fallback when full document-level quality metrics aren't
    available. It uses the trend summary's evidence counts and confidence
    as proxies.  We assume at least one source type contributed so that
    the fallback does not automatically trigger LOW_SOURCE_DIVERSITY.
    """
    total = len(summary.top_supporting_evidence) + len(summary.top_opposing_evidence)
    return DataQualityContext(
        total_documents=total,
        valid_documents=total,
        failed_documents=0,
        avg_extraction_confidence=summary.confidence,
        newest_evidence_at=summary.generated_at,
        source_types={"unknown"},
    )


def _compute_data_quality_score(
    ctx: DataQualityContext,
    config: SuppressionConfig,
    reference_time: datetime,
) -> float:
    """Compute an overall data quality score from the context.

    Returns a value in [0, 1] where higher is better quality.
    Components:
    - Extraction confidence (40% weight)
    - Evidence freshness (30% weight)
    - Document coverage (30% weight)
    """
    # Extraction confidence component
    conf_component = min(ctx.avg_extraction_confidence / 0.8, 1.0)

    # Freshness component
    if ctx.newest_evidence_at is not None:
        if ctx.newest_evidence_at.tzinfo is None:
            newest = ctx.newest_evidence_at.replace(tzinfo=timezone.utc)
        else:
            newest = ctx.newest_evidence_at
        age_hours = (reference_time - newest).total_seconds() / 3600.0
        max_hours = config.max_evidence_staleness_hours
        freshness_component = max(0.0, 1.0 - (age_hours / max_hours))
    else:
        freshness_component = 0.0

    # Document coverage component
    if ctx.total_documents > 0:
        valid_ratio = ctx.valid_documents / ctx.total_documents
        count_factor = min(ctx.valid_documents / 10.0, 1.0)
        coverage_component = valid_ratio * count_factor
    else:
        coverage_component = 0.0

    score = (0.4 * conf_component) + (0.3 * freshness_component) + (0.3 * coverage_component)
    return round(max(0.0, min(1.0, score)), 4)


def evaluate_suppression(
    summary: TrendSummary,
    quality_ctx: DataQualityContext | None = None,
    config: SuppressionConfig = DEFAULT_SUPPRESSION_CONFIG,
    reference_time: datetime | None = None,
) -> SuppressionResult:
    """Evaluate whether a recommendation should be suppressed due to data quality.

    Checks multiple data quality dimensions and returns a SuppressionResult
    indicating whether the recommendation should be suppressed and why.

    Args:
        summary: The trend summary to evaluate.
        quality_ctx: Data quality context. If None, a minimal context is
            built from the trend summary itself.
        config: Suppression thresholds.
        reference_time: Reference time for staleness checks.

    Returns:
        SuppressionResult with suppression decision and reasons.
    """
    if reference_time is None:
        reference_time = datetime.now(timezone.utc)

    ctx = quality_ctx or build_quality_context_from_summary(summary)
    reasons: list[SuppressionReason] = []

    # Check average extraction confidence
    if ctx.avg_extraction_confidence < config.min_avg_extraction_confidence:
        reasons.append(SuppressionReason.LOW_DATA_CONFIDENCE)

    # Check evidence staleness
    if ctx.newest_evidence_at is not None:
        newest = ctx.newest_evidence_at
        if newest.tzinfo is None:
            newest = newest.replace(tzinfo=timezone.utc)
        age_hours = (reference_time - newest).total_seconds() / 3600.0
        if age_hours > config.max_evidence_staleness_hours:
            reasons.append(SuppressionReason.STALE_EVIDENCE)
    elif ctx.total_documents > 0:
        # Have documents but no timestamp — treat as stale
        reasons.append(SuppressionReason.STALE_EVIDENCE)

    # Check source diversity
    if len(ctx.source_types) < config.min_source_types and ctx.total_documents > 0:
        reasons.append(SuppressionReason.LOW_SOURCE_DIVERSITY)

    # Check extraction failure rate
    if ctx.total_documents > 0:
        failure_rate = ctx.failed_documents / ctx.total_documents
        if failure_rate > config.max_extraction_failure_rate:
            reasons.append(SuppressionReason.HIGH_EXTRACTION_FAILURE_RATE)

    # Check minimum valid documents
    if ctx.valid_documents < config.min_valid_documents:
        reasons.append(SuppressionReason.INSUFFICIENT_VALID_DOCUMENTS)

    # Compute overall data quality score
    quality_score = _compute_data_quality_score(ctx, config, reference_time)

    # If quality score is below threshold, add a general suppression reason
    if quality_score < config.min_data_quality_score and SuppressionReason.LOW_DATA_CONFIDENCE not in reasons:
        reasons.append(SuppressionReason.LOW_DATA_CONFIDENCE)

    suppressed = len(reasons) > 0

    if suppressed:
        logger.info(
            "Recommendation suppressed for %s/%s: reasons=%s quality_score=%.3f",
            summary.entity_id, summary.window.value,
            [r.value for r in reasons], quality_score,
        )

    return SuppressionResult(
        suppressed=suppressed,
        reasons=reasons,
        data_quality_score=quality_score,
        context=ctx,
    )


# ---------------------------------------------------------------------------
# Macro-only suppression (Requirements: 10.3)
# ---------------------------------------------------------------------------

MACRO_ONLY_CAVEAT = (
    "[Macro-only signal] This trend direction is driven solely by macro/geopolitical "
    "signals with no supporting company-specific evidence. Recommendation is "
    "informational only and should not be used for automated trading decisions."
)


def evaluate_macro_only_suppression(
    summary: TrendSummary,
    macro_signal_count: int,
    company_signal_count: int,
) -> bool:
    """Evaluate whether a recommendation should be suppressed due to macro-only signals.

    When macro signals are the sole basis for a trend direction change
    (no supporting company-specific signals), the recommendation should
    be forced to informational mode with a macro-only caveat.

    Args:
        summary: The trend summary to evaluate.
        macro_signal_count: Number of macro signals contributing to the trend.
        company_signal_count: Number of company-specific signals contributing.

    Returns:
        True if the recommendation should be suppressed (macro-only), False otherwise.

    Requirements: 10.3
    """
    # No macro signals means no macro-only suppression
    if macro_signal_count <= 0:
        return False

    # If there are company-specific signals, no suppression needed
    if company_signal_count > 0:
        return False

    # Macro signals are the sole basis — suppress
    logger.info(
        "Macro-only suppression triggered for %s/%s: "
        "macro_signals=%d, company_signals=%d, direction=%s",
        summary.entity_id,
        summary.window.value,
        macro_signal_count,
        company_signal_count,
        summary.trend_direction.value,
    )
    return True


# ---------------------------------------------------------------------------
# Pattern-only suppression (Requirements: 9.3)
# ---------------------------------------------------------------------------

PATTERN_ONLY_CAVEAT = (
    "[Pattern-only signal] This trend direction is driven solely by historical "
    "pattern and competitive signals with no supporting company-specific or macro "
    "evidence. Recommendation is informational only."
)


def evaluate_pattern_only_suppression(
    summary: TrendSummary,
    pattern_signal_count: int,
    company_signal_count: int,
    macro_signal_count: int,
) -> bool:
    """Evaluate whether a recommendation should be suppressed due to pattern-only signals.

    When pattern-based signals are the sole basis for a trend direction change
    (no supporting company-specific or macro signals), the recommendation should
    be forced to informational mode with a pattern-only caveat.

    Args:
        summary: The trend summary to evaluate.
        pattern_signal_count: Number of pattern/competitive signals contributing.
        company_signal_count: Number of company-specific signals contributing.
        macro_signal_count: Number of macro signals contributing.

    Returns:
        True if the recommendation should be suppressed (pattern-only), False otherwise.

    Requirements: 9.3
    """
    # No pattern signals means no pattern-only suppression
    if pattern_signal_count <= 0:
        return False

    # If there are company-specific signals, no suppression needed
    if company_signal_count > 0:
        return False

    # If there are macro signals, no suppression needed
    if macro_signal_count > 0:
        return False

    # Pattern signals are the sole basis — suppress
    logger.info(
        "Pattern-only suppression triggered for %s/%s: "
        "pattern_signals=%d, company_signals=%d, macro_signals=%d, direction=%s",
        summary.entity_id,
        summary.window.value,
        pattern_signal_count,
        company_signal_count,
        macro_signal_count,
        summary.trend_direction.value,
    )
    return True
