#!/usr/bin/env python3
"""
EBS Snapshot Enricher - Decommission signal analysis for EC2 EBS snapshots.

Decommission Signals (SN1-SN5):
- SN1: Age > 90 days (cost impact flag)
- SN2: Orphaned — source volume no longer exists
- SN3: Size > 100 GB (high storage cost)
- SN4: No tags (ungoverned, may be orphaned)
- SN5: Duplicate — a newer snapshot exists for the same volume

Usage:
    from runbooks.inventory.enrichers.snapshot_enricher import SnapshotEnricher

    enricher = SnapshotEnricher(profile="ops-profile", region="ap-southeast-2")
    results = enricher.analyze()
    enricher.display_results(results)
"""

import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, List, Optional

import pandas as pd
from botocore.exceptions import ClientError

from runbooks.common.profile_utils import (
    create_operational_session,
    create_timeout_protected_client,
    get_profile_for_operation,
)
from runbooks.common.rich_utils import (
    console,
    create_table,
    print_error,
    print_info,
    print_success,
    print_warning,
)

logger = logging.getLogger(__name__)

# ─────────────────────────────────────────────────────────────────────────────
# CONSTANTS
# ─────────────────────────────────────────────────────────────────────────────

AGE_THRESHOLD_DAYS = 90
SIZE_THRESHOLD_GB = 100

# EBS snapshot storage cost (ap-southeast-2): ~$0.055 / GB-month
SNAPSHOT_STORAGE_COST_PER_GB_MONTH = 0.055

# Known deprecated Lambda runtimes (kept here for cross-use reference)
EOL_RUNTIMES = frozenset(
    [
        "python3.6",
        "python3.7",
        "python3.8",
        "nodejs10.x",
        "nodejs12.x",
        "nodejs14.x",
        "ruby2.5",
        "ruby2.7",
        "java8",
        "dotnetcore1.0",
        "dotnetcore2.0",
        "dotnetcore2.1",
        "dotnetcore3.1",
        "go1.x",
    ]
)


# ─────────────────────────────────────────────────────────────────────────────
# ENUMERATIONS
# ─────────────────────────────────────────────────────────────────────────────


class SnapshotSignal(str, Enum):
    """EBS snapshot decommission signals (SN1-SN5)."""

    SN1_OLD = "SN1"  # Age > 90 days
    SN2_ORPHANED = "SN2"  # Source volume missing
    SN3_LARGE = "SN3"  # Size > 100 GB
    SN4_NO_TAGS = "SN4"  # No resource tags
    SN5_DUPLICATE = "SN5"  # Newer snapshot for same volume exists


class SnapshotRecommendation(str, Enum):
    """Recommended action for a snapshot."""

    DELETE = "DELETE"  # High confidence — decommission candidate
    REVIEW = "REVIEW"  # Medium confidence — needs review
    KEEP = "KEEP"  # No decommission signals


# ─────────────────────────────────────────────────────────────────────────────
# DATA MODEL
# ─────────────────────────────────────────────────────────────────────────────


@dataclass
class SnapshotAnalysis:
    """Analysis result for a single EBS snapshot."""

    snapshot_id: str
    volume_id: Optional[str]
    size_gb: int
    start_time: datetime
    age_days: int
    state: str
    description: str
    tags: Dict[str, str]
    signals: List[SnapshotSignal] = field(default_factory=list)
    monthly_cost: float = 0.0
    recommendation: SnapshotRecommendation = SnapshotRecommendation.KEEP

    def to_dict(self) -> Dict[str, Any]:
        return {
            "snapshot_id": self.snapshot_id,
            "volume_id": self.volume_id,
            "size_gb": self.size_gb,
            "age_days": self.age_days,
            "state": self.state,
            "signals": [s.value for s in self.signals],
            "monthly_cost": round(self.monthly_cost, 2),
            "recommendation": self.recommendation.value,
        }


# ─────────────────────────────────────────────────────────────────────────────
# ENRICHER CLASS
# ─────────────────────────────────────────────────────────────────────────────


