import csv
import json
import os
from datetime import datetime, timezone

from jinja2 import Environment, FileSystemLoader

from stacksage.ai.summarizer import summarize_analysis

SECURITY_REASON_CODES = {
    "security_baseline",
    "public_exposure",
    "audit_logging",
}


def _severity_rank(sev: str | None) -> int:
    order = {
        "critical": 0,
        "high": 1,
        "medium": 2,
        "low": 3,
        "info": 4,
        "informational": 4,
    }
    if not sev:
        return 99
    return order.get(str(sev).strip().lower(), 99)


def _format_money(value) -> str:
    try:
        v = float(value or 0)
    except Exception:
        v = 0.0
    return f"${v:,.2f}"


def _as_reason_codes(value) -> list[str]:
    if not value:
        return []
    if isinstance(value, list):
        return [str(v) for v in value if v is not None]
    if isinstance(value, str):
        return [value]
    return []


def _is_skipped_posture(f: dict) -> bool:
    return (f.get("type") or "").lower() == "posture_check_skipped"


def _is_security_posture_finding(f: dict) -> bool:
    codes = {c.lower() for c in _as_reason_codes(f.get("reason_codes"))}
    return bool(codes & SECURITY_REASON_CODES)


def _savings_usd(f: dict) -> float:
    try:
        return float(f.get("estimated_monthly_savings_usd", 0) or 0)
    except Exception:
        return 0.0


def _build_security_scorecard(findings: list[dict]) -> list[dict]:
    checks = [
        {
            "key": "iam_root_posture",
            "label": "Root MFA + no root keys",
            "fail_types": ["iam_root_mfa_disabled", "iam_root_access_keys_present"],
        },
        {
            "key": "iam_password_policy",
            "label": "Password policy baseline",
            "fail_types": ["iam_password_policy_missing", "iam_password_policy_weak"],
        },
        {
            "key": "iam_access_key_hygiene",
            "label": "Access key hygiene",
            "fail_types": ["iam_access_key_hygiene"],
        },
        {
            "key": "s3_account_public_access_block",
            "label": "S3 account public access block",
            "fail_types": ["s3_account_public_access_block_disabled"],
        },
        {
            "key": "s3_bucket_default_encryption",
            "label": "S3 default encryption",
            "fail_types": ["s3_bucket_default_encryption_missing"],
        },
        {
            "key": "ebs_encryption_by_default",
            "label": "EBS encryption by default",
            "fail_types": ["ebs_encryption_by_default_disabled"],
        },
        {
            "key": "cloudtrail_basics",
            "label": "CloudTrail baseline",
            "fail_types": ["cloudtrail_not_configured"],
        },
        {
            "key": "aws_config_basics",
            "label": "AWS Config recorder",
            "fail_types": ["aws_config_not_enabled", "aws_config_not_recording"],
        },
        {
            "key": "guardduty_enabled",
            "label": "GuardDuty enabled",
            "fail_types": ["guardduty_not_enabled"],
        },
        {
            "key": "securityhub_enabled",
            "label": "Security Hub enabled",
            "fail_types": ["securityhub_not_enabled"],
        },
        {
            "key": "cloudwatch_alarms_presence",
            "label": "CloudWatch alarms present",
            "fail_types": ["cloudwatch_alarms_missing"],
        },
    ]

    types = {f.get("type") for f in findings}
    skipped_checks = set()
    for f in findings:
        if (f.get("type") or "") == "posture_check_skipped":
            evidence = f.get("evidence") if isinstance(f.get("evidence"), dict) else {}
            posture = (
                evidence.get("posture")
                if isinstance(evidence.get("posture"), dict)
                else {}
            )
            check = posture.get("check")
            if check:
                skipped_checks.add(check)

    scorecard = []
    for check in checks:
        status = "pass"
        if any(t in types for t in check["fail_types"]):
            status = "fail"
        elif check["key"] in skipped_checks:
            status = "unknown"
        scorecard.append({"label": check["label"], "status": status})
    return scorecard


