"""Scan CLI commands: security scanning and API extraction."""

import os
import shutil
import sys
from pathlib import Path

import click

from ._core import scan_group
from ._utils import _claude_run, _resolve_ai_mode, _run_via_direct_api

# ---------------------------------------------------------------------------
# Depth presets
# ---------------------------------------------------------------------------

# Security scan profiles (from maeris_mcp.scanners.security_checks)
_PROFILE_QUICK = "owasp_top10_static"   # OWASP Top 10, ~140 checks
_PROFILE_DEFAULT = "owasp_top10_static"
_PROFILE_DEEP = "all_static"            # All 600+ static checks

# File batch sizes per Claude invocation
_LIMIT_QUICK = 3
_LIMIT_DEFAULT = 5
_LIMIT_DEEP = 10

def _check_auth() -> None:
    """Exit with a clear error if the user is not authenticated."""
    from maeris_mcp.auth.token_store import TokenStore
    from ._utils import _repo_config_dir

    ts = TokenStore(config_dir=_repo_config_dir())
    if not ts.is_authenticated():
        click.secho("\n✗ Not authenticated. Run 'maeris login' first.", fg="red")
        sys.exit(1)


def _check_scope(tool_name: str) -> None:
    """Exit with a clear error if the current Maeris scope blocks *tool_name*."""
    from maeris_mcp.auth.scopes import load_maeris_scope, is_maeris_tool_allowed, TOOL_SCOPES

    scope = load_maeris_scope()
    if not is_maeris_tool_allowed(tool_name, scope):
        required = TOOL_SCOPES.get(tool_name, "write")
        click.secho(
            f"\n✗ Your current Maeris scope is '{scope}', "
            f"but '{tool_name}' requires '{required}' scope.\n",
            fg="red",
        )
        click.echo(
            f"  Run this to fix it:\n"
            f"    maeris app scope --maeris {required}\n"
        )
        sys.exit(1)


def _resolve_depth(quick: bool, deep: bool) -> tuple[str, int, str]:
    """Return (scan_profile, file_limit, label) from --quick / --deep flags."""
    if quick:
        return _PROFILE_QUICK, _LIMIT_QUICK, "quick"
    if deep:
        return _PROFILE_DEEP, _LIMIT_DEEP, "deep"
    return _PROFILE_DEFAULT, _LIMIT_DEFAULT, "standard"


def _require_claude() -> str:
    """Return path to the claude binary or exit with a helpful message."""
    claude_bin = shutil.which("claude")
    if not claude_bin:
        click.secho(
            "✗ AI CLI not found on PATH.\n"
            "  Install Claude Code from https://claude.ai/code and run 'claude auth login'.",
            fg="red",
        )
        sys.exit(1)
    return claude_bin


# ---------------------------------------------------------------------------
# maeris scan security
# ---------------------------------------------------------------------------


@scan_group.command("security")
@click.option("--source", "-s", default=None,
              help="Directory to scan (default: cwd).")
@click.option("--quick", "quick", is_flag=True, default=False,
              help="Fast scan: OWASP Top 10 only, 3 files per batch.")
@click.option("--deep", "deep", is_flag=True, default=False,
              help="Thorough scan: all 600+ checks, 10 files per batch.")
@click.option("--profile", "profile", default=None,
              help=(
                  "Security profile to use (overrides --quick/--deep). "
                  "Options: owasp_top10_static, all_static, api_static, "
                  "sast_static, auth_session_static, data_exposure_static, "
                  "infra_static, …"
              ))
@click.option("--model", "model", default="claude-sonnet-4-6", show_default=True,
              help="AI model to use for analysis.")