class SnapshotEnricher:
    """
    EBS snapshot activity enricher with SN1-SN5 decommission signals.

    Discovers owned snapshots, scores them against five decommission signals,
    and surfaces cost-impact estimates for CloudOps review.
    """

    def __init__(
        self,
        profile: Optional[str] = None,
        region: Optional[str] = None,
    ) -> None:
        """
        Initialise with boto3 EC2 client.

        Args:
            profile: AWS profile name (defaults to operational profile)
            region: AWS region (defaults to ap-southeast-2)
        """
        self.profile = get_profile_for_operation("operational", profile)
        self.region = region or "ap-southeast-2"
        self.session = create_operational_session(self.profile)
        self.ec2 = create_timeout_protected_client(self.session, "ec2", region_name=self.region)
        logger.debug("SnapshotEnricher initialised: profile=%s region=%s", self.profile, self.region)

    # ── Discovery ────────────────────────────────────────────────────────────

    def _list_snapshots(self) -> List[Dict[str, Any]]:
        """Return all owned snapshots via paginator."""
        snapshots: List[Dict[str, Any]] = []
        try:
            paginator = self.ec2.get_paginator("describe_snapshots")
            for page in paginator.paginate(OwnerIds=["self"]):
                snapshots.extend(page.get("Snapshots", []))
        except ClientError as exc:
            print_error(f"describe_snapshots failed: {exc}")
        return snapshots

    def _existing_volume_ids(self, volume_ids: List[str]) -> frozenset:
        """Return frozenset of volume IDs that still exist."""
        existing: set = set()
        if not volume_ids:
            return frozenset()
        try:
            # Batch to avoid request-size limits
            for i in range(0, len(volume_ids), 200):
                batch = volume_ids[i : i + 200]
                resp = self.ec2.describe_volumes(Filters=[{"Name": "volume-id", "Values": batch}])
                for vol in resp.get("Volumes", []):
                    existing.add(vol["VolumeId"])
        except ClientError as exc:
            print_warning(f"describe_volumes partial failure: {exc}")
        return frozenset(existing)

    # ── Analysis ─────────────────────────────────────────────────────────────

    def analyze(self, resource_ids: Optional[List[str]] = None) -> List[SnapshotAnalysis]:
        """
        Discover and analyse EBS snapshots.

        Args:
            resource_ids: Optional list of snapshot IDs to limit analysis.
                          Analyses all owned snapshots when None.

        Returns:
            List of SnapshotAnalysis with SN1-SN5 signals populated.
        """
        print_info(f"Fetching snapshots (region={self.region})…")
        raw = self._list_snapshots()
        if resource_ids:
            id_set = frozenset(resource_ids)
            raw = [s for s in raw if s["SnapshotId"] in id_set]

        if not raw:
            print_warning("No snapshots found.")
            return []

        print_info(f"Analysing {len(raw)} snapshots…")

        # Build per-volume latest-snapshot map for SN5 detection
        latest_per_volume: Dict[str, datetime] = {}
        for snap in raw:
            vid = snap.get("VolumeId")
            if not vid:
                continue
            ts = snap["StartTime"]
            if isinstance(ts, str):
                ts = datetime.fromisoformat(ts.replace("Z", "+00:00"))
            if vid not in latest_per_volume or ts > latest_per_volume[vid]:
                latest_per_volume[vid] = ts

        # Collect volume IDs for orphan check
        volume_ids = list({s.get("VolumeId") for s in raw if s.get("VolumeId")})
        existing_volumes = self._existing_volume_ids(volume_ids)

        now = datetime.now(tz=timezone.utc)
        results: List[SnapshotAnalysis] = []

        for snap in raw:
            start_time = snap["StartTime"]
            if isinstance(start_time, str):
                start_time = datetime.fromisoformat(start_time.replace("Z", "+00:00"))
            if start_time.tzinfo is None:
                start_time = start_time.replace(tzinfo=timezone.utc)

            age_days = (now - start_time).days
            size_gb = snap.get("VolumeSize", 0)
            volume_id = snap.get("VolumeId")
            tags_raw = snap.get("Tags") or []
            tags = {t["Key"]: t["Value"] for t in tags_raw}
            monthly_cost = size_gb * SNAPSHOT_STORAGE_COST_PER_GB_MONTH

            signals: List[SnapshotSignal] = []

            # SN1: Age > 90 days
            if age_days > AGE_THRESHOLD_DAYS:
                signals.append(SnapshotSignal.SN1_OLD)

            # SN2: Orphaned volume
            if volume_id and volume_id not in existing_volumes:
                signals.append(SnapshotSignal.SN2_ORPHANED)

            # SN3: Large snapshot
            if size_gb > SIZE_THRESHOLD_GB:
                signals.append(SnapshotSignal.SN3_LARGE)

            # SN4: No tags
            if not tags:
                signals.append(SnapshotSignal.SN4_NO_TAGS)

            # SN5: Duplicate (not the latest snapshot for this volume)
            if volume_id and volume_id in latest_per_volume:
                snap_ts = start_time
                if snap_ts < latest_per_volume[volume_id]:
                    signals.append(SnapshotSignal.SN5_DUPLICATE)

            # Recommendation
            high_signals = {SnapshotSignal.SN2_ORPHANED, SnapshotSignal.SN5_DUPLICATE}
            if any(s in high_signals for s in signals):
                recommendation = SnapshotRecommendation.DELETE
            elif len(signals) >= 2:
                recommendation = SnapshotRecommendation.REVIEW
            elif signals:
                recommendation = SnapshotRecommendation.REVIEW
            else:
                recommendation = SnapshotRecommendation.KEEP

            results.append(
                SnapshotAnalysis(
                    snapshot_id=snap["SnapshotId"],
                    volume_id=volume_id,
                    size_gb=size_gb,
                    start_time=start_time,
                    age_days=age_days,
                    state=snap.get("State", "unknown"),
                    description=snap.get("Description", ""),
                    tags=tags,
                    signals=signals,
                    monthly_cost=monthly_cost,
                    recommendation=recommendation,
                )
            )

        print_success(f"Analysed {len(results)} snapshots.")
        return results

    # ── DataFrame integration ─────────────────────────────────────────────────

    def enrich_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Add SN1-SN5 signal columns to a DataFrame that contains snapshot_id.

        Returns the input DataFrame with additional columns:
            snapshot_signals, snapshot_recommendation, snapshot_monthly_cost_usd
        """
        if "snapshot_id" not in df.columns:
            print_warning("DataFrame missing 'snapshot_id' column — skipping snapshot enrichment.")
            df["snapshot_signals"] = ""
            df["snapshot_recommendation"] = SnapshotRecommendation.KEEP.value
            df["snapshot_monthly_cost_usd"] = 0.0
            return df

        ids = df["snapshot_id"].dropna().tolist()
        analyses = self.analyze(resource_ids=ids if ids else None)
        analysis_map = {a.snapshot_id: a for a in analyses}

        def _signals(sid: str) -> str:
            a = analysis_map.get(sid)
            return ", ".join(s.value for s in a.signals) if a else ""

        def _rec(sid: str) -> str:
            a = analysis_map.get(sid)
            return a.recommendation.value if a else SnapshotRecommendation.KEEP.value

        def _cost(sid: str) -> float:
            a = analysis_map.get(sid)
            return a.monthly_cost if a else 0.0

        df = df.copy()
        df["snapshot_signals"] = df["snapshot_id"].map(_signals)
        df["snapshot_recommendation"] = df["snapshot_id"].map(_rec)
        df["snapshot_monthly_cost_usd"] = df["snapshot_id"].map(_cost)
        return df

    # ── Display ───────────────────────────────────────────────────────────────

    def display_results(self, results: List[SnapshotAnalysis]) -> None:
        """Render a Rich table summarising snapshot decommission candidates."""
        if not results:
            print_warning("No snapshot results to display.")
            return

        table = create_table(
            title="EBS Snapshot Decommission Analysis",
            columns=[
                {"name": "Snapshot ID", "style": "cyan", "no_wrap": True},
                {"name": "Volume ID", "style": "white"},
                {"name": "Age (days)", "style": "bright_yellow", "justify": "right"},
                {"name": "Size (GB)", "style": "bright_yellow", "justify": "right"},
                {"name": "Cost/mo", "style": "white", "justify": "right"},
                {"name": "Signals", "style": "bright_magenta"},
                {"name": "Action", "style": "bold"},
            ],
        )

        for a in sorted(results, key=lambda x: x.recommendation.value):
            signals_str = ", ".join(s.value for s in a.signals) or "—"
            rec = a.recommendation
            if rec == SnapshotRecommendation.DELETE:
                rec_str = f"[bright_red]{rec.value}[/bright_red]"
            elif rec == SnapshotRecommendation.REVIEW:
                rec_str = f"[bright_yellow]{rec.value}[/bright_yellow]"
            else:
                rec_str = f"[bright_green]{rec.value}[/bright_green]"

            table.add_row(
                a.snapshot_id,
                a.volume_id or "—",
                str(a.age_days),
                str(a.size_gb),
                f"${a.monthly_cost:.2f}",
                signals_str,
                rec_str,
            )

        console.print()
        console.print(table)

        # Summary line
        delete_count = sum(1 for a in results if a.recommendation == SnapshotRecommendation.DELETE)
        review_count = sum(1 for a in results if a.recommendation == SnapshotRecommendation.REVIEW)
        total_monthly = sum(a.monthly_cost for a in results)
        console.print(
            f"\n[dim]Total: {len(results)} snapshots | "
            f"DELETE candidates: {delete_count} | REVIEW: {review_count} | "
            f"Monthly storage: ${total_monthly:.2f}[/dim]\n"
        )


def create_snapshot_enricher(
    profile: Optional[str] = None,
    region: Optional[str] = None,
) -> SnapshotEnricher:
    """Factory function for SnapshotEnricher."""
    return SnapshotEnricher(profile=profile, region=region)
