"""StackSage CLI entrypoints: audit, scan, analyze, report.

Provides end-to-end orchestration and writes run provenance for transparency.
"""

import json
import logging
import os
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as pkg_version

import click

try:
    _VERSION = pkg_version("stacksage")
except PackageNotFoundError:
    _VERSION = "dev"

from stacksage.analyzer.analysis import analyze_scan
from stacksage.cli.logging_util import setup_json_logging
from stacksage.cli.report_gen import generate_report
from stacksage.costs import get_cost_summary, get_spend_movers
from stacksage.scanner.aws_scanner import (
    get_enabled_regions,
    get_session_from_role,
    scan_resources,
)


def _require_license():
    from stacksage.licensing import LicenseError, enforce_license

    try:
        return enforce_license()
    except LicenseError as e:
        raise click.ClickException(str(e))


@click.group()
@click.version_option(version=_VERSION, prog_name="stacksage")
@click.option(
    "--log-level", default="INFO", help="Logging level: DEBUG|INFO|WARNING|ERROR"
)
def cli(log_level):
    """StackSage — AWS cost & security audit tool.

    \b
    Two entry-points:
      stacksage scan   — self-serve, no license needed. Free tier: top 50 findings.
      stacksage audit  — licensed only. Full findings + remediation plan.

    Run any command with --help for details.
    """
    setup_json_logging(log_level)


