"""Description refresher - generate and cache L1/L2 descriptions.

This module handles:
1. Connecting to MCP servers temporarily
2. Fetching tool definitions
3. Generating capability summaries via BAML
4. Caching descriptions to .mcp-gateway/descriptions.yaml
"""

from __future__ import annotations

import asyncio
import logging
from datetime import datetime, timezone
from pathlib import Path

import yaml

from pmcp.manifest.loader import Manifest, ServerConfig, load_manifest
from pmcp.manifest.version_checker import (
    detect_package_type,
    get_package_version,
    is_version_newer,
)
from pmcp.types import (
    DescriptionsCache,
    GeneratedServerDescriptions,
    PrebuiltToolInfo,
)

logger = logging.getLogger(__name__)

# Gateway version for cache metadata
GATEWAY_VERSION = "1.0.0"

# Default cache directory
DEFAULT_CACHE_DIR = Path(".mcp-gateway")
DESCRIPTIONS_CACHE_FILE = "descriptions.yaml"


def get_cache_path(cache_dir: Path | None = None) -> Path:
    """Get the path to the descriptions cache file."""
    if cache_dir is None:
        cache_dir = DEFAULT_CACHE_DIR
    return cache_dir / DESCRIPTIONS_CACHE_FILE


def load_descriptions_cache(cache_path: Path | None = None) -> DescriptionsCache | None:
    """Load cached descriptions from file."""
    if cache_path is None:
        cache_path = get_cache_path()

    if not cache_path.exists():
        logger.debug(f"No descriptions cache found at {cache_path}")
        return None

    try:
        with open(cache_path, "r") as f:
            data = yaml.safe_load(f)

        if not data:
            return None

        # Parse servers
        servers = {}
        for name, server_data in data.get("servers", {}).items():
            tools = [PrebuiltToolInfo(**t) for t in server_data.get("tools", [])]
            servers[name] = GeneratedServerDescriptions(
                package=server_data.get("package", ""),
                version=server_data.get("version", ""),
                generated_at=server_data.get("generated_at", ""),
                capability_summary=server_data.get("capability_summary", ""),
                tools=tools,
            )

        return DescriptionsCache(
            generated_at=data.get("generated_at", ""),
            gateway_version=data.get("gateway_version", ""),
            servers=servers,
        )

    except Exception as e:
        logger.warning(f"Failed to load descriptions cache: {e}")
        return None


def save_descriptions_cache(
    cache: DescriptionsCache, cache_path: Path | None = None
) -> None:
    """Save descriptions cache to file."""
    if cache_path is None:
        cache_path = get_cache_path()

    # Ensure directory exists
    cache_path.parent.mkdir(parents=True, exist_ok=True)

    # Write with YAML comments preserved
    yaml_content = f"""# Auto-generated by pmcp refresh
# Do not edit manually - regenerate with: pmcp refresh

generated_at: "{cache.generated_at}"
gateway_version: "{cache.gateway_version}"

servers:
"""

    for name, desc in cache.servers.items():
        yaml_content += f"""  {name}:
    package: "{desc.package}"
    version: "{desc.version}"
    generated_at: "{desc.generated_at}"
    capability_summary: |
{_indent_multiline(desc.capability_summary, 6)}
    tools:
"""
        for tool in desc.tools:
            yaml_content += f"""      - name: "{tool.name}"
        description: "{_escape_yaml_string(tool.description)}"
        short_description: "{_escape_yaml_string(tool.short_description)}"
        tags: {tool.tags}
        risk_hint: "{tool.risk_hint}"
"""

    with open(cache_path, "w") as f:
        f.write(yaml_content)

    logger.info(f"Saved descriptions cache to {cache_path}")


def _indent_multiline(text: str, indent: int) -> str:
    """Indent multiline text for YAML."""
    lines = text.strip().split("\n")
    prefix = " " * indent
    return "\n".join(prefix + line for line in lines)


def _escape_yaml_string(text: str) -> str:
    """Escape special characters in YAML strings."""
    return text.replace('"', '\\"').replace("\n", " ").strip()


