"""
90-day anomaly generation algorithms for cost optimization demonstrations.

Extracted from existing pipeline generate package. Creates deterministic anomaly
events at configurable intervals that demonstrate cost optimization opportunities
and security incidents for presenter reliability.

These algorithms work alongside variance algorithms to create compelling
quarterly demo scenarios.

Simplified Profile Support:
- Supports 5 high-value anomaly profiles with maximum demo distinctiveness
- Handles anomaly types: spike, step_up (permanent increase), step_down
- Supports timing patterns: random, month_end_window
- Includes permanent baseline shifts for infrastructure scaling scenarios
- Maintains random timing within windows for realistic demo patterns
"""

import hashlib
from datetime import datetime
from typing import Dict, List, Any

import numpy as np


def hash_deterministic_seed(resource_id: str, base_seed: str = "anomaly") -> int:
    """
    Create deterministic seed from resource ID for reproducible anomaly patterns.

    Args:
        resource_id: Unique resource identifier
        base_seed: Base seed string for different pattern types

    Returns:
        Deterministic integer seed for this resource
    """
    combined = f"{base_seed}_{resource_id}"
    hash_value = hashlib.md5(combined.encode()).hexdigest()
    return int(hash_value[:8], 16)


def generate_90_day_anomalies(
    costs: List[float],
    timestamps: List[str],
    anomaly_config: Dict[str, Any],
    resource_id: str,
) -> List[float]:
    """
    Apply interval-based anomalies with enhanced type differentiation.

    ENHANCED: Now supports all anomaly types and enhanced timing logic.

    Args:
        costs: Base cost values (may already have variance applied)
        timestamps: ISO timestamp strings
        anomaly_config: Enhanced anomaly configuration from core profiles
        resource_id: Unique resource identifier for deterministic patterns

    Returns:
        Enhanced costs with anomalies applied
    """
    if not costs or not anomaly_config:
        return costs

    # Set up deterministic random state
    np.random.seed(hash_deterministic_seed(resource_id, "anomaly"))

    # Parse enhanced anomaly configuration
    interval_days = anomaly_config.get("interval_days", 90)
    intensity = anomaly_config.get("intensity", 0.8)
    duration_hours = anomaly_config.get("duration_hours", 72)
    timing = anomaly_config.get("timing", "random")

    # ENHANCED: Parse new parameters
    anomaly_type = anomaly_config.get("anomaly_type", "spike")
    frequency = anomaly_config.get("frequency", 0.1)
    business_hours_bias = anomaly_config.get("business_hours_bias", 0.5)
    quarter_end_bias = anomaly_config.get("quarter_end_bias", 0.5)
    month_end_bias = anomaly_config.get("month_end_bias", 0.5)

    # Convert timestamps to datetime objects (handle both strings and date objects)
    dt_timestamps = []
    for ts in timestamps:
        if hasattr(ts, "isoformat"):
            ts = ts.isoformat()
        dt_timestamps.append(datetime.fromisoformat(ts))

    if len(dt_timestamps) < interval_days * 24:  # Less than one interval
        return costs

    # Start with existing costs
    enhanced_costs = costs.copy()

    # ENHANCED: Apply anomalies with type-aware behavior
    enhanced_costs = apply_enhanced_anomalies(
        enhanced_costs, dt_timestamps, anomaly_config, resource_id
    )

    return enhanced_costs


def apply_quarterly_anomalies(
    costs: List[float],
    timestamps: List[datetime],
    interval_days: int,
    intensity: float,
    duration_hours: int,
    timing: str,
    resource_id: str,
) -> List[float]:
    """
    Apply quarterly anomaly events for cost optimization stories.

    Creates predictable anomaly events every 90 days that demonstrate:
    - Cost optimization opportunities
    - Security incident response
    - Business cycle changes
    - Infrastructure scaling events
    """
    np.random.seed(hash_deterministic_seed(resource_id, "quarterly"))
    enhanced = costs.copy()

    # Calculate intervals
    interval_hours = interval_days * 24
    total_hours = len(costs)
    num_intervals = total_hours // interval_hours

    if num_intervals < 1:
        return enhanced

    for interval_idx in range(num_intervals):
        interval_start = interval_idx * interval_hours
        interval_end = min((interval_idx + 1) * interval_hours, total_hours)

        # Determine anomaly timing within this interval
        anomaly_start_offset = calculate_anomaly_timing(
            interval_hours, timing, interval_idx, resource_id
        )

        anomaly_start = interval_start + anomaly_start_offset
        anomaly_end = min(anomaly_start + duration_hours, interval_end)

        if anomaly_start >= total_hours:
            continue

        # Generate anomaly pattern based on interval characteristics
        anomaly_type = determine_anomaly_type(interval_idx, timing, resource_id)
        anomaly_multiplier = calculate_anomaly_intensity(
            anomaly_type, intensity, interval_idx, resource_id
        )

        # Apply anomaly to the time period
        for hour_idx in range(anomaly_start, anomaly_end):
            if hour_idx < len(enhanced):
                enhanced[hour_idx] *= anomaly_multiplier

                # Add some variation within the anomaly period
                variation = np.random.uniform(0.9, 1.1)
                enhanced[hour_idx] *= variation

    return enhanced


