"""CLI entry point and argument parsing."""

from __future__ import annotations

import argparse
import logging
import sys
from pathlib import Path

logger = logging.getLogger("dep_audit")


def _setup_logging(*, verbose: bool = False, quiet: bool = False) -> None:
    """Configure logging level.

    Default:    INFO  (progress + errors)
    --verbose:  DEBUG (extra detail)
    --quiet:    WARNING (errors only)
    """
    if quiet:
        level = logging.WARNING
    elif verbose:
        level = logging.DEBUG
    else:
        level = logging.INFO

    # Clear existing handlers to avoid duplicates on repeated calls (e.g. tests)
    logger.handlers.clear()
    handler = logging.StreamHandler()
    handler.setFormatter(logging.Formatter("%(message)s"))
    logger.setLevel(level)
    logger.addHandler(handler)
    logger.propagate = False


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        prog="dep-audit",
        description="Identify unnecessary dependencies in software projects.",
    )
    parser.add_argument(
        "-v", "--verbose", action="store_true",
        help="Show debug-level output",
    )
    parser.add_argument(
        "-q", "--quiet", action="store_true",
        help="Suppress progress messages (errors only)",
    )
    sub = parser.add_subparsers(dest="command")

    # --- scan ---
    p_scan = sub.add_parser("scan", help="Scan project dependencies")
    p_scan.add_argument("path", nargs="?", default=".", help="Project root (default: .)")
    p_scan.add_argument("--ecosystem", help="Force a specific ecosystem")
    p_scan.add_argument("--target-version", help="Language version the project targets")
    p_scan.add_argument("--include-dev", action="store_true", help="Include dev dependencies")
    p_scan.add_argument("--format", choices=["terminal", "json", "sarif"], default="terminal")
    p_scan.add_argument("--offline", action="store_true", help="Skip deps.dev API calls")
    p_scan.add_argument("--ref", default="HEAD", help="Git ref for remote repos (branch/tag/commit)")
    p_scan.add_argument(
        "--exit-code", action="store_true",
        help="Exit with code 1 if any flagged dependencies are found (for CI)",
    )
    p_scan.add_argument(
        "--min-confidence", type=float, default=0.0, metavar="FLOAT",
        help="With --exit-code: only fail on findings >= this confidence (0.0–1.0, default: 0.0)",
    )
    p_scan.add_argument(
        "--ignore", metavar="PKG", action="append", default=[],
        help="Ignore a package (can be repeated: --ignore six --ignore pytz)",
    )
    p_scan.add_argument(
        "--known", metavar="PKG", action="append", default=[],
        help="Mark a finding as intentional — still shown, but won't trigger --exit-code",
    )

    # --- check ---
    p_check = sub.add_parser("check", help="Look up a single package")
    p_check.add_argument("package", help="Package name")
    p_check.add_argument("--ecosystem", default="python", help="Ecosystem (default: python)")
    p_check.add_argument("--target-version", help="Language version")
    p_check.add_argument("--offline", action="store_true", help="Skip deps.dev API calls")

    # --- db ---
    p_db = sub.add_parser("db", help="Database management")
    db_sub = p_db.add_subparsers(dest="db_command")

    p_db_list = db_sub.add_parser("list", help="List all entries")
    p_db_list.add_argument("ecosystem", nargs="?", default="python")

    p_db_show = db_sub.add_parser("show", help="Show a single entry")
    p_db_show.add_argument("package", help="Package name")
    p_db_show.add_argument("--ecosystem", default="python")

    p_db_export = db_sub.add_parser("export", help="Export discovered entries")
    p_db_export.add_argument("--discovered", action="store_true", required=True)
    p_db_export.add_argument("--ecosystem", default="python")
    p_db_export.add_argument("path", nargs="?", default=".", help="Project root or owner/repo")
    p_db_export.add_argument("--ref", default="HEAD", help="Git ref for remote repos")
    p_db_export.add_argument("--offline", action="store_true", help="Skip deps.dev API calls")

    # --- cache ---
    p_cache = sub.add_parser("cache", help="Cache management")
    cache_sub = p_cache.add_subparsers(dest="cache_command")
    cache_sub.add_parser("clear", help="Clear all cached data")

    args = parser.parse_args(argv)
    _setup_logging(verbose=args.verbose, quiet=args.quiet)

    if args.command is None:
        parser.print_help()
        return 0

    if args.command == "scan":
        return _cmd_scan(args)
    elif args.command == "check":
        return _cmd_check(args)
    elif args.command == "db":
        return _cmd_db(args)
    elif args.command == "cache":
        return _cmd_cache(args)
    else:
        parser.print_help()
        return 1