def scan_security_cmd(
    source: str | None,
    quick: bool,
    deep: bool,
    profile: str | None,
    model: str,
) -> None:
    """Run an LLM-guided static security scan on your codebase.

    \b
    AI analyzes your source files batch by batch using the security_scan
    MCP tool and reports findings with severity, OWASP category, and
    remediation hints.

    \b
    Depth presets:
      (default)  OWASP Top 10, 5 files/batch   — balanced
      --quick    OWASP Top 10, 3 files/batch   — faster, key findings
      --deep     All 600+ checks, 10 files/batch — thorough, takes longer

    \b
    Examples:
      maeris scan security
      maeris scan security --quick
      maeris scan security --deep --source ../my-backend
      maeris scan security --profile sast_static
    """
    if quick and deep:
        click.secho("✗ --quick and --deep are mutually exclusive.", fg="red")
        sys.exit(1)

    _check_auth()
    claude_bin, ai_mode = _resolve_ai_mode()
    target_path = str(Path(source).resolve()) if source else os.getcwd()
    resolved_profile, limit, label = _resolve_depth(quick, deep)
    if profile:
        resolved_profile = profile
        label = f"custom ({profile})"

    _check_scope("security_scan")

    click.echo(
        f"\n  Security scan — {label}  "
        f"(profile: {resolved_profile}, {limit} files/batch)\n"
        f"  Target: {target_path}\n"
    )

    prompt = (
        f"Run a security scan on: {target_path}\n\n"
        f"Use the security_scan MCP tool with these parameters:\n"
        f"  target_path    = {target_path!r}\n"
        f"  scan_profile   = {resolved_profile!r}\n"
        f"  limit          = {limit}   (files per batch)\n\n"
        "Workflow:\n"
        "1. Call security_scan with include_content=true, offset=0, "
        "and the parameters above to get the first file batch.\n"
        "2. Analyze every file in the batch for the security issues covered "
        "by the profile. Identify real findings only — avoid false positives.\n"
        "3. On the next call pass findings=[...] from this batch alongside "
        "the next offset (pagination.nextOffset).\n"
        "4. Repeat until pagination.hasMore is false.\n"
        "5. On the final call pass findings=[...] AND is_last=true to "
        "finalize the scan and receive the full report.\n"
        "6. After the scan finalizes, print a clear summary:\n"
        "   - Total findings by severity (critical / high / medium / low / info)\n"
        "     IMPORTANT: Use the severity counts from the finalization response's "
        "summary (summary.by_severity), NOT your own count of findings you submitted. "
        "The finalization response includes findings from automated scanners "
        "(depScanResults, infraScanResults, jsxScanResults, secretScanResults, "
        "patternScanResults) that run in addition to your analysis. "
        "The summary.total_findings is the true total.\n"
        "   - If automated scanner results are present, list them: "
        "e.g. 'Automated: 12 dependency CVEs, 5 hardcoded secrets, 8 pattern issues'\n"
        "   - Top 3–5 most critical issues: file, line (if known), issue, "
        "and a one-sentence remediation hint\n"
        "7. After printing the summary, call push_to_maeris with "
        "data_type='vulnerabilities' and the scan_id from the finalized scan "
        "to push findings to the Maeris portal.\n"
        "8. Print how many findings were pushed.\n"
        "STOP after printing the result. Do not call any other tools."
    )

    if claude_bin:
        rc = _claude_run(claude_bin, prompt, model, target_path)
    else:
        rc = _run_via_direct_api(prompt, target_path, spinner_message="Running security scan…")
    if rc != 0:
        click.secho(f"\n✗ Security scan failed (exit code {rc}).", fg="red")
        click.secho("  Try 'maeris doctor' to check your setup.", fg="bright_black")
        sys.exit(rc)

    _next_steps(
        "maeris report security --format html -o security.html   Export report",
        "maeris repair contract --dry-run                        Check for drift",
    )


# ---------------------------------------------------------------------------
# maeris scan api
# ---------------------------------------------------------------------------


@scan_group.command("api")
@click.option("--source", "-s", default=None,
              help="Directory to scan (default: cwd).")
@click.option("--quick", "quick", is_flag=True, default=False,
              help="Fast scan: 3 files per batch (hits main API files quickly).")
@click.option("--deep", "deep", is_flag=True, default=False,
              help="Thorough scan: 10 files per batch (scans every source file).")
@click.option("--project-name", "project_name", default=None,
              help="Name for the API collection (defaults to directory name).")
