#!/usr/bin/env python3
"""
APIStrike CLI — Run security tests from the command line.

Usage:
    python cli.py https://api.example.com/endpoint
    python cli.py https://api.example.com/endpoint -m GET
    python cli.py https://api.example.com/endpoint --mode quick --output report.json
    python cli.py https://api.example.com/endpoint -H '{"Authorization":"Bearer tok"}'
"""

from __future__ import annotations

import argparse
import asyncio
import json
import sys
import time

from engine.runner import run_all_suites
from engine.scoring import ENGINE_VERSION, compute_report


# ── ANSI colors ──────────────────────────────────────────────────
class C:
    RST = "\033[0m"
    BOLD = "\033[1m"
    DIM = "\033[2m"
    RED = "\033[91m"
    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    BLUE = "\033[94m"
    MAGENTA = "\033[95m"
    CYAN = "\033[96m"
    WHITE = "\033[97m"
    BG_RED = "\033[41m"
    BG_GREEN = "\033[42m"
    BG_YELLOW = "\033[43m"


def _supports_color() -> bool:
    if sys.platform == "win32":
        # Enable ANSI on Windows 10+
        try:
            import ctypes
            kernel32 = ctypes.windll.kernel32
            kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
            return True
        except Exception:
            return False
    return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()


NO_COLOR = not _supports_color()


def c(code: str, text: str) -> str:
    if NO_COLOR:
        return text
    return f"{code}{text}{C.RST}"


# ── Display helpers ──────────────────────────────────────────────
BANNER = r"""
   ___   ___  ___  ___  __       _ __
  / _ | / _ \/  _// __// /_ ____(_) /__ ___
 / __ |/ ___// / _\ \ / __// __/ /  '_// -_)
/_/ |_/_/  /___//___/ \__//_/ /_/_/\_\ \__/
"""


def print_banner():
    print(c(C.RED, BANNER))
    print(c(C.DIM, f"  engine v{ENGINE_VERSION} — 583 attack vectors — 7 suites"))
    print()


def severity_color(sev: str) -> str:
    return {
        "CRITICAL": C.RED + C.BOLD,
        "HIGH": C.RED,
        "MEDIUM": C.YELLOW,
        "LOW": C.CYAN,
        "INFO": C.DIM,
    }.get(sev, C.DIM)


def status_color(status: str) -> str:
    return {
        "CRASH": C.BG_RED + C.WHITE + C.BOLD,
        "FAIL": C.RED,
        "WARN": C.YELLOW,
        "PASS": C.GREEN,
    }.get(status, C.DIM)


def grade_color(g: str) -> str:
    if g in ("A", "B"):
        return C.GREEN + C.BOLD
    if g == "C":
        return C.YELLOW + C.BOLD
    return C.RED + C.BOLD


def print_progress(msg: str):
    sys.stdout.write(f"\r  {c(C.DIM, '●')} {c(C.DIM, msg):<60}")
    sys.stdout.flush()


def print_score_card(report: dict):
    score = report["score"]
    g = report["grade"]
    s = report["summary"]
    sev = report["severity"]
    meta = report["meta"]
    lat = report.get("latency", {})

    line = "━" * 56
    print(c(C.DIM, f"  {line}"))

    # Score + Grade
    gc = grade_color(g)
    print(f"  {c(C.BOLD, 'Score:')} {c(gc, str(score))}/100   "
          f"{c(C.BOLD, 'Grade:')} {c(gc, g)}   "
          f"{c(C.BOLD, 'Mode:')} {meta['mode']}")
    print()

    # Summary row
    crashes = s["crashes"]
    warnings = s["warnings"]
    passes = s["passes"]
    failures = s["failures"]
    print(f"  {c(C.DIM, 'Tests:')} {s['total']}   "
          f"{c(C.RED, f'Crashes: {crashes}')}   "
          f"{c(C.YELLOW, f'Warnings: {warnings}')}   "
          f"{c(C.GREEN, f'Passed: {passes}')}   "
          f"{c(C.DIM, f'Failed: {failures}')} ")
    print()

    # Severity
    s_crit = sev['CRITICAL']
    s_high = sev['HIGH']
    s_med = sev['MEDIUM']
    s_low = sev['LOW']
    s_info = sev['INFO']
    print(f"  {c(C.DIM, 'Severity')}  "
          f"{c(C.RED + C.BOLD, f'CRIT={s_crit}')}  "
          f"{c(C.RED, f'HIGH={s_high}')}  "
          f"{c(C.YELLOW, f'MED={s_med}')}  "
          f"{c(C.CYAN, f'LOW={s_low}')}  "
          f"{c(C.DIM, f'INFO={s_info}')} ")

    # Latency
    if lat:
        print(f"  {c(C.DIM, 'Latency')}   "
              f"avg={lat['avg_ms']}ms  p95={lat['p95_ms']}ms  "
              f"min={lat['min_ms']}ms  max={lat['max_ms']}ms")

    dur = meta['duration_ms']
    print(f"  {c(C.DIM, f'Duration: {dur}ms')}")
    print(c(C.DIM, f"  {line}"))


