"""MCP Server Profiler for comprehensive server analysis.

Profiles MCP servers: detects tools, resources, prompts, transport,
framework. Generates a server fingerprint hash. Computes complexity
scores and supports side-by-side profile comparisons.

Zero external dependencies. Python 3.11+ stdlib only.
"""

from __future__ import annotations

import hashlib
import json
import re
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any


# ---------------------------------------------------------------------------
# Data Models
# ---------------------------------------------------------------------------

@dataclass
class ToolInfo:
    """Information about a detected MCP tool."""

    name: str
    description: str = ""
    parameters: list[str] = field(default_factory=list)
    has_auth: bool = False
    has_validation: bool = False
    line: int = 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "description": self.description,
            "parameters": self.parameters,
            "has_auth": self.has_auth,
            "has_validation": self.has_validation,
            "line": self.line,
        }


@dataclass
class ResourceInfo:
    """Information about a detected MCP resource."""

    uri: str
    name: str = ""
    description: str = ""
    mime_type: str = ""
    line: int = 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "uri": self.uri,
            "name": self.name,
            "description": self.description,
            "mime_type": self.mime_type,
            "line": self.line,
        }


@dataclass
class PromptInfo:
    """Information about a detected MCP prompt."""

    name: str
    description: str = ""
    arguments: list[str] = field(default_factory=list)
    line: int = 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "description": self.description,
            "arguments": self.arguments,
            "line": self.line,
        }


@dataclass
class TransportInfo:
    """Detected transport configuration."""

    type: str  # stdio, sse, http, websocket
    secure: bool = False
    port: int = 0
    details: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "type": self.type,
            "secure": self.secure,
            "port": self.port,
            "details": self.details,
        }


@dataclass
class FrameworkDetection:
    """Detected framework information."""

    name: str
    version_hint: str = ""
    confidence: str = "HIGH"
    language: str = "python"

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "version_hint": self.version_hint,
            "confidence": self.confidence,
            "language": self.language,
        }


@dataclass
class ServerProfile:
    """Complete profile of an MCP server."""

    name: str = ""
    tools: list[ToolInfo] = field(default_factory=list)
    resources: list[ResourceInfo] = field(default_factory=list)
    prompts: list[PromptInfo] = field(default_factory=list)
    transport: TransportInfo | None = None
    framework: FrameworkDetection | None = None
    fingerprint: str = ""
    complexity_score: int = 0
    complexity_label: str = "Simple"
    dependencies: list[str] = field(default_factory=list)
    capabilities: list[str] = field(default_factory=list)
    file_path: str = ""
    lines_of_code: int = 0
    timestamp: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "tools": [t.to_dict() for t in self.tools],
            "resources": [r.to_dict() for r in self.resources],
            "prompts": [p.to_dict() for p in self.prompts],
            "transport": self.transport.to_dict() if self.transport else None,
            "framework": self.framework.to_dict() if self.framework else None,
            "fingerprint": self.fingerprint,
            "complexity_score": self.complexity_score,
            "complexity_label": self.complexity_label,
            "dependencies": self.dependencies,
            "capabilities": self.capabilities,
            "file_path": self.file_path,
            "lines_of_code": self.lines_of_code,
            "timestamp": self.timestamp,
        }


@dataclass
class ProfileDiff:
    """Difference between two profiles in a single aspect."""

    aspect: str
    value_a: Any = None
    value_b: Any = None
    status: str = "same"  # same, added, removed, changed

    def to_dict(self) -> dict[str, Any]:
        return {
            "aspect": self.aspect,
            "value_a": self.value_a,
            "value_b": self.value_b,
            "status": self.status,
        }


