"""commands — CLI command handlers for the gatehouse command.

Each public function implements one CLI subcommand and receives the parsed
``argparse.Namespace`` object.  Shared library modules handle configuration,
theming, and YAML I/O.  The ``cmd_new_rule`` handler is re-exported from
:mod:`gatehouse.cli.wizard`.

Subcommands:
    init          Initialize a project with .gate_schema.yaml.
    list-rules    List available rules, optionally filtered by schema.
    test-rule     Test a single rule against a file.
    disable-rule  Disable a rule via project-local override.
    enable-rule   Re-enable a previously disabled rule.
    status        Show current enforcement status.
    activate      Print shell commands to activate Gatehouse.
    deactivate    Print shell commands to deactivate Gatehouse.
    lint-rules    Validate all rule YAML files for correctness.
"""

from __future__ import annotations

import argparse
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any

from gatehouse._paths import (
    get_gate_home as _get_gate_home,
    rules_dir as _rules_dir,
    schemas_dir as _schemas_dir,
)
from gatehouse.cli.wizard import cmd_new_rule  # noqa: F401 — re-exported
from gatehouse.lib import config
from gatehouse.lib.theme import colorize
from gatehouse.lib.yaml_loader import load_yaml


# -------------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------------


def _color(text: str, role: str) -> str:
    """Colorize text for stdout so terminal output respects the active theme."""
    return colorize(text, role, stream=sys.stdout)


# -------------------------------------------------------------------------
# Project commands
# -------------------------------------------------------------------------


def cmd_init(args: argparse.Namespace) -> None:
    """Initialize a project with a ``.gate_schema.yaml`` configuration file.

    Create a minimal project config that references the chosen schema.  If the
    file already exists the user is prompted before overwriting.  This command
    is interactive when an existing config is detected.

    Args:
        args: Parsed CLI arguments.  Uses ``args.schema`` for the schema name.
    """
    default_schema = config.get_str("defaults.schema_name")
    project_cfg_name = config.get_str("filenames.project_config")
    err_color = config.get_str("colors.error")
    ok_color = config.get_str("colors.success")
    confirm_char = config.get_str("cli.confirm_char")
    abort_msg = config.get_str("messages.aborted")
    log_dir = config.get_str("defaults.log_directory")
    ext = config.get_str("filenames.schema_extension")

    schema_name: str = args.schema or default_schema

    # 1. Validate that the requested schema exists
    schema_path = _schemas_dir() / f"{schema_name}{ext}"
    if not schema_path.is_file():
        print(_color(f"Error: Schema '{schema_name}' not found at {schema_path}", err_color))
        print("Available schemas:")
        sd = _schemas_dir()
        if sd.is_dir():
            for fname in sorted(os.listdir(sd)):
                if fname.endswith(ext):
                    print(f"  - {fname[:-len(ext)]}")
        sys.exit(1)

    # 2. Check for existing config and prompt before overwriting
    config_path = os.path.join(os.getcwd(), project_cfg_name)
    if os.path.exists(config_path):
        answer = input(
            f"{project_cfg_name} already exists. Overwrite? [y/N]: "
        ).strip().lower()
        if answer != confirm_char:
            print(abort_msg)
            return

    # 3. Generate config file content
    content = (
        f"# {project_cfg_name} — auto-generated by "
        f"'gatehouse init --schema {schema_name}'\n"
        f"\n"
        f'schema: "{schema_name}"\n'
        f"\n"
        f"rule_overrides: {{}}\n"
        f"\n"
        f"logging:\n"
        f"  enabled: true\n"
        f'  directory: "{log_dir}"\n'
    )

    # 4. Write config to disk
    with open(config_path, "w", encoding="utf-8") as fh:
        fh.write(content)

    # 5. Print confirmation summary
    gate_home = str(_get_gate_home())
    print(_color(f"\u2713 Created {project_cfg_name} (schema: {schema_name})", ok_color))
    print(f"  The gate is now active for this project.")
    print(f"  Rules are loaded from: {gate_home}")


# -------------------------------------------------------------------------
# Rule management commands
# -------------------------------------------------------------------------


