"""Agent permissions and user settings management for SuperchargeAI."""

from __future__ import annotations

import json
import os
import re
from pathlib import Path

from supercharge.paths import AmbiguousPrefixError, _task_root

_DEFAULT_MAX_RECURSION_DEPTH = 5
_ENV_MAX_DEPTH = "SUPERCHARGE_MAX_RECURSION_DEPTH"
_ENV_REMAINING = "SUPERCHARGE_RECURSION_REMAINING"
_ENV_TASK_UUID = "SUPERCHARGE_TASK_UUID"
_ENV_WORKER_ID = "SUPERCHARGE_WORKER_ID"

_DEFAULT_FAST_MODELS: set[str] = {"haiku"}
_ENV_FAST_MODELS = "SUPERCHARGE_FAST_MODELS"

_SUPERCHARGE_PERMISSIONS = [
    "Bash(supercharge *)",
    "Bash(cat)",
    "Bash(cat *)",
    "Bash(ls)",
    "Bash(ls *)",
    "Bash(find *)",
    "Bash(head *)",
    "Bash(tail *)",
    "Bash(echo *)",
    "Bash(wc *)",
    "Bash(diff *)",
    "Bash(stat *)",
    "Bash(pwd)",
    "Bash(which *)",
    "Bash(env)",
    "Bash(env *)",
    "Write(.claude/SuperchargeAI/**)",
    "Edit(.claude/SuperchargeAI/**)",
    "WebSearch",
    "WebFetch",
]

# Dangerous command patterns blocked for workers and Task-tool agents.
# The redirect pattern is checked separately with quote-stripping logic
# so that `>` inside quoted strings (e.g., `python -c "x > 0"`) is allowed.
_DANGEROUS_BASH_PATTERNS: list[re.Pattern] = [
    re.compile(r"\bgit\s+push\b"),
    re.compile(r"\bgit\s+commit\b"),
    re.compile(r"\bgit\s+reset\b"),
    re.compile(r"\bgit\s+rebase\b"),
    re.compile(r"\bgit\s+checkout\s+--"),
    re.compile(r"\brm\s+-[a-z]*[rf]"),
    re.compile(r"\bmkfs\b"),
    re.compile(r"\bdd\s+if="),
    re.compile(r"\bcurl\b.*\|\s*(ba)?sh"),
    re.compile(r"\bwget\b.*\|\s*(ba)?sh"),
]

# Matches shell redirects like `> file`, `>> file`, `1> file` but NOT
# stderr redirects `2>&1`, `2>/dev/null`.  Applied after stripping quoted strings.
_REDIRECT_PATTERN = re.compile(r"(?<!2)>{1,2}\s*(?!&)\S")


def _strip_quotes(command: str) -> str:
    """Remove single- and double-quoted strings from a command.

    This allows redirect detection to ignore `>` inside quotes.
    """
    return re.sub(r"""(?:"[^"]*"|'[^']*')""", "", command)


def _is_dangerous_bash(command: str) -> str | None:
    """Check if a bash command matches any dangerous pattern.

    Returns the matched pattern string if dangerous, None if safe.
    Redirect detection strips quoted strings first so that `>` inside
    quotes (e.g., ``python -c "x > 0"``) is not flagged.
    """
    # Check redirect pattern on quote-stripped command
    stripped = _strip_quotes(command)
    if _REDIRECT_PATTERN.search(stripped):
        return _REDIRECT_PATTERN.pattern
    for pattern in _DANGEROUS_BASH_PATTERNS:
        if pattern.search(command):
            return pattern.pattern
    return None

# ── Per-agent tool permissions ──────────────────────────────────────────────
# deep_tools: allowed_tools for deep workers (opus/sonnet)
# fast_tools: allowed_tools for fast workers (haiku) — always read-only
# write_scope: where deep workers may Write/Edit
#   "project" = anywhere in project root
#   "memory"  = memory dir + context file
#   "context" = anywhere in task directory (result.md, notes.md, workers/)