def _extract_tags(tool_name: str, description: str) -> list[str]:
    """Extract tags from tool name and description."""
    tags = []
    text = f"{tool_name} {description}".lower()

    tag_keywords = {
        "browser": ["browser", "navigate", "click", "screenshot", "dom", "page"],
        "file": ["file", "read", "write", "directory", "folder", "path"],
        "db": ["database", "query", "sql", "table", "postgres", "sqlite"],
        "git": ["git", "commit", "branch", "repository", "push", "pull"],
        "http": ["http", "fetch", "request", "api", "url", "download"],
        "search": ["search", "find", "query", "lookup", "resolve"],
        "docs": ["documentation", "docs", "reference", "library"],
        "code": ["code", "function", "class", "module", "script"],
    }

    for tag, keywords in tag_keywords.items():
        if any(kw in text for kw in keywords):
            tags.append(tag)

    return tags or ["general"]


def _infer_risk(tool_name: str, description: str) -> str:
    """Infer risk level from tool name and description."""
    text = f"{tool_name} {description}".lower()

    high_risk = [
        "delete",
        "remove",
        "drop",
        "execute",
        "run",
        "write",
        "modify",
        "create",
        "post",
    ]
    medium_risk = ["navigate", "click", "submit", "input", "form"]

    if any(kw in text for kw in high_risk):
        return "high"
    if any(kw in text for kw in medium_risk):
        return "medium"
    return "low"


async def refresh_server(
    server_config: ServerConfig,
    existing_cache: GeneratedServerDescriptions | None = None,
    force: bool = False,
) -> GeneratedServerDescriptions | None:
    """
    Refresh descriptions for a single server.

    This temporarily connects to the server, fetches tools, and generates summaries.

    Args:
        server_config: Server configuration from manifest
        existing_cache: Existing cached descriptions (for staleness check)
        force: Force regeneration even if not stale

    Returns:
        Generated descriptions or None if failed
    """
    from mcp import ClientSession, StdioServerParameters
    from mcp.client.stdio import stdio_client

    server_name = server_config.name

    # Check for staleness if we have cached data
    if existing_cache and not force:
        # Get current package version
        version, pkg_type = await get_package_version(
            server_config.command, server_config.args
        )

        if version and not is_version_newer(existing_cache.version, version):
            logger.debug(
                f"Server {server_name} is up to date (v{existing_cache.version})"
            )
            return existing_cache

    logger.info(f"Refreshing descriptions for {server_name}...")

    # Determine package info
    pkg_type, pkg_name = detect_package_type(server_config.command, server_config.args)
    if not pkg_name:
        pkg_name = f"{server_config.command} {' '.join(server_config.args)}"

    # Get version
    version, _ = await get_package_version(server_config.command, server_config.args)
    version = version or "unknown"

    try:
        # Create server parameters
        server_params = StdioServerParameters(
            command=server_config.command,
            args=server_config.args,
        )

        # Connect and fetch tools
        async with stdio_client(server_params) as (read, write):
            async with ClientSession(read, write) as session:
                await session.initialize()

                # List tools
                tools_result = await session.list_tools()
                tools = tools_result.tools

                logger.info(f"Found {len(tools)} tools for {server_name}")

                # Convert to PrebuiltToolInfo
                prebuilt_tools = []
                for tool in tools:
                    name = tool.name
                    description = tool.description or ""
                    short_desc = (
                        description[:100] + "..."
                        if len(description) > 100
                        else description
                    )

                    prebuilt_tools.append(
                        PrebuiltToolInfo(
                            name=name,
                            description=description,
                            short_description=short_desc,
                            tags=_extract_tags(name, description),
                            risk_hint=_infer_risk(name, description),
                        )
                    )

                # Generate capability summary
                capability_summary = await _generate_capability_summary(
                    server_name, prebuilt_tools
                )

                return GeneratedServerDescriptions(
                    package=pkg_name,
                    version=version,
                    generated_at=datetime.now(timezone.utc).isoformat(),
                    capability_summary=capability_summary,
                    tools=prebuilt_tools,
                )

    except Exception as e:
        logger.error(f"Failed to refresh {server_name}: {e}")
        return None


