"""DepRaptor CLI v2.0.0"""
from __future__ import annotations

import logging
import shutil
import subprocess
import sys
from pathlib import Path
from typing import List, Optional

import typer
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.table import Table

from ..scanner.dependency_parser import DependencyParser
from ..scanner.confusion_checker import ConfusionChecker, VulnerableDependency
from ..poc.poc_generator import PoCGenerator
from ..report.report_writer import ReportWriter
from ..utils.banner import display_banner
from ..utils.config import get_working_dirs
from ..utils.filesystem import ensure_directories
from ..utils.github import clone_repository, list_org_repos
from ..utils.target_resolver import resolve, TargetType
from ..utils.risk_engine import score_all
from ..utils import app_config

app = typer.Typer(
    add_completion=False,
    help="DepRaptor - Dependency Confusion Vulnerability Scanner",
    no_args_is_help=True,
)
console = Console()


def _setup_logging(output_dir: Path, verbose: bool) -> None:
    log_file = output_dir / "logs" / "scan.log"
    log_file.parent.mkdir(parents=True, exist_ok=True)
    level = logging.DEBUG if verbose else logging.INFO
    handlers: list = [logging.FileHandler(log_file, encoding="utf-8")]
    if verbose:
        handlers.append(logging.StreamHandler(sys.stderr))
    logging.basicConfig(
        level=level,
        format="%(asctime)s %(name)s %(levelname)s %(message)s",
        handlers=handlers,
        force=True,
    )


def _run_secrets_scan(path: Path) -> List[str]:
    if not shutil.which("trufflehog"):
        console.print("[yellow]trufflehog not found - skipping secret scan.[/yellow]")
        return []
    try:
        result = subprocess.run(
            ["trufflehog", "filesystem", str(path), "--json"],
            capture_output=True, text=True, timeout=120,
        )
        return [ln.strip() for ln in result.stdout.splitlines() if ln.strip()]
    except Exception as exc:
        console.print(f"[yellow]Secret scan error: {exc}[/yellow]")
        return []


def _confirm_publish() -> bool:
    console.print(
        Panel(
            "[bold red]You are about to publish packages to public registries.[/bold red]\n"
            "This is IRREVERSIBLE. Only proceed with explicit authorization.",
            title="[red]SAFETY WARNING[/red]",
        )
    )
    return typer.prompt("Type YES to continue, anything else to abort").strip() == "YES"


def _interactive_payload_menu(current_payload: str, current_callback: str):
    try:
        import questionary
    except ImportError:
        console.print("[yellow]Install questionary for interactive mode.[/yellow]")
        return current_payload, current_callback
    payload = questionary.select(
        "Choose PoC payload type:",
        choices=["system_info", "webhook", "discord", "interactsh", "ci_env_dump", "custom"],
        default=current_payload,
    ).ask() or current_payload
    callback = current_callback
    if payload in ("webhook", "discord", "interactsh", "ci_env_dump"):
        callback = questionary.text("Enter callback URL:", default=current_callback).ask() or ""
    return payload, callback


def _publish_pocs(poc_paths: List[Path], vulnerabilities: List[VulnerableDependency],
                  cfg: dict, simulate: bool) -> None:
    from ..publisher import pypi_publisher, npm_publisher
    vuln_map = {v.dependency.name: v.dependency.ecosystem for v in vulnerabilities}
    for poc_path in poc_paths:
        eco = vuln_map.get(poc_path.name, "")
        if eco == "pypi":
            token = cfg.get("pypi_token") or (
                "" if simulate else typer.prompt(f"PyPI token for {poc_path.name}", hide_input=True))
            pypi_publisher.publish(poc_path, token, simulate=simulate)
        elif eco == "npm":
            token = cfg.get("npm_token") or (
                "" if simulate else typer.prompt(f"npm token for {poc_path.name}", hide_input=True))
            npm_publisher.publish(poc_path, token, simulate=simulate)
        else:
            console.print(f"[yellow]No publisher for ecosystem '{eco}' ({poc_path.name})[/yellow]")