@cli.command()
@click.option(
    "--profile",
    default="",
    help="AWS profile name from ~/.aws/credentials or SSO config (e.g. my-sso-profile). "
    "Overridden by --role-arn if both are provided.",
)
@click.option(
    "--role-arn",
    default="",
    help="AWS Role ARN to assume (optional). If empty uses local creds/profile.",
)
@click.option(
    "--external-id",
    default="",
    help="ExternalId to use when assuming the role (optional)",
)
@click.option("--out", default="reports", help="Output directory")
@click.option("--cw-days", default=14, help="CloudWatch lookback days for metrics")
@click.option(
    "--cw-max-queries",
    default=500,
    help="Max CloudWatch metric queries per run (budget)",
)
@click.option(
    "--regions",
    default="",
    help="Comma-separated AWS regions to scan (default: all enabled)",
)
@click.option(
    "--format",
    "fmt",
    default="html",
    type=click.Choice(["json", "html"]),
    help="Report output format",
)
@click.option(
    "--demo",
    is_flag=True,
    default=False,
    help="Run in demo mode with synthetic data (no AWS calls)",
)
@click.option(
    "--live",
    is_flag=True,
    default=False,
    help="Enable live mode: probe CloudWatch/Cost Explorer/Pricing API where permitted",
)
@click.option(
    "--use-cloudwatch",
    is_flag=True,
    default=False,
    help="Enable CloudWatch metrics for utilization (requires permissions)",
)
@click.option(
    "--use-cost-explorer",
    is_flag=True,
    default=False,
    help="Include historical cost summary from Cost Explorer (requires permissions)",
)
@click.option(
    "--live-pricing",
    is_flag=True,
    default=False,
    help="Use AWS Pricing API for EC2 rates",
)
@click.option(
    "--check-tagging",
    "check_tagging",
    is_flag=True,
    default=False,
    help="Check for untagged resources (opt-in to reduce noise)",
)
@click.option(
    "--log-level", default=None, help="Override logging level for this command"
)
@click.option(
    "--quiet", is_flag=True, default=False, help="Reduce console output (errors only)"
)
def audit(
    profile,
    role_arn,
    external_id,
    out,
    cw_days,
    cw_max_queries,
    regions,
    fmt,
    demo,
    live,
    use_cloudwatch,
    use_cost_explorer,
    live_pricing,
    check_tagging,
    log_level,
    quiet,
):
    """Run a licensed end-to-end audit (requires STACKSAGE_LICENSE).

    Requires a valid license key set in the STACKSAGE_LICENSE environment variable.
    Unlocks the full findings list, remediation plan, and all report sections.
    Designed for CI/CD pipelines and automated scheduled audits.

    \b
    For self-serve use without a license, use `stacksage scan` instead.
    To obtain a license: https://stacksageai.com/pricing
    """
    _require_license()
    os.makedirs(out, exist_ok=True)
    if log_level:
        from stacksage.cli.logging_util import setup_json_logging

        setup_json_logging(log_level)

    region_list = []
    account_id = None
    if demo:
        # Synthetic demo data
        data = {
            "ec2": [
                {
                    "InstanceId": "i-demo123",
                    "InstanceType": "t3.micro",
                    "State": "running",
                    "LaunchTime": "2025-01-01T00:00:00Z",
                    "Tags": [],
                    "VpcId": "vpc-demo",
                    "Placement": {"AvailabilityZone": "us-east-1a"},
                    "Region": "us-east-1",
                },
                {
                    "InstanceId": "i-demo456",
                    "InstanceType": "t3.small",
                    "State": "stopped",
                    "LaunchTime": "2025-01-01T00:00:00Z",
                    "Tags": [],
                    "VpcId": "vpc-demo",
                    "Placement": {"AvailabilityZone": "us-east-1b"},
                    "Region": "us-east-1",
                },
            ],
            "ebs": [
                {
                    "VolumeId": "vol-demo1",
                    "Size": 50,
                    "State": "available",
                    "Attachments": [],
                    "CreateTime": "2025-01-01T00:00:00Z",
                    "AvailabilityZone": "us-east-1a",
                    "Tags": [],
                    "Region": "us-east-1",
                }
            ],
            "s3": [
                {
                    "Name": "demo-bucket",
                    "CreationDate": "2025-01-01T00:00:00Z",
                    "Region": "us-east-1",
                }
            ],
            "rds": [
                {
                    "DBInstanceIdentifier": "demo-db",
                    "DBInstanceClass": "db.t3.micro",
                    "Engine": "mysql",
                    "DBInstanceStatus": "available",
                    "AvailabilityZone": "us-east-1a",
                    "VpcId": "vpc-demo",
                    "Region": "us-east-1",
                }
            ],
        }
        session = None
        logging.info("Running in demo mode; generating synthetic dataset.")
    else:
        # --role-arn takes precedence; fall back to --profile, then default creds
        if role_arn:
            session = get_session_from_role(role_arn, external_id=external_id or None)
        elif profile:
            try:
                import boto3

                session = boto3.Session(profile_name=profile)
            except Exception as e:
                raise click.ClickException(
                    f"Could not load AWS profile '{profile}': {e}\n"
                    "Check your ~/.aws/credentials or SSO config."
                )
        else:
            session = get_session_from_role("", external_id=external_id or None)
        try:
            sts = session.client("sts")
            account_id = sts.get_caller_identity().get("Account")
        except Exception:
            account_id = None

        # Load stacksage.yml from CWD (if present)
        from stacksage.config import load_config

        cfg = load_config()
        # CLI flags take precedence; fall back to config file values
        if not regions:
            yml_regions = cfg.config.get("regions") or []
            region_list = yml_regions if yml_regions else get_enabled_regions(session)
        else:
            region_list = [r.strip() for r in regions.split(",") if r.strip()]
        if not check_tagging:
            check_tagging = cfg.config.get("detectors", {}).get(
                "check_tagging_compliance", False
            )
        data = scan_resources(
            session, regions=region_list, include_tags=bool(check_tagging)
        )
        logging.info(
            json.dumps(
                {
                    "event": "scan_complete",
                    "regions": region_list,
                    "counts": {k: len(v) for k, v in data.items()},
                }
            )
        )

    # Consent prompts for sensitive-but-read-only services
    if (use_cost_explorer or live) and not demo:
        click.echo(
            "Consent: You enabled Cost Explorer usage (service/region totals). Proceeding."
        )
    if live_pricing and not demo:
        click.echo(
            "Consent: You enabled AWS Pricing API usage (public metadata). Proceeding."
        )

    # Analyze (write partial outputs even on error)
    if live_pricing:
        # Tell pricing module to use API if available
        os.environ["STACKSAGE_PRICING_MODE"] = "api"
    # Per-account subfolder to keep reports separated
    subdir = out
    if account_id:
        # Use role arn or account id subfolder
        safe_id = account_id
        subdir = os.path.join(out, safe_id)
    os.makedirs(subdir, exist_ok=True)
    raw_path = os.path.join(subdir, "scan_raw.json")
    findings_path = os.path.join(subdir, "findings.json")
    try:
        analysis = analyze_scan(
            data,
            session=session,
            options={
                "live": bool(
                    live or use_cloudwatch or use_cost_explorer or live_pricing
                ),
                "use_cloudwatch": bool(use_cloudwatch or live),
                "use_cost_explorer": bool(use_cost_explorer or live),
                "check_tagging_compliance": bool(check_tagging),
                "metrics_lookback_days": int(cw_days),
                "cw_max_queries": int(cw_max_queries),
                "account_id": account_id,  # Pass account_id to analyzer
                "regions": region_list,
            },
        )
        logging.info(
            json.dumps(
                {
                    "event": "analyze_complete",
                    "findings": len(analysis.get("findings", [])),
                    "estimated_savings": analysis.get("estimated_monthly_savings"),
                }
            )
        )
    finally:
        # Always persist scan_raw.json
        try:
            with open(raw_path, "w") as f:
                json.dump(data, f, indent=2, default=str)
        except Exception as e:
            logging.error(
                json.dumps(
                    {"event": "write_error", "file": "scan_raw.json", "error": str(e)}
                )
            )

    # Optional: enrich with historical cost summary if Cost Explorer is allowed
    if use_cost_explorer or live:
        try:
            analysis["historical_costs"] = get_cost_summary(
                session=session,
                last_n_days=30,
                granularity="MONTHLY",
                filter_by_regions=region_list,
            )
            analysis["spend_movers"] = get_spend_movers(
                session=session,
                lookback_days=7,
                compare_days=7,
                granularity="DAILY",
                filter_by_regions=region_list,
            )
        except Exception as e:
            # Continue silently if CE not permitted
            analysis["historical_costs_error"] = str(e)
            analysis["spend_movers_error"] = str(e)
            logging.warning(
                json.dumps({"event": "cost_explorer_error", "error": str(e)})
            )

    # Persist intermediate and final outputs
    # Persist findings (partial output on failure)
    try:
        # Explicit mode marker for downstream tooling (trial vs paid).
        # Trial has its own entrypoint; this CLI is the paid distribution.
        analysis["mode"] = "paid"
        with open(findings_path, "w") as f:
            json.dump(analysis, f, indent=2)
    except Exception as e:
        logging.error(
            json.dumps(
                {"event": "write_error", "file": "findings.json", "error": str(e)}
            )
        )

    # Render report or print JSON
    if fmt == "html":
        # Persist run provenance for transparency
        from datetime import UTC, datetime

        # Include CW provenance counters if available
        prov = analysis.get("provenance", {})
        run_prov = {
            "mode": "demo" if demo else "live" if live else "heuristic",
            "cw_days": int(cw_days),
            "cw_max_queries": int(cw_max_queries),
            "pricing_mode": os.environ.get("STACKSAGE_PRICING_MODE", "static"),
            "cost_explorer_used": bool(use_cost_explorer or live),
            "account_id": account_id,
            "role_arn": role_arn or None,
            "cloudwatch_queries_attempted": prov.get("cloudwatch_queries_attempted"),
            "cloudwatch_queries_used": prov.get("cloudwatch_queries_used"),
            "cloudwatch_queries_remaining": prov.get("cloudwatch_queries_remaining"),
            "timestamp": datetime.now(UTC).isoformat(),
        }
        try:
            with open(os.path.join(subdir, "run_provenance.json"), "w") as pf:
                json.dump(run_prov, pf, indent=2)
        except Exception:
            pass
        try:
            generate_report(findings_path=findings_path, output_dir=subdir)
            logging.info(
                json.dumps(
                    {
                        "event": "report_generated",
                        "path": os.path.join(subdir, "audit_report.html"),
                    }
                )
            )
            if not quiet:
                click.echo(f"HTML report generated in {subdir}")
        except Exception as e:
            logging.error(json.dumps({"event": "report_error", "error": str(e)}))
            if not quiet:
                click.echo("Report generation failed; partial outputs written.")
    else:
        if not quiet:
            click.echo(json.dumps(analysis))
    if not quiet:
        msg_dir = subdir if account_id else out
        click.echo(f"Audit complete. Artifacts saved to {msg_dir}")


