from __future__ import annotations

import copy
import json
import os
from typing import Any
from urllib.parse import urlparse


VALID_CONFLICT_POLICIES = {"skip", "overwrite", "rename"}
KNOWN_SERVER_KEYS = {
    "command",
    "args",
    "cwd",
    "env",
    "url",
    "headers",
    "enabled",
    "tool_allowlist",
    "max_concurrent",
}
_SENSITIVE_KEYS = {"token", "password", "secret", "authorization", "api_key", "apikey"}


def parse_mcp_root(obj: Any) -> dict[str, Any]:
    if not isinstance(obj, dict):
        raise ValueError("Root JSON must be an object.")
    servers = obj.get("mcpServers")
    if not isinstance(servers, dict):
        raise ValueError("mcpServers must be an object.")
    return obj


def read_mcp_file(path: str, *, max_bytes: int = 512 * 1024) -> dict[str, Any]:
    try:
        st_before = os.stat(path, follow_symlinks=True)
    except Exception as exc:
        raise ValueError("path must point to an existing file") from exc
    with open(path, "rb") as f:
        st_after = os.fstat(f.fileno())
        # Best-effort TOCTOU guard: if target inode changed between stat and open, abort.
        if (st_before.st_dev, st_before.st_ino) != (st_after.st_dev, st_after.st_ino):
            raise ValueError("path changed during read")
        raw = f.read(max_bytes + 1)
    if len(raw) > max_bytes:
        raise ValueError("config too large")
    if raw.startswith(b"\xef\xbb\xbf"):
        raw = raw[3:]
    try:
        text = raw.decode("utf-8")
    except Exception as exc:
        raise ValueError("invalid UTF-8 encoding") from exc
    try:
        parsed = json.loads(text)
    except Exception as exc:
        raise ValueError("invalid JSON body") from exc
    return parse_mcp_root(parsed)


def merge_mcp_servers(
    base: dict[str, Any],
    incoming: dict[str, Any],
    *,
    on_conflict: str = "rename",
) -> tuple[dict[str, Any], list[dict[str, str]]]:
    if on_conflict not in VALID_CONFLICT_POLICIES:
        raise ValueError(f"invalid conflict policy: {on_conflict}")

    base_root = parse_mcp_root(base)
    incoming_root = parse_mcp_root(incoming)
    out = copy.deepcopy(base_root)
    out.setdefault("mcpServers", {})
    out_servers = out["mcpServers"]
    in_servers = incoming_root["mcpServers"]

    conflicts: list[dict[str, str]] = []
    for sid, raw in in_servers.items():
        if not isinstance(sid, str) or not sid.strip() or not isinstance(raw, dict):
            continue
        if sid not in out_servers:
            out_servers[sid] = copy.deepcopy(raw)
            continue
        if on_conflict == "skip":
            conflicts.append({"server_id": sid, "action": "skipped", "resolved_id": sid})
            continue
        if on_conflict == "overwrite":
            out_servers[sid] = copy.deepcopy(raw)
            conflicts.append({"server_id": sid, "action": "overwritten", "resolved_id": sid})
            continue
        # rename
        suffix = 2
        candidate = f"{sid}_{suffix}"
        while candidate in out_servers:
            suffix += 1
            candidate = f"{sid}_{suffix}"
        out_servers[candidate] = copy.deepcopy(raw)
        conflicts.append({"server_id": sid, "action": "renamed", "resolved_id": candidate})
    return out, conflicts


def collect_server_warnings(root: dict[str, Any]) -> list[str]:
    warnings: list[str] = []
    servers = root.get("mcpServers")
    if not isinstance(servers, dict):
        return warnings
    for sid, raw in servers.items():
        if not isinstance(sid, str) or not isinstance(raw, dict):
            continue
        unknown = sorted(k for k in raw.keys() if k not in KNOWN_SERVER_KEYS)
        if unknown:
            warnings.append(f"{sid}: unknown keys ({', '.join(unknown)})")
        has_url = bool(str(raw.get("url") or "").strip())
        has_command = bool(str(raw.get("command") or "").strip())
        if not has_url and not has_command:
            warnings.append(f"{sid}: missing transport (`url` or `command`)")
    return warnings


def collect_portability_warnings(root: dict[str, Any]) -> list[str]:
    warnings: list[str] = []
    servers = root.get("mcpServers")
    if not isinstance(servers, dict):
        return warnings
    for sid, raw in servers.items():
        if not isinstance(sid, str) or not isinstance(raw, dict):
            continue
        command = str(raw.get("command") or "").strip()
        cwd = str(raw.get("cwd") or "").strip()
        url = str(raw.get("url") or "").strip()

        if command.startswith("/") and not os.path.exists(command):
            warnings.append(f"{sid}: command path not found on this host ({command})")
        if cwd.startswith("/") and not os.path.isdir(cwd):
            warnings.append(f"{sid}: cwd path not found on this host ({cwd})")
        if url:
            scheme = (urlparse(url).scheme or "").lower()
            if scheme not in {"http", "https"}:
                warnings.append(f"{sid}: URL scheme '{scheme or 'missing'}' may be unsupported")
    return warnings


def redact_sensitive(value: Any) -> Any:
    """Recursively redact likely secret values by key name."""
    if isinstance(value, dict):
        out: dict[str, Any] = {}
        for k, v in value.items():
            key = str(k)
            lower = key.lower()
            if any(marker in lower for marker in _SENSITIVE_KEYS):
                out[key] = "***REDACTED***"
            else:
                out[key] = redact_sensitive(v)
        return out
    if isinstance(value, list):
        return [redact_sensitive(v) for v in value]
    return value
