#!/usr/bin/env python3
"""Cost-Drop Signal Detector — Pure library module.

Detects month-over-month cost drops across linked accounts and services,
classifies signals by severity, and returns ranked results.

This module contains NO CLI logic, NO Rich formatting, NO file I/O.
It is a pure algorithm library consumed by the CLI layer.

Uses CostExplorerClient.get_costs_paginated() for data fetching with
full pagination support (handles >5,000 group CE API limit).

Author: Runbooks Team
Version: 1.0.0
"""

from __future__ import annotations

from dataclasses import dataclass, asdict
from datetime import date, timedelta
from typing import Any, Dict, List, Optional, Tuple

# Month name mapping for human-readable output
MONTH_NAMES = {
    "01": "Jan",
    "02": "Feb",
    "03": "Mar",
    "04": "Apr",
    "05": "May",
    "06": "Jun",
    "07": "Jul",
    "08": "Aug",
    "09": "Sep",
    "10": "Oct",
    "11": "Nov",
    "12": "Dec",
}

# Signal classification thresholds (percentage drop)
SIGNAL_THRESHOLDS = [
    (80.0, "DECOMMISSION_LIKELY"),
    (50.0, "SIGNIFICANT_REDUCTION"),
    (25.0, "NOTABLE_REDUCTION"),
    (10.0, "MODERATE_REDUCTION"),
]


def calculate_month_aligned_range(
    months_back: int = 3,
    reference_date: Optional[date] = None,
) -> Tuple[str, str]:
    """Calculate month-aligned date range for Cost Explorer queries.

    Cost Explorer MONTHLY granularity returns full calendar months.
    This function ensures start/end align to month boundaries,
    excluding the incomplete current month to prevent false-positive
    drops (e.g., comparing 31-day January to 4-day February).

    Args:
        months_back: Number of full months to look back (default: 3).
        reference_date: Date to calculate from (default: today).

    Returns:
        (start_date, end_date) as YYYY-MM-DD strings, both on 1st of month.
        end_date is exclusive (first of current month).

    Example:
        >>> # On 2026-02-04:
        >>> calculate_month_aligned_range(3)
        ('2025-11-01', '2026-02-01')
        >>> # Returns Nov, Dec, Jan (3 full months, excludes partial Feb)
    """
    if reference_date is None:
        reference_date = date.today()

    # End = first of current month (exclusive: CE won't include partial month)
    end = reference_date.replace(day=1)

    # Walk back N months
    year = end.year
    month = end.month - months_back
    while month <= 0:
        month += 12
        year -= 1
    start = date(year, month, 1)

    return (start.strftime("%Y-%m-%d"), end.strftime("%Y-%m-%d"))


