"""tunectl CLI — Python wrapper for the tunectl VPS tuning toolkit.

Provides 6 subcommands that delegate to the corresponding bash scripts:
  discover  — Detect system environment and output JSON profile
  plan      — Show dry-run tuning plan for a tier (no changes made)
  apply     — Apply tuning changes for a tier (requires root)
  rollback  — Restore system config from backup (requires root)
  audit     — Verify applied tuning against the manifest
  benchmark — Run performance benchmarks (sysbench + fio)

Exit codes: 0=success, 1=operational failure, 2=usage error.
"""

import argparse
import json
import os
import subprocess
import sys

from tunectl import __version__
from tunectl.ui import print_banner, Pipeline

# Valid tier choices for plan/apply
VALID_TIERS = ("conservative", "balanced", "aggressive")

# Resolve the scripts/ directory — supports both dev mode (repo checkout)
# and installed mode (pip install bundles scripts as package_data).
_PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
_PROJECT_ROOT = os.path.dirname(_PACKAGE_DIR)

# Installed mode: scripts live inside the package at tunectl/scripts/
_INSTALLED_SCRIPTS_DIR = os.path.join(_PACKAGE_DIR, "scripts")
# Dev mode: scripts live at the project root at scripts/
_DEV_SCRIPTS_DIR = os.path.join(_PROJECT_ROOT, "scripts")

# Pick whichever directory actually contains the scripts
_SCRIPTS_DIR = (
    _INSTALLED_SCRIPTS_DIR
    if os.path.isdir(_INSTALLED_SCRIPTS_DIR)
    else _DEV_SCRIPTS_DIR
)


def _script_path(name: str) -> str:
    """Return the absolute path to a script in the scripts/ directory."""
    return os.path.join(_SCRIPTS_DIR, name)


def _run_script(script_name: str, args: list[str] | None = None,
                 capture: bool = False) -> int | tuple[int, str, str]:
    """Run a bash script and return its exit code.

    When *capture* is False (default), stdout/stderr pass through to the
    caller and only the exit code is returned.

    When *capture* is True, stdout and stderr are captured and the return
    value is a tuple ``(exit_code, stdout, stderr)``.
    """
    script = _script_path(script_name)
    if not os.path.isfile(script):
        print(f"Error: Script not found: {script}", file=sys.stderr)
        return (1, "", "") if capture else 1

    cmd = ["bash", script]
    if args:
        cmd.extend(args)

    if capture:
        result = subprocess.run(cmd, capture_output=True, text=True)
        return result.returncode, result.stdout, result.stderr

    result = subprocess.run(cmd)
    return result.returncode


# -------------------------------------------------------
# ANSI color helpers
# -------------------------------------------------------

def _use_color() -> bool:
    """Return True when stdout is a TTY and colors should be used."""
    return sys.stdout.isatty()


def _c(code: str, text: str) -> str:
    """Wrap *text* in an ANSI color escape if colors are enabled."""
    if not _use_color():
        return text
    return f"\033[{code}m{text}\033[0m"


def _bold(text: str) -> str:
    return _c("1", text)


def _cyan(text: str) -> str:
    return _c("36", text)


def _green(text: str) -> str:
    return _c("32", text)


def _yellow(text: str) -> str:
    return _c("33", text)


def _dim(text: str) -> str:
    return _c("2", text)


# -------------------------------------------------------
# Formatted discover output
# -------------------------------------------------------