def print_findings(report: dict, show_all: bool = False):
    results = report["results"]

    # Filter to issues only (unless --verbose)
    if not show_all:
        results = [r for r in results if r["status"] in ("CRASH", "WARN", "FAIL")]

    if not results:
        print(f"\n  {c(C.GREEN + C.BOLD, '✓ No issues found.')}")
        return

    # Group by severity
    by_sev = {"CRITICAL": [], "HIGH": [], "MEDIUM": [], "LOW": [], "INFO": []}
    for r in results:
        by_sev.get(r["severity"], by_sev["INFO"]).append(r)

    for sev in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"):
        items = by_sev[sev]
        if not items:
            continue
        sc = severity_color(sev)
        print(f"\n  {c(sc, f'── {sev} ({len(items)}) ──')}")
        for r in items:
            stc = status_color(r["status"])
            cwe = f" {c(C.DIM, r['cwe'])}" if r["cwe"] else ""
            lat_val = r['latency_ms']
            latency = f" {c(C.DIM, f'{lat_val:.0f}ms')}" if lat_val else ""
            print(f"    {c(stc, r['status']):>18s}  {r['test']}{cwe}{latency}")
            if r["detail"]:
                # Truncate long details
                detail = r["detail"][:120]
                print(f"             {c(C.DIM, detail)}")


def print_cwe_summary(report: dict):
    cwe = report.get("cwe_summary", {})
    if not cwe:
        return
    print(f"\n  {c(C.DIM, '── CWE Summary ──')}")
    for k, v in sorted(cwe.items(), key=lambda x: -x[1]):
        print(f"    {c(C.RED, k):>20s}  ×{v}")


def print_categories(report: dict):
    cats = report.get("categories", {})
    if not cats:
        return
    print(f"\n  {c(C.DIM, '── Categories ──')}")
    for cat, data in sorted(cats.items()):
        total = data["total"]
        crashes = data["crashes"]
        warns = data["warnings"]
        passes = data["passes"]
        bar_len = 30
        pass_pct = passes / total if total else 0
        filled = int(bar_len * pass_pct)
        bar = c(C.GREEN, "█" * filled) + c(C.DIM, "░" * (bar_len - filled))
        extra = ""
        if crashes:
            extra += c(C.RED, f" {crashes} crash")
        if warns:
            extra += c(C.YELLOW, f" {warns} warn")
        print(f"    {cat:<20s} {bar} {passes}/{total}{extra}")


# ── Main ─────────────────────────────────────────────────────────
def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        prog="apistrike",
        description="APIStrike — API security testing from the command line.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""examples:
  apistrike https://api.example.com/users
  apistrike https://api.example.com/users -m GET --mode quick
  apistrike https://api.example.com/users -H '{"Authorization":"Bearer tok123"}'
  apistrike https://api.example.com/users -b '{"name":"test"}' -o report.json
  apistrike https://api.example.com/users --severity critical,high
  apistrike https://api.example.com/users --verbose --no-color""",
    )

    p.add_argument("url", help="Target API endpoint URL")
    p.add_argument("-m", "--method", default="POST",
                   choices=["GET", "POST", "PUT", "PATCH", "DELETE"],
                   help="HTTP method (default: POST)")
    p.add_argument("-H", "--headers", default=None,
                   help="Request headers as JSON string")
    p.add_argument("-b", "--body", default=None,
                   help="Request body as JSON string")
    p.add_argument("--mode", default="full", choices=["quick", "full"],
                   help="Scan mode: quick (5 suites) or full (7 suites, default)")
    p.add_argument("-o", "--output", default=None,
                   help="Save full JSON report to file")
    p.add_argument("--severity", default=None,
                   help="Only show findings at these severities (comma-separated: critical,high,medium,low,info)")
    p.add_argument("-v", "--verbose", action="store_true",
                   help="Show all results including PASS")
    p.add_argument("--no-color", action="store_true",
                   help="Disable colored output")
    p.add_argument("--json", action="store_true",
                   help="Output raw JSON report to stdout (for piping)")
    p.add_argument("--quiet", action="store_true",
                   help="Minimal output — just score and critical findings")
    p.add_argument("--badge", action="store_true",
                   help="One-line badge output (great for CI logs & screenshots)")
    p.add_argument("--timeout", type=int, default=300,
                   help="Max scan duration in seconds (default: 300)")
    p.add_argument("--version", action="version",
                   version=f"APIStrike v{ENGINE_VERSION}")

    return p