def days_to_months(days: int) -> int:
    """Convert a days lookback to equivalent full months.

    Maps the --days CLI option to month-aligned ranges:
        30  -> 1 month  (only 1 month, no comparison possible)
        60  -> 2 months (1 comparison pair)
        90  -> 3 months (2 comparison pairs)
        120 -> 4 months (3 comparison pairs)

    Args:
        days: Approximate lookback in days.

    Returns:
        Number of full months.

    Example:
        >>> days_to_months(90)
        3
    """
    return max(1, (days + 15) // 30)


@dataclass
class CostDrop:
    """A detected cost drop between consecutive months."""

    account_id: str
    service: str
    from_month: str
    to_month: str
    previous_cost: float
    current_cost: float
    savings_usd: float
    drop_pct: float
    signal: str
    account_name: str = "unknown"

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dict for JSON serialization."""
        return asdict(self)


def classify_signal(drop_pct: float) -> str:
    """Classify a cost drop percentage into a signal category.

    Args:
        drop_pct: Percentage drop (0-100).

    Returns:
        Signal string (e.g., "DECOMMISSION_LIKELY").

    Example:
        >>> classify_signal(95.0)
        'DECOMMISSION_LIKELY'
        >>> classify_signal(30.0)
        'NOTABLE_REDUCTION'
    """
    for threshold, label in SIGNAL_THRESHOLDS:
        if drop_pct >= threshold:
            return label
    return "MINOR_REDUCTION"


def format_month(iso_date: str) -> str:
    """Convert ISO date to human-readable month (e.g., 'Jan 2026').

    Args:
        iso_date: Date string like '2026-01-01'.

    Returns:
        Human-readable string like 'Jan 2026'.

    Example:
        >>> format_month('2026-01-01')
        'Jan 2026'
    """
    parts = iso_date.split("-")
    if len(parts) >= 2:
        return f"{MONTH_NAMES.get(parts[1], parts[1])} {parts[0]}"
    return iso_date


def detect_cost_drops(
    records: List[Dict[str, Any]],
    min_previous_cost: float = 1.0,
) -> List[CostDrop]:
    """Detect month-over-month cost drops from Cost Explorer records.

    Groups records by (account_id, service), compares consecutive months,
    and returns drops sorted by savings_usd descending.

    Args:
        records: Flat list from CostExplorerClient.get_costs_paginated().
            Each record has: period_start, keys (list), amount.
        min_previous_cost: Minimum previous month cost to consider (filters noise).

    Returns:
        List of CostDrop objects sorted by savings_usd descending.

    Example:
        >>> records = [
        ...     {"period_start": "2026-01-01", "keys": ["123456789012", "Amazon EC2"], "amount": 1000.0},
        ...     {"period_start": "2026-02-01", "keys": ["123456789012", "Amazon EC2"], "amount": 100.0},
        ... ]
        >>> drops = detect_cost_drops(records, min_previous_cost=1.0)
        >>> drops[0].savings_usd
        900.0
    """
    # Group by (account_id, service) -> {month: cost}
    grouped: Dict[tuple, Dict[str, float]] = {}
    for rec in records:
        keys = rec.get("keys", [])
        if len(keys) < 2:
            continue
        key = (keys[0], keys[1])
        month = rec["period_start"]
        amount = rec.get("amount", 0.0)

        if key not in grouped:
            grouped[key] = {}
        grouped[key][month] = amount

    # Compare consecutive months
    drops: List[CostDrop] = []
    for (account_id, service), months in grouped.items():
        sorted_months = sorted(months.keys())
        for i in range(len(sorted_months) - 1):
            prev_month = sorted_months[i]
            curr_month = sorted_months[i + 1]
            prev_cost = months[prev_month]
            curr_cost = months[curr_month]

            if prev_cost < min_previous_cost:
                continue
            if curr_cost >= prev_cost:
                continue

            savings = prev_cost - curr_cost
            drop_pct = (savings / prev_cost) * 100.0

            if drop_pct < 10.0:
                continue

            drops.append(
                CostDrop(
                    account_id=account_id,
                    service=service,
                    from_month=prev_month,
                    to_month=curr_month,
                    previous_cost=round(prev_cost, 2),
                    current_cost=round(curr_cost, 2),
                    savings_usd=round(savings, 2),
                    drop_pct=round(drop_pct, 1),
                    signal=classify_signal(drop_pct),
                )
            )

    drops.sort(key=lambda d: d.savings_usd, reverse=True)
    return drops


def enrich_account_names(
    drops: List[CostDrop],
    account_map: Dict[str, str],
) -> List[CostDrop]:
    """Enrich CostDrop objects with account names from Organizations.

    Args:
        drops: List of CostDrop objects.
        account_map: Dict of account_id -> account_name.

    Returns:
        Same list with account_name fields populated.

    Example:
        >>> drops = [CostDrop(account_id="123", ...)]
        >>> enrich_account_names(drops, {"123": "my-account"})
        >>> drops[0].account_name
        'my-account'
    """
    for drop in drops:
        drop.account_name = account_map.get(drop.account_id, "unknown")
    return drops


def summarize_drops(drops: List[CostDrop]) -> Dict[str, Any]:
    """Generate summary statistics for a list of cost drops.

    Args:
        drops: List of CostDrop objects.

    Returns:
        Dict with total_drops, total_savings_usd, signal_breakdown.

    Example:
        >>> summary = summarize_drops(drops)
        >>> summary["total_savings_usd"]
        82036.85
    """
    signal_breakdown: Dict[str, Dict[str, Any]] = {}
    for drop in drops:
        if drop.signal not in signal_breakdown:
            signal_breakdown[drop.signal] = {"count": 0, "savings_usd": 0.0}
        signal_breakdown[drop.signal]["count"] += 1
        signal_breakdown[drop.signal]["savings_usd"] = round(
            signal_breakdown[drop.signal]["savings_usd"] + drop.savings_usd, 2
        )

    return {
        "total_drops": len(drops),
        "total_savings_usd": round(sum(d.savings_usd for d in drops), 2),
        "signal_breakdown": signal_breakdown,
    }
