"""
Telemetry anomaly processing using Polars vectorized operations.

Six focused functions for business activity anomalies: spikes and progressive changes.
Follows KISS principles and CloudZero coding standards.
"""

from datetime import datetime, timedelta
import polars as pl

from src.shared.anomaly import (
    calculate_anomaly_timing,
    validate_anomaly_config,
    calculate_anomaly_occurrences_in_job,
)
from .step_state_manager import get_step_state_manager


def apply_daily_spike(df: pl.DataFrame, config: dict, resource_id: str) -> pl.DataFrame:
    """
    Apply daily spike anomaly to telemetry unit values.

    Creates dramatic 3-day activity spikes on auto-calculated days for compelling demos.
    Each spike lasts for 3 consecutive days starting from the calculated spike day.
    Applies recurring spikes across all periods within job timeframe.

    Args:
        df: DataFrame with unit_value and timestamp columns
        config: Anomaly configuration (type, intensity)
        resource_id: Unique element identifier

    Returns:
        DataFrame with 3-day spikes applied on all auto-calculated occurrence days
    """
    if len(df) == 0:
        return df

    validate_anomaly_config(config)

    # Calculate timing and intensity
    day, hour, period = calculate_anomaly_timing("daily_spike")
    intensity = _get_spike_intensity(config, "daily_spike", default=10.0)

    # Calculate spike occurrences
    timeline_start, timeline_end = _get_job_timeline_boundaries(df)
    spike_times = calculate_anomaly_occurrences_in_job(
        timeline_start, timeline_end, period, day, hour
    )

    # Apply 3-day spikes at each occurrence
    return _apply_daily_spikes(df, spike_times, intensity)


def _get_spike_intensity(config: dict, anomaly_type: str, default: float) -> float:
    """Get spike intensity from config or use default."""
    return config.get("intensity", default)


def _apply_daily_spikes(df: pl.DataFrame, spike_times: list, intensity: float) -> pl.DataFrame:
    """Apply 3-day spikes at each spike time."""
    result_df = df
    for spike_time in spike_times:
        spike_start_date = spike_time.date()
        spike_dates = [
            spike_start_date,
            spike_start_date + timedelta(days=1),
            spike_start_date + timedelta(days=2)
        ]

        spike_condition = pl.col("timestamp").dt.date().is_in(spike_dates)
        result_df = result_df.with_columns([
            pl.when(spike_condition)
            .then(pl.col("unit_value") * intensity)
            .otherwise(pl.col("unit_value"))
            .alias("unit_value")
        ])

    return result_df


def apply_daily_step_up(df: pl.DataFrame, config: dict, resource_id: str) -> pl.DataFrame:
    """
    Apply daily step up anomaly creating ascending staircase pattern.
    
    Each step: multiply by 2 (2x of previous level).
    Creates clear visual ascending staircase in CloudZero charts.
    """
    if len(df) == 0:
        return df
    
    validate_anomaly_config(config)
    
    # Get step state manager for baseline and increment tracking
    state_manager = get_step_state_manager()
    if not state_manager:
        return df  # Cannot apply step changes without state management
    
    # For multiplicative step up, we don't need baseline cost calculation
    # Each step simply multiplies by 2, creating exponential growth
    baseline_cost = 100.0  # Placeholder - not used in multiplicative approach
    step_multiplier = 2.0  # Each step multiplies by 2
    
    # Save step information for cross-month consistency  
    state_manager.save_step_baseline(resource_id, baseline_cost, "step_up", step_multiplier)
    
    # Apply multiplicative staircase progression to entire dataset
    return _apply_multiplicative_staircase_progression(df, resource_id, "step_up")


def _calculate_baseline_cost(df: pl.DataFrame, state_manager, resource_id: str) -> float:
    """Calculate baseline cost from earliest data or state."""
    
    # Try to get cached baseline from state
    cached_baseline = state_manager.get_baseline_cost(resource_id)
    if cached_baseline is not None:
        return cached_baseline
    
    # Calculate baseline from first 30 days of data (before first step)
    day, hour, period = calculate_anomaly_timing("daily_step_up") 
    timeline_start, timeline_end = _get_job_timeline_boundaries(df)
    step_times = calculate_anomaly_occurrences_in_job(timeline_start, timeline_end, period, day, hour)
    
    if not step_times:
        # No steps planned, use overall mean
        return float(df.select("unit_value").mean().to_numpy()[0])
    
    # Use data before first step as baseline
    first_step_date = step_times[0].date()
    baseline_df = df.filter(pl.col("timestamp").dt.date() < first_step_date)
    
    if len(baseline_df) == 0:
        # Fallback to overall mean if no pre-step data
        return float(df.select("unit_value").mean().to_numpy()[0])
    
    return float(baseline_df.select("unit_value").mean().to_numpy()[0])


