"""Bash intent classifier for dashclaw-agent-intel.

Classifies shell commands by intent, risk, and reversibility using
six validation submodules run as a pipeline.

Uses only the Python standard library + the sibling command_parser module.
"""

import re
from typing import Optional

from dashclaw_agent_intel.command_parser import parse_command

# ---------------------------------------------------------------------------
# Intent lookup tables
# ---------------------------------------------------------------------------

READONLY_COMMANDS = frozenset({
    "cat", "head", "tail", "less", "more", "wc", "file", "stat", "du", "df",
    "ls", "tree", "find", "locate", "which", "whereis", "type",
    "grep", "rg", "awk", "cut", "sort", "uniq", "diff", "comm",
    "echo", "printf", "date", "uname", "whoami", "pwd", "hostname",
    "ps", "top", "htop", "free", "uptime", "env", "printenv",
    "man", "help", "info",
    "sha256sum", "md5sum", "sha1sum", "cksum", "b2sum",
    "base64", "od", "xxd", "hexdump",
    "id", "groups", "lsof", "readlink", "realpath", "basename", "dirname",
    "test", "[", "true", "false", "seq", "yes", "tee",
    "xargs", "tr", "column", "fold", "expand", "unexpand",
    "sed",  # sed without -i is readonly (stdout only); sed_validation handles -i
})

GIT_READONLY_SUBCOMMANDS = frozenset({
    "status", "log", "diff", "show", "branch", "tag", "remote",
    "stash", "describe", "rev-parse", "rev-list", "blame",
    "ls-files", "ls-tree", "config", "reflog", "shortlog",
    "whatchanged", "name-rev", "verify-commit", "verify-tag",
})

GIT_WRITE_SUBCOMMANDS = frozenset({
    "add", "commit", "merge", "cherry-pick", "checkout", "switch",
    "restore", "mv", "rm", "rebase", "pull", "fetch", "stash",
    "push",  # push is write normally; destructive with --force
})

GIT_DESTRUCTIVE_SUBCOMMANDS = frozenset({
    "clean",  # clean -f
    "reset",  # reset --hard
})

WRITE_COMMANDS = frozenset({
    "cp", "mv", "mkdir", "touch", "chmod", "chown", "chgrp", "ln",
    "tee", "install", "patch", "rename",
})

DESTRUCTIVE_COMMANDS = frozenset({
    "rm", "rmdir", "shred", "mkfs", "fdisk", "dd", "truncate",
})

NETWORK_COMMANDS = frozenset({
    "curl", "wget", "ssh", "scp", "rsync", "ping", "traceroute",
    "dig", "nslookup", "host", "nc", "netcat", "telnet", "ftp", "sftp",
})

PROCESS_COMMANDS = frozenset({
    "kill", "killall", "pkill", "nohup", "bg", "fg", "crontab",
    "disown", "wait", "jobs",
})

PACKAGE_COMMANDS = frozenset({
    "npm", "yarn", "pnpm", "pip", "pip3", "pipx",
    "cargo", "go", "gem", "bundle",
    "brew", "apt", "apt-get", "dnf", "yum", "pacman", "snap", "flatpak",
    "composer", "dotnet", "nuget",
})

SYSTEM_ADMIN_COMMANDS = frozenset({
    "systemctl", "service", "journalctl",
    "useradd", "userdel", "usermod", "groupadd", "groupdel",
    "iptables", "ip6tables", "ufw", "firewall-cmd",
    "mount", "umount",
    "reboot", "shutdown", "poweroff", "halt", "init",
    "modprobe", "insmod", "rmmod",
    "sysctl", "ldconfig",
})

# System paths that workspace_write mode should warn about.
SYSTEM_PATHS = (
    "/etc/", "/usr/", "/var/", "/boot/", "/sys/", "/proc/",
    "/sbin/", "/lib/", "/lib64/", "/opt/",
    "/etc", "/usr", "/var", "/boot", "/sys", "/proc",
    "/sbin", "/lib", "/lib64", "/opt",
)

# Sensitive file patterns that boost risk.
SENSITIVE_PATTERNS = re.compile(
    r"(?:\.env|secret|credential|private_key|\.pem|id_rsa|\.key)",
    re.IGNORECASE,
)

# Fork bomb patterns.
_FORK_BOMB_RE = re.compile(r":\(\)\s*\{.*\}|/dev/null.*&\s*\|.*&|fork\s*bomb", re.IGNORECASE)

# Base risk scores by intent.
_RISK_BASE = {
    "readonly": 5,
    "write": 35,
    "destructive": 90,
    "network": 40,
    "process_management": 50,
    "package_management": 30,
    "system_admin": 75,
    "unknown": 20,
}


# ---------------------------------------------------------------------------
# Intent classification
# ---------------------------------------------------------------------------