@dataclass
class ProfileComparison:
    """Side-by-side comparison of two server profiles."""

    profile_a_name: str = ""
    profile_b_name: str = ""
    diffs: list[ProfileDiff] = field(default_factory=list)
    tools_added: list[str] = field(default_factory=list)
    tools_removed: list[str] = field(default_factory=list)
    tools_common: list[str] = field(default_factory=list)
    resources_added: list[str] = field(default_factory=list)
    resources_removed: list[str] = field(default_factory=list)
    resources_common: list[str] = field(default_factory=list)
    complexity_delta: int = 0
    fingerprint_match: bool = False
    timestamp: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "profile_a_name": self.profile_a_name,
            "profile_b_name": self.profile_b_name,
            "diffs": [d.to_dict() for d in self.diffs],
            "tools_added": self.tools_added,
            "tools_removed": self.tools_removed,
            "tools_common": self.tools_common,
            "resources_added": self.resources_added,
            "resources_removed": self.resources_removed,
            "resources_common": self.resources_common,
            "complexity_delta": self.complexity_delta,
            "fingerprint_match": self.fingerprint_match,
            "timestamp": self.timestamp,
        }


# ---------------------------------------------------------------------------
# Detection Patterns
# ---------------------------------------------------------------------------

# Python tool patterns
_PY_TOOL_PATTERNS = [
    re.compile(r'@(?:server\.)?tool\s*(?:\(.*?\))?\s*\n\s*(?:async\s+)?def\s+(\w+)', re.DOTALL),
    re.compile(r'@mcp\.tool\s*(?:\(.*?\))?\s*\n\s*(?:async\s+)?def\s+(\w+)', re.DOTALL),
    re.compile(r'server\.add_tool\(\s*["\'](\w+)["\']'),
    re.compile(r'register_tool\(\s*["\'](\w+)["\']'),
]

# Python resource patterns
_PY_RESOURCE_PATTERNS = [
    re.compile(r'@(?:server\.)?resource\s*\(\s*["\']([^"\']+)["\']'),
    re.compile(r'@mcp\.resource\s*\(\s*["\']([^"\']+)["\']'),
    re.compile(r'server\.add_resource\(\s*["\']([^"\']+)["\']'),
]

# Python prompt patterns
_PY_PROMPT_PATTERNS = [
    re.compile(r'@(?:server\.)?prompt\s*(?:\(.*?\))?\s*\n\s*(?:async\s+)?def\s+(\w+)', re.DOTALL),
    re.compile(r'@mcp\.prompt\s*(?:\(.*?\))?\s*\n\s*(?:async\s+)?def\s+(\w+)', re.DOTALL),
    re.compile(r'server\.add_prompt\(\s*["\'](\w+)["\']'),
]

# TypeScript tool patterns
_TS_TOOL_PATTERNS = [
    re.compile(r'server\.setRequestHandler\(\s*ListToolsRequestSchema'),
    re.compile(r'server\.tool\(\s*["\'](\w+)["\']'),
    re.compile(r'name:\s*["\'](\w+)["\'].*?(?:description|handler)', re.DOTALL),
]

# Framework detection patterns
_FRAMEWORK_PATTERNS: list[tuple[str, str, re.Pattern[str]]] = [
    ("FastMCP", "python", re.compile(r'(?:from\s+fastmcp|from\s+mcp\.server\.fastmcp|FastMCP\s*\()')),
    ("mcp-python-sdk", "python", re.compile(r'from\s+mcp(?:\.server)?\s+import')),
    ("@modelcontextprotocol/sdk", "typescript", re.compile(r'(?:from\s+|require\()["\']@modelcontextprotocol/sdk')),
    ("FastMCP-TS", "typescript", re.compile(r'(?:from\s+|require\()["\']fastmcp')),
    ("mcp-go", "go", re.compile(r'github\.com/(?:mark3labs|modelcontextprotocol)/mcp-go')),
    ("custom", "unknown", re.compile(r'class\s+\w*(?:Server|MCP)\w*')),
]