def _apply_staircase_progression(
    df: pl.DataFrame,
    resource_id: str,
    step_type: str,
    baseline_cost: float,
    absolute_increment: float
) -> pl.DataFrame:
    """Apply staircase progression with cumulative step effects for month-by-month processing."""

    if len(df) == 0:
        return df

    state_manager = get_step_state_manager()
    if not state_manager:
        return df

    # Calculate step occurrences
    step_context = _calculate_step_context(df, step_type)
    if step_context["step_times"] is None:
        return df

    # Apply prior cumulative steps
    result_df = _apply_mixed_prior_steps(
        df, step_context["steps_active_before_month"],
        step_type, absolute_increment
    )

    # Apply current month steps
    result_df = _apply_mixed_current_steps(
        result_df, step_context, resource_id,
        step_type, absolute_increment, state_manager
    )

    return result_df


def _calculate_step_context(df: pl.DataFrame, step_type: str) -> dict:
    """Calculate step timing context for current month."""
    current_month_start = df["timestamp"].min()
    current_month_end = df["timestamp"].max()

    day, hour, period = calculate_anomaly_timing(f"daily_{step_type}")
    timeline_start, timeline_end = _get_job_timeline_boundaries(df)
    step_times = calculate_anomaly_occurrences_in_job(
        timeline_start, timeline_end, period, day, hour
    )

    if not step_times:
        return {"step_times": None}

    steps_active_before_month = sum(
        1 for step_time in step_times if step_time < current_month_start
    )
    steps_within_month = [
        step_time for step_time in step_times
        if current_month_start <= step_time <= current_month_end
    ]

    return {
        "step_times": step_times,
        "steps_active_before_month": steps_active_before_month,
        "steps_within_month": steps_within_month,
    }


def _apply_mixed_prior_steps(
    df: pl.DataFrame,
    steps_active_before_month: int,
    step_type: str,
    absolute_increment: float
) -> pl.DataFrame:
    """Apply prior cumulative steps (mixed multiplicative + additive)."""
    if steps_active_before_month == 0:
        return df

    if steps_active_before_month == 1:
        # First step was multiplicative (5x or 1/5)
        prior_multiplier = 5.0 if step_type == "step_up" else 0.2
        return df.with_columns([
            (pl.col("unit_value") * prior_multiplier).alias("unit_value")
        ])

    # First step multiplicative, then additive steps
    first_multiplier = 5.0 if step_type == "step_up" else 0.2
    additive_steps = steps_active_before_month - 1
    increment = absolute_increment if step_type == "step_up" else -absolute_increment

    return df.with_columns([
        (pl.col("unit_value") * first_multiplier +
         (additive_steps * increment)).alias("unit_value")
    ])


def _apply_mixed_current_steps(
    df: pl.DataFrame,
    step_context: dict,
    resource_id: str,
    step_type: str,
    absolute_increment: float,
    state_manager
) -> pl.DataFrame:
    """Apply steps occurring within current month (mixed logic)."""
    result_df = df
    steps_active_before_month = step_context["steps_active_before_month"]

    for step_num, step_time in enumerate(step_context["steps_within_month"]):
        global_step_num = steps_active_before_month + step_num

        if global_step_num == 0:
            # First step overall: multiplicative
            step_multiplier = 5.0 if step_type == "step_up" else 0.2
            result_df = result_df.with_columns([
                pl.when(pl.col("timestamp") >= step_time)
                .then(pl.col("unit_value") * step_multiplier)
                .otherwise(pl.col("unit_value"))
                .alias("unit_value")
            ])
        else:
            # Subsequent step: additive increment
            increment = absolute_increment if step_type == "step_up" else -absolute_increment
            result_df = result_df.with_columns([
                pl.when(pl.col("timestamp") >= step_time)
                .then(pl.col("unit_value") + increment)
                .otherwise(pl.col("unit_value"))
                .alias("unit_value")
            ])

        # Record step in state for future months
        step_date_str = step_time.strftime("%Y-%m-%d")
        state_manager.save_step_change(
            resource_id, step_type, step_date_str, global_step_num + 1
        )

    return result_df


