"""MCP tool definitions and handlers for kv.

7 tools across 3 profiles:
  safe (default): kv_status, kv_envs, kv_list, kv_run
  mutate (opt-in): kv_set, kv_rm
  reveal (opt-in): kv_get
"""

import json
import subprocess
import sys
import time

from kv.config import find_project_root, list_environments, get_default_env
from kv.store import SecretStore

# -- Tool definitions (MCP JSON Schema format) --------------------------------

TOOLS = {
    # --- safe profile ---
    "kv_status": {
        "name": "kv_status",
        "description": (
            "[SAFE] Show kv project status. "
            "Returns whether a kv project is initialized, the current environment, "
            "and the number of secrets stored."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {},
        },
        "profile": "safe",
    },
    "kv_envs": {
        "name": "kv_envs",
        "description": (
            "[SAFE] List available environments (e.g. dev, staging, prod). "
            "Does not reveal any secret values."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {},
        },
        "profile": "safe",
    },
    "kv_list": {
        "name": "kv_list",
        "description": (
            "[SAFE] List secret key names in an environment. "
            "Returns names only — no secret values are revealed."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "env": {
                    "type": "string",
                    "description": "Environment name (default: project default)",
                },
            },
        },
        "profile": "safe",
    },
    "kv_run": {
        "name": "kv_run",
        "description": (
            "[EXECUTE] Run a command with secrets injected as environment variables. "
            "Returns exit code and redacted stdout/stderr — secret values are "
            "replaced with [REDACTED] before the output reaches the AI context. "
            "Commands are validated against a security policy (allowlist or blocklist) "
            "before execution. Shells, inline code, and network tools are blocked. "
            "Subprocesses run in a network-isolated namespace by default. "
            "The MCP client SHOULD prompt for user confirmation before calling. "
            "60-second timeout; process is killed if it exceeds this."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "argv": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Command and arguments as an array (e.g. [\"python\", \"app.py\"])",
                },
                "env_names": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Secret names to inject. If omitted, all secrets are injected.",
                },
                "env": {
                    "type": "string",
                    "description": "Environment name (default: project default)",
                },
            },
            "required": ["argv"],
        },
        "profile": "safe",
    },
    "kv_api": {
        "name": "kv_api",
        "description": (
            "[SAFE] Make an HTTP API call with credentials injected by the kv agent. "
            "The API key is held in the agent's memory — it never enters your environment, "
            "never appears in tool responses, and never touches the filesystem. "
            "Supports: openai, anthropic, google, github, google-cloud, and custom providers. "
            "The agent adds the correct auth header/param automatically. "
            "Returns the API response body (with any secret values redacted)."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "provider": {
                    "type": "string",
                    "description": "API provider name: openai, anthropic, google, github, google-cloud",
                },
                "path": {
                    "type": "string",
                    "description": "API endpoint path (e.g. /v1/chat/completions, /v1/models)",
                },
                "method": {
                    "type": "string",
                    "description": "HTTP method (default: POST)",
                    "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"],
                },
                "body": {
                    "type": "object",
                    "description": "Request body (JSON). Omit for GET requests.",
                },
                "headers": {
                    "type": "object",
                    "description": "Extra headers to include (auth headers are added automatically).",
                },
                "env": {
                    "type": "string",
                    "description": "Environment name (default: project default)",
                },
            },
            "required": ["provider", "path"],
        },
        "profile": "safe",
    },
    # --- mutate profile ---
    "kv_set": {
        "name": "kv_set",
        "description": (
            "[MUTATE] Store or update an encrypted secret. "
            "The value is encrypted immediately using ChaCha20-Poly1305. "
            "The MCP client SHOULD prompt for user confirmation before calling."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Secret key name (e.g. OPENAI_API_KEY)",
                },
                "value": {
                    "type": "string",
                    "description": "Secret value to store",
                },
                "env": {
                    "type": "string",
                    "description": "Environment name (default: project default)",
                },
            },
            "required": ["name", "value"],
        },
        "profile": "mutate",
    },
    "kv_rm": {
        "name": "kv_rm",
        "description": (
            "[MUTATE] Remove a secret from the encrypted store. "
            "This action is irreversible. "
            "The MCP client SHOULD prompt for user confirmation before calling."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Secret key name to remove",
                },
                "env": {
                    "type": "string",
                    "description": "Environment name (default: project default)",
                },
            },
            "required": ["name"],
        },
        "profile": "mutate",
    },
    # --- reveal profile ---
    "kv_get": {
        "name": "kv_get",
        "description": (
            "[REVEAL] Get a secret value by name. "
            "WARNING: The decrypted value is returned in plaintext to the AI context. "
            "This means the secret will be visible to the language model. "
            "Prefer kv_run to use secrets without revealing them. "
            "The MCP client SHOULD prompt for user confirmation before calling."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Secret key name to retrieve",
                },
                "env": {
                    "type": "string",
                    "description": "Environment name (default: project default)",
                },
            },
            "required": ["name"],
        },
        "profile": "reveal",
    },
}