def _classify_git(parsed: dict) -> str:
    """Classify a git command by its subcommand and flags."""
    sub = parsed.get("subcommand") or ""
    flags = parsed.get("flags", [])

    # Destructive git subcommands.
    if sub == "reset" and "--hard" in flags:
        return "destructive"
    if sub == "clean" and any(f.startswith("-") and "f" in f for f in flags):
        return "destructive"
    if sub == "push" and any(f in ("--force", "-f", "--force-with-lease") for f in flags):
        return "destructive"

    if sub in GIT_DESTRUCTIVE_SUBCOMMANDS:
        # Without the dangerous flags, still treat as write.
        return "write"

    if sub in GIT_READONLY_SUBCOMMANDS:
        return "readonly"
    if sub in GIT_WRITE_SUBCOMMANDS:
        return "write"
    # Unknown git subcommand -> write (safe default).
    return "write"


def _classify_intent(parsed: dict, raw_command: str) -> str:
    """Determine the intent category for a parsed command."""
    base = parsed.get("base_command", "")
    if not base:
        return "unknown"

    # Strip path prefix if present (e.g. /usr/bin/rm -> rm).
    base_name = base.rsplit("/", 1)[-1]

    # Check for sudo wrapper early — used for escalation decisions.
    wrapper = parsed.get("wrapper")

    # Handle mkfs variants (mkfs.ext4, mkfs.xfs, etc.).
    if base_name.startswith("mkfs"):
        return "destructive"

    # Git has special subcommand-level classification.
    if base_name == "git":
        return _classify_git(parsed)

    # Package managers: sudo + package manager = system_admin.
    if base_name in PACKAGE_COMMANDS:
        if wrapper == "sudo":
            return "system_admin"
        return "package_management"

    # Walk through categories in priority order.
    if base_name in DESTRUCTIVE_COMMANDS:
        return "destructive"
    if base_name in SYSTEM_ADMIN_COMMANDS:
        return "system_admin"
    if base_name in PROCESS_COMMANDS:
        return "process_management"
    if base_name in NETWORK_COMMANDS:
        return "network"
    if base_name in WRITE_COMMANDS:
        return "write"
    if base_name in READONLY_COMMANDS:
        return "readonly"

    if wrapper == "sudo":
        return "system_admin"

    return "unknown"


# ---------------------------------------------------------------------------
# Validation submodules (pipeline)
# ---------------------------------------------------------------------------

def _run_read_only_validation(
    parsed: dict, intent: str, mode: str, raw_command: str,
) -> Optional[dict]:
    """Submodule 1: In readonly mode, block write/destructive/process/system/package
    commands and redirections.  Allow safe readonly commands."""
    if mode != "readonly":
        return None

    base = (parsed.get("base_command") or "").rsplit("/", 1)[-1]
    redirections = parsed.get("redirections", [])

    # Block redirections that write to files.
    if redirections:
        return {
            "check": "read_only_validation",
            "result": "block",
            "reason": f"file redirection in readonly mode",
        }

    # Block non-readonly intents.
    if intent in ("write", "destructive", "process_management", "system_admin", "package_management"):
        return {
            "check": "read_only_validation",
            "result": "block",
            "reason": f"{intent} command not allowed in readonly mode",
        }

    return {
        "check": "read_only_validation",
        "result": "allow",
        "reason": "command is safe in readonly mode",
    }


def _run_destructive_command_validation(
    parsed: dict, intent: str, raw_command: str,
) -> Optional[dict]:
    """Submodule 2: Detect catastrophic destructive commands."""
    base = (parsed.get("base_command") or "").rsplit("/", 1)[-1]
    flags = parsed.get("flags", [])
    targets = parsed.get("targets", [])

    # Fork bomb detection.
    if _FORK_BOMB_RE.search(raw_command):
        return {
            "check": "destructive_command",
            "result": "block",
            "reason": "fork bomb detected",
        }

    # SQL injection patterns.
    if re.search(r"DROP\s+TABLE", raw_command, re.IGNORECASE):
        return {
            "check": "destructive_command",
            "result": "block",
            "reason": "DROP TABLE detected",
        }

    # mkfs — always block.
    if base.startswith("mkfs"):
        return {
            "check": "destructive_command",
            "result": "block",
            "reason": f"filesystem format command: {base}",
        }

    # dd with of= — always block.
    if base == "dd":
        of_targets = [t for t in targets if t.startswith("of=")]
        if of_targets or any("of=" in f for f in flags):
            return {
                "check": "destructive_command",
                "result": "block",
                "reason": "dd with output file is destructive",
            }
        # dd without of= in targets — check raw command.
        if "of=" in raw_command:
            return {
                "check": "destructive_command",
                "result": "block",
                "reason": "dd with output file is destructive",
            }

    # rm -rf / or rm -rf /* — catastrophic.
    if base == "rm":
        has_rf = any("r" in f and "f" in f for f in flags) or ("-r" in flags and "-f" in flags)
        if has_rf:
            # Check for root targets.
            for t in targets:
                if t in ("/", "/*", "/.", "/.."):
                    return {
                        "check": "destructive_command",
                        "result": "block",
                        "reason": "rm -rf with root target",
                    }
            # Non-root rm -rf — warn.
            return {
                "check": "destructive_command",
                "result": "warn",
                "reason": "rm -rf on non-root path",
            }
        # Plain rm — warn.
        return {
            "check": "destructive_command",
            "result": "warn",
            "reason": "rm command detected",
        }

    if intent == "destructive":
        return {
            "check": "destructive_command",
            "result": "warn",
            "reason": f"destructive command: {base}",
        }

    return None


