"""
EC2 Detectors

Detects cost optimization opportunities for EC2 instances.
"""

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

from stacksage.pricing import estimate_ec2_monthly_cost


def detect_ec2_generation_upgrades(
    instances: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
    """
    Detects EC2 instances running on old generations where upgrading to newer
    generation can provide better price/performance.

    Common upgrades:
    - t2 → t3 (up to 10% savings + better burst credits)
    - m4 → m5/m6i (5-10% savings + better performance)
    - c4 → c5/c6i (similar savings)
    - r4 → r5/r6i (10-15% savings + better memory bandwidth)
    """
    findings = []
    if not instances:
        return findings

    # Upgrade map: old_gen → (new_gen, savings_pct, confidence, rationale)
    upgrade_map = {
        "t2.nano": (
            "t3.nano",
            0.05,
            0.90,
            "t3 offers better burst credits + 5% lower cost",
        ),
        "t2.micro": (
            "t3.micro",
            0.05,
            0.90,
            "t3 offers better burst credits + 5% lower cost",
        ),
        "t2.small": (
            "t3.small",
            0.05,
            0.90,
            "t3 offers better burst credits + 5% lower cost",
        ),
        "t2.medium": (
            "t3.medium",
            0.05,
            0.90,
            "t3 offers better burst credits + 5% lower cost",
        ),
        "t2.large": (
            "t3.large",
            0.05,
            0.90,
            "t3 offers better burst credits + 5% lower cost",
        ),
        "t2.xlarge": (
            "t3.xlarge",
            0.05,
            0.90,
            "t3 offers better burst credits + 5% lower cost",
        ),
        "t2.2xlarge": (
            "t3.2xlarge",
            0.05,
            0.90,
            "t3 offers better burst credits + 5% lower cost",
        ),
        "m4.large": (
            "m5.large",
            0.08,
            0.85,
            "m5 is 8% cheaper with better CPU performance",
        ),
        "m4.xlarge": (
            "m5.xlarge",
            0.08,
            0.85,
            "m5 is 8% cheaper with better CPU performance",
        ),
        "m4.2xlarge": (
            "m5.2xlarge",
            0.08,
            0.85,
            "m5 is 8% cheaper with better CPU performance",
        ),
        "m4.4xlarge": (
            "m5.4xlarge",
            0.08,
            0.85,
            "m5 is 8% cheaper with better CPU performance",
        ),
        "c4.large": (
            "c5.large",
            0.10,
            0.85,
            "c5 is 10% cheaper with better compute performance",
        ),
        "c4.xlarge": (
            "c5.xlarge",
            0.10,
            0.85,
            "c5 is 10% cheaper with better compute performance",
        ),
        "c4.2xlarge": (
            "c5.2xlarge",
            0.10,
            0.85,
            "c5 is 10% cheaper with better compute performance",
        ),
        "c4.4xlarge": (
            "c5.4xlarge",
            0.10,
            0.85,
            "c5 is 10% cheaper with better compute performance",
        ),
        "r4.large": (
            "r5.large",
            0.12,
            0.85,
            "r5 is 12% cheaper with better memory bandwidth",
        ),
        "r4.xlarge": (
            "r5.xlarge",
            0.12,
            0.85,
            "r5 is 12% cheaper with better memory bandwidth",
        ),
        "r4.2xlarge": (
            "r5.2xlarge",
            0.12,
            0.85,
            "r5 is 12% cheaper with better memory bandwidth",
        ),
        "r4.4xlarge": (
            "r5.4xlarge",
            0.12,
            0.85,
            "r5 is 12% cheaper with better memory bandwidth",
        ),
    }

    for inst in instances:
        instance_type = inst.get("InstanceType", "")
        state = (
            inst.get("State", {}).get("Name", "")
            if isinstance(inst.get("State"), dict)
            else inst.get("State", "")
        )
        region = inst.get("Region")
        region_arg = f" --region {region}" if region else ""

        # Only recommend for running instances (stopped instances already have a detector)
        if state not in ["running"]:
            continue

        if instance_type in upgrade_map:
            new_type, savings_pct, confidence, rationale = upgrade_map[instance_type]

            current_cost = estimate_ec2_monthly_cost(instance_type, region=None)
            estimated_savings = current_cost * savings_pct

            # Only recommend if savings ≥ $2/month (reduce noise)
            if estimated_savings < 2.0:
                continue

            # Dynamic severity based on savings
            if estimated_savings >= 20:
                severity = "medium"
            else:
                severity = "low"

            findings.append(
                {
                    "type": "ec2_generation_upgrade",
                    "resource_type": "ec2",
                    "id": inst.get("InstanceId"),
                    "region": region
                    or inst.get("Placement", {}).get("AvailabilityZone", "unknown"),
                    "current_instance_type": instance_type,
                    "recommended_instance_type": new_type,
                    "current_monthly_cost_usd": round(current_cost, 2),
                    "estimated_monthly_savings_usd": round(estimated_savings, 2),
                    "potential_savings": round(estimated_savings, 2),
                    "savings_percentage": int(savings_pct * 100),
                    "confidence": confidence,
                    "severity": severity,
                    "recommended_action": "upgrade-instance-generation",
                    "explanation": f"Upgrade from {instance_type} to {new_type}. {rationale}. Newer generations offer better price/performance. Test in non-prod first.",
                    "evidence": {
                        "inventory": {
                            "state": state,
                            "instance_type": instance_type,
                            "recommended_type": new_type,
                        },
                        "estimation": {
                            "savings_percentage": int(savings_pct * 100),
                            "monthly_cost_usd": round(current_cost, 2),
                        },
                    },
                    "verification_commands": [
                        f"aws ec2 describe-instances --instance-ids {inst.get('InstanceId')}{region_arg}",
                        f"aws ec2 describe-instance-types --instance-types {instance_type} {new_type}{region_arg}",
                    ],
                    "remediation_commands": [
                        "# Test in non-production environment first!",
                        "# Step 1: Stop instance (requires downtime)",
                        f"aws ec2 stop-instances --instance-ids {inst.get('InstanceId')}",
                        "# Step 2: Wait for stopped state",
                        f"aws ec2 wait instance-stopped --instance-ids {inst.get('InstanceId')}",
                        "# Step 3: Change instance type",
                        f"aws ec2 modify-instance-attribute --instance-id {inst.get('InstanceId')} --instance-type {new_type}",
                        "# Step 4: Start instance",
                        f"aws ec2 start-instances --instance-ids {inst.get('InstanceId')}",
                        "# Step 5: Monitor performance after upgrade",
                    ],
                }
            )

    return findings


def detect_stopped_ec2(instances: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """
    Detect EC2 instances in stopped state that are likely abandoned.

    Only flags instances stopped for >14 days to avoid false positives during:
    - Maintenance windows
    - Troubleshooting
    - Planned shutdowns for cost savings

    Note: Stopped instances still incur EBS storage costs.

    Production threshold: 14 days to avoid false positives
    """
    findings = []
    from datetime import timezone

    now = datetime.now(timezone.utc)

    # Production threshold
    MIN_DAYS_STOPPED = 14

    for i in instances:
        if i.get("State") == "stopped":
            region = i.get("Region")
            region_arg = f" --region {region}" if region else ""
            # Check StateTransitionReason or launch time
            # AWS doesn't always provide stop time, so we use heuristics
            state_reason = i.get("StateTransitionReason", "")

            # Try to extract stop date from state reason (format: "User initiated (YYYY-MM-DD HH:MM:SS GMT)")
            days_stopped = None
            if "User initiated" in state_reason:
                try:
                    import re

                    match = re.search(
                        r"(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})", state_reason
                    )
                    if match:
                        stop_time_str = match.group(1)
                        stop_time = datetime.strptime(
                            stop_time_str, "%Y-%m-%d %H:%M:%S"
                        ).replace(tzinfo=timezone.utc)
                        days_stopped = (now - stop_time).days
                except Exception:
                    pass

            # If we can't determine stop time, check launch time as fallback
            # If instance is very old and stopped, it's likely abandoned
            if days_stopped is None:
                launch_time_str = i.get("LaunchTime")
                if launch_time_str:
                    try:
                        if isinstance(launch_time_str, str):
                            launch_time = datetime.fromisoformat(
                                launch_time_str.replace("Z", "+00:00")
                            )
                        else:
                            launch_time = launch_time_str

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

                        days_since_launch = (now - launch_time).days
                        # If instance is >90 days old and stopped, likely abandoned
                        if days_since_launch < 90:
                            continue
                    except Exception:
                        pass
            elif days_stopped < MIN_DAYS_STOPPED:
                # Skip if stopped less than threshold (may be temporary)
                continue

            iid = i.get("InstanceId")
            monthly = estimate_ec2_monthly_cost(i.get("InstanceType"), region=None)

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

            explanation = f"Instance stopped for >{MIN_DAYS_STOPPED} days. Stopped instances still incur EBS storage costs (~30% of running cost). Consider terminating if no longer needed."
            if days_stopped:
                explanation = f"Instance stopped for {days_stopped} days. Stopped instances still incur EBS storage costs. Consider terminating if no longer needed."

            findings.append(
                {
                    "type": "stopped_ec2",
                    "resource_type": "ec2",
                    "id": iid,
                    "region": region,
                    "instance_type": i.get("InstanceType"),
                    "estimated_monthly_cost_usd": monthly,
                    "estimated_monthly_savings_usd": monthly,
                    "potential_savings": monthly,
                    "confidence": 0.85,
                    "severity": severity,
                    "recommended_action": "terminate-if-unused",
                    "explanation": explanation,
                    "evidence": {
                        "inventory": {
                            "state": "stopped",
                            "instance_type": i.get("InstanceType"),
                            "launch_time": i.get("LaunchTime"),
                            "state_transition_reason": i.get("StateTransitionReason"),
                        },
                        "assessment": {
                            "days_stopped": days_stopped,
                            "min_days_stopped": MIN_DAYS_STOPPED,
                        },
                    },
                    "verification_commands": [
                        f"aws ec2 describe-instances --instance-ids {iid}{region_arg}",
                    ],
                    "remediation_commands": [
                        "# Verify instance is not needed, then terminate:",
                        f"aws ec2 describe-instances --instance-ids {iid}",
                        f"aws ec2 terminate-instances --instance-ids {iid}",
                    ],
                }
            )
    return findings


def detect_idle_ec2(
    session,
    instances: List[Dict[str, Any]],
    cpu_threshold: float = 5.0,
    days: int = 14,
    budget=None,
    cw_avg_func=None,
) -> List[Dict[str, Any]]:
    """
    Idle EC2 detector with CloudWatch budget/error provenance.

    Args:
        session: boto3 session
        instances: List of EC2 instance dicts
        cpu_threshold: CPU threshold below which instance is considered idle
        days: Lookback period for metrics
        budget: MetricBudget instance
        cw_avg_func: CloudWatch avg function (passed in to avoid circular import)
    """
    findings = []
    if session is None:
        return findings
    if not instances:
        return findings
    if cw_avg_func is None:
        # Import here to avoid circular dependency
        from stacksage.analyzer.metrics import cw_avg

        cw_avg_func = cw_avg

    def _tag_map(tags: Any) -> Dict[str, str]:
        out: Dict[str, str] = {}
        if isinstance(tags, list):
            for t in tags:
                if not isinstance(t, dict):
                    continue
                k = t.get("Key")
                if not k:
                    continue
                out[str(k)] = "" if t.get("Value") is None else str(t.get("Value"))
        return out

    def _has_autoscaling(tags: Dict[str, str]) -> bool:
        # We treat ASG-managed instances as an exclusion to reduce false positives.
        for k in tags.keys():
            kl = k.lower()
            if kl == "aws:autoscaling:groupname" or kl.endswith(
                ":autoscaling:groupname"
            ):
                return True
        return False

    def _has_do_not_stop(tags: Dict[str, str]) -> bool:
        # Escape hatch: customers can mark instances that should never be stopped.
        keys = {k.lower() for k in tags.keys()}
        return bool(
            keys
            & {
                "do-not-stop",
                "do_not_stop",
                "stacksage:do-not-stop",
                "stacksage:do_not_stop",
            }
        )

    from datetime import timezone

    now = datetime.now(timezone.utc)

    # Counter-like metrics: query daily Sum so the value is an average daily total
    # across the lookback window.
    daily_period = 86400
    net_bytes_threshold_per_day = 1e6 * (daily_period / 300.0)  # ~= 288MB/day
    disk_ops_threshold_per_day = 1.0 * (daily_period / 300.0)  # ~= 288 ops/day

    for inst in instances:
        iid = inst.get("InstanceId")
        if not iid:
            continue

        tags = _tag_map(inst.get("Tags"))
        if _has_do_not_stop(tags):
            continue
        if _has_autoscaling(tags):
            continue

        # Avoid flagging brand-new instances (deployments, migrations, testing).
        launch_time_str = inst.get("LaunchTime")
        if launch_time_str:
            try:
                if isinstance(launch_time_str, str):
                    launch_time = datetime.fromisoformat(
                        launch_time_str.replace("Z", "+00:00")
                    )
                else:
                    launch_time = launch_time_str
                if launch_time.tzinfo is None:
                    launch_time = launch_time.replace(tzinfo=timezone.utc)
                if (now - launch_time).days < 7:
                    continue
            except Exception:
                # If time parsing fails, continue with utilization checks.
                pass

        region = inst.get("Region")
        region_arg = f" --region {region}" if region else ""
        cw_errors: List[str] = []
        cw_detail: Dict[str, Any] = {"metrics": []}
        cpu = cw_avg_func(
            session,
            "AWS/EC2",
            "CPUUtilization",
            [{"Name": "InstanceId", "Value": iid}],
            days=days,
            region_name=region,
            budget=budget,
            errors=cw_errors,
            evidence=cw_detail,
        )
        net_in = cw_avg_func(
            session,
            "AWS/EC2",
            "NetworkIn",
            [{"Name": "InstanceId", "Value": iid}],
            period=daily_period,
            days=days,
            statistic="Sum",
            region_name=region,
            budget=budget,
            errors=cw_errors,
            evidence=cw_detail,
        )
        net_out = cw_avg_func(
            session,
            "AWS/EC2",
            "NetworkOut",
            [{"Name": "InstanceId", "Value": iid}],
            period=daily_period,
            days=days,
            statistic="Sum",
            region_name=region,
            budget=budget,
            errors=cw_errors,
            evidence=cw_detail,
        )
        if cpu is None and budget and budget.remaining <= 0:
            findings.append(
                {
                    "type": "idle_ec2",
                    "resource_type": "ec2",
                    "id": iid,
                    "instance_type": inst.get("InstanceType"),
                    "metric_status": "skipped_budget",
                    "confidence": 0.4,
                    "severity": "low",
                    "recommended_action": "review-utilization",
                    "explanation": "Metric query skipped due to budget exhaustion",
                    "evidence": {
                        "inventory": {
                            "instance_type": inst.get("InstanceType"),
                            "launch_time": inst.get("LaunchTime"),
                        },
                        "utilization": {
                            "status": "skipped_budget",
                            "lookback_days": days,
                        },
                    },
                    "verification_commands": [
                        f"aws ec2 describe-instances --instance-ids {iid}{region_arg}",
                        f"aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization --dimensions Name=InstanceId,Value={iid} --start-time $(date -u -v-{days}d '+%Y-%m-%dT%H:%M:%S') --end-time $(date -u '+%Y-%m-%dT%H:%M:%S') --period 300 --statistics Average{region_arg}",
                    ],
                    "remediation_commands": [],
                    "metrics_error": ";".join(cw_errors) if cw_errors else None,
                    "cw_metrics": cw_detail.get("metrics"),
                }
            )
            continue
        if cpu is None:
            # No data; record errors if any and skip
            continue

        # Optional additional signal: disk ops (only query if already low CPU+network).
        disk_read_ops = None
        disk_write_ops = None
        if (
            cpu < cpu_threshold
            and (net_in or 0) < net_bytes_threshold_per_day
            and (net_out or 0) < net_bytes_threshold_per_day
        ):
            disk_read_ops = cw_avg_func(
                session,
                "AWS/EC2",
                "DiskReadOps",
                [{"Name": "InstanceId", "Value": iid}],
                period=daily_period,
                days=days,
                statistic="Sum",
                region_name=region,
                budget=budget,
                errors=cw_errors,
                evidence=cw_detail,
            )
            disk_write_ops = cw_avg_func(
                session,
                "AWS/EC2",
                "DiskWriteOps",
                [{"Name": "InstanceId", "Value": iid}],
                period=daily_period,
                days=days,
                statistic="Sum",
                region_name=region,
                budget=budget,
                errors=cw_errors,
                evidence=cw_detail,
            )

        disk_ok = True
        if disk_read_ops is not None or disk_write_ops is not None:
            disk_ok = (disk_read_ops or 0) < disk_ops_threshold_per_day and (
                disk_write_ops or 0
            ) < disk_ops_threshold_per_day

        if (
            cpu < cpu_threshold
            and (net_in or 0) < net_bytes_threshold_per_day
            and (net_out or 0) < net_bytes_threshold_per_day
            and disk_ok
        ):
            findings.append(
                {
                    "type": "idle_ec2",
                    "resource_type": "ec2",
                    "id": iid,
                    "instance_type": inst.get("InstanceType"),
                    "cpu_avg": cpu,
                    "net_in_avg": net_in,
                    "net_out_avg": net_out,
                    "disk_read_ops_avg": disk_read_ops,
                    "disk_write_ops_avg": disk_write_ops,
                    "confidence": 0.8,
                    "severity": "medium",
                    "recommended_action": "consider-stopping-or-rightsize",
                    "explanation": f"Low CPU ({cpu:.2f}%) and low network usage over last {days} days.",
                    "evidence": {
                        "inventory": {
                            "instance_type": inst.get("InstanceType"),
                            "launch_time": inst.get("LaunchTime"),
                        },
                        "utilization": {
                            "cpu_avg": cpu,
                            "net_in_avg": net_in,
                            "net_out_avg": net_out,
                            "disk_read_ops_avg": disk_read_ops,
                            "disk_write_ops_avg": disk_write_ops,
                            "cpu_threshold": cpu_threshold,
                            "net_bytes_threshold_per_day": net_bytes_threshold_per_day,
                            "disk_ops_threshold_per_day": disk_ops_threshold_per_day,
                            "lookback_days": days,
                        },
                    },
                    "verification_commands": [
                        f"aws ec2 describe-instances --instance-ids {iid}{region_arg}",
                        f"aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization --dimensions Name=InstanceId,Value={iid} --start-time $(date -u -v-{days}d '+%Y-%m-%dT%H:%M:%S') --end-time $(date -u '+%Y-%m-%dT%H:%M:%S') --period 300 --statistics Average{region_arg}",
                        f"aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name NetworkIn --dimensions Name=InstanceId,Value={iid} --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_arg}",
                        f"aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name NetworkOut --dimensions Name=InstanceId,Value={iid} --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_arg}",
                    ],
                    "remediation_commands": [
                        f"aws ec2 stop-instances --instance-ids {iid}"
                    ],
                    "metrics_error": ";".join(cw_errors) if cw_errors else None,
                    "cw_metrics": cw_detail.get("metrics"),
                }
            )
    return findings