def _display_summary(dependencies, vulnerabilities, poc_paths, risk_results, output_dir) -> None:
    console.print("\n[bold cyan]===  Scan Summary  ===[/bold cyan]\n")
    t = Table(show_header=False, box=None)
    t.add_column("Metric", style="cyan")
    t.add_column("Value", style="yellow")
    t.add_row("Total Dependencies", str(len(dependencies)))
    t.add_row("Vulnerable Dependencies", str(len(vulnerabilities)))
    t.add_row("Generated PoC Packages", str(len(poc_paths)))
    t.add_row("Output Directory", str(output_dir))
    console.print(t)
    if vulnerabilities:
        console.print("\n[bold yellow]Vulnerabilities:[/bold yellow]\n")
        vt = Table()
        vt.add_column("Package", style="red")
        vt.add_column("Ecosystem", style="cyan")
        vt.add_column("Risk", style="yellow")
        vt.add_column("Source", style="dim")
        risk_map = {r.package: r.score for r in (risk_results or [])}
        for vuln in vulnerabilities[:15]:
            vt.add_row(vuln.dependency.name, vuln.dependency.ecosystem,
                       f"{risk_map.get(vuln.dependency.name, 0):.1f}/10",
                       vuln.dependency.source)
        console.print(vt)
        if len(vulnerabilities) > 15:
            console.print(f"[dim]... and {len(vulnerabilities) - 15} more (see report.md)[/dim]")
    else:
        console.print("\n[green]No vulnerabilities detected[/green]")
    console.print(f"\n[dim]Report: {output_dir / 'report.md'}[/dim]")


def _scan_single(project_path: Path, output_dir: Path, cfg: dict,
                 payload_type: str, callback_url: str, autopublish: bool,
                 simulate: bool, scan_secrets: bool, interactive: bool) -> None:
    log = logging.getLogger(__name__)
    log.info(f"Scanning: {project_path}")

    console.print("\n[cyan]Parsing dependencies...[/cyan]")
    with Progress(SpinnerColumn(), TextColumn("{task.description}"), transient=True) as p:
        p.add_task("Scanning files...", total=None)
        dependencies = DependencyParser(project_path).parse_all()
    console.print(f"[green]v[/green] Found {len(dependencies)} dependencies")

    if not dependencies:
        console.print("[yellow]No dependencies found.[/yellow]")
        return

    threads = cfg.get("threads", 10)
    console.print(f"\n[cyan]Checking registries ({threads} threads)...[/cyan]")
    with Progress(SpinnerColumn(), TextColumn("{task.description}"), transient=True) as p:
        p.add_task("Querying registries...", total=None)
        vulnerabilities = ConfusionChecker(dependencies, max_threads=threads).check_all()
    console.print(f"[green]v[/green] Found {len(vulnerabilities)} vulnerable dependencies")

    risk_results = score_all(vulnerabilities, project_path)

    if interactive and vulnerabilities:
        payload_type, callback_url = _interactive_payload_menu(payload_type, callback_url)

    poc_paths: List[Path] = []
    if vulnerabilities:
        console.print("\n[cyan]Generating PoC packages...[/cyan]")
        with Progress(SpinnerColumn(), TextColumn("{task.description}"), transient=True) as p:
            p.add_task("Creating PoCs...", total=None)
            poc_paths = PoCGenerator(output_dir / "pocs", payload_type, callback_url).generate_all(vulnerabilities)
        console.print(f"[green]v[/green] Generated {len(poc_paths)} PoC packages")

    secrets: List[str] = []
    if scan_secrets:
        console.print("\n[cyan]Running secret scan...[/cyan]")
        secrets = _run_secrets_scan(project_path)
        console.print(f"[green]v[/green] Secret scan done ({len(secrets)} findings)")

    console.print("\n[cyan]Writing reports...[/cyan]")
    ReportWriter(output_dir).generate_all(
        str(project_path), dependencies, vulnerabilities, poc_paths, risk_results, secrets)
    console.print(f"[green]v[/green] Reports saved to {output_dir}")

    if poc_paths:
        should_publish = False
        if autopublish:
            if simulate:
                console.print("[yellow][SIMULATE] Would publish packages to public registries.[/yellow]")
            else:
                should_publish = _confirm_publish()
        elif interactive:
            # In interactive mode, ask the user if they want to publish
            try:
                import questionary
                should_publish = questionary.confirm(
                    f"Publish {len(poc_paths)} PoC package(s) to public registries?",
                    default=False,
                ).ask()
            except ImportError:
                answer = typer.prompt("Publish PoC packages to public registries? (yes/no)", default="no")
                should_publish = answer.strip().lower() in ("yes", "y")
            if should_publish:
                should_publish = _confirm_publish()  # safety double-confirm
        if should_publish:
            _publish_pocs(poc_paths, vulnerabilities, cfg, simulate)

    _display_summary(dependencies, vulnerabilities, poc_paths, risk_results, output_dir)


