"""Reusable notebook functions for runbooks PyPI (CxO-grade).

Usage in notebooks:
    from runbooks.notebooks.helpers import run_cli, validate_auth, assign_tier
"""

import json
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Any


def run_cli(cmd: list[str], export_json: Path | None = None, timeout: int = 120) -> dict[str, Any]:
    """Run a runbooks CLI command and return parsed JSON output.

    Args:
        cmd: CLI command as list (e.g., ["runbooks", "cert", "triage", ...])
        export_json: Path to JSON export file (if CLI writes one)
        timeout: Subprocess timeout in seconds

    Returns:
        Parsed JSON dict from export file, or empty structure on failure.
    """
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)

    if result.returncode == 0 and export_json and export_json.exists():
        with open(export_json) as f:
            return json.load(f)

    # Graceful failure — return empty structure, print actionable message
    if result.returncode != 0:
        err = result.stderr.strip().split("\n")[-1] if result.stderr else "Unknown error"
        print(f"CLI warning: {err}")
        print("Tip: Run 'aws sso login' on your host if credentials expired")

    return {"certificates": [], "summary": {"total_certificates": 0, "by_expiry_bucket": {}}, "scan_metadata": {}}


def risk_level(certs: list[dict]) -> str:
    """Determine risk level from certificate list."""
    in_use_expired = sum(1 for c in certs if c.get("expiry_bucket") == "EXPIRED" and c.get("in_use"))
    critical = sum(1 for c in certs if c.get("expiry_bucket") == "CRITICAL_7D")
    warning = sum(1 for c in certs if c.get("expiry_bucket") == "WARNING_30D")
    if in_use_expired:
        return "CRITICAL"
    if critical:
        return "HIGH"
    if warning:
        return "MEDIUM"
    return "HEALTHY"


def filter_expiring(certs: list[dict], days: int) -> list[dict]:
    """Filter and sort certificates expiring within N days (in-use only)."""
    expiring = [c for c in certs if 0 < (c.get("days_until_expiry") or 999) <= days and c.get("in_use")]
    expiring.sort(key=lambda c: c.get("days_until_expiry") or 999)
    return expiring


def filter_removable(certs: list[dict]) -> list[dict]:
    """Filter expired certificates that are not in use (safe to remove)."""
    return [c for c in certs if c.get("expiry_bucket") == "EXPIRED" and not c.get("in_use")]


def build_email(risk: str, total: int, accounts: int, expiring: list[dict], removable: list[dict], mode: str) -> str:
    """Build stakeholder email template from certificate data."""
    in_use_expired = sum(1 for c in expiring if (c.get("days_until_expiry") or 0) <= 0)
    warning = sum(1 for c in expiring if 0 < (c.get("days_until_expiry") or 999) <= 30)

    expiring_list = (
        "\n".join(
            f"  - {c['domain_name']} ({c.get('days_until_expiry', '?')}d, "
            f"{c.get('account_name') or c.get('account_id', '?')})"
            for c in expiring[:10]
        )
        or "  None"
    )

    removable_list = (
        "\n".join(
            f"  - {c['domain_name']} ({c.get('account_name') or c.get('account_id', '?')})" for c in removable[:10]
        )
        or "  None"
    )

    return f"""Subject: Weekly Certificate Status — {risk}: {total} certs ({datetime.now().strftime("%d %b %Y")})

Dear Leadership Team,

Certificate status report for your review.

Risk Level: {risk}
Total Certificates: {total} | Accounts Scanned: {accounts}

Certificates Expiring Soon (action required):
{expiring_list}

Expired Certificates — Safe to Remove:
{removable_list}

Recommended Actions:
  1. Renew {in_use_expired} expired in-use certificates immediately
  2. Remove {len(removable)} expired unused certificates
  3. Schedule renewal for {warning} certificates expiring within 30 days

Generated via: runbooks cert triage --mode {mode}
"""


# ---------------------------------------------------------------------------
# Generic helper functions — reusable across all cloudops notebooks
# ---------------------------------------------------------------------------


def validate_auth(profile: str) -> bool:
    """Check if AWS SSO session is valid for the given profile.

    Returns True if authenticated, False with actionable message if not.
    """
    result = subprocess.run(
        ["aws", "sts", "get-caller-identity", "--profile", profile],
        capture_output=True,
        text=True,
        timeout=10,
    )
    if result.returncode == 0:
        identity = json.loads(result.stdout)
        print(f"Authenticated: {identity.get('Arn', 'unknown')}")
        return True
    print(f"Authentication failed for profile '{profile}'")
    print("Run on your host: aws sso login --profile=" + profile)
    return False


def detect_workspace() -> Path:
    """Walk up from CWD to find project root (contains .git + CLAUDE.md)."""
    current = Path.cwd()
    for parent in [current] + list(current.parents):
        if (parent / ".git").exists() and (parent / "CLAUDE.md").exists():
            return parent
        if (parent / ".git").exists() and (parent / "Taskfile.yml").exists():
            return parent
    return current


DEFAULT_COST_TIERS = {
    "Critical": 5000.0,
    "High": 1000.0,
    "Medium": 100.0,
}


