"""Core period calculation API - main entry points."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService

    from .types import TibberPricesPeriodConfig

from .outlier_filtering import (
    filter_price_outliers,
)
from .period_building import (
    add_interval_ends,
    build_periods,
    calculate_reference_prices,
    extend_periods_across_midnight,
    filter_periods_by_end_date,
    filter_periods_by_min_length,
    filter_superseded_periods,
    split_intervals_by_day,
)
from .period_statistics import (
    extract_period_summaries,
)
from .types import TibberPricesThresholdConfig

# Flex limits to prevent degenerate behavior (see docs/development/period-calculation-theory.md)
MAX_SAFE_FLEX = 0.50  # 50% - hard cap: above this, period detection becomes unreliable
MAX_OUTLIER_FLEX = 0.25  # 25% - cap for outlier filtering: above this, spike detection too permissive


def calculate_periods(
    all_prices: list[dict],
    *,
    config: TibberPricesPeriodConfig,
    time: TibberPricesTimeService,
) -> dict[str, Any]:
    """
    Calculate price periods (best or peak) from price data.

    This function identifies periods but does NOT store full interval data redundantly.
    It returns lightweight period summaries that reference the original price data.

    Steps:
    1. Split prices by day and calculate daily averages
    2. Calculate reference prices (min/max per day)
    3. Build periods based on criteria
    4. Filter by minimum length
    5. Add interval ends
    6. Filter periods by end date
    7. Extract period summaries (start/end times, not full price data)

    Args:
        all_prices: All price data points from yesterday/today/tomorrow.
        config: Period configuration containing reverse_sort, flex, min_distance_from_avg,
                min_period_length, threshold_low, and threshold_high.
        time: TibberPricesTimeService instance (required).

    Returns:
        Dict with:
        - periods: List of lightweight period summaries (start/end times only)
        - metadata: Config and statistics
        - reference_data: Daily min/max/avg for on-demand annotation

    """
    # Import logger at the start of function
    import logging  # noqa: PLC0415

    from .types import INDENT_L0  # noqa: PLC0415

    _LOGGER = logging.getLogger(__name__)  # noqa: N806

    # Extract config values
    reverse_sort = config.reverse_sort
    flex_raw = config.flex  # Already normalized to positive by get_period_config()
    min_distance_from_avg = config.min_distance_from_avg
    min_period_length = config.min_period_length
    threshold_low = config.threshold_low
    threshold_high = config.threshold_high

    # CRITICAL: Hard cap flex at 50% to prevent degenerate behavior
    # Above 50%, period detection becomes unreliable (too many intervals qualify)
    # NOTE: flex_raw is already positive from normalization in get_period_config()
    flex = flex_raw
    if flex_raw > MAX_SAFE_FLEX:
        flex = MAX_SAFE_FLEX
        _LOGGER.warning(
            "Flex %.1f%% exceeds maximum safe value! Capping at %.0f%%. "
            "Recommendation: Use 15-20%% with relaxation enabled, or 25-35%% without relaxation.",
            flex_raw * 100,
            MAX_SAFE_FLEX * 100,
        )

    if not all_prices:
        return {
            "periods": [],
            "metadata": {
                "total_periods": 0,
                "config": {
                    "reverse_sort": reverse_sort,
                    "flex": flex,
                    "min_distance_from_avg": min_distance_from_avg,
                    "min_period_length": min_period_length,
                },
            },
            "reference_data": {
                "ref_prices": {},
                "avg_prices": {},
            },
        }

    # Ensure prices are sorted chronologically
    all_prices_sorted = sorted(all_prices, key=lambda p: p["startsAt"])

    # Step 1: Split by day and calculate averages
    intervals_by_day, avg_price_by_day = split_intervals_by_day(all_prices_sorted, time=time)

    # Step 2: Calculate reference prices (min or max per day)
    ref_prices = calculate_reference_prices(intervals_by_day, reverse_sort=reverse_sort)

    # Step 2.5: Filter price outliers (smoothing for period formation only)
    # This runs BEFORE period formation to prevent isolated price spikes
    # from breaking up otherwise continuous periods

    # CRITICAL: Cap flexibility for outlier filtering at 25%
    # High flex (>25%) makes outlier detection too permissive, accepting
    # unstable price contexts as "normal". This breaks period formation.
    # User's flex setting still applies to period criteria (in_flex check).

    # Import details logger locally (core.py imports logger locally in function)
    _LOGGER_DETAILS = logging.getLogger(__name__ + ".details")  # noqa: N806

    outlier_flex = min(abs(flex) * 100, MAX_OUTLIER_FLEX * 100)
    if abs(flex) * 100 > MAX_OUTLIER_FLEX * 100:
        _LOGGER_DETAILS.debug(
            "%sOutlier filtering: Using capped flex %.1f%% (user setting: %.1f%%)",
            INDENT_L0,
            outlier_flex,
            abs(flex) * 100,
        )

    all_prices_smoothed = filter_price_outliers(
        all_prices_sorted,
        outlier_flex,  # Use capped flex for outlier detection
        min_period_length,
    )

    # Step 3: Build periods
    price_context = {
        "ref_prices": ref_prices,
        "avg_prices": avg_price_by_day,
        "intervals_by_day": intervals_by_day,  # Needed for day volatility calculation
        "flex": flex,
        "min_distance_from_avg": min_distance_from_avg,
    }
    raw_periods = build_periods(
        all_prices_smoothed,  # Use smoothed prices for period formation
        price_context,
        reverse_sort=reverse_sort,
        level_filter=config.level_filter,
        gap_count=config.gap_count,
        time=time,
    )

    _LOGGER.debug(
        "%sAfter build_periods: %d raw periods found (flex=%.1f%%, level_filter=%s)",
        INDENT_L0,
        len(raw_periods),
        abs(flex) * 100,
        config.level_filter or "None",
    )

    # Step 4: Filter by minimum length
    raw_periods = filter_periods_by_min_length(raw_periods, min_period_length, time=time)
    _LOGGER.debug(
        "%sAfter filter_by_min_length (>= %d min): %d periods remain",
        INDENT_L0,
        min_period_length,
        len(raw_periods),
    )

    # Step 5: Add interval ends
    add_interval_ends(raw_periods, time=time)

    # Step 6: Filter periods by end date (keep periods ending yesterday or later)
    # This ensures coordinator cache contains yesterday/today/tomorrow periods
    # Sensors filter further for today+tomorrow, services can access all cached periods
    raw_periods = filter_periods_by_end_date(raw_periods, time=time)

    # Step 7: Extract lightweight period summaries (no full price data)
    # Note: Periods are filtered by end date to keep yesterday/today/tomorrow.
    # This preserves periods that started day-before-yesterday but end yesterday.
    thresholds = TibberPricesThresholdConfig(
        threshold_low=threshold_low,
        threshold_high=threshold_high,
        threshold_volatility_moderate=config.threshold_volatility_moderate,
        threshold_volatility_high=config.threshold_volatility_high,
        threshold_volatility_very_high=config.threshold_volatility_very_high,
        reverse_sort=reverse_sort,
    )
    period_summaries = extract_period_summaries(
        raw_periods,
        all_prices_sorted,
        price_context,
        thresholds,
        time=time,
    )

    # Step 8: Cross-day extension for late-night periods
    # If a best-price period ends near midnight and tomorrow has continued low prices,
    # extend the period across midnight to give users the full cheap window
    period_summaries = extend_periods_across_midnight(
        period_summaries,
        all_prices_sorted,
        price_context,
        time=time,
        reverse_sort=reverse_sort,
    )

    # Step 9: Filter superseded periods
    # When tomorrow data is available, late-night today periods that were found via
    # relaxation may be obsolete if tomorrow has significantly better alternatives
    period_summaries = filter_superseded_periods(
        period_summaries,
        time=time,
        reverse_sort=reverse_sort,
    )

    return {
        "periods": period_summaries,  # Lightweight summaries only
        "metadata": {
            "total_periods": len(period_summaries),
            "config": {
                "reverse_sort": reverse_sort,
                "flex": flex,
                "min_distance_from_avg": min_distance_from_avg,
                "min_period_length": min_period_length,
            },
        },
        "reference_data": {
            "ref_prices": {k.isoformat(): v for k, v in ref_prices.items()},
            "avg_prices": {k.isoformat(): v for k, v in avg_price_by_day.items()},
        },
    }