def _format_discover_output(data: dict) -> str:
    """Render the discover JSON as a human-readable summary."""
    lines: list[str] = []

    # --- System Info header ---
    lines.append(_bold("System Information"))
    lines.append(_dim("─" * 40))

    info_fields = [
        ("OS", data.get("os_version", "unknown")),
        ("Kernel", data.get("kernel_version", "unknown")),
        ("RAM", f"{data.get('ram_mb', 0)} MB"),
        ("CPUs", str(data.get("cpu_count", 0))),
        ("Disk type", data.get("disk_type", "unknown")),
        ("Virt type", data.get("virt_type", "unknown")),
    ]
    for label, value in info_fields:
        padded = f"{label + ':':>28}"
        lines.append(f"  {_cyan(padded)}  {value}")

    # --- Swap status ---
    lines.append("")
    lines.append(_bold("Swap Status"))
    lines.append(_dim("─" * 40))

    swap_configured = data.get("swap_configured", False)
    swap_type = data.get("swap_type", "none")
    swap_total = data.get("swap_total_mb", 0)

    if swap_configured:
        padded = f"{'Configured:':>28}"
        lines.append(f"  {_cyan(padded)}  {_green('yes')}")
        padded = f"{'Type:':>28}"
        lines.append(f"  {_cyan(padded)}  {swap_type}")
        padded = f"{'Total:':>28}"
        lines.append(f"  {_cyan(padded)}  {swap_total} MB")
    else:
        padded = f"{'Configured:':>28}"
        lines.append(f"  {_cyan(padded)}  {_yellow('no')}")

    # --- Key sysctl values ---
    sysctl = data.get("sysctl_values", {})
    if sysctl:
        lines.append("")
        lines.append(_bold("Key sysctl Values"))
        lines.append(_dim("─" * 40))

        # Show a curated selection of the most important params
        key_params = [
            "vm.swappiness",
            "vm.dirty_ratio",
            "vm.dirty_background_ratio",
            "vm.vfs_cache_pressure",
            "vm.min_free_kbytes",
            "vm.overcommit_memory",
            "vm.max_map_count",
            "net.core.rmem_max",
            "net.core.wmem_max",
            "net.core.somaxconn",
            "net.core.default_qdisc",
            "net.ipv4.tcp_congestion_control",
            "net.ipv4.ip_local_port_range",
            "fs.inotify.max_user_watches",
        ]
        for param in key_params:
            if param in sysctl:
                padded = f"{param + ':':>42}"
                lines.append(f"  {_cyan(padded)}  {sysctl[param]}")

    # --- Mount options ---
    mount_opts = data.get("mount_options", "")
    if mount_opts and mount_opts != "unknown":
        lines.append("")
        lines.append(_bold("Root Mount Options"))
        lines.append(_dim("─" * 40))
        lines.append(f"  {mount_opts}")

    return "\n".join(lines)


# -------------------------------------------------------
# Interactive tier selection
# -------------------------------------------------------

# Entry counts per tier (from tune-manifest.json tiered filtering)
_TIER_INFO = {
    "conservative": {"entries": 51, "desc": "safe, minimal changes"},
    "balanced":     {"entries": 80, "desc": "recommended balance"},
    "aggressive":   {"entries": 86, "desc": "maximum performance"},
}


def _detect_ram_mb() -> int:
    """Run discover.sh and extract RAM in MB from the JSON output."""
    rc, stdout, _stderr = _run_script("discover.sh", capture=True)
    if rc != 0:
        return 0
    try:
        data = json.loads(stdout)
        return int(data.get("ram_mb", 0))
    except (json.JSONDecodeError, ValueError, TypeError):
        return 0


def _recommend_tier(ram_mb: int) -> str:
    """Return the recommended tier name based on system RAM."""
    if ram_mb < 2048:
        return "conservative"
    elif ram_mb <= 8192:
        return "balanced"
    else:
        return "aggressive"


def _select_tier_interactive() -> str:
    """Auto-discover system RAM and present an interactive tier menu.

    Returns the selected tier string.  Uses only ``input()`` for prompts
    (no external dependencies).
    """
    ram_mb = _detect_ram_mb()
    recommended = _recommend_tier(ram_mb)

    # Map tier names to a stable ordered list
    tier_order = ["conservative", "balanced", "aggressive"]
    rec_idx = tier_order.index(recommended)  # 0-based
    default_choice = rec_idx + 1             # 1-based for display

    # Header
    if ram_mb > 0:
        print(f"\nSystem RAM: {ram_mb} MB\n")
    else:
        print()

    print("Select tuning tier:")
    for i, tier in enumerate(tier_order, start=1):
        info = _TIER_INFO[tier]
        label = f"{tier:14s} ({info['entries']} entries - {info['desc']})"
        if tier == recommended:
            label = f"{tier:14s} ({info['entries']} entries - recommended for this system)"
        print(f"  {i}) {label}")

    # Prompt
    print()
    raw = input(f"Enter choice [1-3] (default: {default_choice}): ").strip()

    if raw == "":
        chosen = default_choice
    else:
        try:
            chosen = int(raw)
        except ValueError:
            chosen = default_choice

    if chosen < 1 or chosen > 3:
        chosen = default_choice

    selected = tier_order[chosen - 1]
    print(f"\n→ Selected tier: {selected}\n")
    return selected