def cmd_list_rules(args: argparse.Namespace) -> None:
    """List available rules, optionally filtered by schema.

    When ``--schema`` is given, display only the rules included in that schema
    together with their severity and enabled state.  Otherwise list every rule
    file found in the rules directory.

    Args:
        args: Parsed CLI arguments.  Uses ``args.schema`` for optional filter.
    """
    rd = _rules_dir()
    hline = config.get_str("formatting.horizontal_line_char")
    ext = config.get_str("filenames.rule_extension")
    err_color = config.get_str("colors.error")
    default_severity = config.get_str("defaults.severity")
    sev_block = config.get_str("severities.block")
    lbl_block = config.get_str("labels.status_block")
    lbl_warn = config.get_str("labels.status_warn")
    sep_w = config.get_int("formatting.rule_list_separator_width")

    if args.schema:
        schema_file = _schemas_dir() / f"{args.schema}{ext}"
        if not schema_file.is_file():
            print(_color(f"Schema '{args.schema}' not found at {schema_file}", err_color))
            sys.exit(1)
        schema_data = load_yaml(str(schema_file))
        if not schema_data:
            print(_color(f"Schema '{args.schema}' is empty.", err_color))
            sys.exit(1)

        print(f"\nRules in schema '{args.schema}':")
        print(f"{hline * sep_w}")

        rules_list = schema_data.get("rules", [])
        for entry in rules_list:
            if isinstance(entry, str):
                entry = {"id": entry}
            rule_id = entry.get("id", "")
            severity = entry.get("severity", "")
            enabled = entry.get("enabled", True)

            rule_path = rd / f"{rule_id}{ext}"
            desc = ""
            if rule_path.is_file():
                rule_data = load_yaml(str(rule_path))
                desc = rule_data.get("description", "")
                if not severity:
                    severity = rule_data.get("defaults", {}).get(
                        "severity", default_severity
                    )

            lbl_off = config.get_str("labels.status_off")
            status = (
                _color(lbl_off, "dim") if not enabled
                else (
                    _color(lbl_block, config.get_str("colors.block"))
                    if severity == sev_block
                    else _color(lbl_warn, config.get_str("colors.warn"))
                )
            )
            print(f"  {status:>20s}  {rule_id:<30s}  {desc}")
    else:
        if not rd.is_dir():
            print("No rules directory found.")
            return

        print(f"\nAvailable rules ({rd}):")
        print(f"{hline * sep_w}")

        for filename in sorted(os.listdir(rd)):
            if not filename.endswith(ext):
                continue
            rule_id = filename[:-len(ext)]
            rule_data = load_yaml(str(rd / filename))
            desc = rule_data.get("description", "")
            severity = rule_data.get("defaults", {}).get(
                "severity", default_severity
            )
            status = (
                _color(lbl_block, config.get_str("colors.block"))
                if severity == sev_block
                else _color(lbl_warn, config.get_str("colors.warn"))
            )
            print(f"  {status:>20s}  {rule_id:<30s}  {desc}")

    print()