def _scan_github_url(url: str, repos_dir: Path, output_dir: Path, cfg: dict,
                     payload: str, callback: str, autopublish: bool, simulate: bool,
                     scan_secrets: bool, interactive: bool, token: str = "") -> None:
    console.print(f"\n[cyan]Cloning:[/cyan] {url}")
    with Progress(SpinnerColumn(), TextColumn("{task.description}"), transient=True) as p:
        p.add_task("Cloning...", total=None)
        project_path = clone_repository(url, repos_dir, token)
    if not project_path:
        console.print(f"[red]Failed to clone {url}[/red]")
        return
    console.print(f"[green]v[/green] Cloned to {project_path}")
    _scan_single(project_path, output_dir, cfg, payload, callback,
                 autopublish, simulate, scan_secrets, interactive)


@app.command()
def scan(
    target: str = typer.Argument(..., help="Local path | GitHub URL | org:name | list:file | paths:file"),
    payload: Optional[str] = typer.Option(None, "--payload", "-p",
                                help="Payload: system_info|webhook|discord|interactsh|ci_env_dump|custom"),
    callback: str = typer.Option("", "--callback", "-c", help="Callback URL for webhook/discord/interactsh"),
    threads: Optional[int] = typer.Option(None, "--threads", "-t", help="Registry check threads"),
    output: Optional[str] = typer.Option(None, "--output", "-o", help="Output directory"),
    repo_dir: Optional[str] = typer.Option(None, "--repo-dir", help="Directory for cloned repos"),
    simulate: bool = typer.Option(False, "--simulate", help="Dry-run: build PoCs but skip publishing"),
    autopublish: bool = typer.Option(False, "--autopublish", help="Publish PoC packages after generation"),
    github_token: Optional[str] = typer.Option(None, "--github-token", help="GitHub personal access token"),
    secrets: bool = typer.Option(False, "--secrets", help="Run TruffleHog secret scan"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose logging"),
) -> None:
    """Scan for dependency confusion vulnerabilities."""
    display_banner()
    cfg = app_config.merge(app_config.load(), threads=threads, verbose=verbose)
    dirs = get_working_dirs()
    output_dir = Path(output) if output else dirs["results"]
    repos_dir = Path(repo_dir) if repo_dir else dirs["repos"]
    ensure_directories([output_dir, repos_dir, output_dir / "pocs", output_dir / "logs"])
    _setup_logging(output_dir, cfg.get("verbose", False))

    # Interactive only when NO explicit flags were passed by the user
    # payload=None means user did NOT pass --payload
    interactive = (payload is None and not callback and not autopublish and not simulate)
    # Resolve payload to actual value (default: system_info)
    resolved_payload = payload or cfg.get("default_payload", "system_info")

    token = github_token or cfg.get("github_token", "")
    resolved = resolve(target)

    if resolved.type == TargetType.GITHUB_ORG:
        console.print(f"[cyan]Fetching repos for org:[/cyan] {resolved.value}")
        repo_urls = list_org_repos(resolved.value, token)
        if not repo_urls:
            console.print("[red]No repositories found.[/red]")
            raise typer.Exit(1)
        console.print(f"[green]v[/green] {len(repo_urls)} repos found")
        for url in repo_urls:
            _scan_github_url(url, repos_dir, output_dir, cfg, resolved_payload, callback,
                             autopublish, simulate, secrets, interactive, token)

    elif resolved.type == TargetType.REPO_LIST:
        for url in resolved.paths:
            _scan_github_url(url, repos_dir, output_dir, cfg, resolved_payload, callback,
                             autopublish, simulate, secrets, interactive, token)

    elif resolved.type == TargetType.PATH_LIST:
        for path_str in resolved.paths:
            p = Path(path_str).resolve()
            if not p.exists():
                console.print(f"[yellow]Skipping missing path: {p}[/yellow]")
                continue
            _scan_single(p, output_dir, cfg, resolved_payload, callback,
                         autopublish, simulate, secrets, interactive)

    elif resolved.type == TargetType.GITHUB_REPO:
        _scan_github_url(target, repos_dir, output_dir, cfg, resolved_payload, callback,
                         autopublish, simulate, secrets, interactive, token)

    else:
        project_path = Path(target).resolve()
        if not project_path.exists():
            console.print(f"[red]Path does not exist:[/red] {target}")
            raise typer.Exit(1)
        _scan_single(project_path, output_dir, cfg, resolved_payload, callback,
                     autopublish, simulate, secrets, interactive)


@app.command()
def doctor() -> None:
    """Check availability of required external tools."""
    display_banner()
    tools = ["git", "npm", "python", "twine", "cargo", "gem", "trufflehog"]
    table = Table(title="Environment Check")
    table.add_column("Tool", style="cyan")
    table.add_column("Status")
    table.add_column("Path", style="dim")
    for tool in tools:
        path = shutil.which(tool)
        table.add_row(tool, "[green]found[/green]" if path else "[red]not found[/red]", path or "-")
    console.print(table)


if __name__ == "__main__":
    app()