def _cmd_scan(args: argparse.Namespace) -> int:
    from dep_audit.config import load_config
    from dep_audit.github import is_github_target
    from dep_audit.scanner import format_report, scan, scan_remote

    # Route to local or remote scan
    if is_github_target(args.path):
        cfg: dict = {}
        ignore: set[str] = set(args.ignore)
        known: set[str] = set(args.known)
        results = scan_remote(
            repo_url=args.path,
            ref=args.ref,
            ecosystem=args.ecosystem,
            target_version=args.target_version,
            include_dev=args.include_dev,
            offline=args.offline,
            ignore=ignore,
        )
        if not results:
            logger.error("No supported lockfiles found in the remote repository.")
            return 1
    else:
        project_root = Path(args.path).resolve()
        if not project_root.is_dir():
            logger.error("Error: %s is not a directory", project_root)
            return 1

        cfg = load_config(project_root)
        # CLI --ignore merges with config ignore list
        ignore = set(cfg.get("ignore", [])) | set(args.ignore)
        # CLI --known merges with config known list
        known = set(cfg.get("known", [])) | set(args.known)

        # CLI flags override config; config overrides built-in defaults
        target_version = args.target_version or cfg.get("target-version")
        offline = args.offline or bool(cfg.get("offline", False))
        exit_code = args.exit_code or bool(cfg.get("exit-code", False))
        # CLI --min-confidence overrides config (config only applies when CLI is at default 0.0)
        if args.min_confidence == 0.0 and "min-confidence" in cfg:
            args.min_confidence = float(cfg["min-confidence"])

        if ignored := ignore:
            logger.debug("Ignoring %d package(s): %s", len(ignored), ", ".join(sorted(ignored)))

        results = scan(
            project_root=project_root,
            ecosystem=args.ecosystem or cfg.get("ecosystem"),
            target_version=target_version,
            include_dev=args.include_dev,
            offline=offline,
            ignore=ignore,
        )
        if not results:
            logger.error("No supported ecosystems detected.")
            return 1

        # Use effective values for exit-code (remote path uses args directly)
        args.exit_code = exit_code

    min_confidence = getattr(args, "min_confidence", 0.0)
    has_flagged = False
    for result in results:
        output = format_report(result, args.format)
        print(output)
        if any(
            c.classification != "ok" and c.confidence >= min_confidence
            and c.name not in known
            for c in result.classifications
        ):
            has_flagged = True

    # Warn about known findings so they stay visible in CI logs
    if known:
        known_found = sorted({
            c.name
            for result in results
            for c in result.classifications
            if c.classification != "ok" and c.name in known
        })
        if known_found:
            logger.info(
                "  %d known finding(s) suppressed from --exit-code: %s",
                len(known_found), ", ".join(known_found),
            )

    if args.exit_code and has_flagged:
        return 1
    return 0


def _cmd_check(args: argparse.Namespace) -> int:
    from dep_audit import depsdev
    from dep_audit import ecosystems as eco_mod
    from dep_audit.classify import classify_package
    from dep_audit.db import load_junk_db

    name = args.package
    ecosystem = args.ecosystem
    target_version = args.target_version or eco_mod.resolve_target_version(ecosystem)

    junk_db = load_junk_db(ecosystem)
    c = classify_package(ecosystem, name, "", target_version, True, junk_db)

    print()
    if c.classification != "ok":
        print(f"  {name} · {c.classification} · confidence: {c.confidence:.2f}")
        print()
        if c.replacement:
            eco_label = eco_mod.display_name(ecosystem)
            since_str = f" (stdlib since {eco_label} {c.stdlib_since})" if c.stdlib_since else ""
            print(f"  Replace with: {c.replacement}{since_str}")
            print()
        if c.flags:
            for flag in c.flags:
                print(f"  · {flag}")
            print()
    else:
        print(f"  {name} · ok")
        print()
        print("  Not flagged. No known unnecessary usage.")
        print()

    # deps.dev enrichment (skipped in offline mode)
    if not args.offline:
        pkg_data = depsdev.get_package(ecosystem, name)
        if pkg_data:
            versions = pkg_data.get("versions", [])
            latest_version = ""
            if versions:
                latest_version = versions[-1].get("versionKey", {}).get("version", "")

            if latest_version:
                deprecated, _ = depsdev.is_deprecated(ecosystem, name, latest_version)
                ver_data = depsdev.get_version(ecosystem, name, latest_version)
                advisories = len(ver_data.get("advisoryKeys", [])) if ver_data else 0

                sys_name = depsdev.system_name(ecosystem)
                print("  Ecosystem data:")
                print(f"    deps.dev:    https://deps.dev/{sys_name}/{name}")
                print(f"    deprecated:  {'yes' if deprecated else 'no'}")
                print(f"    advisories:  {advisories}")
                print()
        else:
            print("  (deps.dev data unavailable)")
            print()

    return 0


