"""Local implementations of the file + shell kernel tools (docs/kernel_spec.md §3–§4).

Fresh stdlib implementations, independent of the legacy LangChain `@tool`
versions in tools/file_ops.py and tools/shell.py (those stay for the old path
until Phase 5). Each returns a plain string — success or an actionable error
the model can act on (spec §5).

`make_kernel_tools(workspace)` returns the ToolSpecs scoped to a workspace dir.
Write/edit/shell carry capability tags so the runner's policy gate governs them.
"""
from __future__ import annotations

import os
import shlex
import subprocess

from core.tools import CAP_FS_WRITE, CAP_SHELL, ToolSpec

_READ_WINDOW = 5_000          # chars returned by read_file without offset/limit
_SHELL_OUTPUT_CAP = 20_000
_SHELL_TIMEOUT_DEFAULT = 120

# Last-resort static block list (mirrors tools/shell.py). The policy gate is the
# real boundary (spec §4); this only stops obviously destructive binaries even
# when a permissive gate is wired.
_BLOCKED_BINARIES = frozenset({
    "mkfs", "shred", "wipefs", "fdisk", "diskutil",
    "dd", "sudo", "launchctl", "csrutil", "kill", "mkfifo",
    "chmod", "chown", "srm", "nohup", "at", "crontab", "nc", "ncat",
})
_DANGEROUS_RM_TARGETS = frozenset({
    "/", "/etc", "/usr", "/var", "/System", "/Applications", "/Library",
})


def _resolve(workspace: str, path: str) -> str:
    """Expand ~ and resolve relative paths against the workspace."""
    expanded = os.path.expanduser(path)
    if not os.path.isabs(expanded):
        expanded = os.path.join(workspace, expanded)
    return os.path.normpath(expanded)


def _outside_workspace(workspace: str, path: str) -> bool:
    ws = os.path.normpath(os.path.expanduser(workspace))
    try:
        return os.path.commonpath([ws, path]) != ws
    except ValueError:
        return True  # different drives / unrelated roots


# ─── file tools ──────────────────────────────────────────────────────────────
def _read_file(workspace: str, path: str, offset: int | None = None, limit: int | None = None) -> str:
    target = _resolve(workspace, path)
    if not os.path.isfile(target):
        return f"File not found: {path}"
    try:
        with open(target, encoding="utf-8", errors="replace") as fh:
            text = fh.read()
    except OSError as exc:
        return f"Could not read {path}: {exc}"
    if offset is not None or limit is not None:
        start = max(0, int(offset or 0))
        end = start + int(limit) if limit is not None else len(text)
        window = text[start:end]
        suffix = "" if end >= len(text) else "\n[truncated; use offset/limit to read more]"
        return window + suffix
    if len(text) > _READ_WINDOW:
        return (
            text[:_READ_WINDOW]
            + f"\n[truncated at {_READ_WINDOW} chars; pass offset/limit to read more]"
        )
    return text


def _write_file(workspace: str, path: str, content: str) -> str:
    target = _resolve(workspace, path)
    if _outside_workspace(workspace, target):
        return f"Refusing to write outside the workspace: {path}"
    try:
        parent = os.path.dirname(target)
        if parent:
            os.makedirs(parent, exist_ok=True)
        with open(target, "w", encoding="utf-8") as fh:
            fh.write(content)
    except OSError as exc:
        return f"Could not write {path}: {exc}"
    if not os.path.isfile(target):
        return f"Write reported success but {path} does not exist."
    return f"Wrote {len(content)} chars to {target}"


def _edit_file(workspace: str, path: str, old_string: str, new_string: str) -> str:
    target = _resolve(workspace, path)
    if _outside_workspace(workspace, target):
        return f"Refusing to edit outside the workspace: {path}"
    if not os.path.isfile(target):
        return f"File not found: {path}"
    try:
        with open(target, encoding="utf-8") as fh:
            text = fh.read()
    except OSError as exc:
        return f"Could not read {path}: {exc}"
    count = text.count(old_string)
    if count == 0:
        return (
            f"old_string not found in {path}. Read the file and copy the exact "
            "text to replace (including whitespace)."
        )
    if count > 1:
        return (
            f"old_string is ambiguous in {path}: found {count} matches. Add "
            "surrounding context so it uniquely identifies one location."
        )
    updated = text.replace(old_string, new_string, 1)
    try:
        with open(target, "w", encoding="utf-8") as fh:
            fh.write(updated)
    except OSError as exc:
        return f"Could not write {path}: {exc}"
    return f"Edited {target} (1 replacement)"


def _list_directory(workspace: str, path: str = ".") -> str:
    target = _resolve(workspace, path)
    if not os.path.isdir(target):
        return f"Not a directory: {path}"
    try:
        entries = sorted(os.listdir(target))
    except OSError as exc:
        return f"Could not list {path}: {exc}"
    if not entries:
        return f"{target} is empty."
    lines = []
    for name in entries:
        full = os.path.join(target, name)
        if os.path.isdir(full):
            lines.append(f"{name}/  (dir)")
        else:
            try:
                size = os.path.getsize(full)
            except OSError:
                size = 0
            lines.append(f"{name}  ({size} bytes)")
    return "\n".join(lines)