# Transport detection patterns
_TRANSPORT_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
    ("stdio", re.compile(r'(?:stdio|StdioServerTransport|run_stdio|stdio_server)')),
    ("sse", re.compile(r'(?:SSE|SseServerTransport|sse_server|server-sent-events)', re.IGNORECASE)),
    ("http", re.compile(r'(?:StreamableHTTP|HttpServerTransport|http_server|express\(\))')),
    ("websocket", re.compile(r'(?:WebSocket|ws_server|wss://)', re.IGNORECASE)),
]


# ---------------------------------------------------------------------------
# Detection Functions
# ---------------------------------------------------------------------------

def _detect_tools(code: str) -> list[ToolInfo]:
    """Detect MCP tools from source code."""
    tools: list[ToolInfo] = []
    seen: set[str] = set()

    for pattern in _PY_TOOL_PATTERNS + _TS_TOOL_PATTERNS:
        for m in pattern.finditer(code):
            name = m.group(1) if m.lastindex and m.lastindex >= 1 else ""
            if not name or name in seen:
                continue
            seen.add(name)
            line = code[:m.start()].count('\n') + 1

            # Check for auth/validation in surrounding context
            start = max(0, m.start() - 200)
            end = min(len(code), m.end() + 500)
            context = code[start:end].lower()
            has_auth = any(kw in context for kw in [
                "auth", "token", "bearer", "permission", "role",
            ])
            has_validation = any(kw in context for kw in [
                "validate", "schema", "pydantic", "isinstance", "type(",
            ])

            # Extract parameters from function signature
            params = _extract_params(code, m.end())

            tools.append(ToolInfo(
                name=name,
                parameters=params,
                has_auth=has_auth,
                has_validation=has_validation,
                line=line,
            ))

    return tools


def _extract_params(code: str, start: int) -> list[str]:
    """Extract parameter names from a function definition."""
    # Look for the function signature
    chunk = code[start:start + 300]
    paren_match = re.search(r'\(([^)]*)\)', chunk)
    if not paren_match:
        return []
    params_str = paren_match.group(1)
    params = []
    for part in params_str.split(','):
        part = part.strip()
        if not part or part == 'self' or part == 'cls':
            continue
        # Extract just the name
        name = part.split(':')[0].split('=')[0].strip()
        if name and name != '*' and name != '**':
            # Strip leading * or **
            name = name.lstrip('*')
            if name:
                params.append(name)
    return params


def _detect_resources(code: str) -> list[ResourceInfo]:
    """Detect MCP resources from source code."""
    resources: list[ResourceInfo] = []
    seen: set[str] = set()

    for pattern in _PY_RESOURCE_PATTERNS:
        for m in pattern.finditer(code):
            uri = m.group(1) if m.lastindex and m.lastindex >= 1 else ""
            if not uri or uri in seen:
                continue
            seen.add(uri)
            line = code[:m.start()].count('\n') + 1
            resources.append(ResourceInfo(uri=uri, name=uri, line=line))

    return resources


def _detect_prompts(code: str) -> list[PromptInfo]:
    """Detect MCP prompts from source code."""
    prompts: list[PromptInfo] = []
    seen: set[str] = set()

    for pattern in _PY_PROMPT_PATTERNS:
        for m in pattern.finditer(code):
            name = m.group(1) if m.lastindex and m.lastindex >= 1 else ""
            if not name or name in seen:
                continue
            seen.add(name)
            line = code[:m.start()].count('\n') + 1
            args = _extract_params(code, m.end())
            prompts.append(PromptInfo(name=name, arguments=args, line=line))

    return prompts


def _detect_framework(code: str) -> FrameworkDetection:
    """Detect the MCP framework used."""
    for name, lang, pattern in _FRAMEWORK_PATTERNS:
        if pattern.search(code):
            # Try to detect version
            version = _detect_version(code)
            confidence = "HIGH" if name != "custom" else "LOW"
            return FrameworkDetection(
                name=name,
                version_hint=version,
                confidence=confidence,
                language=lang,
            )
    return FrameworkDetection(name="unknown", confidence="LOW")