def cmd_test_rule(args: argparse.Namespace) -> None:
    """Test a single rule against a Python file and report pass/fail.

    Build a temporary single-rule schema, invoke the gate engine in a
    subprocess, and print the result.  Uses a temp directory so the
    installed schemas directory is never modified.

    Args:
        args: Parsed CLI arguments.  Uses ``args.rule_id`` and ``args.file``.
    """
    gate_home = str(_get_gate_home())
    rule_id: str = args.rule_id
    filepath: str = args.file
    ext = config.get_str("filenames.rule_extension")
    err_color = config.get_str("colors.error")
    ok_color = config.get_str("colors.success")
    engine_mod = config.get_str("cli.engine_module")
    test_schema_name = config.get_str("cli.test_schema_name")
    test_schema_ver = config.get_str("cli.test_schema_version")

    if not os.path.isfile(filepath):
        print(_color(f"File not found: {filepath}", err_color))
        sys.exit(1)

    rule_path = _rules_dir() / f"{rule_id}{ext}"
    if not rule_path.is_file():
        print(_color(f"Rule not found: {rule_id}", err_color))
        sys.exit(1)

    with tempfile.TemporaryDirectory() as tmp_dir:
        tmp_path = Path(tmp_dir)

        tmp_rules = tmp_path / config.get_str("directories.rules")
        tmp_schemas = tmp_path / config.get_str("directories.schemas")
        tmp_rules.mkdir()
        tmp_schemas.mkdir()

        shutil.copy(str(rule_path), str(tmp_rules / f"{rule_id}{ext}"))

        test_schema = tmp_schemas / f"{test_schema_name}{ext}"
        test_schema.write_text(
            'schema:\n'
            f'  name: "{test_schema_name}"\n'
            f'  version: "{test_schema_ver}"\n'
            'scope:\n'
            '  gated_paths: [""]\n'
            'rules:\n'
            f'  - id: "{rule_id}"\n',
            encoding="utf-8",
        )

        test_config = tmp_path / "config.yaml"
        test_config.write_text(
            f'schema: "{test_schema_name}"\n'
            'logging:\n'
            '  enabled: false\n',
            encoding="utf-8",
        )

        result = subprocess.run(
            [
                sys.executable, "-m", engine_mod,
                "--file", filepath,
                "--schema", str(test_config),
            ],
            capture_output=True,
            text=True,
            env={
                **os.environ,
                config.get_str("env_vars.gate_home"): str(tmp_path),
            },
        )

    if result.returncode == 0:
        print(_color(f"\u2713 {filepath} passes rule '{rule_id}'", ok_color))
    else:
        print(_color(f"\u2717 {filepath} violates rule '{rule_id}':", err_color))
        print(result.stderr)
        sys.exit(result.returncode)


def cmd_disable_rule(args: argparse.Namespace) -> None:
    """Disable a rule by adding a severity-off override to the project config.

    Write a ``rule_overrides`` entry to the project-local
    ``.gate_schema.yaml``.  Installed package files are never modified.

    Args:
        args: Parsed CLI arguments.  Uses ``args.rule_id``.
    """
    rule_id: str = args.rule_id
    project_cfg_name = config.get_str("filenames.project_config")
    sev_off = config.get_str("severities.off")
    err_color = config.get_str("colors.error")
    ok_color = config.get_str("colors.success")

    import yaml

    local_schema = Path.cwd() / project_cfg_name
    if not local_schema.exists():
        print(_color(
            f"No {project_cfg_name} found. Run 'gatehouse init' first.",
            err_color,
        ))
        sys.exit(1)

    schema_data: dict[str, Any] = load_yaml(str(local_schema)) or {}
    schema_data.setdefault("rule_overrides", {})
    schema_data["rule_overrides"][rule_id] = {"severity": sev_off}

    with open(str(local_schema), "w", encoding="utf-8") as fh:
        yaml.dump(schema_data, fh, default_flow_style=False, sort_keys=False)

    print(_color(
        f"\u2713 Disabled rule '{rule_id}' in {project_cfg_name} (project-local)",
        ok_color,
    ))


def cmd_enable_rule(args: argparse.Namespace) -> None:
    """Re-enable a previously disabled rule by removing its override.

    Delete the matching ``rule_overrides`` entry from the project-local
    ``.gate_schema.yaml`` so the schema default takes effect again.

    Args:
        args: Parsed CLI arguments.  Uses ``args.rule_id``.
    """
    rule_id: str = args.rule_id
    project_cfg_name = config.get_str("filenames.project_config")
    err_color = config.get_str("colors.error")
    ok_color = config.get_str("colors.success")

    import yaml

    local_schema = Path.cwd() / project_cfg_name
    if not local_schema.exists():
        print(_color(
            f"No {project_cfg_name} found. Run 'gatehouse init' first.",
            err_color,
        ))
        sys.exit(1)

    schema_data: dict[str, Any] = load_yaml(str(local_schema)) or {}
    overrides: dict[str, Any] = schema_data.get("rule_overrides", {})

    if rule_id not in overrides:
        print(_color(f"Rule '{rule_id}' has no override to remove.", "dim"))
        return

    del overrides[rule_id]

    with open(str(local_schema), "w", encoding="utf-8") as fh:
        yaml.dump(schema_data, fh, default_flow_style=False, sort_keys=False)

    print(_color(
        f"\u2713 Enabled rule '{rule_id}' (override removed from {project_cfg_name})",
        ok_color,
    ))