# ─── shell tool ──────────────────────────────────────────────────────────────
def _static_shell_block(command: str) -> str | None:
    """Return a denial string if the command trips a last-resort static rule."""
    try:
        argv = shlex.split(command)
    except ValueError:
        return "Blocked: could not parse command safely."
    if not argv:
        return "No command provided."
    binary = os.path.basename(argv[0])
    if binary in _BLOCKED_BINARIES:
        return f"Blocked: '{binary}' is a potentially destructive or privileged command."
    if binary == "rm" and any(
        t.rstrip("/") in _DANGEROUS_RM_TARGETS for t in argv[1:] if not t.startswith("-")
    ):
        return "Blocked: command targets a protected system path."
    lower = command.lower()
    if "curl" in lower and any(p in lower for p in ("| sh", "| bash", "|sh", "|bash")):
        return "Blocked: piping curl to shell is not allowed."
    if "`" in command or "$(" in command:
        return "Blocked: shell substitution (backtick or $()) is not permitted."
    return None


def _shell_execute(workspace: str, command: str, timeout_s: int = _SHELL_TIMEOUT_DEFAULT) -> str:
    command = (command or "").strip()
    if not command:
        return "No command provided."
    blocked = _static_shell_block(command)
    if blocked is not None:
        return blocked
    cwd = os.path.expanduser(workspace)
    if not os.path.isdir(cwd):
        cwd = os.path.expanduser("~")
    try:
        proc = subprocess.run(
            command,
            shell=True,
            cwd=cwd,
            capture_output=True,
            text=True,
            timeout=max(1, int(timeout_s)),
        )
    except subprocess.TimeoutExpired:
        return f"Command timed out after {timeout_s}s: {command[:120]}"
    except OSError as exc:
        return f"Command could not start: {exc}"
    parts = []
    if proc.stdout:
        parts.append(proc.stdout.rstrip("\n"))
    if proc.stderr:
        parts.append("[stderr]\n" + proc.stderr.rstrip("\n"))
    parts.append(f"[exit {proc.returncode}]")
    out = "\n".join(parts)
    if len(out) > _SHELL_OUTPUT_CAP:
        out = out[:_SHELL_OUTPUT_CAP] + "\n[truncated]"
    return out


# ─── factory ─────────────────────────────────────────────────────────────────
def make_kernel_tools(workspace: str) -> list[ToolSpec]:
    """Build the five locally-implemented kernel ToolSpecs scoped to *workspace*."""
    ws = os.path.normpath(os.path.expanduser(workspace))

    return [
        ToolSpec(
            name="read_file",
            description="Read a local text file. Returns up to 5000 chars; pass offset/limit to page through larger files.",
            parameters={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "File path (absolute, ~, or workspace-relative)."},
                    "offset": {"type": "integer", "description": "Start character offset."},
                    "limit": {"type": "integer", "description": "Max characters to return."},
                },
                "required": ["path"],
            },
            fn=lambda path, offset=None, limit=None: _read_file(ws, path, offset, limit),
        ),
        ToolSpec(
            name="write_file",
            description="Create or overwrite a text file. Creates parent directories.",
            parameters={
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "content": {"type": "string"},
                },
                "required": ["path", "content"],
            },
            fn=lambda path, content: _write_file(ws, path, content),
            capabilities=frozenset({CAP_FS_WRITE}),
        ),
        ToolSpec(
            name="edit_file",
            description="Replace one unique occurrence of old_string with new_string in a file. Add context if old_string is ambiguous.",
            parameters={
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "old_string": {"type": "string"},
                    "new_string": {"type": "string"},
                },
                "required": ["path", "old_string", "new_string"],
            },
            fn=lambda path, old_string, new_string: _edit_file(ws, path, old_string, new_string),
            capabilities=frozenset({CAP_FS_WRITE}),
        ),
        ToolSpec(
            name="list_directory",
            description="List a directory's contents with sizes and file/dir markers.",
            parameters={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "Directory path; defaults to the workspace."},
                },
                "required": [],
            },
            fn=lambda path=".": _list_directory(ws, path),
        ),
        ToolSpec(
            name="shell_execute",
            description="Run a shell command in the workspace. Governed by the security policy; dangerous binaries are blocked.",
            parameters={
                "type": "object",
                "properties": {
                    "command": {"type": "string"},
                    "timeout_s": {"type": "integer", "description": "Max seconds before the command is killed (default 120)."},
                },
                "required": ["command"],
            },
            fn=lambda command, timeout_s=_SHELL_TIMEOUT_DEFAULT: _shell_execute(ws, command, timeout_s),
            capabilities=frozenset({CAP_SHELL}),
        ),
    ]


def path_is_outside_workspace(workspace: str, path: str) -> bool:
    """Helper for a policy gate (spec §4 execution hygiene): is *path* outside the workspace?"""
    return _outside_workspace(workspace, _resolve(workspace, path))