def _write_summary_md(
    *,
    output_dir: str,
    account_id: str | None,
    timestamp_iso: str,
    regions: list[str],
    findings: list[dict],
    top_savings: list[dict],
    estimated_monthly_savings: float,
    estimated_monthly_cost: float,
    provenance: dict | None,
    spend_movers: dict | None,
):
    os.makedirs(output_dir, exist_ok=True)

    security_findings = [
        f
        for f in findings
        if _is_security_posture_finding(f) and not _is_skipped_posture(f)
    ]
    scorecard = _build_security_scorecard(findings)

    top_security = []
    for f in sorted(
        security_findings,
        key=lambda x: (_severity_rank(x.get("severity")), x.get("type") or ""),
    ):
        sev = (f.get("severity") or "").lower()
        if sev in {"critical", "high", "medium"}:
            top_security.append(f)
        if len(top_security) >= 5:
            break

    skipped = [f for f in findings if _is_skipped_posture(f)]

    def prov_flag(key: str) -> str:
        if not isinstance(provenance, dict):
            return "unknown"
        v = provenance.get(key)
        if v is True:
            return "on"
        if v is False:
            return "off"
        return "unknown"

    lines: list[str] = []
    lines.append("# StackSage Summary")
    lines.append("")
    lines.append(f"- Account: {account_id or 'unknown'}")
    lines.append(f"- Timestamp: {timestamp_iso}")
    lines.append(f"- Regions scanned: {', '.join(regions) if regions else 'unknown'}")
    lines.append(
        "- Opt-ins: "
        + ", ".join(
            [
                f"CloudWatch={prov_flag('cloudwatch_enabled')}",
                f"CostExplorer={prov_flag('cost_explorer_enabled')}",
                (
                    f"PricingAPI={prov_flag('pricing_mode')}"
                    if isinstance(provenance, dict) and "pricing_mode" in provenance
                    else "PricingAPI=unknown"
                ),
            ]
        )
    )
    lines.append("")
    lines.append(f"- Estimated monthly cost: {_format_money(estimated_monthly_cost)}")
    lines.append(
        f"- Estimated monthly savings opportunities: {_format_money(estimated_monthly_savings)}"
    )

    lines.append("")
    lines.append("## Security posture scorecard")
    if scorecard:
        lines.append("| Check | Status |")
        lines.append("| --- | --- |")
        status_map = {
            "pass": "Pass",
            "fail": "Fail",
            "unknown": "Unknown",
        }
        for row in scorecard:
            label = row.get("label") or "Unknown"
            status = status_map.get(row.get("status"), row.get("status") or "Unknown")
            lines.append(f"| {label} | {status} |")
    else:
        lines.append("- No scorecard data available.")

    lines.append("")
    lines.append("## Security posture findings")
    if top_security:
        for f in top_security:
            sev = (f.get("severity") or "unknown").upper()
            ftype = f.get("type") or f.get("resource_type") or "unknown"
            region = f.get("region")
            resource = (
                f.get("resource") or f.get("resource_id") or f.get("resource_arn")
            )
            extra = ""
            if region:
                extra += f" ({region})"
            if resource:
                extra += f" [{resource}]"
            summary = f.get("summary") or f.get("title") or f.get("description") or ""
            lines.append(f"- {sev}: {ftype}{extra} — {summary}".rstrip(" — "))
    else:
        if security_findings:
            lines.append(
                "- No critical/high/medium security posture findings detected."
            )
        else:
            lines.append(
                "- No security posture findings detected (or insufficient permissions to assess)."
            )

    lines.append("")
    lines.append("## Top cost opportunities")
    if top_savings:
        for f in top_savings:
            ftype = f.get("type") or f.get("resource_type") or "unknown"
            resource = (
                f.get("resource") or f.get("resource_id") or f.get("resource_arn")
            )
            savings = _format_money(f.get("estimated_monthly_savings_usd"))
            region = f.get("region")
            extra = ""
            if region:
                extra += f" ({region})"
            if resource:
                extra += f" [{resource}]"
            summary = f.get("summary") or f.get("title") or f.get("description") or ""
            lines.append(f"- {savings}/mo: {ftype}{extra} — {summary}".rstrip(" — "))
    else:
        lines.append("- No savings opportunities detected.")

    if isinstance(spend_movers, dict):
        total_delta = spend_movers.get("total_delta")
        total_pct = spend_movers.get("total_pct_change")
        min_delta = spend_movers.get("min_delta_usd")
        lines.append("")
        lines.append("## Spend movers (period-over-period)")
        if total_delta is not None:
            delta_line = f"- Total change: {_format_money(total_delta)}"
            if total_pct is not None:
                delta_line = f"{delta_line} ({total_pct}%)"
            lines.append(delta_line)
        if min_delta is not None:
            lines.append(f"- Threshold: {_format_money(min_delta)}")

        def _append_top(label: str, items: list[dict]):
            if not items:
                return
            lines.append(f"- {label}:")
            for row in items[:3]:
                key = row.get("key") or "unknown"
                delta = row.get("delta")
                pct = row.get("pct_change")
                suffix = f" ({pct}%)" if pct is not None else ""
                lines.append(f"  - {key}: {_format_money(delta)}{suffix}")

        _append_top(
            "Top service increases",
            spend_movers.get("top_increases_by_service") or [],
        )
        _append_top(
            "Top service decreases",
            spend_movers.get("top_decreases_by_service") or [],
        )
        _append_top(
            "Top region increases",
            spend_movers.get("top_increases_by_region") or [],
        )
        _append_top(
            "Top region decreases",
            spend_movers.get("top_decreases_by_region") or [],
        )

    if skipped:
        lines.append("")
        lines.append("## Checks skipped (missing permissions or service disabled)")
        for f in skipped[:10]:
            evidence = f.get("evidence") if isinstance(f.get("evidence"), dict) else {}
            check = evidence.get("check") or evidence.get("check_name") or "unknown"
            service = evidence.get("service") or "unknown"
            reason = evidence.get("reason") or f.get("summary") or ""
            lines.append(f"- {service}: {check} — {reason}".rstrip(" — "))
        if len(skipped) > 10:
            lines.append(f"- (+{len(skipped) - 10} more)")

    lines.append("")
    lines.append("## Next actions")
    lines.append(
        "- Review any CRITICAL/HIGH findings first and confirm exposure changes with your security owner."
    )
    lines.append(
        "- Apply the top cost savings candidates, then re-run the audit to validate changes."
    )
    lines.append(
        "- If you see many ‘Skipped’ checks, expand the read-only IAM policy (see docs/privacy-access.md)."
    )
    lines.append("")

    summary_path = os.path.join(output_dir, "summary.md")
    with open(summary_path, "w") as f:
        f.write("\n".join(lines))