def calculate_anomaly_timing(
    interval_hours: int, timing: str, interval_idx: int, resource_id: str
) -> int:
    """
    Calculate when within the interval the anomaly should occur.

    For 90-day demos, this creates predictable but varied timing.
    """
    np.random.seed(hash_deterministic_seed(resource_id, f"timing_{interval_idx}"))

    if timing == "early":
        # First 30% of interval
        return int(np.random.uniform(0, interval_hours * 0.3))
    elif timing == "mid":
        # Middle 40% of interval
        start = int(interval_hours * 0.3)
        end = int(interval_hours * 0.7)
        return int(np.random.uniform(start, end))
    elif timing == "late":
        # Last 30% of interval
        start = int(interval_hours * 0.7)
        return int(
            np.random.uniform(start, interval_hours - 72)
        )  # Leave room for 3-day duration
    elif timing == "ninety_day_cycle":
        # Specific timing for quarterly cycles (e.g., month 2.5 of quarter)
        return int(interval_hours * 0.75)  # 75% through the 90-day period
    else:  # random
        # Anywhere in the interval, but leave room for full duration
        return int(np.random.uniform(0, max(1, interval_hours - 72)))


def determine_anomaly_type(interval_idx: int, timing: str, resource_id: str) -> str:
    """
    Determine the type of anomaly for this interval.

    Creates consistent anomaly types that tell a story across quarters.
    """
    np.random.seed(hash_deterministic_seed(resource_id, f"type_{interval_idx}"))

    # Create patterns that rotate across intervals
    anomaly_types = [
        "cost_optimization",  # Sudden cost reduction from optimization
        "security_incident",  # Spike from security response
        "scaling_event",  # Growth from business scaling
        "migration_spike",  # Temporary spike from infrastructure migration
    ]

    # For quarterly cycles, create predictable patterns
    if timing == "ninety_day_cycle":
        return anomaly_types[interval_idx % len(anomaly_types)]

    # For other timing, still be deterministic but more varied
    type_seed = hash_deterministic_seed(resource_id, f"anomaly_type_{interval_idx}")
    np.random.seed(type_seed)
    return np.random.choice(anomaly_types)


def calculate_anomaly_intensity(
    anomaly_type: str, base_intensity: float, interval_idx: int, resource_id: str
) -> float:
    """
    Calculate the intensity multiplier for this anomaly.

    Different anomaly types have different characteristics:
    - cost_optimization: 0.1x to 0.4x (major cost reduction)
    - security_incident: 2.0x to 5.0x (emergency scaling)
    - scaling_event: 1.5x to 3.0x (planned growth)
    - migration_spike: 2.5x to 4.0x (temporary infrastructure doubling)
    """
    np.random.seed(hash_deterministic_seed(resource_id, f"intensity_{interval_idx}"))

    if anomaly_type == "cost_optimization":
        # Dramatic cost reduction (10% to 40% of original)
        min_mult = 0.1
        max_mult = 0.4
    elif anomaly_type == "security_incident":
        # Emergency response scaling (200% to 500%)
        min_mult = 2.0
        max_mult = 5.0
    elif anomaly_type == "scaling_event":
        # Business growth (150% to 300%)
        min_mult = 1.5
        max_mult = 3.0
    elif anomaly_type == "migration_spike":
        # Infrastructure migration (250% to 400%)
        min_mult = 2.5
        max_mult = 4.0
    else:
        # Default range
        min_mult = 0.5
        max_mult = 2.0

    # Apply base intensity as a scaling factor
    intensity_range = (max_mult - min_mult) * base_intensity
    adjusted_max = min_mult + intensity_range

    return np.random.uniform(min_mult, adjusted_max)