def get_tools_for_profiles(profiles):
    """Return tool definitions for the given set of enabled profiles.

    Always includes 'safe'. 'mutate' and 'reveal' are opt-in.
    """
    result = []
    for tool in TOOLS.values():
        if tool["profile"] in profiles:
            # Return MCP-compliant tool def (without internal 'profile' field)
            result.append({
                "name": tool["name"],
                "description": tool["description"],
                "inputSchema": tool["inputSchema"],
            })
    return result


# -- Tool handlers ------------------------------------------------------------

MAX_ARGV_COUNT = 50
MAX_ARGV_LEN = 4096
RUN_TIMEOUT = 60
MAX_LIST_KEYS = 1000


def _get_env_name(args, default_env):
    """Extract environment name from tool args, falling back to project default."""
    return args.get("env") or default_env


def handle_kv_status(args, store, default_env):
    """Handle kv_status tool call."""
    if store is None:
        from kv.agent import agent_request
        result = agent_request("status")
        text = f"project: initialized (via agent daemon)\npid: {result.get('pid')}\nenvironments: {result.get('environments')}\nsecrets: {result.get('secrets')}"
        return {"content": [{"type": "text", "text": text}], "isError": False}
    root = store.root
    envs = list_environments(root)
    count = store.env_count(default_env)
    text = f"project: initialized\nroot: {root}\ndefault_env: {default_env}\nenvironments: {len(envs)}\nsecrets ({default_env}): {count}"
    return {"content": [{"type": "text", "text": text}], "isError": False}


def handle_kv_envs(args, store, default_env):
    """Handle kv_envs tool call."""
    if store is None:
        from kv.agent import agent_request
        result = agent_request("envs")
        envs = result.get("environments", [])
        text = "\n".join(envs) if envs else "(no environments)"
        return {"content": [{"type": "text", "text": text}], "isError": False}
    root = store.root
    envs = list_environments(root)
    text = "\n".join(envs) if envs else "(no environments)"
    return {"content": [{"type": "text", "text": text}], "isError": False}


def handle_kv_list(args, store, default_env):
    """Handle kv_list tool call. Returns key names only — no values."""
    env = _get_env_name(args, default_env)
    if store is None:
        from kv.agent import agent_request
        result = agent_request("list", env=env)
        names = result.get("keys", [])[:MAX_LIST_KEYS]
        text = "\n".join(names) if names else "(no secrets)"
        return {"content": [{"type": "text", "text": text}], "isError": False}
    secrets = store.list_secrets(env)
    names = [k for k, v in secrets][:MAX_LIST_KEYS]
    text = "\n".join(names) if names else "(no secrets)"
    return {"content": [{"type": "text", "text": text}], "isError": False}



