"""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__

# 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:
        lines.append(f"  {_cyan(label + ':'):>30s}  {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:
        lines.append(f"  {_cyan('Configured:'):>30s}  {_green('yes')}")
        lines.append(f"  {_cyan('Type:'):>30s}  {swap_type}")
        lines.append(f"  {_cyan('Total:'):>30s}  {swap_total} MB")
    else:
        lines.append(f"  {_cyan('Configured:'):>30s}  {_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:
                lines.append(f"  {_cyan(param + ':'):>48s}  {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)


# -------------------------------------------------------
# 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)

    if rc != 0:
        # Pass through errors from the script
        if stderr:
            print(stderr, end="", file=sys.stderr)
        if stdout:
            print(stdout, end="")
        return rc

    # --json: emit raw JSON
    if getattr(args, "json", False):
        print(stdout, end="")
        return 0

    # Parse JSON and render formatted summary
    try:
        data = json.loads(stdout)
    except json.JSONDecodeError as exc:
        print(f"Error: Failed to parse discover output: {exc}",
              file=sys.stderr)
        # Fall back to raw output so the user still sees something
        print(stdout, end="")
        return 1

    print(_format_discover_output(data))
    return 0


def cmd_plan(args: argparse.Namespace) -> int:
    """Run tune.sh in dry-run mode for the specified tier."""
    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."""
    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")
    elif args.backup:
        script_args.extend(["--backup", args.backup])
    return _run_script("rollback.sh", script_args)


def cmd_audit(args: argparse.Namespace) -> int:
    """Run audit.sh to verify applied tuning."""
    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")
    elif args.compare:
        script_args.append("--compare")
    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=True,
        metavar="TIER",
        help="Tuning tier: conservative, balanced, or aggressive",
    )
    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=True,
        metavar="TIER",
        help="Tuning tier: conservative, balanced, or aggressive",
    )
    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:
        parser.print_help()
        sys.exit(2)

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


if __name__ == "__main__":
    main()