def _cmd_db(args: argparse.Namespace) -> int:
    if args.db_command == "list":
        return _cmd_db_list(args)
    elif args.db_command == "show":
        return _cmd_db_show(args)
    elif args.db_command == "export":
        return _cmd_db_export(args)
    else:
        logger.error("Usage: dep-audit db {list|show|export}")
        return 1


def _cmd_db_list(args: argparse.Namespace) -> int:
    from dep_audit.db import list_entries

    ecosystem = args.ecosystem
    groups = list_entries(ecosystem)

    total = sum(len(v) for v in groups.values())
    print(f"\n  {ecosystem}: {total} entries\n")

    for type_name, names in sorted(groups.items()):
        print(f"  {type_name} ({len(names)}):")
        print(f"    {', '.join(sorted(names))}")
        print()

    return 0


def _cmd_db_show(args: argparse.Namespace) -> int:
    from dep_audit.db import get_entry_path, get_junk_entry

    entry = get_junk_entry(args.ecosystem, args.package)
    if entry is None:
        logger.error("No entry found for %s in %s", args.package, args.ecosystem)
        return 1

    # Pretty-print the TOML content
    path = get_entry_path(args.ecosystem, args.package)
    if path.exists():
        print(path.read_text(encoding="utf-8"))
    else:
        import json
        print(json.dumps(entry, indent=2, default=str))

    return 0


def _cmd_db_export(args: argparse.Namespace) -> int:
    from dep_audit.generate import discover_new, format_toml_entry
    from dep_audit.github import is_github_target

    ecosystem = args.ecosystem

    # Run a scan to get classifications
    if is_github_target(args.path):
        from dep_audit.scanner import scan_remote
        results = scan_remote(
            repo_url=args.path,
            ref=args.ref,
            ecosystem=ecosystem,
            offline=args.offline,
        )
    else:
        from dep_audit.scanner import scan
        project_root = Path(args.path).resolve()
        if not project_root.is_dir():
            logger.error("Error: %s is not a directory", project_root)
            return 1
        results = scan(
            project_root=project_root,
            ecosystem=ecosystem,
            offline=args.offline,
        )

    if not results:
        logger.error("No results — nothing to export.")
        return 1

    total_discovered = 0
    for result in results:
        discovered = discover_new(result.classifications, result.ecosystem)
        if not discovered:
            continue

        total_discovered += len(discovered)

        # Print TOML to stdout for review
        for c in discovered:
            print(f"# --- {c.name} ---")
            print(format_toml_entry(c, result.ecosystem))

    if total_discovered == 0:
        logger.info("  No new entries discovered.")
    else:
        logger.info("\n  %d entries found.", total_discovered)
        logger.info(
            "\n  NOTE: each block above is a separate TOML file, not one combined document.\n"
            "  TOML has no multi-document separator, so the combined output is not valid TOML.\n"
            "  Save each # --- <name> --- block as its own file:\n"
            "    src/dep_audit/db/%s/<name>.toml",
            ecosystem,
        )

    return 0


def _cmd_cache(args: argparse.Namespace) -> int:
    from dep_audit import cache

    if args.cache_command == "clear":
        cache.clear()
        print("Cache cleared.")
        return 0
    else:
        logger.error("Usage: dep-audit cache {clear}")
        return 1


if __name__ == "__main__":
    sys.exit(main())