@cli.command()
@click.option(
    "--profile",
    default="",
    help="AWS profile name from ~/.aws/credentials (e.g. my-company). "
    "Uses default credentials if not set.",
)
@click.option(
    "--role-arn",
    default="",
    help="AWS Role ARN to assume (optional). Takes precedence over --profile.",
)
@click.option(
    "--external-id",
    default="",
    help="ExternalId to use when assuming the role (optional).",
)
@click.option("--out", default="reports", help="Output directory for report artifacts.")
@click.option("--cw-days", default=14, help="CloudWatch lookback days for metrics.")
@click.option(
    "--cw-max-queries",
    default=500,
    help="Max CloudWatch metric queries per run (cost budget).",
)
@click.option(
    "--regions",
    default="",
    help="Comma-separated AWS regions to scan (default: all enabled regions).",
)
@click.option(
    "--use-cloudwatch",
    is_flag=True,
    default=False,
    help="Enable CloudWatch metrics for utilisation-based findings (recommended).",
)
@click.option(
    "--use-cost-explorer",
    is_flag=True,
    default=False,
    help="Include historical cost summary from Cost Explorer.",
)
@click.option(
    "--check-tagging",
    is_flag=True,
    default=False,
    help="Flag resources missing required tags (opt-in).",
)
@click.option(
    "--no-browser",
    is_flag=True,
    default=False,
    help="Do not open the HTML report in a browser after the scan.",
)
@click.option(
    "--demo",
    is_flag=True,
    default=False,
    help="Run with synthetic data — no AWS calls, no credentials needed.",
)
@click.option(
    "--log-level", default=None, help="Override logging level for this command."
)
@click.option("--quiet", is_flag=True, default=False, help="Suppress progress output.")
def scan(
    profile,
    role_arn,
    external_id,
    out,
    cw_days,
    cw_max_queries,
    regions,
    use_cloudwatch,
    use_cost_explorer,
    check_tagging,
    no_browser,
    demo,
    log_level,
    quiet,
):
    """Scan your AWS account — no license needed.

    Runs 100% on your machine. All AWS calls are read-only. No data leaves your
    environment.

    \b
    Free tier (no license):
      - Full infrastructure scan across all supported services.
      - HTML report shows top 50 findings sorted by estimated monthly savings.
      - Security & posture checks always shown in full.
      - Remediation plan not included.

    \b
    With a license (STACKSAGE_LICENSE env var):
      - All findings visible, no cap.
      - Remediation plan included in report.
      - Unlocks all report sections.

    \b
    Quick start (local credentials or SSO profile):
      stacksage scan
      stacksage scan --profile my-sso-profile

    \b
    With an assumed IAM role:
      stacksage scan --role-arn arn:aws:iam::123456789012:role/StackSageReadOnly

    \b
    Recommended flags for a thorough scan:
      stacksage scan --use-cloudwatch --use-cost-explorer --check-tagging

    \b
    To get a license: https://stacksageai.com/pricing
    """
    import webbrowser

    from stacksage.analyzer.analysis import analyze_scan
    from stacksage.cli.free_tier import apply_free_tier_gate, is_licensed
    from stacksage.cli.telemetry import check_and_prompt_consent, send_anonymous_ping

    if log_level:
        from stacksage.cli.logging_util import setup_json_logging

        setup_json_logging(log_level)

    os.makedirs(out, exist_ok=True)

    # ── Telemetry consent (first run only, non-blocking) ──────────────────────
    if not demo:
        try:
            consented = check_and_prompt_consent()
        except Exception:
            consented = False
    else:
        consented = False

    # ── Session setup ─────────────────────────────────────────────────────────
    account_id = None
    region_list = []

    if demo:
        session = None
        region_list = ["us-east-1"]
        account_id = "000000000000"
        data = {
            "ec2": [
                {
                    "InstanceId": "i-demo123",
                    "InstanceType": "t3.micro",
                    "State": "running",
                    "LaunchTime": "2025-01-01T00:00:00Z",
                    "Tags": [],
                    "VpcId": "vpc-demo",
                    "Placement": {"AvailabilityZone": "us-east-1a"},
                    "Region": "us-east-1",
                },
                {
                    "InstanceId": "i-demo456",
                    "InstanceType": "t3.small",
                    "State": "stopped",
                    "LaunchTime": "2025-01-01T00:00:00Z",
                    "Tags": [],
                    "VpcId": "vpc-demo",
                    "Placement": {"AvailabilityZone": "us-east-1b"},
                    "Region": "us-east-1",
                },
            ],
            "ebs": [
                {
                    "VolumeId": "vol-demo1",
                    "Size": 50,
                    "State": "available",
                    "Attachments": [],
                    "CreateTime": "2025-01-01T00:00:00Z",
                    "AvailabilityZone": "us-east-1a",
                    "Tags": [],
                    "Region": "us-east-1",
                }
            ],
            "s3": [
                {
                    "Name": "demo-bucket",
                    "CreationDate": "2025-01-01T00:00:00Z",
                    "Region": "us-east-1",
                }
            ],
            "rds": [
                {
                    "DBInstanceIdentifier": "demo-db",
                    "DBInstanceClass": "db.t3.micro",
                    "Engine": "mysql",
                    "DBInstanceStatus": "available",
                    "AvailabilityZone": "us-east-1a",
                    "VpcId": "vpc-demo",
                    "Region": "us-east-1",
                }
            ],
        }
        if not quiet:
            click.echo("Running in demo mode — no AWS calls made.")
    else:
        import boto3

        # Profile takes precedence over env creds; role-arn overrides profile.
        if role_arn:
            session = get_session_from_role(role_arn, external_id=external_id or None)
        elif profile:
            try:
                session = boto3.Session(profile_name=profile)
            except Exception as e:
                raise click.ClickException(
                    f"Could not load AWS profile '{profile}': {e}\n"
                    "Check your ~/.aws/credentials or ~/.aws/config file."
                )
        else:
            session = get_session_from_role("")

        try:
            sts = session.client("sts")
            account_id = sts.get_caller_identity().get("Account")
        except Exception:
            account_id = None

        # Load stacksage.yml from CWD (if present)
        from stacksage.config import load_config

        cfg = load_config()
        # CLI flags take precedence; fall back to config file values
        if not regions:
            yml_regions = cfg.config.get("regions") or []
            region_list = yml_regions if yml_regions else get_enabled_regions(session)
        else:
            region_list = [r.strip() for r in regions.split(",") if r.strip()]
        if not check_tagging:
            check_tagging = cfg.config.get("detectors", {}).get(
                "check_tagging_compliance", False
            )

        if not quiet:
            click.echo(
                f"Scanning {len(region_list)} region(s)"
                + (f" for account {account_id}" if account_id else "")
                + "…"
            )
        data = scan_resources(
            session, regions=region_list, include_tags=bool(check_tagging)
        )

    # ── Output directory (per-account subfolder) ──────────────────────────────
    subdir = os.path.join(out, account_id) if account_id else out
    os.makedirs(subdir, exist_ok=True)

    raw_path = os.path.join(subdir, "scan_raw.json")
    findings_path = os.path.join(subdir, "findings.json")

    # Always persist raw scan (even on analysis error)
    try:
        with open(raw_path, "w") as f:
            json.dump(data, f, indent=2, default=str)
    except Exception as e:
        logging.warning("Could not write scan_raw.json: %s", e)

    # ── Analyse ───────────────────────────────────────────────────────────────
    if not quiet:
        click.echo("Analysing findings…")

    analysis = analyze_scan(
        data,
        session=session,
        options={
            "live": bool(use_cloudwatch or use_cost_explorer),
            "use_cloudwatch": bool(use_cloudwatch),
            "use_cost_explorer": bool(use_cost_explorer),
            "check_tagging_compliance": bool(check_tagging),
            "metrics_lookback_days": int(cw_days),
            "cw_max_queries": int(cw_max_queries),
            "account_id": account_id,
            "regions": region_list,
        },
    )

    # Optional Cost Explorer enrichment
    if use_cost_explorer and not demo:
        try:
            from stacksage.costs import get_cost_summary, get_spend_movers

            analysis["historical_costs"] = get_cost_summary(
                session=session, last_n_days=30, granularity="MONTHLY"
            )
            analysis["spend_movers"] = get_spend_movers(
                session=session, lookback_days=7, compare_days=7, granularity="DAILY"
            )
        except Exception as e:
            analysis["historical_costs_error"] = str(e)
            logging.warning("Cost Explorer unavailable: %s", e)

    # ── Free-tier gate ────────────────────────────────────────────────────────
    analysis = apply_free_tier_gate(analysis)

    total_findings = analysis.get(
        "free_tier_total_findings", len(analysis.get("findings", []))
    )
    banner = analysis.get("free_tier_banner")

    # ── Persist findings ──────────────────────────────────────────────────────
    try:
        with open(findings_path, "w") as f:
            json.dump(analysis, f, indent=2, default=str)
    except Exception as e:
        logging.error("Could not write findings.json: %s", e)

    # ── Generate HTML report ──────────────────────────────────────────────────
    report_path = os.path.join(subdir, "audit_report.html")
    try:
        generate_report(findings_path=findings_path, output_dir=subdir)
    except Exception as e:
        logging.error("Report generation failed: %s", e)
        if not quiet:
            click.echo(f"Report generation failed: {e}")

    # ── Console summary ───────────────────────────────────────────────────────
    if not quiet:
        visible = len(analysis.get("findings", []))
        savings = analysis.get("estimated_monthly_savings") or 0
        click.echo(
            f"\n✅  Scan complete — {visible} findings, ~${savings:,.0f}/mo estimated savings"
        )
        if banner:
            click.echo(f"\n⚠️   {banner}")
        if not is_licensed():
            click.echo(
                "\n💡  Set STACKSAGE_LICENSE to unlock all findings + remediation plan."
            )
        click.echo(f"\n📄  Report: {report_path}")

    # ── Open report in browser ────────────────────────────────────────────────
    if not no_browser and os.path.exists(report_path):
        try:
            webbrowser.open(f"file://{os.path.abspath(report_path)}")
        except Exception:
            pass  # Non-fatal — user can open manually

    # ── Anonymous telemetry ping ──────────────────────────────────────────────
    if consented and not demo:
        send_anonymous_ping(
            findings_count=total_findings,
            region_count=len(region_list),
        )