def _detect_version(code: str) -> str:
    """Detect version from source code."""
    patterns = [
        re.compile(r'__version__\s*=\s*["\']([^"\']+)["\']'),
        re.compile(r'"version"\s*:\s*"([^"]+)"'),
        re.compile(r"'version'\s*:\s*'([^']+)'"),
    ]
    for p in patterns:
        m = p.search(code)
        if m:
            return m.group(1)
    return ""


def _detect_transport(code: str) -> TransportInfo:
    """Detect the transport protocol."""
    for ttype, pattern in _TRANSPORT_PATTERNS:
        if pattern.search(code):
            secure = bool(re.search(r'(?:tls|ssl|https|wss)', code, re.IGNORECASE))
            port = 0
            port_match = re.search(r'port\s*[=:]\s*(\d+)', code)
            if port_match:
                port = int(port_match.group(1))
            return TransportInfo(type=ttype, secure=secure, port=port)
    return TransportInfo(type="stdio")


def _detect_dependencies(code: str) -> list[str]:
    """Detect imported dependencies."""
    deps: set[str] = set()
    # Python imports
    for m in re.finditer(r'(?:from|import)\s+([\w.]+)', code):
        mod = m.group(1).split('.')[0]
        if mod not in {'os', 'sys', 'json', 'typing', 'pathlib',
                       'dataclasses', 'enum', 'collections', 're',
                       'abc', 'functools', 'datetime', 'hashlib',
                       'math', 'io', 'contextlib', 'unittest',
                       'tempfile', 'shutil', 'copy', 'inspect',
                       'logging', 'warnings', 'textwrap', 'html',
                       'http', 'urllib', 'asyncio', 'socket',
                       '__future__'}:
            deps.add(mod)
    # JavaScript requires
    for m in re.finditer(r'require\(["\']([^"\']+)["\']\)', code):
        deps.add(m.group(1).split('/')[0])
    # ES imports
    for m in re.finditer(r'from\s+["\']([^"\']+)["\']', code):
        pkg = m.group(1)
        if not pkg.startswith('.'):
            deps.add(pkg.split('/')[0])
    return sorted(deps)


def _compute_fingerprint(profile: ServerProfile) -> str:
    """Compute a deterministic fingerprint hash for the server profile."""
    parts = [
        profile.name,
        profile.framework.name if profile.framework else "",
        profile.transport.type if profile.transport else "",
        ",".join(sorted(t.name for t in profile.tools)),
        ",".join(sorted(r.uri for r in profile.resources)),
        ",".join(sorted(p.name for p in profile.prompts)),
        ",".join(sorted(profile.dependencies)),
    ]
    content = "|".join(parts)
    return hashlib.sha256(content.encode()).hexdigest()[:16]


def _compute_complexity(profile: ServerProfile) -> tuple[int, str]:
    """Compute complexity score and label.

    Complexity = tools * 3 + resources * 2 + prompts + dependencies * 1.5
    """
    score = (
        len(profile.tools) * 3
        + len(profile.resources) * 2
        + len(profile.prompts)
        + int(len(profile.dependencies) * 1.5)
    )

    if score >= 50:
        label = "Very Complex"
    elif score >= 30:
        label = "Complex"
    elif score >= 15:
        label = "Moderate"
    elif score >= 5:
        label = "Simple"
    else:
        label = "Minimal"

    return score, label


# ---------------------------------------------------------------------------
# Core Functions
# ---------------------------------------------------------------------------