_AGENT_PERMISSIONS: dict[str, dict] = {
    "code": {
        "deep_tools": ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
        "fast_tools": ["Read", "Glob", "Grep"],
        "write_scope": "project",
    },
    "plan": {
        "deep_tools": ["Read", "Write", "Glob", "Grep"],
        "fast_tools": ["Read", "Glob", "Grep"],
        "write_scope": "context",
    },
    "review": {
        "deep_tools": ["Read", "Write", "Bash", "Glob", "Grep"],
        "fast_tools": ["Read", "Glob", "Grep"],
        "write_scope": "context",
    },
    "document": {
        "deep_tools": ["Read", "Write", "Edit", "Glob", "Grep"],
        "fast_tools": ["Read", "Glob", "Grep"],
        "write_scope": "project",
    },
    "research": {
        "deep_tools": ["Read", "Write", "Bash", "Glob", "Grep", "WebSearch", "WebFetch"],
        "fast_tools": ["Read", "Glob", "Grep", "WebSearch", "WebFetch"],
        "write_scope": "context",
    },
    "consistency": {
        "deep_tools": ["Read", "Write", "Glob", "Grep"],
        "fast_tools": ["Read", "Glob", "Grep"],
        "write_scope": "context",
    },
    "memory": {
        "deep_tools": ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
        "fast_tools": ["Read", "Glob", "Grep"],
        "write_scope": "memory",
    },
}

_DEFAULT_PERMS = _AGENT_PERMISSIONS["code"]


def _make_can_use_tool(
    agent_type: str,
    task_dir: Path,
    worker_id: str,
    project_root: str,
):
    """Create a can_use_tool callback scoped to agent type and worker."""
    from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny

    perms = _AGENT_PERMISSIONS.get(agent_type, _DEFAULT_PERMS)
    write_scope = perms["write_scope"]

    worker_file = str(task_dir / "workers" / f"{worker_id}.md")
    worker_subdir = str(task_dir / "workers" / worker_id) + "/"
    memory_dir = str(Path(project_root) / ".claude" / "SuperchargeAI" / "memory") + "/"

    async def _can_use_tool(tool_name, input_data, context):
        # Block task creation and dangerous commands
        if tool_name == "Bash":
            cmd = input_data.get("command", "")
            if "supercharge task init" in cmd:
                return PermissionResultDeny(
                    message="Only the orchestrator creates task workspaces.",
                )
            matched = _is_dangerous_bash(cmd)
            if matched:
                return PermissionResultDeny(
                    message=f"Blocked dangerous command pattern: {matched}",
                )

        # Scope Write/Edit by agent type
        if tool_name in ("Write", "Edit"):
            file_path = input_data.get("file_path", "")

            if write_scope == "project":
                if project_root and not file_path.startswith(project_root):
                    return PermissionResultDeny(
                        message=f"Write restricted to project: {project_root}",
                    )
            elif write_scope == "memory":
                if not (
                    file_path.startswith(memory_dir)
                    or file_path == worker_file
                    or file_path.startswith(worker_subdir)
                ):
                    return PermissionResultDeny(
                        message="Write restricted to memory dir and context file.",
                    )
            elif write_scope == "context":
                task_dir_str = str(task_dir)
                if not (
                    file_path.startswith(task_dir_str + "/")
                    or file_path == task_dir_str
                ):
                    return PermissionResultDeny(
                        message=f"Write restricted to task directory: {task_dir}",
                    )
            else:
                raise ValueError(f"Unknown write_scope: {write_scope!r}")

        return PermissionResultAllow()

    return _can_use_tool


def _get_fast_models() -> set[str]:
    """Return the set of model names that use fast (fire-and-forget) mode.

    If SUPERCHARGE_FAST_MODELS is set, it replaces the default entirely.
    Comma-separated, case-insensitive. Empty string = no fast models.
    """
    env_val = os.environ.get(_ENV_FAST_MODELS)
    if env_val is not None:
        return {m.strip().lower() for m in env_val.split(",") if m.strip()}
    return _DEFAULT_FAST_MODELS