@cli.command()
@click.option(
    "--in-file", default="reports/scan_raw.json", help="Path to raw scan JSON"
)
@click.option(
    "--out", default="reports/findings.json", help="Path to write findings JSON"
)
@click.option(
    "--demo",
    is_flag=True,
    default=False,
    help="Analyze demo data instead of reading file",
)
@click.option(
    "--log-level", default=None, help="Override logging level for this command"
)
def analyze(in_file, out, demo, log_level):
    _require_license()
    if not os.path.exists(in_file):
        if not demo:
            raise click.UsageError(f"Input file not found: {in_file}")
    if demo:
        raw = {
            "ec2": [
                {
                    "InstanceId": "i-demo123",
                    "InstanceType": "t3.micro",
                    "State": "running",
                    "Placement": {"AvailabilityZone": "us-east-1a"},
                    "Region": "us-east-1",
                }
            ],
            "ebs": [
                {
                    "VolumeId": "vol-demo1",
                    "Size": 50,
                    "State": "available",
                    "Attachments": [],
                    "AvailabilityZone": "us-east-1a",
                    "Region": "us-east-1",
                }
            ],
            "s3": [{"Name": "demo-bucket", "Region": "us-east-1"}],
            "rds": [
                {
                    "DBInstanceIdentifier": "demo-db",
                    "DBInstanceClass": "db.t3.micro",
                    "Engine": "mysql",
                    "DBInstanceStatus": "available",
                    "AvailabilityZone": "us-east-1a",
                    "Region": "us-east-1",
                }
            ],
        }
        session = None
    else:
        with open(in_file) as f:
            raw = json.load(f)
        from boto3 import Session

        session = Session()
    analysis = analyze_scan(raw, session=session)
    with open(out, "w") as f:
        json.dump(analysis, f, indent=2)
    print("Findings saved to", out)


@cli.command()
@click.option(
    "--findings", default="reports/findings.json", help="Path to findings JSON"
)
@click.option("--out", default="reports", help="Output directory")
@click.option(
    "--log-level", default=None, help="Override logging level for this command"
)
def report(findings, out, log_level):
    _require_license()
    generate_report(findings_path=findings, output_dir=out)


if __name__ == "__main__":
    cli()