def profile_server(code: str, file_path: str = "") -> ServerProfile:
    """Profile an MCP server from its source code.

    Args:
        code: Source code string.
        file_path: Optional file path for metadata.

    Returns:
        ServerProfile with detected tools, resources, prompts, etc.
    """
    tools = _detect_tools(code)
    resources = _detect_resources(code)
    prompts = _detect_prompts(code)
    framework = _detect_framework(code)
    transport = _detect_transport(code)
    dependencies = _detect_dependencies(code)
    lines = code.count('\n') + 1

    # Determine capabilities
    capabilities: list[str] = []
    if tools:
        capabilities.append("tools")
    if resources:
        capabilities.append("resources")
    if prompts:
        capabilities.append("prompts")

    # Extract name
    name = _detect_server_name(code) or file_path.split('/')[-1] if file_path else "unknown"

    profile = ServerProfile(
        name=name,
        tools=tools,
        resources=resources,
        prompts=prompts,
        transport=transport,
        framework=framework,
        dependencies=dependencies,
        capabilities=capabilities,
        file_path=file_path,
        lines_of_code=lines,
        timestamp=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
    )

    # Compute fingerprint and complexity
    profile.fingerprint = _compute_fingerprint(profile)
    score, label = _compute_complexity(profile)
    profile.complexity_score = score
    profile.complexity_label = label

    return profile


def _detect_server_name(code: str) -> str:
    """Detect the server name from code."""
    patterns = [
        re.compile(r'(?:Server|FastMCP|McpServer)\s*\(\s*["\']([^"\']+)["\']'),
        re.compile(r'server_name\s*=\s*["\']([^"\']+)["\']'),
        re.compile(r'name\s*[=:]\s*["\']([^"\']+)["\']'),
    ]
    for p in patterns:
        m = p.search(code)
        if m:
            return m.group(1)
    return ""


def compare_profiles(
    a: ServerProfile,
    b: ServerProfile,
) -> ProfileComparison:
    """Compare two server profiles side-by-side.

    Args:
        a: First server profile.
        b: Second server profile.

    Returns:
        ProfileComparison with detailed diffs.
    """
    diffs: list[ProfileDiff] = []

    # Framework comparison
    fw_a = a.framework.name if a.framework else "unknown"
    fw_b = b.framework.name if b.framework else "unknown"
    diffs.append(ProfileDiff(
        aspect="framework",
        value_a=fw_a,
        value_b=fw_b,
        status="same" if fw_a == fw_b else "changed",
    ))

    # Transport comparison
    tr_a = a.transport.type if a.transport else "unknown"
    tr_b = b.transport.type if b.transport else "unknown"
    diffs.append(ProfileDiff(
        aspect="transport",
        value_a=tr_a,
        value_b=tr_b,
        status="same" if tr_a == tr_b else "changed",
    ))

    # Tools diff
    tools_a = {t.name for t in a.tools}
    tools_b = {t.name for t in b.tools}
    added = sorted(tools_b - tools_a)
    removed = sorted(tools_a - tools_b)
    common = sorted(tools_a & tools_b)

    diffs.append(ProfileDiff(
        aspect="tools_count",
        value_a=len(a.tools),
        value_b=len(b.tools),
        status="same" if len(a.tools) == len(b.tools) else "changed",
    ))

    # Resources diff
    res_a = {r.uri for r in a.resources}
    res_b = {r.uri for r in b.resources}
    res_added = sorted(res_b - res_a)
    res_removed = sorted(res_a - res_b)
    res_common = sorted(res_a & res_b)

    diffs.append(ProfileDiff(
        aspect="resources_count",
        value_a=len(a.resources),
        value_b=len(b.resources),
        status="same" if len(a.resources) == len(b.resources) else "changed",
    ))

    # Complexity delta
    delta = b.complexity_score - a.complexity_score
    diffs.append(ProfileDiff(
        aspect="complexity",
        value_a=a.complexity_score,
        value_b=b.complexity_score,
        status="same" if delta == 0 else "changed",
    ))

    # Dependencies diff
    deps_a = set(a.dependencies)
    deps_b = set(b.dependencies)
    new_deps = sorted(deps_b - deps_a)
    removed_deps = sorted(deps_a - deps_b)
    if new_deps or removed_deps:
        diffs.append(ProfileDiff(
            aspect="dependencies",
            value_a=sorted(deps_a),
            value_b=sorted(deps_b),
            status="changed",
        ))

    # Lines of code
    diffs.append(ProfileDiff(
        aspect="lines_of_code",
        value_a=a.lines_of_code,
        value_b=b.lines_of_code,
        status="same" if a.lines_of_code == b.lines_of_code else "changed",
    ))

    return ProfileComparison(
        profile_a_name=a.name,
        profile_b_name=b.name,
        diffs=diffs,
        tools_added=added,
        tools_removed=removed,
        tools_common=common,
        resources_added=res_added,
        resources_removed=res_removed,
        resources_common=res_common,
        complexity_delta=delta,
        fingerprint_match=a.fingerprint == b.fingerprint,
        timestamp=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
    )


