"""Cluster discovery: find hub endpoints.

Supports two discovery methods:
  1. Manual: read endpoints from a config file
  2. Tailscale: query Tailscale API for tagged devices
"""
from __future__ import annotations

import json
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)


@dataclass
class ClusterEndpoint:
    """A discovered cluster hub endpoint."""
    address: str         # IP or hostname
    port: int = 8000     # Overseer API port
    name: str = ""       # Human-readable name
    source: str = ""     # Discovery source ("manual", "tailscale")
    metadata: dict[str, Any] = field(default_factory=dict)

    @property
    def url(self) -> str:
        return f"http://{self.address}:{self.port}"

    def to_dict(self) -> dict[str, Any]:
        return {
            "address": self.address,
            "port": self.port,
            "name": self.name,
            "source": self.source,
            "url": self.url,
        }


def discover_manual(config_path: str | Path) -> list[ClusterEndpoint]:
    """Read hub endpoints from a JSON config file.

    Expected format:
        {
            "endpoints": [
                {"address": "100.81.247.14", "port": 8000, "name": "hub-1"}
            ]
        }
    """
    path = Path(config_path)
    if not path.exists():
        logger.warning("Discovery config not found: %s", path)
        return []

    try:
        data = json.loads(path.read_text())
        endpoints = []
        for entry in data.get("endpoints", []):
            endpoints.append(ClusterEndpoint(
                address=entry["address"],
                port=entry.get("port", 8000),
                name=entry.get("name", ""),
                source="manual",
            ))
        return endpoints
    except Exception:
        logger.exception("Failed to read discovery config: %s", path)
        return []


def discover_tailscale(
    tag: str = "greymatter",
    api_key: str | None = None,
    tailnet: str | None = None,
) -> list[ClusterEndpoint]:
    """Discover hub endpoints via Tailscale API.

    Looks for devices with the specified tag. Requires TAILSCALE_API_KEY
    and TAILSCALE_TAILNET environment variables (or explicit params).
    """
    import os

    api_key = api_key or os.environ.get("TAILSCALE_API_KEY")
    tailnet = tailnet or os.environ.get("TAILSCALE_TAILNET")

    if not api_key or not tailnet:
        logger.debug("Tailscale discovery skipped: no API key or tailnet configured")
        return []

    try:
        import urllib.request

        url = f"https://api.tailscale.com/api/v2/tailnet/{tailnet}/devices"
        req = urllib.request.Request(url)
        req.add_header("Authorization", f"Bearer {api_key}")

        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read())

        endpoints = []
        target_tag = f"tag:{tag}"
        for device in data.get("devices", []):
            tags = device.get("tags", [])
            if target_tag in tags:
                addresses = device.get("addresses", [])
                if addresses:
                    endpoints.append(ClusterEndpoint(
                        address=addresses[0],
                        port=8000,
                        name=device.get("hostname", ""),
                        source="tailscale",
                        metadata={"device_id": device.get("id", "")},
                    ))
        return endpoints
    except Exception:
        logger.exception("Tailscale discovery failed")
        return []


def discover_all(
    config_path: str | Path | None = None,
    tailscale_tag: str = "greymatter",
) -> list[ClusterEndpoint]:
    """Run all discovery methods and return combined results.

    Manual config takes priority (listed first). Deduplicates by address.
    """
    endpoints: list[ClusterEndpoint] = []
    seen_addresses: set[str] = set()

    # Manual discovery first
    if config_path:
        for ep in discover_manual(config_path):
            if ep.address not in seen_addresses:
                endpoints.append(ep)
                seen_addresses.add(ep.address)

    # Tailscale discovery
    for ep in discover_tailscale(tag=tailscale_tag):
        if ep.address not in seen_addresses:
            endpoints.append(ep)
            seen_addresses.add(ep.address)

    return endpoints