def handle_kv_run(args, store, default_env):
    """Handle kv_run tool call. Runs command with secrets as env vars.

    Captures stdout/stderr and redacts any secret values before returning.
    The agent gets useful output (errors, logs) but never sees secret values.

    If store is None, delegates to the kv agent daemon via Unix socket.
    """
    import os

    argv = args.get("argv")
    if not argv or not isinstance(argv, list):
        return {"content": [{"type": "text", "text": "error: argv is required and must be an array"}], "isError": True}

    # Validate argv
    if len(argv) > MAX_ARGV_COUNT:
        return {"content": [{"type": "text", "text": f"error: too many arguments (max {MAX_ARGV_COUNT})"}], "isError": True}
    for i, arg in enumerate(argv):
        if not isinstance(arg, str):
            return {"content": [{"type": "text", "text": f"error: argv[{i}] must be a string"}], "isError": True}
        if len(arg) > MAX_ARGV_LEN:
            return {"content": [{"type": "text", "text": f"error: argv[{i}] exceeds max length ({MAX_ARGV_LEN})"}], "isError": True}

    env_name = _get_env_name(args, default_env)

    # Delegate to agent daemon if store is None (daemon mode)
    if store is None:
        from kv.agent import agent_request
        try:
            result = agent_request("run", argv=argv, env=env_name)
            if "error" in result:
                text = f"error: {result['error']}"
                is_error = True
            else:
                parts = [f"exit code: {result.get('exit_code', 1)}"]
                if result.get("stdout", "").strip():
                    parts.append(f"stdout:\n{result['stdout'].rstrip()}")
                if result.get("stderr", "").strip():
                    parts.append(f"stderr:\n{result['stderr'].rstrip()}")
                text = "\n".join(parts)
                is_error = result.get("exit_code", 1) != 0
        except Exception as e:
            text = f"error: agent daemon unavailable — {e}"
            is_error = True
        return {"content": [{"type": "text", "text": text}], "isError": is_error}

    # Direct mode — store has the key
    # Apply same security checks as daemon mode
    from kv.sandbox import check_command, wrap_argv, redact, scan_leaked_files
    from kv.config import get_run_policy, is_network_allowed

    # Command validation (allowlist + blocklist)
    policy, allowed = get_run_policy(store.root)
    cmd_allowlist = allowed if policy == "allowlist" else None
    safe, reason = check_command(argv, cmd_allowlist)
    if not safe:
        return {"content": [{"type": "text", "text": f"error: {reason}"}], "isError": True}

    all_secrets = store.get_all_secrets(env_name)

    # Filter to requested env_names if specified
    env_names = args.get("env_names")
    if env_names is not None:
        secrets = {k: v for k, v in all_secrets.items() if k in env_names}
    else:
        secrets = all_secrets

    # Build env: inherit current + overlay secrets
    run_env = dict(os.environ)
    run_env.update(secrets)

    # Network isolation — controlled by config, not by caller
    needs_network = is_network_allowed(store.root, argv)
    run_argv = wrap_argv(argv, network=needs_network)

    import tempfile as _tmpmod
    scan_dirs = [os.getcwd(), _tmpmod.gettempdir()]
    before_time = time.time()

    is_error = False
    try:
        result = subprocess.run(
            run_argv,
            env=run_env,
            shell=False,
            timeout=RUN_TIMEOUT,
            capture_output=True,
            text=True,
        )
        # Redact secret values from output before returning to agent
        stdout = redact(result.stdout, all_secrets) if result.stdout else ""
        stderr = redact(result.stderr, all_secrets) if result.stderr else ""

        # Post-execution file scan
        leaked = scan_leaked_files(all_secrets, before_time, scan_dirs)
        leak_warning = ""
        if leaked:
            for lpath, name in leaked:
                try:
                    os.unlink(lpath)
                except OSError:
                    pass
            leak_names = [f"{name} → {lpath}" for lpath, name in leaked]
            leak_warning = (
                f"\n[SECURITY] detected and deleted {len(leaked)} "
                f"file(s) containing secrets: {', '.join(leak_names)}"
            )

        parts = [f"exit code: {result.returncode}"]
        if stdout.strip():
            parts.append(f"stdout:\n{stdout.rstrip()}")
        if stderr.strip():
            parts.append(f"stderr:\n{stderr.rstrip()}")
        if leak_warning:
            parts.append(leak_warning)
        text = "\n".join(parts)

    except subprocess.TimeoutExpired:
        text = f"error: command timed out after {RUN_TIMEOUT}s (process killed)"
        is_error = True
    except FileNotFoundError:
        text = f"error: command not found: {argv[0]}"
        is_error = True
    except Exception as exc:
        text = f"error: {type(exc).__name__}"
        is_error = True

    return {"content": [{"type": "text", "text": text}], "isError": is_error}