def _csv_safe(value):
    if isinstance(value, list):
        # For common simple lists like reason_codes, keep it readable.
        if all(isinstance(x, (str, int, float, bool)) or x is None for x in value):
            return ";".join("" if v is None else str(v) for v in value)
        return json.dumps(value, sort_keys=True)
    if isinstance(value, dict):
        return json.dumps(value, sort_keys=True)
    return value


def _write_remediation_plan(
    *,
    output_dir: str,
    account_id: str | None,
    timestamp_iso: str,
    findings: list[dict],
):
    os.makedirs(output_dir, exist_ok=True)

    def _risk_and_rollback(f: dict) -> tuple[str, bool, str]:
        ftype = (f.get("type") or "").lower()
        action = (f.get("recommended_action") or "").lower()

        if ftype == "unused_eip" or action == "release-eip":
            return (
                "medium",
                True,
                "Allocate a new Elastic IP and update any DNS / allowlists if you later need a static IP.",
            )

        if ftype in {"old_snapshot", "snapshot_consolidation"} or action.endswith(
            "delete"
        ):
            return (
                "high",
                True,
                "Snapshot deletion is irreversible. Before deleting, confirm retention/compliance needs and consider creating a fresh snapshot or copying to a backup vault/account.",
            )

        if ftype == "gp2_to_gp3_migration" or action in {"migrate-to-gp3"}:
            return (
                "medium",
                True,
                "You can usually revert by modifying the volume type back to gp2 (note: performance characteristics may differ).",
            )

        return ("low", True, "Verify the change and document rollback before applying.")

    def _category_owner(f: dict) -> tuple[str, str]:
        ftype = (f.get("type") or "").lower()
        if ftype in {
            "unused_eip",
            "gp2_to_gp3_migration",
            "old_snapshot",
            "snapshot_consolidation",
            "unattached_ebs",
        }:
            return ("cost", "finops/devops")
        return ("other", "devops")

    def _ensure_verification_steps(*, f: dict, commands: list[str]) -> list[str]:
        """Ensure destructive actions are preceded by explicit verification commands.

        This is intentionally conservative: we prefer adding a harmless read-only
        describe step even if the detector already included one.
        """

        ftype = (f.get("type") or "").lower()
        rid = f.get("id") or "unknown"
        region = f.get("region")

        def has_substring(sub: str) -> bool:
            return any(sub in c for c in commands)

        augmented = list(commands)

        # Release EIP is destructive (may break allowlists/DNS); add a describe step.
        if ftype == "unused_eip" and (has_substring("aws ec2 release-address") or True):
            if not has_substring("aws ec2 describe-addresses"):
                prefix = "# Verify EIP is truly unused (no association):"
                cmd = f"aws ec2 describe-addresses --public-ips {rid}"
                if region:
                    cmd = cmd + f" --region {region}"
                augmented = [prefix, cmd, "# If unassociated, release:"] + augmented

        # Snapshot deletion is irreversible; add describe-snapshots if missing.
        if has_substring("aws ec2 delete-snapshot"):
            if not has_substring("aws ec2 describe-snapshots"):
                prefix = "# Verify snapshot is not needed (retention/compliance):"
                cmd = f"aws ec2 describe-snapshots --snapshot-ids {rid}"
                if region:
                    cmd = cmd + f" --region {region}"
                augmented = [
                    prefix,
                    cmd,
                    "# Delete only after verification:",
                ] + augmented

        # Volume deletion is destructive; add describe-volumes if missing.
        if has_substring("aws ec2 delete-volume"):
            if not has_substring("aws ec2 describe-volumes"):
                prefix = "# Verify volume is safe to delete (attachments/usage):"
                cmd = f"aws ec2 describe-volumes --volume-ids {rid}"
                if region:
                    cmd = cmd + f" --region {region}"
                augmented = [prefix, cmd] + augmented

        return augmented

    def _title_for(f: dict) -> str:
        ftype = (f.get("type") or "").lower()
        rid = f.get("id") or "unknown"
        if ftype == "unused_eip":
            return f"Release unused Elastic IP ({rid})"
        if ftype == "old_snapshot":
            return f"Delete old snapshot after verification ({rid})"
        if ftype == "snapshot_consolidation":
            return f"Consolidate snapshot retention for volume ({rid})"
        if ftype == "gp2_to_gp3_migration":
            return f"Migrate EBS volume gp2 → gp3 ({rid})"
        return (
            f"{(f.get('recommended_action') or ftype or 'remediate').strip()} ({rid})"
        )

    allow_types = {
        "unused_eip",
        "old_snapshot",
        "snapshot_consolidation",
        "gp2_to_gp3_migration",
    }

    actions: list[dict] = []
    seen: set[str] = set()
    for f in findings:
        cmds = f.get("remediation_commands")
        if not isinstance(cmds, list) or not cmds:
            continue

        ftype = (f.get("type") or "").lower()
        if ftype not in allow_types:
            continue

        rid = f.get("id") or "unknown"
        region = f.get("region")
        action_key = f"{ftype}:{region}:{rid}"
        if action_key in seen:
            continue
        seen.add(action_key)

        risk, requires_approval, rollback = _risk_and_rollback(f)
        category, owner = _category_owner(f)

        try:
            savings = float(f.get("estimated_monthly_savings_usd", 0) or 0)
        except Exception:
            savings = 0.0

        commands = [str(c) for c in cmds if c is not None]
        commands = _ensure_verification_steps(f=f, commands=commands)

        actions.append(
            {
                "action_id": action_key,
                "title": _title_for(f),
                "category": category,
                "owner": owner,
                "risk": risk,
                "requires_approval": requires_approval,
                "account_id": account_id,
                "region": region,
                "resource_type": f.get("resource_type") or None,
                "resource_id": rid,
                "finding_type": f.get("type"),
                "recommended_action": f.get("recommended_action"),
                "estimated_monthly_savings_usd": round(savings, 2),
                "commands": commands,
                "rollback": rollback,
                "notes": "Generated from a read-only audit. Verify impact and ownership before applying.",
            }
        )

    def _risk_rank(r: str) -> int:
        return {"high": 0, "medium": 1, "low": 2}.get((r or "").lower(), 9)

    actions.sort(
        key=lambda a: (
            _risk_rank(a.get("risk")),
            -float(a.get("estimated_monthly_savings_usd", 0) or 0),
            str(a.get("title") or ""),
        )
    )

    plan = {
        "version": 1,
        "generated_at": timestamp_iso,
        "account_id": account_id,
        "actions": actions,
        "totals": {
            "actions": len(actions),
            "estimated_monthly_savings_usd": round(
                sum(
                    float(a.get("estimated_monthly_savings_usd", 0) or 0)
                    for a in actions
                ),
                2,
            ),
        },
        "scope": {
            "included_finding_types": sorted(list(allow_types)),
        },
    }

    json_path = os.path.join(output_dir, "remediation_plan.json")
    with open(json_path, "w") as f:
        json.dump(plan, f, indent=2)

    md_lines: list[str] = []
    md_lines.append("# StackSage Remediation Plan (v1)")
    md_lines.append("")
    md_lines.append(f"- Account: {account_id or 'unknown'}")
    md_lines.append(f"- Generated: {timestamp_iso}")
    md_lines.append(
        f"- Estimated savings (actions in this plan): ${plan['totals']['estimated_monthly_savings_usd']:,.2f}/mo"
    )
    md_lines.append("")
    md_lines.append(
        "This plan is generated from a read-only audit. Treat it as a checklist: verify ownership, blast radius, and compliance/retention before applying."
    )

    if not actions:
        md_lines.append("")
        md_lines.append("No remediation actions were generated for this run.")
    else:
        # Group for readability: category -> risk
        categories = ["cost", "other"]
        for cat in categories:
            cat_actions = [
                a for a in actions if (a.get("category") or "").lower() == cat
            ]
            if not cat_actions:
                continue
            md_lines.append("")
            md_lines.append(
                "## Cost optimization actions" if cat == "cost" else "## Other actions"
            )

            for risk in ["high", "medium", "low"]:
                bucket = [
                    a for a in cat_actions if (a.get("risk") or "").lower() == risk
                ]
                if not bucket:
                    continue
                md_lines.append("")
                md_lines.append(f"### {risk.capitalize()} risk")
                for a in bucket:
                    md_lines.append("")
                    md_lines.append(f"#### {a.get('title')}")
                    md_lines.append(f"- Owner: {a.get('owner') or 'devops'}")
                    md_lines.append(
                        f"- Estimated savings: ${float(a.get('estimated_monthly_savings_usd', 0) or 0):,.2f}/mo"
                    )
                    md_lines.append(
                        f"- Resource: {a.get('resource_type') or 'unknown'} {a.get('resource_id') or 'unknown'}"
                    )
                    if a.get("region"):
                        md_lines.append(f"- Region: {a.get('region')}")
                    md_lines.append(
                        f"- Requires approval: {'yes' if a.get('requires_approval') else 'no'}"
                    )
                    md_lines.append("")
                    md_lines.append("Commands:")
                    md_lines.append("```bash")
                    for c in a.get("commands") or []:
                        md_lines.append(str(c))
                    md_lines.append("```")
                    md_lines.append("")
                    md_lines.append("Rollback:")
                    md_lines.append(f"- {a.get('rollback')}")

    md_path = os.path.join(output_dir, "remediation_plan.md")
    with open(md_path, "w") as f:
        f.write("\n".join(md_lines))