# -------------------------------------------------------------------------
# Status commands
# -------------------------------------------------------------------------


def _print_verbose_status(
    rules_dir: Path, schemas_dir: Path, ext: str
) -> None:
    """Print verbose details so the user can diagnose configuration problems.

    Called by ``cmd_status`` when the ``--verbose`` flag is set to reveal
    resolved rules, schemas, and scope config.

    Args:
        rules_dir: Path to the rules directory.
        schemas_dir: Path to the schemas directory.
        ext: Rule/schema file extension.
    """
    sep_w = config.get_int("formatting.status_separator_width")
    hline = config.get_str("formatting.horizontal_line_char")
    ok_color = config.get_str("colors.success")

    print(f"\n  {_color('Resolved Rules:', 'bold')}")
    if rules_dir.is_dir():
        rule_files = sorted(
            f for f in os.listdir(rules_dir) if f.endswith(ext)
        )
        if rule_files:
            for rf in rule_files:
                rule_data = load_yaml(str(rules_dir / rf))
                name = rule_data.get("name", rf)
                sev = rule_data.get("defaults", {}).get("severity", "?")
                enabled = rule_data.get("defaults", {}).get("enabled", True)
                state = _color("on", ok_color) if enabled else _color("off", "dim")
                print(f"    {rf:<30s}  sev={sev:<6s}  {state}  ({name})")
        else:
            print(f"    {_color('(no rule files)', 'dim')}")
    else:
        print(f"    {_color('(rules dir missing)', 'dim')}")

    print(f"\n  {_color('Schemas:', 'bold')}")
    if schemas_dir.is_dir():
        schema_files = sorted(
            f for f in os.listdir(schemas_dir) if f.endswith(ext)
        )
        if schema_files:
            for sf in schema_files:
                schema_data = load_yaml(str(schemas_dir / sf))
                rule_count = len(schema_data.get("rules", []))
                inherits = schema_data.get("extends") or schema_data.get("inherits", "")
                extra = f" (inherits: {inherits})" if inherits else ""
                print(f"    {sf:<30s}  {rule_count} rules{extra}")
        else:
            print(f"    {_color('(no schema files)', 'dim')}")
    else:
        print(f"    {_color('(schemas dir missing)', 'dim')}")

    print(f"  {hline * sep_w}")