def _run_mode_validation(
    parsed: dict, intent: str, mode: str, workspace: Optional[str],
) -> Optional[dict]:
    """Submodule 3: In workspace_write mode, warn when commands target system paths."""
    if mode != "workspace_write":
        return None

    targets = parsed.get("targets", [])
    redirections = parsed.get("redirections", [])

    all_paths = list(targets) + [r.get("target", "") for r in redirections]

    for path in all_paths:
        for sys_path in SYSTEM_PATHS:
            if path.startswith(sys_path):
                return {
                    "check": "mode_validation",
                    "result": "warn",
                    "reason": f"target '{path}' is a system path",
                }
    return None


def _run_sed_validation(
    parsed: dict, mode: str,
) -> Optional[dict]:
    """Submodule 4: Handle sed -i specifically."""
    base = (parsed.get("base_command") or "").rsplit("/", 1)[-1]
    if base != "sed":
        return None

    flags = parsed.get("flags", [])
    has_inplace = any(f == "-i" or f.startswith("-i") for f in flags)

    if has_inplace:
        if mode == "readonly":
            return {
                "check": "sed_validation",
                "result": "block",
                "reason": "sed -i (in-place edit) blocked in readonly mode",
            }
        return {
            "check": "sed_validation",
            "result": "warn",
            "reason": "sed -i modifies files in place",
        }

    # sed without -i is stdout-only.
    return {
        "check": "sed_validation",
        "result": "allow",
        "reason": "sed without -i outputs to stdout only",
    }


def _run_path_validation(
    parsed: dict, workspace: Optional[str],
) -> Optional[dict]:
    """Submodule 5: Warn on ../ traversal and ~ home directory references."""
    targets = parsed.get("targets", [])
    redirections = parsed.get("redirections", [])

    all_paths = list(targets) + [r.get("target", "") for r in redirections]

    for path in all_paths:
        if "../" in path or path == "..":
            return {
                "check": "path_validation",
                "result": "warn",
                "reason": f"path traversal detected: '{path}'",
            }
        if path.startswith("~"):
            return {
                "check": "path_validation",
                "result": "warn",
                "reason": f"home directory reference: '{path}'",
            }
    return None


def _run_command_semantics(
    parsed: dict, intent: str,
) -> dict:
    """Submodule 6: Informational classification. Always runs, always 'allow'."""
    base = (parsed.get("base_command") or "").rsplit("/", 1)[-1]
    sub = parsed.get("subcommand")
    label = f"{base} {sub}" if sub else base
    return {
        "check": "command_semantics",
        "result": "allow",
        "reason": f"classified as {intent}: {label}" if label else f"classified as {intent}",
    }


# ---------------------------------------------------------------------------
# Risk score computation
# ---------------------------------------------------------------------------

def _compute_risk(
    intent: str, validations: list[dict], parsed: dict, raw_command: str,
) -> int:
    """Compute risk score 0-100 from intent, validations, and targets."""
    score = _RISK_BASE.get(intent, 20)

    # Sensitive target boost.
    targets = parsed.get("targets", [])
    redirections = parsed.get("redirections", [])
    all_paths = list(targets) + [r.get("target", "") for r in redirections]

    for path in all_paths:
        if SENSITIVE_PATTERNS.search(path):
            score += 15
            break  # Only boost once.

    # Validation result boosts.
    for v in validations:
        if v["result"] == "block":
            score = max(85, score)
        elif v["result"] == "warn":
            score += 10

    return min(score, 100)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def classify_bash(
    command: str,
    mode: str = "workspace_write",
    workspace: Optional[str] = None,
) -> dict:
    """Classify a bash command by intent, risk, and reversibility.

    Args:
        command: The raw shell command string.
        mode: One of "readonly", "workspace_write", or "full_access".
        workspace: Optional workspace root path for path validation.

    Returns:
        A dict with keys: intent, risk_score, reversible, validations, parsed.
    """
    parsed = parse_command(command)
    intent = _classify_intent(parsed, command)

    # --- Run all 6 validation submodules (pipeline) ---
    validations: list[dict] = []

    v1 = _run_read_only_validation(parsed, intent, mode, command)
    if v1 is not None:
        validations.append(v1)

    v2 = _run_destructive_command_validation(parsed, intent, command)
    if v2 is not None:
        validations.append(v2)

    v3 = _run_mode_validation(parsed, intent, mode, workspace)
    if v3 is not None:
        validations.append(v3)

    v4 = _run_sed_validation(parsed, mode)
    if v4 is not None:
        validations.append(v4)

    v5 = _run_path_validation(parsed, workspace)
    if v5 is not None:
        validations.append(v5)

    v6 = _run_command_semantics(parsed, intent)
    validations.append(v6)

    # --- Compute derived fields ---
    risk_score = _compute_risk(intent, validations, parsed, command)
    reversible = intent != "destructive"

    return {
        "intent": intent,
        "risk_score": risk_score,
        "reversible": reversible,
        "validations": validations,
        "parsed": parsed,
    }