def generate_report(findings_path="reports/findings.json", output_dir="reports"):
    with open(findings_path) as f:
        data = json.load(f)
    findings = data.get("findings") or []
    estimated = float(data.get("estimated_monthly_savings") or 0.0)
    total_cost = float(data.get("estimated_monthly_cost") or 0.0)
    # metadata placeholders
    account_id = data.get("account_id")
    regions = sorted(list({f.get("region") for f in findings if f.get("region")}))
    # convert file mtime to ISO for readability
    ts_epoch = os.path.getmtime(findings_path)
    timestamp_iso = datetime.fromtimestamp(ts_epoch, tz=timezone.utc).strftime(
        "%Y-%m-%d %H:%M:%S UTC"
    )
    # Free-tier state — passed through from apply_free_tier_gate()
    free_tier = bool(data.get("free_tier") or False)
    free_tier_banner = data.get("free_tier_banner") or None
    free_tier_hidden_count = int(data.get("free_tier_hidden_count") or 0)
    free_tier_total_findings = int(
        data.get("free_tier_total_findings") or len(findings)
    )
    free_tier_hidden_savings_usd = float(
        data.get("free_tier_hidden_savings_usd") or 0.0
    )
    # aggregates
    by_type = {}
    for f in findings:
        t = f.get("resource_type") or f.get("type")
        by_type[t] = by_type.get(t, 0) + 1
    # top-5 savings: exclude zero or missing savings to avoid noise
    sorted_findings = sorted(
        findings,
        key=lambda x: _savings_usd(x),
        reverse=True,
    )
    top_savings = [f for f in sorted_findings if _savings_usd(f) > 0][:5]

    # top-5 cost drivers: findings with a known monthly cost regardless of savings
    top_cost_items = sorted(
        [f for f in findings if float(f.get("estimated_monthly_cost_usd") or 0) > 0],
        key=lambda x: float(x.get("estimated_monthly_cost_usd") or 0),
        reverse=True,
    )[:5]

    security_posture_findings = [
        f
        for f in findings
        if _is_security_posture_finding(f) and not _is_skipped_posture(f)
    ]
    financial_findings = [
        f
        for f in findings
        if (not _is_security_posture_finding(f)) and (not _is_skipped_posture(f))
    ]
    skipped_findings = [f for f in findings if _is_skipped_posture(f)]
    security_scorecard = _build_security_scorecard(findings)

    security_posture_findings_sorted = sorted(
        security_posture_findings,
        key=lambda x: (_severity_rank(x.get("severity")), x.get("type") or ""),
    )
    financial_findings_sorted = sorted(
        financial_findings,
        key=lambda x: (
            -_savings_usd(x),
            _severity_rank(x.get("severity")),
            x.get("type") or "",
        ),
    )
    # Summarizer should consider only positive-saving drivers
    data_for_ai = {
        **data,
        "findings": [f for f in findings if _savings_usd(f) > 0],
    }
    ai_summary = summarize_analysis(data_for_ai)
    executive_summary = ai_summary.get("text")
    # Load templates from the package templates directory
    templates_dir = os.path.normpath(
        os.path.join(os.path.dirname(__file__), "..", "templates")
    )
    env = Environment(loader=FileSystemLoader(templates_dir))
    tpl = env.get_template("report.html.jinja")
    # Historical costs (optional)
    hist = data.get("historical_costs")
    hist_total = hist.get("total_unblended_cost") if isinstance(hist, dict) else None
    hist_window = hist.get("time_window") if isinstance(hist, dict) else None
    hist_top_services = None
    hist_top_regions = None
    spend_movers = data.get("spend_movers") if isinstance(data, dict) else None

    if isinstance(hist, dict):
        breakdown = hist.get("breakdown_by_service_region") or {}
        if isinstance(breakdown, dict) and breakdown:
            service_totals = []
            region_totals = {}
            for service, regions_map in breakdown.items():
                if not isinstance(regions_map, dict):
                    continue
                svc_total = 0.0
                for region, amount in regions_map.items():
                    try:
                        v = float(amount or 0)
                    except Exception:
                        v = 0.0
                    svc_total += v
                    region_totals[region] = region_totals.get(region, 0.0) + v
                service_totals.append({"service": service, "cost": round(svc_total, 2)})

            service_totals.sort(key=lambda x: x["cost"], reverse=True)
            hist_top_services = service_totals[:8]

            regions_sorted = [
                {"region": r, "cost": round(c, 2)}
                for r, c in sorted(
                    region_totals.items(), key=lambda kv: kv[1], reverse=True
                )
            ]
            hist_top_regions = regions_sorted[:8]

    # If no explicit estimated_monthly_cost was persisted in the findings JSON,
    # derive it from Cost Explorer data (normalize CE window → 30-day month).
    if not total_cost and isinstance(hist, dict):
        raw_ce_total = float(hist.get("total_unblended_cost") or 0)
        ce_lookback = int(hist.get("lookback_days") or 30)
        if raw_ce_total:
            total_cost = round(raw_ce_total * 30.0 / ce_lookback, 2)

    # Basic distribution by resource_type
    distribution = {}
    for f in findings:
        t = f.get("resource_type") or f.get("type")
        distribution[t] = distribution.get(t, 0) + 1

    # Determine pricing mode for provenance badges
    import os as _os

    pricing_mode = (_os.environ.get("STACKSAGE_PRICING_MODE", "static")).lower()

    html_out = tpl.render(
        estimated_monthly_savings=estimated,
        estimated_monthly_cost=total_cost,
        executive_summary=executive_summary,
        findings=findings,
        pricing_version="2025-12",
        top_savings=top_savings,
        top_cost_items=top_cost_items,
        security_posture_findings=security_posture_findings_sorted,
        financial_findings=financial_findings_sorted,
        skipped_findings=skipped_findings,
        counts_by_type=by_type,
        distribution=distribution,
        account_id=account_id,
        regions=regions,
        timestamp=timestamp_iso,
        historical_total=hist_total,
        historical_window=hist_window,
        historical_top_services=hist_top_services,
        historical_top_regions=hist_top_regions,
        spend_movers=spend_movers,
        security_scorecard=security_scorecard,
        provenance=data.get("provenance"),
        pricing_mode=pricing_mode,
        free_tier=free_tier,
        free_tier_banner=free_tier_banner,
        free_tier_hidden_count=free_tier_hidden_count,
        free_tier_total_findings=free_tier_total_findings,
        free_tier_hidden_savings_usd=free_tier_hidden_savings_usd,
    )
    os.makedirs(output_dir, exist_ok=True)
    html_file = os.path.join(output_dir, "audit_report.html")
    with open(html_file, "w") as f:
        f.write(html_out)

    _write_summary_md(
        output_dir=output_dir,
        account_id=account_id,
        timestamp_iso=timestamp_iso,
        regions=regions,
        findings=findings,
        top_savings=top_savings,
        estimated_monthly_savings=estimated,
        estimated_monthly_cost=total_cost,
        provenance=data.get("provenance") if isinstance(data, dict) else None,
        spend_movers=spend_movers,
    )

    # Paid-only artifact: remediation plan.
    # Default to paid for backward compatibility with older findings.json.
    if (data.get("mode") or "paid").lower() == "paid":
        _write_remediation_plan(
            output_dir=output_dir,
            account_id=account_id,
            timestamp_iso=timestamp_iso,
            findings=findings,
        )
    # CSV export of findings
    csv_file = os.path.join(output_dir, "findings.csv")
    if findings:
        cleaned = [{k: _csv_safe(v) for k, v in f.items()} for f in findings]
        fieldnames = sorted({k for f in cleaned for k in f.keys()})
        with open(csv_file, "w", newline="") as cf:
            writer = csv.DictWriter(cf, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(cleaned)
        print("Findings CSV written to:", csv_file)
    print("HTML report written to:", html_file)
    return html_file