def validate_anomaly_intervals(
    costs: List[float], timestamps: List[str], expected_interval_days: int = 90
) -> Dict[str, Any]:
    """
    Validate that anomalies occur at expected intervals.

    Args:
        costs: Enhanced cost values with anomalies
        timestamps: ISO timestamp strings
        expected_interval_days: Expected days between anomalies

    Returns:
        Validation results for anomaly timing and intensity
    """
    # Handle both string and date object inputs
    dt_timestamps = []
    for ts in timestamps:
        if hasattr(ts, "isoformat"):
            ts = ts.isoformat()
        dt_timestamps.append(datetime.fromisoformat(ts))

    # Detect anomaly periods (cost spikes > 150% or drops < 50%)
    anomaly_periods = []
    baseline = np.median(costs)

    for i, cost in enumerate(costs):
        if cost > baseline * 1.5 or cost < baseline * 0.5:
            anomaly_periods.append((i, dt_timestamps[i], cost))

    # Group consecutive anomaly hours into events
    anomaly_events = []
    current_event = None

    for hour_idx, timestamp, cost in anomaly_periods:
        if current_event is None:
            current_event = {
                "start_hour": hour_idx,
                "start_time": timestamp,
                "end_hour": hour_idx,
                "end_time": timestamp,
                "max_cost": cost,
                "min_cost": cost,
            }
        elif hour_idx == current_event["end_hour"] + 1:
            # Consecutive hour, extend current event
            current_event["end_hour"] = hour_idx
            current_event["end_time"] = timestamp
            current_event["max_cost"] = max(current_event["max_cost"], cost)
            current_event["min_cost"] = min(current_event["min_cost"], cost)
        else:
            # Gap found, save current event and start new one
            anomaly_events.append(current_event)
            current_event = {
                "start_hour": hour_idx,
                "start_time": timestamp,
                "end_hour": hour_idx,
                "end_time": timestamp,
                "max_cost": cost,
                "min_cost": cost,
            }

    if current_event:
        anomaly_events.append(current_event)

    # Calculate intervals between events
    intervals_days = []
    for i in range(1, len(anomaly_events)):
        prev_end = anomaly_events[i - 1]["end_time"]
        curr_start = anomaly_events[i]["start_time"]
        interval = (curr_start - prev_end).total_seconds() / (24 * 3600)
        intervals_days.append(interval)

    return {
        "anomaly_events_detected": len(anomaly_events),
        "average_interval_days": sum(intervals_days) / len(intervals_days)
        if intervals_days
        else 0,
        "interval_consistency": {
            "target_interval": expected_interval_days,
            "actual_intervals": intervals_days,
            "within_tolerance": sum(
                1 for d in intervals_days if abs(d - expected_interval_days) < 30
            )
            if intervals_days
            else 0,
        },
        "anomaly_characteristics": [
            {
                "duration_hours": event["end_hour"] - event["start_hour"] + 1,
                "intensity_ratio": max(
                    event["max_cost"] / baseline, baseline / event["min_cost"]
                ),
                "start_time": event["start_time"].isoformat(),
            }
            for event in anomaly_events
        ],
    }


def create_deterministic_anomaly_seed(
    reference_data: dict, anomaly_profile: str
) -> str:
    """
    Create deterministic seed for anomaly generation from reference data and profile.

    This ensures the same reference data + anomaly profile combination
    produces the same anomaly pattern every time.
    """
    # Combine key fields to create unique identifier
    cloud_provider = reference_data.get("cloud_provider", "unknown")
    service = reference_data.get("service", "unknown")
    usage_family = reference_data.get("usage_family", "unknown")

    return f"{cloud_provider}_{service}_{usage_family}_{anomaly_profile}"


# ENHANCED ANOMALY FUNCTIONS