def _get_remaining_depth() -> int:
    """Return remaining recursion budget for worker spawning.

    Heuristic:
    1. _ENV_REMAINING is set -> we're inside a worker, use it.
    2. Not set -> orchestrator level:
       a) _ENV_MAX_DEPTH is set (via settings.json env) -> use it.
       b) Fall back to _DEFAULT_MAX_RECURSION_DEPTH.
    """
    remaining = os.environ.get(_ENV_REMAINING)
    if remaining is not None:
        return int(remaining)
    max_depth = os.environ.get(_ENV_MAX_DEPTH)
    if max_depth is not None:
        return int(max_depth)
    return _DEFAULT_MAX_RECURSION_DEPTH


def _is_fast_mode(model: str | None) -> bool:
    """Check if the model should use fast (fire-and-forget) mode."""
    if model is None:
        return False
    fast_models = _get_fast_models()
    return any(fast in model.lower() for fast in fast_models)


def _resolve_worker_prefix(prefix: str) -> tuple[str, Path] | None:
    """Resolve a worker ID prefix to ``(full_worker_id, worker_file_path)``.

    Scans ``workers/`` directories across all task directories for ``.md``
    files whose stem starts with *prefix*.

    Returns ``None`` when no match is found.

    Raises:
        ValueError: if *prefix* is a hex string shorter than 8 characters.
        AmbiguousPrefixError: if *prefix* matches more than one worker file.
    """
    root = _task_root()
    if not root.exists():
        return None

    # Validate minimum length for short hex prefixes
    if re.fullmatch(r"[0-9a-f]+", prefix) and len(prefix) < 8:
        raise ValueError(
            f"Prefix must be at least 8 hex characters, got {len(prefix)}: {prefix!r}"
        )

    matches: list[tuple[str, Path]] = []  # (full_worker_id, file_path)

    for agent_dir in root.iterdir():
        if not agent_dir.is_dir():
            continue
        for task_dir in agent_dir.iterdir():
            if not task_dir.is_dir():
                continue
            workers_dir = task_dir / "workers"
            if not workers_dir.is_dir():
                continue
            for worker_file in workers_dir.iterdir():
                if not worker_file.is_file() or worker_file.suffix != ".md":
                    continue
                stem = worker_file.stem
                if stem == prefix:
                    # Exact match — return immediately
                    return (stem, worker_file)
                if stem.startswith(prefix):
                    matches.append((stem, worker_file))

    if len(matches) == 0:
        return None
    if len(matches) == 1:
        return matches[0]
    raise AmbiguousPrefixError(prefix, [m[0] for m in matches])


def _find_worker_file(worker_id: str) -> Path | None:
    """Search for a worker file by ID or prefix across all task directories.

    Returns the worker file ``Path`` on success, ``None`` if not found.

    Raises:
        AmbiguousPrefixError: if a short prefix matches multiple worker files.
    """
    result = _resolve_worker_prefix(worker_id)
    if result is None:
        return None
    return result[1]


def _user_settings_path() -> Path:
    """Return path to user-level settings.json, respecting CLAUDE_CONFIG_DIR."""
    from supercharge.paths import _user_config_dir

    return _user_config_dir() / "settings.json"


def _add_user_permissions(settings_path: Path) -> list[str]:
    """Add SuperchargeAI permissions to settings.json. Returns list of newly added entries."""
    if settings_path.exists():
        settings = json.loads(settings_path.read_text())
    else:
        settings = {}

    perms = settings.setdefault("permissions", {})
    allow = perms.setdefault("allow", [])

    added = []
    for entry in _SUPERCHARGE_PERMISSIONS:
        if entry not in allow:
            allow.append(entry)
            added.append(entry)

    settings_path.parent.mkdir(parents=True, exist_ok=True)
    settings_path.write_text(json.dumps(settings, indent=2) + "\n")
    return added


def _remove_user_permissions(settings_path: Path) -> int:
    """Remove SuperchargeAI permissions from settings.json. Returns count removed."""
    if not settings_path.exists():
        return 0

    settings = json.loads(settings_path.read_text())
    perms = settings.get("permissions", {})
    allow = perms.get("allow", [])

    original_len = len(allow)
    allow = [entry for entry in allow if entry not in _SUPERCHARGE_PERMISSIONS]
    removed = original_len - len(allow)

    if removed > 0:
        perms["allow"] = allow
        settings["permissions"] = perms
        settings_path.write_text(json.dumps(settings, indent=2) + "\n")

    return removed