async def run(args: argparse.Namespace) -> dict:
    # Parse headers/body
    headers = None
    if args.headers:
        try:
            headers = json.loads(args.headers)
        except json.JSONDecodeError:
            print(c(C.RED, "  error: invalid JSON in --headers"))
            sys.exit(1)

    body = None
    if args.body:
        try:
            body = json.loads(args.body)
        except json.JSONDecodeError:
            print(c(C.RED, "  error: invalid JSON in --body"))
            sys.exit(1)

    # Progress messages
    phases = [
        "adversarial injection tests",
        "protocol abuse tests",
        "load & stress tests",
        "edge-case tests",
        "consistency tests",
        "reflection detection",
        "information leakage scan",
        "computing score",
    ]

    if not args.json and not args.quiet and not args.badge:
        print_banner()
        print(f"  {c(C.BOLD, 'Target:')}  {args.url}")
        print(f"  {c(C.BOLD, 'Method:')}  {args.method}")
        print(f"  {c(C.BOLD, 'Mode:')}    {args.mode}")
        print()

    start = time.perf_counter()

    if not args.json and not args.quiet and not args.badge:
        print_progress("starting engine...")

    try:
        suite_output = await asyncio.wait_for(
            run_all_suites(
                url=args.url,
                method=args.method.upper(),
                headers=headers,
                body=body,
                mode=args.mode,
            ),
            timeout=args.timeout,
        )
    except asyncio.TimeoutError:
        if not args.json:
            print(c(C.RED, f"\n  error: scan timed out after {args.timeout}s"))
        sys.exit(3)

    if not args.json and not args.quiet and not args.badge:
        print_progress("computing verdict...")

    report = compute_report(
        suite_output=suite_output,
        target=args.url,
        method=args.method.upper(),
        mode=args.mode,
    )

    wall_time = round((time.perf_counter() - start) * 1000, 1)

    if not args.json and not args.quiet and not args.badge:
        sys.stdout.write("\r" + " " * 70 + "\r")  # Clear progress line

    return report


def display(report: dict, args: argparse.Namespace):
    # Raw JSON mode — just dump and exit
    if args.json:
        json.dump(report, sys.stdout, indent=2, default=str)
        print()
        return

    # Badge mode — one clean line
    if args.badge:
        sev = report["severity"]
        s = report["summary"]
        grade = report["grade"]
        score = report["score"]
        gc = grade_color(grade)
        line = (f"APIStrike Score: {c(gc, f'{grade} ({score})')}"
                f" | Crashes: {s['crashes']}"
                f" | Critical: {sev['CRITICAL']}"
                f" | High: {sev['HIGH']}")
        print(line)
        return

    # Quiet mode — just score + crits
    if args.quiet:
        s = report["summary"]
        sev = report["severity"]
        print(f"{report['score']}/100 {report['grade']} "
              f"tests={s['total']} crashes={s['crashes']} "
              f"crit={sev['CRITICAL']} high={sev['HIGH']}")
        crits = [r for r in report["results"]
                 if r["severity"] == "CRITICAL" and r["status"] in ("CRASH", "WARN", "FAIL")]
        for cr in crits:
            print(f"  CRITICAL: {cr['test']}: {cr['detail'][:100]}")
        return

    # Full display
    print_score_card(report)

    # Filter by severity if requested
    if args.severity:
        allowed = {s.strip().upper() for s in args.severity.split(",")}
        filtered_results = [r for r in report["results"] if r["severity"] in allowed]
        filtered_report = {**report, "results": filtered_results}
        print_findings(filtered_report, show_all=args.verbose)
    else:
        print_findings(report, show_all=args.verbose)

    print_cwe_summary(report)
    print_categories(report)
    print()

    # Save JSON report
    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2, default=str)
        print(f"  {c(C.GREEN, '✓')} Report saved to {c(C.BOLD, args.output)}")
        print()


def main():
    parser = build_parser()
    args = parser.parse_args()

    global NO_COLOR
    if args.no_color:
        NO_COLOR = True

    report = asyncio.run(run(args))
    display(report, args)

    # Exit code based on severity
    sev = report["severity"]
    if sev["CRITICAL"] > 0:
        sys.exit(2)
    if sev["HIGH"] > 0:
        sys.exit(1)
    sys.exit(0)


if __name__ == "__main__":
    main()