@click.option("--model", "model", default="claude-sonnet-4-6", show_default=True,
              help="AI model to use for analysis.")
def scan_api_cmd(
    source: str | None,
    quick: bool,
    deep: bool,
    project_name: str | None,
    model: str,
) -> None:
    """Extract API definitions from your codebase using LLM analysis.

    \b
    AI scans your source for HTTP calls, endpoints, headers, auth patterns,
    and request/response payloads using the api_scan MCP tool, then produces
    a structured API collection you can import into Maeris.

    \b
    Depth presets:
      (default)  5 files/batch  — balanced
      --quick    3 files/batch  — faster, hits main API files
      --deep     10 files/batch — thorough, scans everything

    \b
    Examples:
      maeris scan api
      maeris scan api --quick
      maeris scan api --deep --source ../my-frontend
      maeris scan api --project-name "My App v2"
    """
    if quick and deep:
        click.secho("✗ --quick and --deep are mutually exclusive.", fg="red")
        sys.exit(1)

    _check_auth()
    claude_bin, ai_mode = _resolve_ai_mode()
    target_path = str(Path(source).resolve()) if source else os.getcwd()
    _, limit, label = _resolve_depth(quick, deep)
    proj_name = project_name or Path(target_path).name

    _check_scope("api_scan")

    click.echo(
        f"\n  API scan — {label}  ({limit} files/batch)\n"
        f"  Target: {target_path}\n"
        f"  Project: {proj_name}\n"
    )

    prompt = (
        f"Extract all API definitions from: {target_path}\n\n"
        f"Use the api_scan MCP tool with these parameters:\n"
        f"  target_path  = {target_path!r}\n"
        f"  project_name = {proj_name!r}\n"
        f"  limit        = {limit}   (files per batch)\n\n"
        "Workflow:\n"
        "1. Call api_scan with include_content=true, offset=0, and the "
        "parameters above to get the first file batch.\n"
        "2. Analyze every file for HTTP API calls: endpoint, method, headers, "
        "auth, request/response payload, query params, path params, base URL.\n"
        "3. On the next call pass apis=[...] from this batch alongside the "
        "next offset (pagination.nextOffset). Also pass "
        "http_clients=[...] listing every HTTP library detected "
        "(axios, fetch, requests, httpx, got, superagent, …).\n"
        "4. Repeat until pagination.hasMore is false.\n"
        "5. On the final call pass apis=[...] AND is_last=true to finalize.\n"
        "6. After the scan finalizes, print a clear summary:\n"
        "   - Total APIs by method (GET / POST / PUT / PATCH / DELETE)\n"
        "   - HTTP clients detected\n"
        "   - Auth patterns found (bearer, API key, basic auth, OAuth, …)\n"
        "7. After printing the summary, call get_stored_apis to retrieve "
        "the scanned APIs, then call add_apis_bulk with:\n"
        f"   - collection_name = {proj_name!r}\n"
        "   - apis = the full list of scanned APIs\n"
        "   - auto_environment = true\n"
        f"   - project_name = {proj_name!r}\n"
        "   - extracted_environment = the extractedEnvironment array from the "
        "api_scan finalization response\n"
        "   This automatically creates/reuses an environment, populates it with "
        "base_url and other env fields, parameterizes all API URLs as "
        "{{base_url}}/path, and selects the environment.\n"
        "8. Print how many APIs were imported and the environment setup result.\n"
        "STOP after printing the result. Do not call any other tools. "
        "Do not ask the user any questions."
    )

    if claude_bin:
        rc = _claude_run(claude_bin, prompt, model, target_path)
    else:
        rc = _run_via_direct_api(prompt, target_path, spinner_message="Running API scan…")
    if rc != 0:
        click.secho(f"\n✗ API scan failed (exit code {rc}).", fg="red")
        click.secho("  Try 'maeris doctor' to check your setup.", fg="bright_black")
        sys.exit(rc)

    _next_steps(
        "maeris api list                         View extracted APIs",
        "maeris flow create --name <flow>        Create an API flow",
        "maeris report coverage                  Check test coverage",
    )