# -------------------------------------------------------
# Subcommand handlers
# -------------------------------------------------------

def cmd_discover(args: argparse.Namespace) -> int:
    """Run discover.sh and show formatted system info."""
    rc, stdout, stderr = _run_script("discover.sh", capture=True)

    # --json: emit raw JSON (no banner/pipeline)
    if getattr(args, "json", False):
        if rc != 0:
            if stderr:
                print(stderr, end="", file=sys.stderr)
        print(stdout, end="")
        return rc

    with Pipeline("Discover") as p:
        if rc != 0:
            if stderr:
                p.fail(stderr.strip().split("\n")[0])
            return rc

        try:
            data = json.loads(stdout)
        except json.JSONDecodeError as exc:
            p.fail(f"Failed to parse discover output: {exc}")
            print(stdout, end="")
            return 1

        p.step(f"OS: {data.get('os_version', 'unknown')}")
        p.step(f"Kernel: {data.get('kernel_version', 'unknown')}")
        p.step(f"RAM: {data.get('ram_mb', 0)} MB ({data.get('cpu_count', 0)} CPUs)")

        swap = data.get("swap_configured", False)
        if swap:
            p.step(f"Swap: {data.get('swap_type', 'unknown')} ({data.get('swap_total_mb', 0)} MB)")
        else:
            p.step("Swap: not configured")

        p.step(f"Disk: {data.get('disk_type', 'unknown')} ({data.get('virt_type', 'unknown')})")
        p.ok("Discovery complete")

    return 0


def cmd_plan(args: argparse.Namespace) -> int:
    """Run tune.sh in dry-run mode for the specified tier."""
    if args.tier is None:
        args.tier = _select_tier_interactive()
    with Pipeline(f"Plan ({args.tier})") as p:
        p.step(f"Previewing {args.tier} tier changes...")
    print()
    return _run_script("tune.sh", ["--dry-run", "--tier", args.tier])


def cmd_apply(args: argparse.Namespace) -> int:
    """Run tune.sh in apply mode for the specified tier."""
    if args.tier is None:
        args.tier = _select_tier_interactive()
    with Pipeline(f"Apply ({args.tier})") as p:
        p.step(f"Applying {args.tier} tier tuning...")
    print()
    return _run_script("tune.sh", ["--apply", "--tier", args.tier])


def cmd_rollback(args: argparse.Namespace) -> int:
    """Run rollback.sh to restore from backup."""
    script_args: list[str] = []
    if args.list:
        script_args.append("--list")
        with Pipeline("Rollback (list)") as p:
            p.step("Listing available backups...")
        print()
    elif args.backup:
        script_args.extend(["--backup", args.backup])
        with Pipeline(f"Rollback ({args.backup})") as p:
            p.step(f"Restoring from backup {args.backup}...")
        print()
    else:
        with Pipeline("Rollback (latest)") as p:
            p.step("Restoring from most recent backup...")
        print()
    return _run_script("rollback.sh", script_args)


def cmd_audit(args: argparse.Namespace) -> int:
    """Run audit.sh to verify applied tuning."""
    with Pipeline("Audit") as p:
        p.step("Verifying 86 manifest entries against live system...")
    print()
    return _run_script("audit.sh")


def cmd_benchmark(args: argparse.Namespace) -> int:
    """Run benchmark.sh for performance measurement."""
    script_args: list[str] = []
    if args.baseline:
        script_args.append("--baseline")
        label = "Benchmark (baseline)"
    elif args.compare:
        script_args.append("--compare")
        label = "Benchmark (compare)"
    else:
        label = "Benchmark"
    with Pipeline(label) as p:
        p.step("Running sysbench + fio benchmarks...")
    print()
    return _run_script("benchmark.sh", script_args)


# -------------------------------------------------------
# Tier validation helper
# -------------------------------------------------------

def _validate_tier(value: str) -> str:
    """Validate the --tier argument against allowed values."""
    if value not in VALID_TIERS:
        raise argparse.ArgumentTypeError(
            f"invalid tier '{value}'. Valid tiers: {', '.join(VALID_TIERS)}"
        )
    return value


# -------------------------------------------------------
# Argument parser construction
# -------------------------------------------------------