def apply_daily_step_down(df: pl.DataFrame, config: dict, resource_id: str) -> pl.DataFrame:
    """
    Apply daily step down anomaly creating descending staircase pattern.
    
    Each step: divide by 2 (1/2 of previous level).
    Creates clear visual descending staircase in CloudZero charts.
    """
    if len(df) == 0:
        return df
    
    validate_anomaly_config(config)
    
    # Get step state manager for baseline and increment tracking
    state_manager = get_step_state_manager()
    if not state_manager:
        return df  # Cannot apply step changes without state management
    
    # For multiplicative step down, we don't need baseline cost calculation
    # Each step simply divides by 2, so no risk of negative values
    baseline_cost = 100.0  # Placeholder - not used in multiplicative approach
    step_divisor = 2.0  # Each step divides by 2
    
    # Save step information for cross-month consistency  
    state_manager.save_step_baseline(resource_id, baseline_cost, "step_down", step_divisor)
    
    # Apply multiplicative staircase progression to entire dataset
    return _apply_multiplicative_staircase_progression(df, resource_id, "step_down")


def apply_hourly_spike(df: pl.DataFrame, config: dict, resource_id: str) -> pl.DataFrame:
    """
    Apply hourly spike anomaly - affects specific hours within a day.
    
    Creates dramatic cost spike during auto-calculated hour range on auto-calculated day.
    Uses auto-calculated timing for simplified configuration.
    """
    if len(df) == 0:
        return df
    
    # Validate configuration
    validate_anomaly_config(config)
    
    # Auto-determine timing and period based on anomaly type
    duration_hours = 4  # Smart default: 4-hour spike window  
    day, hour, period = calculate_anomaly_timing("hourly_spike")
    
    # Auto-calculate intensity if variance config is available
    if "variance_config" in config and "intensity" not in config:
        intensity = calculate_anomaly_intensity("hourly_spike", config["variance_config"])
    else:
        intensity = config.get("intensity", 18.0)  # Increased to rise above 360% CV variance
    
    # Get job timeline boundaries
    timeline_start, timeline_end = _get_job_timeline_boundaries(df)
    
    # Calculate all spike occurrences within job timeframe
    spike_times = calculate_anomaly_occurrences_in_job(
        timeline_start, timeline_end, period, day, hour
    )
    
    # Apply spikes to all occurrence time windows using vectorized operations
    result_df = df
    for spike_time in spike_times:
        spike_end_time = spike_time + timedelta(hours=duration_hours)
        result_df = result_df.with_columns([
            pl.when(
                (pl.col("timestamp") >= spike_time) &
                (pl.col("timestamp") < spike_end_time)
            )
            .then(pl.col("unit_value") * intensity)
            .otherwise(pl.col("unit_value"))
            .alias("unit_value")
        ])
    
    return result_df


def apply_hourly_step_up(df: pl.DataFrame, config: dict, resource_id: str) -> pl.DataFrame:
    """
    Apply hourly step up anomaly creating ascending staircase pattern.
    
    Each step: multiply by 2 (2x of previous level).
    Creates clear visual ascending staircase in CloudZero charts.
    """
    if len(df) == 0:
        return df
    
    validate_anomaly_config(config)
    
    # Get step state manager for baseline and increment tracking
    state_manager = get_step_state_manager()
    if not state_manager:
        return df  # Cannot apply step changes without state management
    
    # For multiplicative step up, we don't need baseline cost calculation
    # Each step simply multiplies by 2, creating exponential growth
    baseline_cost = 100.0  # Placeholder - not used in multiplicative approach
    step_multiplier = 2.0  # Each step multiplies by 2
    
    # Save step information for cross-month consistency  
    state_manager.save_step_baseline(resource_id, baseline_cost, "step_up", step_multiplier)
    
    # Apply multiplicative staircase progression to entire dataset
    return _apply_multiplicative_staircase_progression(df, resource_id, "step_up")