def apply_enhanced_anomalies(
    costs: List[float],
    timestamps: List[datetime],
    anomaly_config: Dict[str, Any],
    resource_id: str,
) -> List[float]:
    """
    Apply anomalies with enhanced type differentiation and timing logic.

    ENHANCED: Supports all anomaly types with proper behavior differentiation.
    """
    interval_days = anomaly_config.get("interval_days", 90)
    anomaly_type = anomaly_config.get("anomaly_type", "spike")
    frequency = anomaly_config.get("frequency", 0.1)

    enhanced_costs = costs.copy()

    # Calculate intervals
    interval_hours = interval_days * 24
    total_hours = len(costs)
    num_intervals = total_hours // interval_hours

    if num_intervals < 1:
        return enhanced_costs

    for interval_idx in range(num_intervals):
        interval_start = interval_idx * interval_hours
        interval_end = min((interval_idx + 1) * interval_hours, total_hours)

        # Check if anomaly should occur in this interval
        np.random.seed(hash_deterministic_seed(resource_id, f"interval_{interval_idx}"))
        if frequency < 1.0 and np.random.random() > frequency:
            continue  # Skip this interval

        # Determine anomaly behavior based on type
        anomaly_behavior = determine_anomaly_behavior(
            anomaly_config, interval_idx, resource_id
        )

        # Determine timing within interval
        anomaly_start_offset = apply_timing_bias(
            anomaly_config, interval_hours, interval_idx, resource_id
        )

        anomaly_start = interval_start + anomaly_start_offset
        anomaly_end = min(anomaly_start + anomaly_behavior["duration"], interval_end)

        if anomaly_start >= total_hours:
            continue

        # Apply anomaly based on type
        if anomaly_type in ["spike", "dip", "irregular"]:
            # Temporary anomaly
            enhanced_costs = apply_temporary_anomaly(
                enhanced_costs,
                anomaly_start,
                anomaly_end,
                anomaly_behavior,
                resource_id,
            )
        elif anomaly_type in ["step_up", "step_down"]:
            # Permanent change
            enhanced_costs = apply_permanent_anomaly(
                enhanced_costs,
                anomaly_start,
                total_hours,
                anomaly_behavior,
                resource_id,
            )

    # Normalize costs to preserve total cost (cost conservation)
    original_total = sum(costs)
    enhanced_total = sum(enhanced_costs)

    if enhanced_total > 0 and original_total > 0:
        normalization_factor = original_total / enhanced_total
        enhanced_costs = [cost * normalization_factor for cost in enhanced_costs]

    return enhanced_costs


def determine_anomaly_behavior(
    anomaly_config: Dict[str, Any], interval_idx: int, resource_id: str
) -> Dict[str, Any]:
    """
    Determine anomaly behavior based on profile type and timing.

    ENHANCED: Maps profile anomaly types to specific behaviors.
    """
    anomaly_type = anomaly_config.get("anomaly_type", "spike")
    timing = anomaly_config.get("timing", "random")
    intensity = anomaly_config.get("intensity", 0.8)
    duration_hours = anomaly_config.get("duration_hours", 72)

    if anomaly_type == "step_down":
        # Permanent cost reduction (cost optimization)
        cost_optimization_factor = anomaly_config.get("cost_optimization_factor", 0.5)
        return {
            "story_type": "cost_optimization",
            "duration": 999,  # Permanent
            "intensity": cost_optimization_factor,
            "permanent": True,
        }

    elif anomaly_type == "step_up":
        # Permanent cost increase (scaling/growth)
        scaling_factor = anomaly_config.get("scaling_factor", 2.0)
        return {
            "story_type": "scaling_event",
            "duration": 999,  # Permanent
            "intensity": scaling_factor,
            "permanent": True,
        }

    elif anomaly_type == "spike":
        # Temporary spike - determine story based on timing and context
        emergency_multiplier = anomaly_config.get("emergency_multiplier", 1.0)
        final_intensity = intensity * emergency_multiplier

        if timing == "business_hours":
            story_type = "user_activity_surge"
        elif timing == "monthly_end":
            story_type = "reporting_spike"
        elif timing == "quarterly":
            story_type = "quarterly_business_surge"
        elif timing == "ninety_day_cycle":
            story_type = "infrastructure_spike"
        else:
            story_type = "general_spike"

        return {
            "story_type": story_type,
            "duration": duration_hours,
            "intensity": final_intensity,
            "permanent": False,
        }

    elif anomaly_type == "dip":
        # Temporary cost reduction
        return {
            "story_type": "temporary_optimization",
            "duration": duration_hours,
            "intensity": intensity,  # Should be < 1.0 for dips
            "permanent": False,
        }

    elif anomaly_type == "irregular":
        # Chaotic/unpredictable behavior
        return {
            "story_type": "chaotic_workload",
            "duration": duration_hours,
            "intensity": intensity,
            "permanent": False,
        }

    else:
        # Default to spike behavior
        return {
            "story_type": "default_spike",
            "duration": duration_hours,
            "intensity": intensity,
            "permanent": False,
        }


