"""
Network resource cost optimization detectors.

This module contains detectors for:
- NAT Gateways (advisory and idle)
- NAT Gateway egress data-processing cost (CloudWatch-backed dollar estimate)
- VPC Endpoints: S3 gateway missing
- VPC Endpoints: DynamoDB gateway missing
- Elastic IPs
- Load Balancers (ELB/ALB/NLB)
"""

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

from stacksage.pricing import (
    PRICING_EIP_UNUSED_MONTH,
    PRICING_NAT_DATA_GB,
    PRICING_NAT_HOURLY,
    elb_monthly_cost,
    get_regional_multiplier,
)


def _uptime_fraction(created_time_str: Any, days_window: int = 30) -> float:
    """Estimate uptime fraction in the last days_window based on CreateTime if available."""
    try:
        from datetime import timezone

        now = datetime.now(timezone.utc)
        if isinstance(created_time_str, str):
            created_time = datetime.fromisoformat(
                created_time_str.replace("Z", "+00:00")
            )
        elif created_time_str:
            created_time = created_time_str
        else:
            return 1.0
        if created_time.tzinfo is None:
            created_time = created_time.replace(tzinfo=timezone.utc)
        hours_active = (now - created_time).total_seconds() / 3600.0
        return max(0.0, min(1.0, hours_active / (days_window * 24.0)))
    except Exception:
        return 1.0