def apply_hourly_step_down(df: pl.DataFrame, config: dict, resource_id: str) -> pl.DataFrame:
    """
    Apply hourly step down anomaly creating descending staircase pattern.
    
    Each step: divide by 2 (1/2 of previous level).
    Creates clear visual descending staircase in CloudZero charts.
    """
    if len(df) == 0:
        return df
    
    validate_anomaly_config(config)
    
    # Get step state manager for baseline and increment tracking
    state_manager = get_step_state_manager()
    if not state_manager:
        return df  # Cannot apply step changes without state management
    
    # For multiplicative step down, we don't need baseline cost calculation
    # Each step simply divides by 2, so no risk of negative values
    baseline_cost = 100.0  # Placeholder - not used in multiplicative approach
    step_divisor = 2.0  # Each step divides by 2
    
    # Save step information for cross-month consistency  
    state_manager.save_step_baseline(resource_id, baseline_cost, "step_down", step_divisor)
    
    # Apply multiplicative staircase progression to entire dataset
    return _apply_multiplicative_staircase_progression(df, resource_id, "step_down")


def _apply_multiplicative_staircase_progression(
    df: pl.DataFrame,
    resource_id: str,
    step_type: str
) -> pl.DataFrame:
    """Apply multiplicative staircase progression (×2 for step_up, ÷2 for step_down)."""

    if len(df) == 0:
        return df

    state_manager = get_step_state_manager()
    if not state_manager:
        return df

    # Calculate step occurrences
    step_context = _calculate_step_context(df, step_type)
    if step_context["step_times"] is None:
        return df

    # Apply prior cumulative steps
    result_df = _apply_multiplicative_prior_steps(
        df, step_context["steps_active_before_month"], step_type
    )

    # Apply current month steps
    result_df = _apply_multiplicative_current_steps(
        result_df, step_context, resource_id, step_type, state_manager
    )

    return result_df


def _apply_multiplicative_prior_steps(
    df: pl.DataFrame,
    steps_active_before_month: int,
    step_type: str
) -> pl.DataFrame:
    """Apply prior cumulative steps (pure multiplicative)."""
    if steps_active_before_month == 0:
        return df

    if step_type == "step_up":
        # Each step multiplies by 2, so after N steps: original × (2^N)
        cumulative_multiplier = 2.0 ** steps_active_before_month
        return df.with_columns([
            (pl.col("unit_value") * cumulative_multiplier).alias("unit_value")
        ])

    # step_down: Each step divides by 2, so after N steps: original ÷ (2^N)
    cumulative_divisor = 2.0 ** steps_active_before_month
    return df.with_columns([
        (pl.col("unit_value") / cumulative_divisor).alias("unit_value")
    ])


def _apply_multiplicative_current_steps(
    df: pl.DataFrame,
    step_context: dict,
    resource_id: str,
    step_type: str,
    state_manager
) -> pl.DataFrame:
    """Apply steps occurring within current month (pure multiplicative)."""
    result_df = df
    steps_active_before_month = step_context["steps_active_before_month"]

    for step_num, step_time in enumerate(step_context["steps_within_month"]):
        if step_type == "step_up":
            # Each step multiplies by 2
            result_df = result_df.with_columns([
                pl.when(pl.col("timestamp") >= step_time)
                .then(pl.col("unit_value") * 2.0)
                .otherwise(pl.col("unit_value"))
                .alias("unit_value")
            ])
        else:  # step_down
            # Each step divides by 2
            result_df = result_df.with_columns([
                pl.when(pl.col("timestamp") >= step_time)
                .then(pl.col("unit_value") / 2.0)
                .otherwise(pl.col("unit_value"))
                .alias("unit_value")
            ])

        # Record step in state for future months
        global_step_num = steps_active_before_month + step_num
        step_date_str = step_time.strftime("%Y-%m-%d")
        state_manager.save_step_change(
            resource_id, step_type, step_date_str, global_step_num + 1
        )

    return result_df


# Helper functions for implementation (stubs ready for use)

def _map_period_day_to_condition(day: int, period: str) -> str:
    """
    STUB: Map CloudZero period day to Polars condition.
    
    Will convert period specifications to DataFrame conditions.
    """
    # TODO: Implement period mapping logic
    return "true"  # Placeholder condition


def _get_job_timeline_boundaries(df: pl.DataFrame) -> tuple[datetime, datetime]:
    """
    Extract job timeline boundaries from DataFrame.
    
    Analyzes timestamp column to determine actual data boundaries.
    """
    if len(df) == 0 or "timestamp" not in df.columns:
        now = datetime.now()
        return (now, now)
    
    # Get actual min/max timestamps from the data
    min_timestamp = df["timestamp"].min()
    max_timestamp = df["timestamp"].max()
    
    return (min_timestamp, max_timestamp)