"""
Discovery client: build candidate set from proxy list and static config; fetch commands per server.

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com

Purpose: Implements TZ FR-7 step 1–2 and discovery operations from section 3.1.
Primary discovery: mcp-proxy server list. Optional: static server list from config.
Returns raw candidates with per-server command list and schemas; no validation or
catalog build in this module (caller's responsibility).
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union
from urllib.parse import urlparse

from mcp_proxy_adapter.client.jsonrpc_client import JsonRpcClient

logger = logging.getLogger(__name__)

# StaticServerEntry not imported to avoid circular import (orchestration_config
# -> validators -> simple_config -> orchestration_section -> orchestration_config).
# static_servers entries must have .host, .port, .id (e.g. StaticServerEntry).


@dataclass
class DiscoveryCandidate:
    """
    Single discovery candidate with endpoint identity and fetched command data.

    Attributes:
        host: Server host (e.g. hostname or IP).
        port: Server port.
        id: Server identifier (from proxy metadata.uuid, server_id, or static config).
        commands_response: Raw response from GET {base_url}/commands (command list and schemas).
        scheme: Protocol used for base_url (http or https).
    """

    host: str
    port: int
    id: str  # noqa: A003
    commands_response: Dict[str, Any] = field(default_factory=dict)
    scheme: str = "http"


def _parse_proxy_server_entry(
    entry: Dict[str, Any],
) -> Optional[tuple[str, int, str, str]]:
    """
    Normalize one proxy list entry to (host, port, id, scheme).

    Uses server_url for host/port/scheme; id from metadata.uuid (if valid UUID4) or server_id.
    Returns None if entry is missing server_url or cannot be parsed.
    """
    server_url = entry.get("server_url") or entry.get("url") or ""
    if not server_url or not isinstance(server_url, str):
        return None
    parsed = urlparse(server_url)
    host = parsed.hostname or "localhost"
    port = parsed.port
    if port is None:
        port = 443 if (parsed.scheme or "").lower() == "https" else 80
    scheme = (parsed.scheme or "http").lower()

    raw_id = None
    metadata = entry.get("metadata")
    if isinstance(metadata, dict):
        raw_id = metadata.get("uuid")
    if not raw_id:
        raw_id = entry.get("server_id") or entry.get("id")
    if not raw_id:
        raw_id = f"{host}:{port}"
    sid = str(raw_id).strip()
    if not sid:
        sid = f"{host}:{port}"
    return (host, port, sid, scheme)


def _proxy_response_to_candidates(
    proxy_response: Union[Dict[str, Any], List[Any]],
) -> List[tuple[str, int, str, str]]:
    """
    Convert proxy list response to list of (host, port, id, scheme).

    Handles response shape: list at top level, or under "servers" / "data" key.
    """
    raw_list: List[Dict[str, Any]] = []
    if isinstance(proxy_response, list):
        raw_list = [e for e in proxy_response if isinstance(e, dict)]
    elif isinstance(proxy_response, dict):
        raw_list = proxy_response.get("servers") or proxy_response.get("data") or []
        if not isinstance(raw_list, list):
            raw_list = []
        raw_list = [e for e in raw_list if isinstance(e, dict)]

    seen: set[tuple[str, int, str]] = set()
    result: List[tuple[str, int, str, str]] = []
    for entry in raw_list:
        parsed = _parse_proxy_server_entry(entry)
        if parsed is None:
            continue
        host, port, sid, scheme = parsed
        key = (host, port, sid)
        if key in seen:
            continue
        seen.add(key)
        result.append((host, port, sid, scheme))
    return result


def _static_entries_to_candidates(
    static_servers: Optional[List[Any]],
) -> List[tuple[str, int, str, str]]:
    """
    Convert static config entries to (host, port, id, scheme). Scheme defaults to http.

    Each entry must have .host, .port, .id (e.g. StaticServerEntry).
    """
    if not static_servers:
        return []
    return [
        (
            getattr(e, "host", ""),
            getattr(e, "port", 0),
            getattr(e, "id", "").strip(),
            "http",
        )
        for e in static_servers
    ]


def _merge_and_normalize(
    proxy_candidates: List[tuple[str, int, str, str]],
    static_candidates: List[tuple[str, int, str, str]],
) -> List[tuple[str, int, str, str]]:
    """
    Merge proxy and static candidates and deduplicate by (host, port, id).

    First occurrence wins (proxy list first, then static).
    """
    seen: set[tuple[str, int, str]] = set()
    result: List[tuple[str, int, str, str]] = []
    for item in proxy_candidates + static_candidates:
        host, port, sid, scheme = item
        key = (host, port, sid)
        if key in seen:
            continue
        seen.add(key)
        result.append(item)
    return result


async def _fetch_commands_for_candidate(
    host: str, port: int, scheme: str
) -> Dict[str, Any]:
    """
    Fetch GET {base_url}/commands for one server using JsonRpcClient.get_commands_list().

    Returns raw response dict; on failure returns dict with single key "error" (message).
    """
    protocol = "https" if scheme == "https" else "http"
    client = JsonRpcClient(protocol=protocol, host=host, port=port)
    try:
        result = await client.get_commands_list()
        return result if isinstance(result, dict) else {}
    except Exception as exc:  # noqa: BLE001
        logger.debug(
            "Discovery: failed to fetch commands for %s://%s:%s: %s",
            scheme,
            host,
            port,
            exc,
        )
        return {"error": str(exc)}
    finally:
        await client.close()


async def run_discovery(
    proxy_url: str,
    static_servers: Optional[List[Any]] = None,
) -> List[DiscoveryCandidate]:
    """
    Build initial candidate set from proxy discovery and optional static list; fetch commands per server.

    (1) Calls proxy list_proxy_servers (GET /list or /servers) to get server list.
    (2) Merges with static_servers from config.
    (3) Normalizes by (host, port, id) identity and deduplicates.
    (4) For each candidate, fetches command list and schemas via GET {base_url}/commands.
    (5) Returns list of DiscoveryCandidate with host, port, id, and commands_response.

    No filtering by reachability or ID match is performed; all candidates are returned
    with their fetched data (or error in commands_response). Caller is responsible for
    validation and filtering.

    Args:
        proxy_url: Base URL of the mcp-proxy (e.g. http://localhost:3005).
        static_servers: Optional list of static server entries (host, port, id).

    Returns:
        List of DiscoveryCandidate with commands_response populated per server.
    """
    # Proxy list: use JsonRpcClient only for list_proxy_servers (it creates its own client inside)
    proxy_client = JsonRpcClient(host="127.0.0.1", port=1)
    try:
        proxy_response = await proxy_client.list_proxy_servers(proxy_url)
    finally:
        await proxy_client.close()

    proxy_candidates = _proxy_response_to_candidates(proxy_response)
    static_candidates = _static_entries_to_candidates(static_servers)
    merged = _merge_and_normalize(proxy_candidates, static_candidates)

    result: List[DiscoveryCandidate] = []
    for host, port, sid, scheme in merged:
        commands_response = await _fetch_commands_for_candidate(host, port, scheme)
        result.append(
            DiscoveryCandidate(
                host=host,
                port=port,
                id=sid,
                commands_response=commands_response,
                scheme=scheme,
            )
        )
    return result