# ---------------------------------------------------------------------------
# Format helpers
# ---------------------------------------------------------------------------

def format_profile_json(profile: ServerProfile) -> str:
    """Serialize a ServerProfile to JSON."""
    return json.dumps(profile.to_dict(), indent=2)


def format_profile_text(profile: ServerProfile) -> str:
    """Format a ServerProfile as human-readable text."""
    lines = [
        "=" * 60,
        f"MCP SERVER PROFILE: {profile.name}",
        "=" * 60,
        f"Framework: {profile.framework.name if profile.framework else 'unknown'}"
        f" ({profile.framework.language if profile.framework else '?'})",
        f"Transport: {profile.transport.type if profile.transport else 'unknown'}"
        f" (secure: {profile.transport.secure if profile.transport else False})",
        f"Fingerprint: {profile.fingerprint}",
        f"Complexity: {profile.complexity_score} ({profile.complexity_label})",
        f"Lines: {profile.lines_of_code}",
        "",
        f"Tools ({len(profile.tools)}):",
    ]
    for t in profile.tools:
        auth_mark = " [AUTH]" if t.has_auth else ""
        val_mark = " [VAL]" if t.has_validation else ""
        lines.append(f"  - {t.name}{auth_mark}{val_mark}")

    lines.append(f"\nResources ({len(profile.resources)}):")
    for r in profile.resources:
        lines.append(f"  - {r.uri}")

    lines.append(f"\nPrompts ({len(profile.prompts)}):")
    for p in profile.prompts:
        lines.append(f"  - {p.name}")

    lines.append(f"\nDependencies ({len(profile.dependencies)}):")
    for d in profile.dependencies:
        lines.append(f"  - {d}")

    lines.append("")
    return "\n".join(lines)


def format_comparison_json(comparison: ProfileComparison) -> str:
    """Serialize a ProfileComparison to JSON."""
    return json.dumps(comparison.to_dict(), indent=2)


def format_comparison_text(comparison: ProfileComparison) -> str:
    """Format a ProfileComparison as human-readable text."""
    lines = [
        "=" * 60,
        "PROFILE COMPARISON",
        "=" * 60,
        f"A: {comparison.profile_a_name}",
        f"B: {comparison.profile_b_name}",
        f"Fingerprint Match: {'Yes' if comparison.fingerprint_match else 'No'}",
        f"Complexity Delta: {comparison.complexity_delta:+d}",
        "",
    ]

    for d in comparison.diffs:
        if d.status != "same":
            lines.append(f"  [{d.status.upper()}] {d.aspect}: {d.value_a} -> {d.value_b}")

    if comparison.tools_added:
        lines.append(f"\n  Tools Added: {', '.join(comparison.tools_added)}")
    if comparison.tools_removed:
        lines.append(f"  Tools Removed: {', '.join(comparison.tools_removed)}")
    if comparison.resources_added:
        lines.append(f"  Resources Added: {', '.join(comparison.resources_added)}")
    if comparison.resources_removed:
        lines.append(f"  Resources Removed: {', '.join(comparison.resources_removed)}")

    lines.append("")
    return "\n".join(lines)