def assign_tier(cost: float, thresholds: dict[str, float] | None = None) -> str:
    """Assign cost tier: Critical / High / Medium / Low.

    Args:
        cost: Dollar amount to classify.
        thresholds: Optional override of tier boundaries.
                    Default: Critical > $5K, High > $1K, Medium > $100.
    """
    tiers = thresholds or DEFAULT_COST_TIERS
    if cost >= tiers.get("Critical", 5000.0):
        return "Critical"
    if cost >= tiers.get("High", 1000.0):
        return "High"
    if cost >= tiers.get("Medium", 100.0):
        return "Medium"
    return "Low"


def format_cost(amount: float, currency: str = "USD") -> str:
    """Format cost as locale-aware string with currency symbol.

    Examples: format_cost(1234.56) -> '$1,234.56'
              format_cost(1234.56, 'NZD') -> 'NZ$1,234.56'
    """
    symbols = {"USD": "$", "NZD": "NZ$", "AUD": "A$", "EUR": "€", "GBP": "£"}
    symbol = symbols.get(currency, currency + " ")
    return f"{symbol}{amount:,.2f}"


def build_cli_command(group: str, cmd: str, params: dict[str, Any]) -> list[str]:
    """Build a runbooks CLI command list from parameters dict.

    Args:
        group: CLI group (e.g., "finops", "cert", "inventory", "security")
        cmd: Subcommand (e.g., "dashboard", "triage", "collect", "assess")
        params: Dict of {flag_name: value}. Bool True adds --flag, False skips.
                None values are skipped. Str/int/float become --flag value.

    Returns:
        Command list ready for subprocess.run() or run_cli().
    """
    args = ["runbooks", group, cmd]
    for key, val in params.items():
        flag = f"--{key}" if not key.startswith("--") else key
        if val is None or val is False:
            continue
        if val is True:
            args.append(flag)
        else:
            args.extend([flag, str(val)])
    return args


def parse_dashboard_json(json_path: str | Path) -> dict[str, Any]:
    """Parse runbooks finops dashboard JSON export into analysis-ready structure.

    Args:
        json_path: Path to the JSON file produced by runbooks finops dashboard --export json.

    Returns:
        Dict with keys: "services" (list[dict]), "accounts" (list[dict]),
        "summary" (dict), "raw" (original parsed JSON).
        Returns empty structure if file missing or unparseable.
    """
    path = Path(json_path)
    empty = {"services": [], "accounts": [], "summary": {}, "raw": {}}

    if not path.exists():
        print(f"No data file at {path} — authenticate and re-run")
        return empty

    try:
        with open(path) as f:
            raw = json.load(f)
    except (json.JSONDecodeError, OSError) as e:
        print(f"Failed to parse {path}: {e}")
        return empty

    services = raw.get("services", raw.get("cost_by_service", []))
    accounts = raw.get("accounts", raw.get("cost_by_account", []))
    summary = raw.get("summary", raw.get("metadata", {}))
    cost_metric = raw.get("cost_metric", "UnblendedCost")

    return {"services": services, "accounts": accounts, "summary": summary, "cost_metric": cost_metric, "raw": raw}


def render_cost_summary(
    data: list[dict],
    title: str = "Cost Summary",
    cost_key: str = "cost",
    name_key: str = "name",
    cost_metric: str = "UnblendedCost",
) -> None:
    """Render a cost summary as a styled pandas DataFrame with tier coloring.

    Args:
        data: List of dicts with at least cost_key and name_key fields.
        title: Display title for the table.
        cost_key: Key containing the dollar amount.
        name_key: Key containing the item name.
        cost_metric: Cost metric used (shown in title for transparency).
    """
    title = f"{title} ({cost_metric})"
    try:
        import pandas as pd
    except ImportError:
        print(f"--- {title} ---")
        for item in data[:20]:
            print(f"  {item.get(name_key, '?')}: {format_cost(item.get(cost_key, 0))}")
        return

    if not data:
        print(f"{title}: No data available — authenticate and re-run")
        return

    df = pd.DataFrame(data[:20])
    if cost_key in df.columns:
        df["Tier"] = df[cost_key].apply(assign_tier)
        df["Cost"] = df[cost_key].apply(format_cost)

    display_df = df.rename(columns={name_key: "Name", cost_key: "Amount"}) if name_key in df.columns else df

    try:
        from IPython.display import display as ipy_display

        ipy_display(display_df.style.set_caption(title).hide(axis="index"))
    except ImportError:
        print(f"\n{title}")
        print(display_df.to_string(index=False))


def export_evidence(data: dict, notebook_name: str, outpath: str = "tmp/cloud-infrastructure/evidence") -> Path:
    """Export evidence JSON with timestamp and metadata.

    Args:
        data: Evidence data dict to export.
        notebook_name: Name of the source notebook (e.g., 'cert-weekly-report').
        outpath: Output directory (created if needed).

    Returns:
        Path to the written JSON file.
    """
    import hashlib

    out_dir = Path(outpath)
    out_dir.mkdir(parents=True, exist_ok=True)

    timestamp = datetime.now().strftime("%Y-%m-%d")
    filename = f"{notebook_name}-{timestamp}.json"
    filepath = out_dir / filename

    envelope = {
        "metadata": {
            "notebook": notebook_name,
            "timestamp": datetime.now().isoformat(),
            "generator": "runbooks.notebooks.helpers",
        },
        "data": data,
    }

    content = json.dumps(envelope, indent=2, default=str)
    filepath.write_text(content)

    sha256 = hashlib.sha256(content.encode()).hexdigest()
    print(f"Evidence exported: {filepath} (SHA256: {sha256[:16]}...)")
    return filepath