def cmd_status(args: argparse.Namespace) -> None:
    """Display the current Gatehouse enforcement status dashboard.

    Print mode, home directory, gate script location, rule/schema availability,
    and project-level overrides.  When ``--verbose`` is passed, delegate to
    ``_print_verbose_status`` for expanded detail.

    Args:
        args: Parsed CLI arguments.  Uses ``args.verbose`` for detail level.
    """
    gate_home = str(_get_gate_home())
    env_mode = config.get_str("env_vars.mode")
    env_gate_home = config.get_str("env_vars.gate_home")
    mode_off = config.get_str("modes.off")
    mode_hard = config.get_str("modes.hard")
    mode_soft = config.get_str("modes.soft")
    project_cfg_name = config.get_str("filenames.project_config")
    ext = config.get_str("filenames.schema_extension")
    sep_w = config.get_int("formatting.status_separator_width")
    hline = config.get_str("formatting.horizontal_line_char")
    lbl_header = config.get_str("labels.status_header")
    lbl_found = config.get_str("labels.found")
    lbl_not_found = config.get_str("labels.not_found")
    lbl_custom_sev = config.get_str("labels.custom_severity")
    lbl_unknown = config.get_str("labels.unknown_schema")
    ok_color = config.get_str("colors.success")
    err_color = config.get_str("colors.error")
    mc = config.get("mode_colors")
    ml = config.get("mode_labels")
    home_auto = config.get_str("messages.home_auto")
    home_env = config.get_str("messages.home_env")

    mode = os.environ.get(env_mode, mode_off).lower()
    if mode not in (mode_hard, mode_soft, mode_off):
        mode = mode_off

    print()
    print(f"  {_color(lbl_header, 'bold')}")
    print(f"  {hline * sep_w}")
    print(f"  Mode:      {_color(ml[mode], mc[mode])}")
    home_source = home_auto if not os.environ.get(env_gate_home) else home_env
    print(f"  Home:      {_color(gate_home, 'cyan')} {home_source}")

    gate_path = _get_gate_home() / "python_gate"
    gate_on_path = shutil.which("python_gate")
    if gate_path.is_file():
        print(f"  Gate:      {_color(lbl_found, ok_color)} ({gate_path})")
    elif gate_on_path:
        print(f"  Gate:      {_color(lbl_found, ok_color)} ({gate_on_path})")
    else:
        print(f"  Gate:      {_color(lbl_not_found, err_color)}")

    rd = _rules_dir()
    sd = _schemas_dir()
    rules_ok = rd.is_dir() and any(f.endswith(ext) for f in os.listdir(rd))
    schemas_ok = sd.is_dir() and any(f.endswith(ext) for f in os.listdir(sd))
    print(
        f"  Rules:     "
        f"{_color(lbl_found, ok_color) if rules_ok else _color(lbl_not_found, err_color)} "
        f"({rd})"
    )
    print(
        f"  Schemas:   "
        f"{_color(lbl_found, ok_color) if schemas_ok else _color(lbl_not_found, err_color)} "
        f"({sd})"
    )

    schema_path = os.path.join(os.getcwd(), project_cfg_name)
    if os.path.isfile(schema_path):
        project_config = load_yaml(schema_path)
        schema_name = project_config.get("schema", lbl_unknown)
        print(f"  Project:   {_color(schema_name, 'cyan')} ({project_cfg_name} found)")

        overrides: dict[str, Any] = project_config.get("rule_overrides", {})
        if overrides:
            print(f"  Overrides:")
            for rid, ovr in overrides.items():
                sev = (
                    ovr.get("severity", lbl_custom_sev)
                    if isinstance(ovr, dict)
                    else str(ovr)
                )
                print(f"    {rid}: {_color(sev, 'yellow')}")
    else:
        print(
            f"  Project:   "
            f"{_color(f'no {project_cfg_name} in current directory', 'dim')}"
        )

    print(f"  {hline * sep_w}")

    if getattr(args, "verbose", False):
        _print_verbose_status(rd, sd, ext)

    shell_activate = config.get_str("shell_commands.activate")
    shell_deactivate = config.get_str("shell_commands.deactivate")
    if mode == mode_off:
        print(
            f"  {_color('To activate:', 'dim')} "
            f"{shell_activate.format(mode=mode_hard)}"
        )
    else:
        print(f"  {_color('To deactivate:', 'dim')} {shell_deactivate}")

    print()


# -------------------------------------------------------------------------
# Activation commands
# -------------------------------------------------------------------------


def cmd_activate(args: argparse.Namespace) -> None:
    """Print shell commands the user should run to activate Gatehouse.

    Output includes the environment variable export and a python alias.
    The user copies and pastes the printed commands into their shell.

    Args:
        args: Parsed CLI arguments.  Uses ``args.mode`` (hard or soft).
    """
    mode_hard = config.get_str("modes.hard")
    mode_soft = config.get_str("modes.soft")
    err_color = config.get_str("colors.error")
    invalid_msg = config.get_str("messages.invalid_mode")
    shell_cmd = config.get_str("shell_commands.activate")
    alias_cmd = config.get_str("shell_commands.alias")
    persist_hint = config.get_str("messages.persist_hint")
    ml = config.get("mode_labels")

    mode: str = args.mode or mode_hard
    if mode not in (mode_hard, mode_soft):
        print(_color(f"  Mode must be {invalid_msg}.", err_color))
        sys.exit(1)

    gate_on_path = shutil.which("python_gate")

    print()
    print(f"  {_color('Run these commands in your shell:', 'bold')}")
    print()
    print(f"    {shell_cmd.format(mode=mode)}")
    if gate_on_path:
        print(f"    {alias_cmd}")
    else:
        gate_path = _get_gate_home() / "python_gate"
        print(f'    alias python="{gate_path}"')
    print()

    color = "yellow" if mode == mode_soft else "red"
    print(f"  {_color(ml[mode], color)}")
    print()
    print(f"  {_color(f'Add to {persist_hint} to persist across sessions.', 'dim')}")
    print()