def handle_kv_api(args, store, default_env):
    """Handle kv_api tool call. Makes HTTP API call with credentials injected.

    If store is None (daemon mode), delegates to the agent daemon.
    The API key never appears in the response — it stays in the daemon's memory.
    """
    provider = args.get("provider")
    path = args.get("path")

    if not provider:
        return {"content": [{"type": "text", "text": "error: provider is required (openai, anthropic, google, github, google-cloud)"}], "isError": True}
    if not path:
        return {"content": [{"type": "text", "text": "error: path is required (e.g. /v1/chat/completions)"}], "isError": True}

    env_name = _get_env_name(args, default_env)

    # Always delegate to daemon — daemon holds secrets and makes the call
    if store is None:
        from kv.agent import agent_request
        try:
            result = agent_request(
                "api",
                provider=provider,
                path=path,
                method=args.get("method", "POST"),
                body=args.get("body"),
                headers=args.get("headers", {}),
                env=env_name,
            )
            if "error" in result:
                text = f"HTTP {result.get('status', 'error')}: {result['error']}"
                if result.get("body"):
                    text += f"\n{json.dumps(result['body']) if isinstance(result['body'], (dict, list)) else result['body']}"
                return {"content": [{"type": "text", "text": text}], "isError": True}
            else:
                body = result.get("body", {})
                text = json.dumps(body, indent=2) if isinstance(body, (dict, list)) else str(body)
                return {"content": [{"type": "text", "text": text}], "isError": False}
        except Exception as e:
            return {"content": [{"type": "text", "text": f"error: agent daemon unavailable — {e}"}], "isError": True}

    # Direct mode (no daemon) — use shared API call logic
    from kv.providers import get_provider, make_api_call

    prov = get_provider(provider)
    if not prov:
        from kv.providers import list_providers
        available = ", ".join(list_providers())
        return {"content": [{"type": "text", "text": f"error: unknown provider '{provider}'. available: {available}"}], "isError": True}

    all_secrets = store.get_all_secrets(env_name)
    secret_value = all_secrets.get(prov["secret_name"])
    if not secret_value:
        return {"content": [{"type": "text", "text": f"error: secret '{prov['secret_name']}' not found in env '{env_name}'"}], "isError": True}

    result = make_api_call(
        provider_name=provider,
        secret_value=secret_value,
        path=path,
        method=args.get("method", "POST"),
        body=args.get("body"),
        extra_headers=args.get("headers", {}),
        secrets_for_redact=all_secrets,
    )

    if "error" in result:
        text = f"HTTP {result.get('status', 'error')}: {result['error']}"
        if result.get("body"):
            text += f"\n{json.dumps(result['body']) if isinstance(result['body'], (dict, list)) else result['body']}"
        return {"content": [{"type": "text", "text": text}], "isError": True}

    body = result.get("body", {})
    text = json.dumps(body, indent=2) if isinstance(body, (dict, list)) else str(body)
    return {"content": [{"type": "text", "text": text}], "isError": False}


def handle_kv_set(args, store, default_env):
    """Handle kv_set tool call."""
    if store is None:
        return {"content": [{"type": "text", "text": "error: kv_set requires direct vault access (not available in daemon mode). Stop the agent and restart the MCP server, or use the CLI: kv set KEY"}], "isError": True}

    name = args.get("name")
    value = args.get("value")
    if not name or value is None:
        return {"content": [{"type": "text", "text": "error: name and value are required"}], "isError": True}

    env = _get_env_name(args, default_env)
    store.set_secret(env, name, value)
    text = f"stored {name} in {env}"
    return {"content": [{"type": "text", "text": text}], "isError": False}


def handle_kv_rm(args, store, default_env):
    """Handle kv_rm tool call."""
    if store is None:
        return {"content": [{"type": "text", "text": "error: kv_rm requires direct vault access (not available in daemon mode). Use the CLI: kv rm KEY"}], "isError": True}

    name = args.get("name")
    if not name:
        return {"content": [{"type": "text", "text": "error: name is required"}], "isError": True}

    env = _get_env_name(args, default_env)
    existed = store.remove_secret(env, name)
    if existed:
        text = f"removed {name} from {env}"
    else:
        text = f"{name} not found in {env}"
    return {"content": [{"type": "text", "text": text}], "isError": False}


def handle_kv_get(args, store, default_env):
    """Handle kv_get tool call. WARNING: returns plaintext secret value."""
    if store is None:
        return {"content": [{"type": "text", "text": "error: kv_get requires direct vault access (not available in daemon mode). The agent daemon never returns secret values — this is by design."}], "isError": True}

    name = args.get("name")
    if not name:
        return {"content": [{"type": "text", "text": "error: name is required"}], "isError": True}

    env = _get_env_name(args, default_env)
    value = store.get_secret(env, name)
    if value is None:
        text = f"{name} not found in {env}"
        return {"content": [{"type": "text", "text": text}], "isError": True}

    return {"content": [{"type": "text", "text": value}], "isError": False}


# -- Dispatcher ---------------------------------------------------------------

HANDLERS = {
    "kv_status": handle_kv_status,
    "kv_envs": handle_kv_envs,
    "kv_list": handle_kv_list,
    "kv_run": handle_kv_run,
    "kv_api": handle_kv_api,
    "kv_set": handle_kv_set,
    "kv_rm": handle_kv_rm,
    "kv_get": handle_kv_get,
}