def build_parser() -> argparse.ArgumentParser:
    """Build and return the argument parser with all subcommands."""
    parser = argparse.ArgumentParser(
        prog="tunectl",
        description="tunectl — VPS Performance Tuning Toolkit",
        epilog=(
            "Workflow: discover → plan → apply → audit → benchmark\n"
            "Run 'tunectl <command> --help' for command-specific options."
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "--version",
        action="version",
        version=f"tunectl {__version__}",
    )

    subparsers = parser.add_subparsers(
        title="commands",
        dest="command",
        metavar="<command>",
    )

    # --- discover ---
    sub_discover = subparsers.add_parser(
        "discover",
        help="Detect system environment and show formatted summary",
        description=(
            "Run environment discovery. Detects OS, kernel, RAM, CPU, disk type,\n"
            "swap configuration, and current sysctl values. Shows a formatted,\n"
            "colored summary by default. Use --json for machine-readable output.\n"
            "Read-only — no system modifications. Works without root."
        ),
    )
    sub_discover.add_argument(
        "--json",
        action="store_true",
        default=False,
        help="Output raw JSON instead of formatted summary",
    )
    sub_discover.set_defaults(func=cmd_discover)

    # --- plan ---
    sub_plan = subparsers.add_parser(
        "plan",
        help="Show dry-run tuning plan for a tier (no changes made)",
        description=(
            "Display a read-only plan of all tuning changes for the selected tier.\n"
            "No system files are modified. Works without root."
        ),
    )
    sub_plan.add_argument(
        "--tier",
        type=_validate_tier,
        required=False,
        default=None,
        metavar="TIER",
        help="Tuning tier: conservative, balanced, or aggressive (interactive if omitted)",
    )
    sub_plan.set_defaults(func=cmd_plan)

    # --- apply ---
    sub_apply = subparsers.add_parser(
        "apply",
        help="Apply tuning changes for a tier (requires root)",
        description=(
            "Apply tuning changes for the selected tier. Creates a timestamped\n"
            "backup before making any modifications. Requires root privileges."
        ),
    )
    sub_apply.add_argument(
        "--tier",
        type=_validate_tier,
        required=False,
        default=None,
        metavar="TIER",
        help="Tuning tier: conservative, balanced, or aggressive (interactive if omitted)",
    )
    sub_apply.set_defaults(func=cmd_apply)

    # --- rollback ---
    sub_rollback = subparsers.add_parser(
        "rollback",
        help="Restore system config from backup (requires root)",
        description=(
            "Restore system configuration from a previous backup created by\n"
            "'tunectl apply'. Uses the most recent backup by default.\n"
            "Requires root for restore. Use --list to view backups without root."
        ),
    )
    rollback_group = sub_rollback.add_mutually_exclusive_group()
    rollback_group.add_argument(
        "--list",
        action="store_true",
        help="List available backups (does not require root)",
    )
    rollback_group.add_argument(
        "--backup",
        metavar="TIMESTAMP",
        help="Restore from a specific backup timestamp",
    )
    sub_rollback.set_defaults(func=cmd_rollback)

    # --- audit ---
    sub_audit = subparsers.add_parser(
        "audit",
        help="Verify applied tuning against the manifest",
        description=(
            "Run verification checks for all 86 manifest entries against live\n"
            "system state. Reports PASS/FAIL/SKIP per entry with a summary.\n"
            "Read-only — no system modifications. Works without root."
        ),
    )
    sub_audit.set_defaults(func=cmd_audit)

    # --- benchmark ---
    sub_benchmark = subparsers.add_parser(
        "benchmark",
        help="Run performance benchmarks (sysbench + fio)",
        description=(
            "Run CPU, memory, and disk I/O benchmarks using sysbench and fio.\n"
            "Supports baseline capture and before/after comparison.\n"
            "Works without root."
        ),
    )
    bench_group = sub_benchmark.add_mutually_exclusive_group()
    bench_group.add_argument(
        "--baseline",
        action="store_true",
        help="Save results as baseline for later comparison",
    )
    bench_group.add_argument(
        "--compare",
        action="store_true",
        help="Compare current results against saved baseline",
    )
    sub_benchmark.set_defaults(func=cmd_benchmark)

    return parser


# -------------------------------------------------------
# Entry point
# -------------------------------------------------------

def main() -> None:
    """Main entry point for the tunectl CLI."""
    parser = build_parser()
    args = parser.parse_args()

    if args.command is None:
        print_banner()
        parser.print_help()
        sys.exit(2)

    exit_code = args.func(args)
    sys.exit(exit_code)


if __name__ == "__main__":
    main()