async def _generate_capability_summary(
    server_name: str, tools: list[PrebuiltToolInfo]
) -> str:
    """Generate capability summary for a server using BAML or fallback."""
    if not tools:
        return f"• {server_name} (0 tools): No tools available"

    # Try BAML summarization
    try:
        from pmcp.baml_client import b
        from pmcp.baml_client.types import ToolDescription

        tool_descriptions = [
            ToolDescription(
                server_name=server_name,
                tool_name=t.name,
                description=t.short_description,
            )
            for t in tools
        ]

        async with asyncio.timeout(30):
            result = await b.SummarizeCapabilities(tool_descriptions)

        # Format as compact summary
        lines = []
        for cat in result.categories:
            lines.append(f"• {cat.name} ({len(tools)} tools): {cat.summary}")
        return "\n".join(lines)

    except ImportError:
        logger.debug("BAML not available, using template fallback")
    except Exception as e:
        logger.warning(f"BAML summarization failed: {e}")

    # Fallback: simple template
    capabilities = set()
    for tool in tools:
        for tag in tool.tags:
            capabilities.add(tag)

    cap_str = ", ".join(sorted(capabilities)[:5])
    return f"• {server_name.title()} ({len(tools)} tools): {cap_str}"


async def refresh_all(
    manifest: Manifest | None = None,
    cache_path: Path | None = None,
    force: bool = False,
    servers: list[str] | None = None,
) -> DescriptionsCache:
    """
    Refresh descriptions for all or selected servers.

    Args:
        manifest: Manifest to use (loads default if None)
        cache_path: Path to cache file
        force: Force regeneration even if not stale
        servers: List of server names to refresh (all if None)

    Returns:
        Updated descriptions cache
    """
    if manifest is None:
        manifest = load_manifest()

    # Load existing cache
    existing_cache = load_descriptions_cache(cache_path)
    existing_servers = existing_cache.servers if existing_cache else {}

    # Determine which servers to refresh
    target_servers = servers or list(manifest.servers.keys())

    # Refresh each server
    new_servers: dict[str, GeneratedServerDescriptions] = {}

    for name in target_servers:
        server_config = manifest.get_server(name)
        if not server_config:
            logger.warning(f"Server {name} not found in manifest")
            continue

        existing = existing_servers.get(name)

        try:
            result = await refresh_server(server_config, existing, force)
            if result:
                new_servers[name] = result
            elif existing:
                # Keep existing if refresh failed
                new_servers[name] = existing
        except Exception as e:
            logger.error(f"Error refreshing {name}: {e}")
            if existing:
                new_servers[name] = existing

    # Merge with existing cache (keep servers not in target list)
    for name, desc in existing_servers.items():
        if name not in new_servers:
            new_servers[name] = desc

    # Create new cache
    cache = DescriptionsCache(
        generated_at=datetime.now(timezone.utc).isoformat(),
        gateway_version=GATEWAY_VERSION,
        servers=new_servers,
    )

    # Save cache
    save_descriptions_cache(cache, cache_path)

    return cache


async def check_staleness(
    manifest: Manifest | None = None,
    cache_path: Path | None = None,
) -> dict[str, tuple[str, str]]:
    """
    Check which servers have newer versions available.

    Returns:
        Dict of server_name -> (cached_version, latest_version) for stale servers
    """
    if manifest is None:
        manifest = load_manifest()

    existing_cache = load_descriptions_cache(cache_path)
    if not existing_cache:
        logger.info("No cache found, all servers need refresh")
        return {}

    stale_servers: dict[str, tuple[str, str]] = {}

    for name, desc in existing_cache.servers.items():
        server_config = manifest.get_server(name)
        if not server_config:
            continue

        version, _ = await get_package_version(
            server_config.command, server_config.args
        )

        if version and is_version_newer(desc.version, version):
            stale_servers[name] = (desc.version, version)
            logger.info(f"Server {name} is stale: {desc.version} -> {version}")

    return stale_servers