def apply_timing_bias(
    anomaly_config: Dict[str, Any],
    interval_hours: int,
    interval_idx: int,
    resource_id: str,
) -> int:
    """
    Apply timing bias to determine when anomaly occurs within interval.

    ENHANCED: Uses bias parameters from profile configuration.
    """
    timing = anomaly_config.get("timing", "random")
    duration_hours = anomaly_config.get("duration_hours", 72)

    np.random.seed(hash_deterministic_seed(resource_id, f"timing_{interval_idx}"))

    if timing == "business_hours":
        bias = anomaly_config.get("business_hours_bias", 0.8)
        if np.random.random() < bias:
            # Place during business hours (assume first business day of interval)
            # Business hours: 9 AM - 5 PM on weekdays
            business_start = 9  # Hours from interval start to 9 AM
            business_duration = 8  # 8-hour business day
            return int(
                np.random.uniform(business_start, business_start + business_duration)
            )

    elif timing == "monthly_end":
        bias = anomaly_config.get("month_end_bias", 0.6)
        if np.random.random() < bias:
            # Place in last 20% of interval (month-end)
            return int(
                interval_hours * 0.8 + np.random.uniform(0, interval_hours * 0.2)
            )

    elif timing == "quarterly":
        bias = anomaly_config.get("quarter_end_bias", 0.7)
        if np.random.random() < bias:
            # Place in last 10% of interval (quarter-end)
            return int(
                interval_hours * 0.9 + np.random.uniform(0, interval_hours * 0.1)
            )

    elif timing == "ninety_day_cycle":
        # Specific timing for quarterly demos (75% through cycle)
        return int(interval_hours * 0.75)

    # Default: random timing within interval (leave room for duration)
    max_start = max(1, interval_hours - duration_hours)
    return int(np.random.uniform(0, max_start))


def apply_temporary_anomaly(
    costs: List[float],
    start_hour: int,
    end_hour: int,
    anomaly_behavior: Dict[str, Any],
    resource_id: str,
) -> List[float]:
    """
    Apply temporary anomaly (spike, dip, irregular) to cost time series.
    """
    enhanced = costs.copy()
    intensity = anomaly_behavior["intensity"]

    np.random.seed(hash_deterministic_seed(resource_id, f"temp_anomaly_{start_hour}"))

    for hour_idx in range(start_hour, min(end_hour, len(enhanced))):
        # Apply base intensity
        enhanced[hour_idx] *= intensity

        # Add variation within the anomaly period for organic feel
        variation = np.random.uniform(0.9, 1.1)
        enhanced[hour_idx] *= variation

        # Apply reasonable bounds
        base_cost = costs[hour_idx] if hour_idx < len(costs) else 1.0
        enhanced[hour_idx] = max(
            enhanced[hour_idx], base_cost * 0.01
        )  # Min 1% of original
        enhanced[hour_idx] = min(
            enhanced[hour_idx], base_cost * 20.0
        )  # Max 20x original

    return enhanced


def apply_permanent_anomaly(
    costs: List[float],
    start_hour: int,
    total_hours: int,
    anomaly_behavior: Dict[str, Any],
    resource_id: str,
) -> List[float]:
    """
    Apply permanent anomaly (step_up, step_down) to cost time series.
    """
    enhanced = costs.copy()
    intensity = anomaly_behavior["intensity"]

    # Apply permanent change from start_hour onwards
    for hour_idx in range(start_hour, min(total_hours, len(enhanced))):
        enhanced[hour_idx] *= intensity

        # Apply reasonable bounds for permanent changes
        base_cost = costs[hour_idx] if hour_idx < len(costs) else 1.0
        enhanced[hour_idx] = max(
            enhanced[hour_idx], base_cost * 0.05
        )  # Min 5% of original
        enhanced[hour_idx] = min(
            enhanced[hour_idx], base_cost * 10.0
        )  # Max 10x original

    return enhanced


def is_business_hours(dt: datetime) -> bool:
    """Check if datetime falls within business hours (M-F 9-5)."""
    return dt.weekday() < 5 and 9 <= dt.hour < 17