"""
Validate server candidate: reachability and server-reported ID match (TZ FR-7).

This module provides stateless validation of a single candidate: the candidate
must be reachable (GET /health), and the server-reported identity must match
the expected UUID4. Used by the polling loop to filter the active server list.

Server-reported identity: obtained from GET /health response. The server
must expose only the identifier from config (registration.instance_uuid).
Checked fields (in order): top-level "instance_uuid", "uuid", "server_id"; or
components.proxy_registration.instance_uuid, .uuid. Value must be valid UUID4.
If missing or invalid, validation fails with invalid_id; if present but not
equal to expected_server_id, id_mismatch.

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from __future__ import annotations

import logging
import uuid
from dataclasses import dataclass
from typing import Any, Dict, Optional

import httpx

# Endpoint path for reachability and identity (documented project convention).
HEALTH_PATH = "/health"

logger = logging.getLogger(__name__)


@dataclass
class ValidationResult:
    """
    Result of validating a server candidate.

    Attributes:
        valid: True if candidate is reachable and reported ID matches expected UUID4.
        reason: None when valid; otherwise one of "unreachable", "id_mismatch", "invalid_id".
        reported_id: Server-reported identity (when available); for diagnostics.
    """

    valid: bool
    reason: Optional[str] = None
    reported_id: Optional[str] = None

    def __post_init__(self) -> None:
        """Ensure reason is set when not valid."""
        if not self.valid and self.reason is None:
            self.reason = "invalid_id"


def _is_valid_uuid4(value: str) -> bool:
    """
    Return True if value is a non-empty string and valid UUID4.

    Args:
        value: String to check.

    Returns:
        True if valid UUID4, False otherwise.
    """
    if not value or not isinstance(value, str):
        return False
    value_stripped = value.strip()
    if not value_stripped:
        return False
    try:
        u = uuid.UUID(value_stripped)
        return u.version == 4
    except (ValueError, TypeError, AttributeError):
        return False


def _extract_reported_id(body: Dict[str, Any]) -> Optional[str]:
    """
    Extract server-reported identity from GET /health response body.

    Checked fields (in order): top-level instance_uuid, uuid, server_id;
    then components.proxy_registration.instance_uuid, .uuid.
    Returns the first non-empty string value found; does not validate UUID4.

    Args:
        body: Parsed JSON response from GET /health.

    Returns:
        Raw string value of the first found identity field, or None.
    """
    for key in ("instance_uuid", "uuid", "server_id"):
        val = body.get(key)
        if isinstance(val, str) and val.strip():
            return val.strip()
    components = body.get("components")
    if isinstance(components, dict):
        proxy_reg = components.get("proxy_registration")
        if isinstance(proxy_reg, dict):
            for key in ("instance_uuid", "uuid"):
                val = proxy_reg.get(key)
                if isinstance(val, str) and val.strip():
                    return val.strip()
    return None


async def validate_candidate(
    host: str,
    port: int,
    expected_server_id: str,
    *,
    protocol: str = "http",
    timeout: float = 10.0,
    verify_ssl: bool = False,
) -> ValidationResult:
    """
    Validate a server candidate: reachable and server-reported ID matches expected UUID4.

    Performs (1) reachability check via GET {protocol}://{host}:{port}/health,
    (2) extraction of server-reported identity from the response,
    (3) UUID4 validation of reported value,
    (4) comparison with expected_server_id (case-insensitive).
    Logs errors for id_mismatch and invalid_id per TZ FR-7.

    Args:
        host: Server host (e.g. hostname or IP).
        port: Server port.
        expected_server_id: Expected server identifier (must be valid UUID4).
        protocol: "http" or "https". Default "http".
        timeout: Request timeout in seconds. Default 10.0.
        verify_ssl: If True, verify HTTPS server certificate. Default False for flexibility.

    Returns:
        ValidationResult with valid=True if reachable and ID match; otherwise valid=False
        and reason one of "unreachable", "id_mismatch", "invalid_id".
    """
    base_url = f"{protocol}://{host}:{port}"
    url = f"{base_url.rstrip('/')}{HEALTH_PATH}"

    try:
        async with httpx.AsyncClient(verify=verify_ssl, timeout=timeout) as client:
            response = await client.get(url)
            response.raise_for_status()
    except (httpx.HTTPError, httpx.RequestError, OSError) as e:
        logger.debug(
            "Candidate unreachable: %s:%s %s",
            host,
            port,
            type(e).__name__,
            exc_info=False,
        )
        return ValidationResult(valid=False, reason="unreachable")

    try:
        body = response.json()
    except (ValueError, TypeError) as e:
        logger.error(
            "Candidate %s:%s: invalid JSON from %s: %s",
            host,
            port,
            HEALTH_PATH,
            e,
        )
        return ValidationResult(valid=False, reason="invalid_id")

    reported = _extract_reported_id(body)
    if not reported:
        logger.error(
            "Candidate %s:%s: no server-reported identity in %s (expected UUID4)",
            host,
            port,
            HEALTH_PATH,
        )
        return ValidationResult(valid=False, reason="invalid_id", reported_id=None)

    if not _is_valid_uuid4(reported):
        logger.error(
            "Candidate %s:%s: server-reported ID is not valid UUID4: %r",
            host,
            port,
            reported,
        )
        return ValidationResult(valid=False, reason="invalid_id", reported_id=reported)

    expected_normalized = expected_server_id.strip().lower()
    reported_normalized = reported.lower()
    if expected_normalized != reported_normalized:
        logger.error(
            "Candidate %s:%s: ID mismatch: expected %r, server reported %r",
            host,
            port,
            expected_server_id,
            reported,
        )
        return ValidationResult(valid=False, reason="id_mismatch", reported_id=reported)

    return ValidationResult(valid=True, reason=None, reported_id=reported)