# ---------------------------------------------------------------------------
# maeris scan push  — retry pushing the last security scan to Maeris
# ---------------------------------------------------------------------------


@scan_group.command("push")
@click.option("--source", "-s", default=None,
              help="Directory that was scanned (default: cwd).")
def scan_push_cmd(source: str | None) -> None:
    """Push the last security scan findings to Maeris.

    \b
    Use this if a previous 'maeris scan security' completed the scan
    but failed to push (e.g. expired auth, network error).

    \b
    Examples:
      maeris scan push
      maeris scan push --source ../my-backend
    """
    import asyncio
    import hashlib
    import json
    from uuid import uuid4
    from datetime import datetime, timezone

    from rich.console import Console
    from maeris_mcp.auth.token_store import TokenStore
    from ._utils import _repo_config_dir, _spinner

    _check_auth()

    target_path = str(Path(source).resolve()) if source else os.getcwd()

    # Locate the cached findings
    root_hash = hashlib.sha256(target_path.encode()).hexdigest()[:16]
    config_dir = os.getenv("MAERIS_CONFIG_DIR") or str(Path.home() / ".maeris")
    cache_path = Path(config_dir) / "security_scans" / root_hash / "findings.json"

    if not cache_path.exists():
        click.secho(
            f"\n✗ No cached scan found for: {target_path}\n"
            "  Run 'maeris scan security' first.",
            fg="red",
        )
        sys.exit(1)

    try:
        data = json.loads(cache_path.read_text())
        raw_findings = data if isinstance(data, list) else data.get("findings", [])
    except Exception as e:
        click.secho(f"\n✗ Failed to read cached scan: {e}", fg="red")
        sys.exit(1)

    if not raw_findings:
        click.secho("\n✗ Cached scan has no findings to push.", fg="red")
        sys.exit(1)

    click.echo(f"\n  Found {len(raw_findings)} cached findings for: {target_path}\n")

    async def _push() -> None:
        from maeris_mcp.types.security import SecurityFinding, SeverityCounts, ScanSummary, SecurityScanResult
        from maeris_mcp.storage.security_store import SecurityScanStore
        from maeris_mcp.tools.maeris_sync import handle_push_to_maeris

        # Rebuild findings
        findings = [SecurityFinding.model_validate(f) for f in raw_findings]

        # Compute summary
        sev_counts = {"low": 0, "medium": 0, "high": 0, "critical": 0}
        owasp_counts: dict[str, int] = {}
        for f in findings:
            sev = f.severity.value if f.severity else "medium"
            sev_counts[sev] = sev_counts.get(sev, 0) + 1
            if f.owasp_category:
                code = f.owasp_category.value
                owasp_counts[code] = owasp_counts.get(code, 0) + 1

        scan_id = str(uuid4())
        result = SecurityScanResult(
            scan_id=scan_id,
            target_path=target_path,
            scan_timestamp=datetime.now(timezone.utc).isoformat(),
            findings=findings,
            summary=ScanSummary(
                total_findings=len(findings),
                by_severity=SeverityCounts(**sev_counts),
                by_owasp_category=owasp_counts,
                files_scanned=0,
            ),
            errors=[],
        )

        store = SecurityScanStore()
        store.store(result)
        store.finalize_scan(scan_id)

        responses = await handle_push_to_maeris(
            store, {"data_type": "vulnerabilities", "scan_id": scan_id}
        )

        for r in responses:
            resp = json.loads(r.text)
            status = resp.get("status", "unknown")
            created = resp.get("findings_created", [])
            if status == "pushed":
                click.secho(
                    f"\n  ✓ {len(created)} findings pushed to Maeris.\n",
                    fg="green",
                )
            else:
                click.secho(
                    f"\n  ⚠ Push completed with status: {status}\n"
                    f"    Created: {len(created)} findings\n"
                    f"    Failures: {resp.get('findings_failed', [])}\n",
                    fg="yellow",
                )

    with _spinner("Pushing findings to Maeris…"):
        asyncio.run(_push())

    _next_steps(
        "maeris report security --format html -o security.html   Export report",
    )