def cmd_deactivate(args: argparse.Namespace) -> None:
    """Print the shell command the user should run to deactivate Gatehouse.

    Output includes the unset command for the environment variable.

    Args:
        args: Parsed CLI arguments (unused but required by dispatch).
    """
    shell_cmd = config.get_str("shell_commands.deactivate")
    info_msg = config.get_str("messages.deactivate_info")

    print()
    print(f"  {_color('Run this command in your shell:', 'bold')}")
    print()
    print(f"    {shell_cmd}")
    print()
    print(f"  {_color(info_msg, 'dim')}")
    print()


# -------------------------------------------------------------------------
# Rule validation
# -------------------------------------------------------------------------


def cmd_lint_rules(args: argparse.Namespace) -> None:
    """Validate all rule YAML files for structural correctness.

    Scan every rule file in the rules directory and check for required
    top-level keys, check/error/defaults sections, and valid severity values.
    Exit with code 1 if any errors are found.

    Args:
        args: Parsed CLI arguments (unused but required by dispatch).
    """
    ext = config.get_str("filenames.schema_extension")
    ok_color = config.get_str("colors.success")
    err_color = config.get_str("colors.error")
    required_keys = config.get_list("lint_rules.required_keys")
    required_check_keys = config.get_list("lint_rules.required_check_keys")
    required_error_keys = config.get_list("lint_rules.required_error_keys")
    required_defaults_keys = config.get_list("lint_rules.required_defaults_keys")
    valid_severities = config.get_list("severities.valid_choices")

    rd = _rules_dir()
    if not rd.is_dir():
        print(_color("  No rules directory found.", err_color))
        sys.exit(1)

    rule_files = sorted(f for f in os.listdir(rd) if f.endswith(ext))
    if not rule_files:
        print(_color("  No rule files found.", err_color))
        sys.exit(1)

    total_errors = 0
    print()
    for rf in rule_files:
        errors: list[str] = []
        rule_data = load_yaml(str(rd / rf))

        if not isinstance(rule_data, dict):
            errors.append("File is not a YAML mapping")
        else:
            for key in required_keys:
                if key not in rule_data:
                    errors.append(f"Missing top-level key: '{key}'")

            check = rule_data.get("check", {})
            if isinstance(check, dict):
                for key in required_check_keys:
                    if key not in check:
                        errors.append(f"Missing check.{key}")

            err_section = rule_data.get("error", {})
            if isinstance(err_section, dict):
                for key in required_error_keys:
                    if key not in err_section:
                        errors.append(f"Missing error.{key}")

            defaults = rule_data.get("defaults", {})
            if isinstance(defaults, dict):
                for key in required_defaults_keys:
                    if key not in defaults:
                        errors.append(f"Missing defaults.{key}")
                sev = defaults.get("severity")
                if sev and sev not in valid_severities:
                    errors.append(
                        f"Invalid severity '{sev}' "
                        f"(valid: {', '.join(valid_severities)})"
                    )

        if errors:
            total_errors += len(errors)
            print(f"  {_color(rf, err_color)}")
            for e in errors:
                print(f"    - {e}")
        else:
            print(f"  {_color(rf, ok_color)} — valid")

    print()
    if total_errors:
        print(
            _color(
                f"  {total_errors} error(s) across {len(rule_files)} file(s)",
                err_color,
            )
        )
        sys.exit(1)
    else:
        print(
            _color(
                f"  All {len(rule_files)} rule(s) valid",
                ok_color,
            )
        )