def detect_nat_gateways(nat_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """Advisory: NAT gateways incur fixed + data processing cost; suggest S3 endpoint."""
    findings = []
    for ng in nat_list:
        nat_id = ng.get("NatGatewayId")
        region = ng.get("Region")
        regional_multiplier = get_regional_multiplier(region)
        base_monthly = round(PRICING_NAT_HOURLY * 730 * regional_multiplier, 2)
        uptime = _uptime_fraction(ng.get("CreateTime"))
        adjusted_monthly = round(base_monthly * uptime, 2)
        findings.append(
            {
                "type": "nat_gateway",
                "resource_type": "nat",
                "id": nat_id,
                "region": region,
                "state": ng.get("State"),
                "estimated_monthly_cost_usd": base_monthly,
                "adjusted_monthly_cost_usd": adjusted_monthly,
                "estimated_monthly_savings_usd": 0,
                "potential_savings": 0,
                "confidence": 0.7,
                "severity": "low",
                "recommended_action": "review-and-consider-vpc-endpoint",
                "explanation": "NAT gateway present. NAT has hourly + data processing cost. If used primarily for S3 access, use S3 VPC endpoint to save data processing charges.",
                "estimation_method": "base_hourly+uptime",
                "assumptions": [
                    "no data processing included",
                    "uptime estimated from CreateTime",
                    "regional multiplier applied via pricing tables",
                ],
                "evidence": {
                    "inventory": {
                        "state": ng.get("State"),
                        "create_time": ng.get("CreateTime"),
                        "subnet_id": ng.get("SubnetId"),
                    },
                    "estimation": {
                        "base_monthly_cost_usd": base_monthly,
                        "adjusted_monthly_cost_usd": adjusted_monthly,
                    },
                },
                "verification_commands": [
                    f"aws ec2 describe-nat-gateways --nat-gateway-ids {nat_id} --region {region}",
                ],
                "remediation_commands": [],
            }
        )
    return findings


def detect_idle_nat_gateways(
    session,
    nat_list: List[Dict[str, Any]],
    days: int = 14,
    budget=None,
    cw_avg_func=None,
    bytes_threshold_gb_per_day: float = 1.0,
) -> List[Dict[str, Any]]:
    """
    Detect NAT gateways with very low data processed using CloudWatch metrics.

    Flags NAT gateways where both inbound and outbound bytes are below the
    threshold over the lookback window.
    """
    findings = []
    if session is None:
        return findings
    if not nat_list:
        return findings

    if cw_avg_func is None:
        from stacksage.analyzer.metrics import cw_avg as cw_avg_func

    daily_period = 86400
    bytes_threshold = bytes_threshold_gb_per_day * (1024**3)

    for ng in nat_list:
        nat_id = ng.get("NatGatewayId")
        region = ng.get("Region")
        if not nat_id or not region:
            continue

        cw_errors: List[str] = []
        cw_detail: Dict[str, Any] = {"metrics": []}

        bytes_out = cw_avg_func(
            session,
            "AWS/NATGateway",
            "BytesOutToDestination",
            [{"Name": "NatGatewayId", "Value": nat_id}],
            period=daily_period,
            days=days,
            statistic="Sum",
            region_name=region,
            budget=budget,
            errors=cw_errors,
            evidence=cw_detail,
        )

        bytes_in = cw_avg_func(
            session,
            "AWS/NATGateway",
            "BytesInFromSource",
            [{"Name": "NatGatewayId", "Value": nat_id}],
            period=daily_period,
            days=days,
            statistic="Sum",
            region_name=region,
            budget=budget,
            errors=cw_errors,
            evidence=cw_detail,
        )

        if bytes_out is None or bytes_in is None:
            continue

        if bytes_out < bytes_threshold and bytes_in < bytes_threshold:
            regional_multiplier = get_regional_multiplier(region)
            base_monthly = round(PRICING_NAT_HOURLY * 730 * regional_multiplier, 2)
            uptime = _uptime_fraction(ng.get("CreateTime"))
            adjusted_monthly = round(base_monthly * uptime, 2)
            data_gb_per_day = (bytes_out + bytes_in) / (1024**3)
            est_data_cost = round(
                data_gb_per_day * 30 * PRICING_NAT_DATA_GB * regional_multiplier, 2
            )

            severity = "medium" if adjusted_monthly >= 30 else "low"
            findings.append(
                {
                    "type": "idle_nat_gateway",
                    "resource_type": "nat",
                    "id": nat_id,
                    "region": region,
                    "state": ng.get("State"),
                    "estimated_monthly_cost_usd": base_monthly,
                    "adjusted_monthly_cost_usd": adjusted_monthly,
                    "estimated_monthly_savings_usd": adjusted_monthly,
                    "potential_savings": adjusted_monthly,
                    "confidence": 0.8,
                    "severity": severity,
                    "recommended_action": "review-and-delete-if-unused",
                    "explanation": (
                        "NAT gateway shows very low data processed "
                        f"(~{data_gb_per_day:.2f} GB/day across last {days} days). "
                        "If unused, consider deleting to save hourly charges."
                    ),
                    "estimation_method": "base_hourly+uptime",
                    "assumptions": [
                        "data processing cost estimated from CloudWatch bytes",
                        "uptime estimated from CreateTime",
                        "regional multiplier applied via pricing tables",
                    ],
                    "evidence": {
                        "inventory": {
                            "state": ng.get("State"),
                            "create_time": ng.get("CreateTime"),
                            "subnet_id": ng.get("SubnetId"),
                        },
                        "utilization": {
                            "bytes_out_avg_per_day": bytes_out,
                            "bytes_in_avg_per_day": bytes_in,
                            "threshold_bytes_per_day": bytes_threshold,
                            "lookback_days": days,
                        },
                        "estimation": {
                            "base_monthly_cost_usd": base_monthly,
                            "adjusted_monthly_cost_usd": adjusted_monthly,
                            "estimated_data_processing_cost_usd": est_data_cost,
                        },
                    },
                    "verification_commands": [
                        f"aws ec2 describe-nat-gateways --nat-gateway-ids {nat_id} --region {region}",
                        f"aws cloudwatch get-metric-statistics --namespace AWS/NATGateway --metric-name BytesOutToDestination --dimensions Name=NatGatewayId,Value={nat_id} --start-time $(date -u -v-{days}d '+%Y-%m-%dT%H:%M:%S') --end-time $(date -u '+%Y-%m-%dT%H:%M:%S') --period 86400 --statistics Sum --region {region}",
                        f"aws cloudwatch get-metric-statistics --namespace AWS/NATGateway --metric-name BytesInFromSource --dimensions Name=NatGatewayId,Value={nat_id} --start-time $(date -u -v-{days}d '+%Y-%m-%dT%H:%M:%S') --end-time $(date -u '+%Y-%m-%dT%H:%M:%S') --period 86400 --statistics Sum --region {region}",
                    ],
                    "remediation_commands": [],
                    "metrics_error": ";".join(cw_errors) if cw_errors else None,
                    "cw_metrics": cw_detail.get("metrics"),
                }
            )
    return findings


def detect_missing_s3_endpoint(
    vpc_endpoints: List[Dict[str, Any]], vpc_ids: List[str]
) -> List[Dict[str, Any]]:
    """Detects missing S3 VPC gateway endpoint to avoid NAT data processing charges."""
    findings = []
    has_s3 = any(
        ("s3" in (ep.get("ServiceName", "")) and ep.get("VpcEndpointType") == "Gateway")
        for ep in vpc_endpoints
    )
    if not has_s3:
        findings.append(
            {
                "type": "missing_s3_vpc_endpoint",
                "resource_type": "vpc",
                "id": ",".join(vpc_ids) if vpc_ids else "unknown",
                "estimated_monthly_cost_usd": 0,
                "estimated_monthly_savings_usd": 0,
                "potential_savings": 0,
                "confidence": 0.6,
                "severity": "low",
                "recommended_action": "create-vpc-endpoint",
                "explanation": "No S3 gateway VPC endpoint detected. For private instances accessing S3, adding a gateway endpoint avoids NAT data processing charges.",
                "evidence": {
                    "inventory": {
                        "vpc_ids": vpc_ids,
                        "vpc_endpoints_seen": len(vpc_endpoints or []),
                        "s3_gateway_endpoint_present": False,
                    }
                },
                "verification_commands": [
                    "aws ec2 describe-vpc-endpoints --filters Name=service-name,Values=com.amazonaws.*.s3",
                ],
                "remediation_commands": [],
            }
        )
    return findings


def detect_unused_eips(eips: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """Detects unused Elastic IPs that incur charges."""
    findings = []
    for a in eips:
        if not a.get("InstanceId"):
            findings.append(
                {
                    "type": "unused_eip",
                    "resource_type": "eip",
                    "id": a.get("PublicIp"),
                    "region": a.get("Region"),
                    "estimated_monthly_cost_usd": PRICING_EIP_UNUSED_MONTH,
                    "estimated_monthly_savings_usd": PRICING_EIP_UNUSED_MONTH,
                    "potential_savings": PRICING_EIP_UNUSED_MONTH,
                    "confidence": 0.95,
                    "severity": "low",
                    "recommended_action": "release-eip",
                    "explanation": "Elastic IP is not attached to any instance; AWS charges for unused EIPs.",
                    "evidence": {
                        "inventory": {
                            "public_ip": a.get("PublicIp"),
                            "instance_id": a.get("InstanceId"),
                            "allocation_id": a.get("AllocationId"),
                            "association_id": a.get("AssociationId"),
                        }
                    },
                    "verification_commands": [
                        f"aws ec2 describe-addresses --public-ips {a.get('PublicIp')} --region {a.get('Region')}",
                    ],
                    "remediation_commands": [
                        f"aws ec2 release-address --public-ip {a.get('PublicIp')}"
                    ],
                }
            )
    return findings


def detect_idle_elb(
    session,
    elbs: List[Dict[str, Any]],
    req_threshold_per_day: float = 10.0,
    days: int = 14,
    budget=None,
    cw_avg_func=None,
) -> List[Dict[str, Any]]:
    """
    Idle ELB/ALB/NLB detector with ARN-first fallback and error provenance.

    Excludes load balancers created <7 days ago to avoid flagging:
    - New deployments still being tested
    - Load balancers in staging/setup phase
    - Recently migrated workloads

    Args:
        session: Boto3 session for CloudWatch access
        elbs: List of load balancer dictionaries
        req_threshold_per_day: Minimum request count threshold (default: 10.0)
        days: Lookback period in days (default: 14)
        budget: MetricBudget instance for query throttling
        cw_avg_func: CloudWatch average function (defaults to importing from analysis.py)
    """
    findings = []
    if session is None:
        return findings
    if not elbs:
        return findings

    # Handle circular dependency by accepting cw_avg as parameter
    if cw_avg_func is None:
        from stacksage.analyzer.metrics import cw_avg as cw_avg_func

    from datetime import timezone

    now = datetime.now(timezone.utc)

    # For counter-like CloudWatch metrics, use daily Sum so the value is an
    # average daily total over the lookback window.
    daily_period = 86400
    bytes_threshold_per_day = 4096.0 * (daily_period / 300.0)  # ~= 1.18MB/day

    def _cw_error_classification(
        cw_errors: List[str], namespace: str, metric: str
    ) -> str | None:
        # errors look like: cw_error:{namespace}:{metric}:{classification}
        prefix = f"cw_error:{namespace}:{metric}:"
        for e in reversed(cw_errors or []):
            if isinstance(e, str) and e.startswith(prefix):
                return e.split(":")[-1]
        return None

    def _metric_status_from_errors(
        cw_errors: List[str], namespace: str, metric: str
    ) -> str:
        cls = _cw_error_classification(cw_errors, namespace, metric)
        if cls in ("no_data", "not_authorized", "throttle"):
            return cls
        if cls is None:
            return "no_data"
        return "error"

    for lb in elbs:
        name = lb.get("Name")
        region = lb.get("Region")
        if not name:
            continue

        # Check creation time - skip if <7 days old
        created_time_str = lb.get("CreatedTime")
        if created_time_str:
            try:
                if isinstance(created_time_str, str):
                    created_time = datetime.fromisoformat(
                        created_time_str.replace("Z", "+00:00")
                    )
                else:
                    created_time = created_time_str

                if created_time.tzinfo is None:
                    created_time = created_time.replace(tzinfo=timezone.utc)

                days_old = (now - created_time).days
                if days_old < 7:
                    continue  # Skip new load balancers
            except Exception:
                pass  # If we can't parse, proceed with check

        cw_errors: List[str] = []
        cw_detail: Dict[str, Any] = {"metrics": []}
        metric_format_used = None
        lb_type = (lb.get("Type") or "").lower()
        has_elbv2_fields = bool(
            lb.get("LoadBalancerArn") or lb.get("LoadBalancerResourceId")
        )

        primary_metric = None
        primary_namespace = None
        primary_dimension = None
        primary_threshold = None

        # Determine the correct namespace + primary metric by LB type.
        if has_elbv2_fields and lb_type == "network":
            # NLB: use ProcessedBytes + flows as signals (RequestCount is not a primary NLB metric)
            primary_namespace = "AWS/NetworkELB"
            primary_metric = "ProcessedBytes"
            primary_dimension = {
                "Name": "LoadBalancer",
                "Value": lb.get("LoadBalancerResourceId"),
            }
            primary_threshold = bytes_threshold_per_day
        elif has_elbv2_fields:
            # Default elbv2 to ALB-like semantics
            primary_namespace = "AWS/ApplicationELB"
            primary_metric = "RequestCount"
            primary_dimension = {
                "Name": "LoadBalancer",
                "Value": lb.get("LoadBalancerResourceId"),
            }
            primary_threshold = req_threshold_per_day
        else:
            primary_namespace = "AWS/ELB"
            primary_metric = "RequestCount"
            primary_dimension = {"Name": "LoadBalancerName", "Value": name}
            primary_threshold = req_threshold_per_day

        # Some scans may not have LoadBalancerResourceId; fall back to ARN (often no-data, but avoid hard failures).
        if primary_dimension.get("Value"):
            metric_format_used = "resource-id" if has_elbv2_fields else "classic"
        else:
            metric_format_used = "arn" if has_elbv2_fields else "classic"
            primary_dimension["Value"] = lb.get("LoadBalancerArn")

        primary_val = None
        if primary_dimension.get("Value"):
            statistic = (
                "Sum"
                if primary_metric in ("RequestCount", "ProcessedBytes")
                else "Average"
            )
            period = daily_period if statistic == "Sum" else 300
            primary_val = cw_avg_func(
                session,
                primary_namespace,
                primary_metric,
                [primary_dimension],
                period=period,
                days=days,
                statistic=statistic,
                region_name=region,
                budget=budget,
                errors=cw_errors,
                evidence=cw_detail,
            )

        if primary_val is None:
            if budget and budget.remaining <= 0:
                findings.append(
                    {
                        "type": "idle_elb",
                        "resource_type": "elb",
                        "id": name,
                        "region": region,
                        "metric_status": "skipped_budget",
                        "elb_metric_format": metric_format_used,
                        "confidence": 0.4,
                        "severity": "low",
                        "recommended_action": "review-lb-usage",
                        "explanation": "Metric query skipped due to budget exhaustion",
                        "evidence": {
                            "inventory": {
                                "type": lb.get("Type"),
                                "created_time": lb.get("CreatedTime"),
                                "metric_format": metric_format_used,
                            },
                            "utilization": {
                                "status": "skipped_budget",
                                "lookback_days": days,
                            },
                        },
                        "verification_commands": [
                            f"aws elbv2 describe-load-balancers --names {name} --region {region}",
                        ],
                        "remediation_commands": [],
                        "metrics_error": ";".join(cw_errors) if cw_errors else None,
                        "cw_metrics": cw_detail.get("metrics"),
                    }
                )
                continue

            metric_status = _metric_status_from_errors(
                cw_errors, primary_namespace, primary_metric
            )
            findings.append(
                {
                    "type": "idle_elb",
                    "resource_type": "elb",
                    "id": name,
                    "region": region,
                    "metric_status": metric_status,
                    "elb_metric_format": metric_format_used,
                    "confidence": 0.3,
                    "severity": "low",
                    "recommended_action": "review-lb-usage",
                    "explanation": "Could not determine load balancer traffic from CloudWatch metrics (no data or insufficient permissions).",
                    "evidence": {
                        "inventory": {
                            "type": lb.get("Type"),
                            "created_time": lb.get("CreatedTime"),
                            "metric_format": metric_format_used,
                        },
                        "utilization": {
                            "status": metric_status,
                            "lookback_days": days,
                        },
                    },
                    "verification_commands": [
                        f"aws elbv2 describe-load-balancers --names {name} --region {region}",
                        f"aws cloudwatch get-metric-statistics --namespace {primary_namespace} --metric-name {primary_metric} --dimensions {primary_dimension['Name']}={primary_dimension['Value']} --start-time $(date -u -v-{days}d '+%Y-%m-%dT%H:%M:%S') --end-time $(date -u '+%Y-%m-%dT%H:%M:%S') --period 86400 --statistics Sum --region {region}",
                    ],
                    "remediation_commands": [],
                    "metrics_error": ";".join(cw_errors) if cw_errors else None,
                    "cw_metrics": cw_detail.get("metrics"),
                }
            )
            continue

        # Multi-signal confirmation for elbv2 load balancers.
        secondary_vals: Dict[str, Any] = {}
        if has_elbv2_fields and lb_type != "classic":
            if (
                primary_namespace == "AWS/ApplicationELB"
                and primary_metric == "RequestCount"
            ):
                if primary_val < req_threshold_per_day:
                    # Confirm near-idle with connections + bytes.
                    active_conn = cw_avg_func(
                        session,
                        "AWS/ApplicationELB",
                        "ActiveConnectionCount",
                        [primary_dimension],
                        days=days,
                        region_name=region,
                        budget=budget,
                        errors=cw_errors,
                        evidence=cw_detail,
                    )
                    processed_bytes = cw_avg_func(
                        session,
                        "AWS/ApplicationELB",
                        "ProcessedBytes",
                        [primary_dimension],
                        period=daily_period,
                        days=days,
                        statistic="Sum",
                        region_name=region,
                        budget=budget,
                        errors=cw_errors,
                        evidence=cw_detail,
                    )
                    secondary_vals["active_connection_count_avg"] = active_conn
                    secondary_vals["processed_bytes_avg"] = processed_bytes
            elif (
                primary_namespace == "AWS/NetworkELB"
                and primary_metric == "ProcessedBytes"
            ):
                if primary_val < bytes_threshold_per_day:
                    active_flow = cw_avg_func(
                        session,
                        "AWS/NetworkELB",
                        "ActiveFlowCount",
                        [primary_dimension],
                        days=days,
                        region_name=region,
                        budget=budget,
                        errors=cw_errors,
                        evidence=cw_detail,
                    )
                    secondary_vals["active_flow_count_avg"] = active_flow

        is_idle = primary_val < float(primary_threshold)
        # Strengthen idle decision when we have additional signals.
        if is_idle and "active_connection_count_avg" in secondary_vals:
            v = secondary_vals.get("active_connection_count_avg")
            if v is not None and v >= 0.1:
                is_idle = False
        if is_idle and "active_flow_count_avg" in secondary_vals:
            v = secondary_vals.get("active_flow_count_avg")
            if v is not None and v >= 1.0:
                is_idle = False
        if is_idle and "processed_bytes_avg" in secondary_vals:
            v = secondary_vals.get("processed_bytes_avg")
            if v is not None and v >= bytes_threshold_per_day:
                is_idle = False

        if is_idle:
            # Estimate base monthly cost (excluding LCU) by LB type
            cost_type = "classic"
            if has_elbv2_fields and lb_type == "network":
                cost_type = "nlb"
            elif has_elbv2_fields:
                cost_type = "alb"
            base_monthly_cost = elb_monthly_cost(cost_type, region)
            # Adjust by uptime if we have CreatedTime
            uptime = _uptime_fraction(lb.get("CreatedTime"))
            adjusted_monthly = round(base_monthly_cost * uptime, 2)
            # Dynamic severity based on cost
            if base_monthly_cost >= 50:
                severity = "high"
            elif base_monthly_cost >= 20:
                severity = "medium"
            else:
                severity = "low"
            findings.append(
                {
                    "type": "idle_elb",
                    "resource_type": "elb",
                    "id": name,
                    "region": region,
                    "request_avg": (
                        primary_val if primary_metric == "RequestCount" else None
                    ),
                    "processed_bytes_avg": (
                        primary_val
                        if primary_metric == "ProcessedBytes"
                        else secondary_vals.get("processed_bytes_avg")
                    ),
                    **secondary_vals,
                    "elb_metric_format": metric_format_used,
                    "confidence": 0.8 if secondary_vals else 0.7,
                    "severity": severity,
                    "estimated_monthly_cost_usd": base_monthly_cost,
                    "adjusted_monthly_cost_usd": adjusted_monthly,
                    "estimated_monthly_savings_usd": base_monthly_cost,
                    "recommended_action": "delete-or-disable",
                    "explanation": "Load balancer shows near-zero request volume; removing it saves its base hourly cost.",
                    "estimation_method": "base_hourly+uptime",
                    "assumptions": [
                        "LCU not included when metrics insufficient",
                        "uptime estimated from CreatedTime",
                        "regional multiplier applied via pricing tables",
                    ],
                    "evidence": {
                        "inventory": {
                            "type": lb.get("Type"),
                            "created_time": lb.get("CreatedTime"),
                            "metric_format": metric_format_used,
                        },
                        "utilization": {
                            "primary_metric": primary_metric,
                            "primary_value": primary_val,
                            "primary_threshold": primary_threshold,
                            "secondary_metrics": secondary_vals,
                            "lookback_days": days,
                        },
                    },
                    "verification_commands": [
                        f"aws elbv2 describe-load-balancers --names {name} --region {region}",
                        f"aws cloudwatch get-metric-statistics --namespace {primary_namespace} --metric-name {primary_metric} --dimensions {primary_dimension['Name']}={primary_dimension['Value']} --start-time $(date -u -v-{days}d '+%Y-%m-%dT%H:%M:%S') --end-time $(date -u '+%Y-%m-%dT%H:%M:%S') --period 86400 --statistics Sum --region {region}",
                    ],
                    "remediation_commands": [],
                    "metrics_error": ";".join(cw_errors) if cw_errors else None,
                    "cw_metrics": cw_detail.get("metrics"),
                }
            )
    return findings


def detect_lb_empty_target_groups(
    elbs: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
    """
    Detect ALB/NLB load balancers with all target groups empty (no registered targets).

    Load balancers with empty target groups are typically:
    - Leftover from decommissioned applications
    - Created during testing/POC and forgotten
    - Part of cleanup that wasn't completed

    Only flags ALB/NLB (not Classic ELB) as target groups are an ELBv2 concept.
    Requires load balancer to be >7 days old to avoid flagging active deployments.

    Args:
        elbs: List of load balancer dictionaries with target group data

    Returns:
        List of findings for load balancers with empty target groups
    """
    findings = []
    from datetime import datetime, timezone

    now = datetime.now(timezone.utc)
    MIN_DAYS_OLD = 7

    for lb in elbs:
        lb_type = lb.get("Type", "")
        # Only check ALB/NLB (target groups are ELBv2 concept)
        if lb_type not in ("application", "network"):
            continue

        name = lb.get("Name")
        region = lb.get("Region")
        arn = lb.get("LoadBalancerArn")
        if not name or not region:
            continue

        # Check if LB is old enough (>7 days)
        created_time_str = lb.get("CreatedTime")
        if created_time_str:
            try:
                if isinstance(created_time_str, str):
                    created_time = datetime.fromisoformat(
                        created_time_str.replace("Z", "+00:00")
                    )
                else:
                    created_time = created_time_str

                if created_time.tzinfo is None:
                    created_time = created_time.replace(tzinfo=timezone.utc)

                days_old = (now - created_time).days
                if days_old < MIN_DAYS_OLD:
                    continue  # Skip new load balancers
            except Exception:
                pass

        target_groups = lb.get("TargetGroups", [])

        # Skip if no target groups (unusual but not necessarily a problem)
        if not target_groups:
            continue

        # Check if all target groups are empty
        all_empty = True
        total_tg_count = len(target_groups)
        empty_tg_names = []

        for tg in target_groups:
            total_targets = tg.get("TotalTargetCount", 0)
            if total_targets > 0:
                all_empty = False
                break
            empty_tg_names.append(tg.get("TargetGroupName", "unknown"))

        if not all_empty:
            continue

        # Calculate cost
        monthly_cost = elb_monthly_cost(lb_type, region)

        # Dynamic severity based on cost
        if monthly_cost >= 50:
            severity = "high"
        elif monthly_cost >= 20:
            severity = "medium"
        else:
            severity = "low"

        findings.append(
            {
                "type": "lb_empty_target_groups",
                "resource_type": "elb",
                "id": name,
                "region": region,
                "estimated_monthly_cost_usd": monthly_cost,
                "estimated_monthly_savings_usd": monthly_cost,
                "potential_savings": monthly_cost,
                "confidence": 0.90,
                "severity": severity,
                "recommended_action": "delete-if-unused",
                "explanation": f"{lb_type.upper()} load balancer with {total_tg_count} target group(s), all empty (0 registered targets). Likely leftover from decommissioned application.",
                "evidence": {
                    "inventory": {
                        "load_balancer_type": lb_type,
                        "state": lb.get("State"),
                        "created_time": created_time_str,
                        "target_group_count": total_tg_count,
                        "empty_target_groups": empty_tg_names,
                    },
                    "assessment": {
                        "days_old": days_old if "days_old" in locals() else None,
                        "all_target_groups_empty": True,
                    },
                },
                "verification_commands": [
                    "# Check load balancer details",
                    f"aws elbv2 describe-load-balancers --names {name} --region {region}",
                    "",
                    "# List all target groups",
                    f"aws elbv2 describe-target-groups --load-balancer-arn {arn} --region {region}",
                    "",
                    "# Check target health for each group (example for first group)",
                ]
                + [
                    f"aws elbv2 describe-target-health --target-group-arn {tg.get('TargetGroupArn')} --region {region}"
                    for tg in target_groups[:3]  # Show first 3 as examples
                ],
                "remediation_commands": [
                    "# Delete load balancer (this also deletes associated listeners):",
                    f"aws elbv2 delete-load-balancer --load-balancer-arn {arn} --region {region}",
                    "",
                    "# Then delete target groups:",
                ]
                + [
                    f"aws elbv2 delete-target-group --target-group-arn {tg.get('TargetGroupArn')} --region {region}"
                    for tg in target_groups
                ],
            }
        )

    return findings


def detect_nat_gateway_egress_cost(
    session,
    nat_list: List[Dict[str, Any]],
    vpc_endpoints: List[Dict[str, Any]] = None,
    days: int = 14,
    min_monthly_cost_usd: float = 5.0,
    budget=None,
    cw_avg_func=None,
) -> List[Dict[str, Any]]:
    """
    Identify NAT Gateways with quantified egress data-processing costs using
    CloudWatch metrics.

    AWS charges $0.045/GB for data processed through a NAT Gateway (both
    in and out).  Unlike the ``detect_idle_nat_gateways`` detector (which
    flags *unused* gateways), this detector surfaces *actively used* gateways
    whose data-processing bill can be reduced by adding VPC endpoints for
    specific AWS services (S3, DynamoDB) that would otherwise route through the
    NAT.

    CloudWatch metrics used:
    - ``AWS/NATGateway::BytesOutToDestination`` (Sum over window)
    - ``AWS/NATGateway::BytesInFromSource`` (Sum over window)

    Cross-reference: if the VPC hosting the NAT also lacks an S3 or DynamoDB
    gateway endpoint, the finding explicitly calls this out as the primary
    remediation path.

    Args:
        session: Boto3 session for CloudWatch access.
        nat_list: List of NAT Gateway dicts from the scanner.
        vpc_endpoints: Current VPC endpoint list from the scanner — used to
            identify which specific endpoints are missing.  Optional; if omitted
            the endpoint cross-check is skipped.
        days: CloudWatch lookback window in days (default 14).
        min_monthly_cost_usd: Only surface findings with projected monthly
            egress cost >= this threshold (default $5).
        budget: MetricBudget for CW query throttling.
        cw_avg_func: Injected CloudWatch helper (defaults to ``cw_avg``).

    Returns:
        One finding per NAT gateway whose projected monthly data-processing
        cost exceeds ``min_monthly_cost_usd``.
    """
    findings = []
    if session is None or not nat_list:
        return findings

    if cw_avg_func is None:
        from stacksage.analyzer.metrics import cw_avg as cw_avg_func

    # Build a set of VPC IDs that already have S3 / DynamoDB gateway endpoints
    vpcs_with_s3_endpoint: set = set()
    vpcs_with_dynamodb_endpoint: set = set()
    if vpc_endpoints:
        for ep in vpc_endpoints:
            svc = (ep.get("ServiceName") or "").lower()
            ep_type = ep.get("VpcEndpointType", "")
            vpc_id = ep.get("VpcId") or ep.get("vpc_id")
            if ep_type == "Gateway" and vpc_id:
                if "s3" in svc:
                    vpcs_with_s3_endpoint.add(vpc_id)
                if "dynamodb" in svc:
                    vpcs_with_dynamodb_endpoint.add(vpc_id)

    daily_period = 86400

    for ng in nat_list:
        nat_id = ng.get("NatGatewayId")
        region = ng.get("Region")
        vpc_id = ng.get("VpcId")

        if not nat_id or not region:
            continue

        cw_errors: List[str] = []
        cw_detail: Dict[str, Any] = {"metrics": []}

        bytes_out = cw_avg_func(
            session,
            "AWS/NATGateway",
            "BytesOutToDestination",
            [{"Name": "NatGatewayId", "Value": nat_id}],
            period=daily_period,
            days=days,
            statistic="Sum",
            region_name=region,
            budget=budget,
            errors=cw_errors,
            evidence=cw_detail,
        )

        bytes_in = cw_avg_func(
            session,
            "AWS/NATGateway",
            "BytesInFromSource",
            [{"Name": "NatGatewayId", "Value": nat_id}],
            period=daily_period,
            days=days,
            statistic="Sum",
            region_name=region,
            budget=budget,
            errors=cw_errors,
            evidence=cw_detail,
        )

        # Skip if CW returned nothing (budget exhausted, permission error, etc.)
        if bytes_out is None or bytes_in is None:
            continue

        # Compute projected monthly cost from lookback avg
        # bytes_out/bytes_in are average-per-day values (Sum / n_datapoints)
        total_gb_per_day = (bytes_out + bytes_in) / (1024**3)
        total_gb_per_month = total_gb_per_day * 30

        regional_multiplier = get_regional_multiplier(region)
        monthly_egress_cost = round(
            total_gb_per_month * PRICING_NAT_DATA_GB * regional_multiplier, 2
        )

        if monthly_egress_cost < min_monthly_cost_usd:
            continue

        # Determine missing endpoints for cross-reference advice
        missing_endpoints = []
        endpoint_savings_hint = ""
        if vpc_endpoints is not None:
            if vpc_id and vpc_id not in vpcs_with_s3_endpoint:
                missing_endpoints.append("S3")
            if vpc_id and vpc_id not in vpcs_with_dynamodb_endpoint:
                missing_endpoints.append("DynamoDB")

        if missing_endpoints:
            endpoint_savings_hint = (
                f"  S3 and/or DynamoDB gateway endpoints are missing for VPC {vpc_id}. "
                f"Traffic to these services ({', '.join(missing_endpoints)}) currently "
                "passes through the NAT and incurs data-processing charges. "
                "Adding Gateway endpoints eliminates this cost for those services."
            )

        severity = "high" if monthly_egress_cost >= 50 else "medium"

        findings.append(
            {
                "type": "nat_gateway_egress_cost",
                "resource_type": "nat",
                "id": nat_id,
                "region": region,
                "vpc_id": vpc_id,
                "state": ng.get("State"),
                "estimated_monthly_cost_usd": monthly_egress_cost,
                "estimated_monthly_savings_usd": 0,  # Savings depend on which traffic moves to endpoints
                "potential_savings": 0,
                "confidence": 0.75,
                "severity": severity,
                "recommended_action": "add-vpc-gateway-endpoints-for-s3-dynamodb",
                "explanation": (
                    f"NAT Gateway {nat_id} processed ~{total_gb_per_day:.2f} GB/day "
                    f"over the last {days} days, projecting ~${monthly_egress_cost:.2f}/mo "
                    f"in data-processing charges (at ${PRICING_NAT_DATA_GB}/GB)."
                    + (endpoint_savings_hint if endpoint_savings_hint else "")
                ),
                "estimation_method": "cloudwatch_sum_extrapolated_30d",
                "assumptions": [
                    f"CDW lookback {days} days, daily average extrapolated to 30 days",
                    f"Rate: ${PRICING_NAT_DATA_GB}/GB data processing (regional multiplier applied)",
                    "S3 and DynamoDB gateway endpoints are free; traffic via endpoint has no data-processing charge",
                ],
                "evidence": {
                    "inventory": {
                        "nat_gateway_id": nat_id,
                        "vpc_id": vpc_id,
                        "state": ng.get("State"),
                        "region": region,
                    },
                    "utilization": {
                        "bytes_out_avg_per_day": bytes_out,
                        "bytes_in_avg_per_day": bytes_in,
                        "total_gb_per_day": round(total_gb_per_day, 4),
                        "total_gb_per_month_projected": round(total_gb_per_month, 2),
                        "lookback_days": days,
                    },
                    "estimation": {
                        "monthly_egress_cost_usd": monthly_egress_cost,
                        "rate_per_gb_usd": PRICING_NAT_DATA_GB,
                        "regional_multiplier": regional_multiplier,
                    },
                    "cross_reference": {
                        "missing_gateway_endpoints": (
                            missing_endpoints if vpc_endpoints is not None else None
                        ),
                        "vpc_id": vpc_id,
                    },
                },
                "verification_commands": [
                    f"aws ec2 describe-nat-gateways --nat-gateway-ids {nat_id} --region {region}",
                    (
                        f"aws cloudwatch get-metric-statistics "
                        f"--namespace AWS/NATGateway --metric-name BytesOutToDestination "
                        f"--dimensions Name=NatGatewayId,Value={nat_id} "
                        f"--start-time $(date -u -v-{days}d '+%Y-%m-%dT%H:%M:%S') "
                        f"--end-time $(date -u '+%Y-%m-%dT%H:%M:%S') "
                        f"--period 86400 --statistics Sum --region {region}"
                    ),
                    (
                        f"aws cloudwatch get-metric-statistics "
                        f"--namespace AWS/NATGateway --metric-name BytesInFromSource "
                        f"--dimensions Name=NatGatewayId,Value={nat_id} "
                        f"--start-time $(date -u -v-{days}d '+%Y-%m-%dT%H:%M:%S') "
                        f"--end-time $(date -u '+%Y-%m-%dT%H:%M:%S') "
                        f"--period 86400 --statistics Sum --region {region}"
                    ),
                ]
                + (
                    [
                        f"aws ec2 describe-vpc-endpoints --filters Name=vpc-id,Values={vpc_id} "
                        f"Name=vpc-endpoint-type,Values=Gateway --region {region}"
                    ]
                    if vpc_id
                    else []
                ),
                "remediation_commands": (
                    [
                        "# Add S3 gateway endpoint (free, no data-processing charge):",
                        f"aws ec2 create-vpc-endpoint --vpc-id {vpc_id} "
                        f"--service-name com.amazonaws.{region}.s3 "
                        f"--vpc-endpoint-type Gateway --region {region}",
                        "# Add DynamoDB gateway endpoint (free):",
                        f"aws ec2 create-vpc-endpoint --vpc-id {vpc_id} "
                        f"--service-name com.amazonaws.{region}.dynamodb "
                        f"--vpc-endpoint-type Gateway --region {region}",
                    ]
                    if vpc_id
                    else []
                ),
                "metrics_error": ";".join(cw_errors) if cw_errors else None,
                "cw_metrics": cw_detail.get("metrics"),
            }
        )

    return findings


def detect_missing_dynamodb_endpoint(
    vpc_endpoints: List[Dict[str, Any]],
    vpc_ids: List[str] = None,
) -> List[Dict[str, Any]]:
    """
    Detect missing DynamoDB VPC Gateway endpoint.

    DynamoDB Gateway endpoints are free and eliminate NAT Gateway data-processing
    charges ($0.045/GB) for DynamoDB traffic from private subnets.  They are the
    most common missed optimisation for Lambda/ECS architectures that access
    DynamoDB through a NAT.

    Mirrors the pattern of ``detect_missing_s3_endpoint``.

    Args:
        vpc_endpoints: VPC endpoint list from the scanner.
        vpc_ids: List of VPC IDs found in the account (used in the finding id).

    Returns:
        Zero or one finding — one if no DynamoDB Gateway endpoint is detected.
    """
    findings = []
    has_dynamodb = any(
        (
            "dynamodb" in (ep.get("ServiceName") or "").lower()
            and ep.get("VpcEndpointType") == "Gateway"
        )
        for ep in (vpc_endpoints or [])
    )

    if not has_dynamodb:
        resolved_vpc_ids = vpc_ids or []
        findings.append(
            {
                "type": "missing_dynamodb_vpc_endpoint",
                "resource_type": "vpc",
                "id": ",".join(resolved_vpc_ids) if resolved_vpc_ids else "unknown",
                "estimated_monthly_cost_usd": 0,
                "estimated_monthly_savings_usd": 0,
                "potential_savings": 0,
                "confidence": 0.6,
                "severity": "low",
                "recommended_action": "create-dynamodb-vpc-gateway-endpoint",
                "explanation": (
                    "No DynamoDB Gateway VPC endpoint detected. "
                    "Lambda and ECS workloads in private subnets accessing DynamoDB "
                    "currently route traffic through the NAT Gateway, incurring "
                    f"${PRICING_NAT_DATA_GB}/GB data-processing charges. "
                    "Adding a Gateway endpoint is free and eliminates this cost."
                ),
                "evidence": {
                    "inventory": {
                        "vpc_ids": resolved_vpc_ids,
                        "vpc_endpoints_seen": len(vpc_endpoints or []),
                        "dynamodb_gateway_endpoint_present": False,
                    }
                },
                "verification_commands": [
                    "aws ec2 describe-vpc-endpoints --filters "
                    "Name=service-name,Values=com.amazonaws.*.dynamodb "
                    "Name=vpc-endpoint-type,Values=Gateway",
                ],
                "remediation_commands": [
                    "# Replace <vpc-id> and <region> with your values:",
                    "aws ec2 create-vpc-endpoint "
                    "--vpc-id <vpc-id> "
                    "--service-name com.amazonaws.<region>.dynamodb "
                    "--vpc-endpoint-type Gateway",
                ],
            }
        )
    return findings
