"""
Client device discovery API endpoint.

Fetches ARP, DHCP lease, and wireless registration data from a RouterOS device
via the NATS command proxy, merges by MAC address, and returns a unified client list.

All routes are tenant-scoped under:
    /api/tenants/{tenant_id}/devices/{device_id}/clients

RLS is enforced via get_db() (app_user engine with tenant context).
RBAC: viewer and above (read-only operation).
"""

import asyncio
import uuid
from datetime import datetime, timezone
from typing import Any

import structlog
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.database import get_db
from app.middleware.rbac import require_min_role
from app.middleware.tenant_context import CurrentUser, get_current_user
from app.models.device import Device
from app.services import routeros_proxy

logger = structlog.get_logger(__name__)

router = APIRouter(tags=["clients"])


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


async def _check_tenant_access(
    current_user: CurrentUser, tenant_id: uuid.UUID, db: AsyncSession
) -> None:
    """Verify the current user is allowed to access the given tenant."""
    if current_user.is_super_admin:
        from app.database import set_tenant_context

        await set_tenant_context(db, str(tenant_id))
        return
    if current_user.tenant_id != tenant_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Access denied: you do not belong to this tenant.",
        )


async def _check_device_online(db: AsyncSession, device_id: uuid.UUID) -> Device:
    """Verify the device exists and is online. Returns the Device object."""
    result = await db.execute(
        select(Device).where(Device.id == device_id)  # type: ignore[arg-type]
    )
    device = result.scalar_one_or_none()
    if device is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Device {device_id} not found",
        )
    if device.status != "online":
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Device is offline -- client discovery requires a live connection.",
        )
    return device


# ---------------------------------------------------------------------------
# MAC-address merge logic
# ---------------------------------------------------------------------------


def _normalize_mac(mac: str) -> str:
    """Normalize a MAC address to uppercase colon-separated format."""
    return mac.strip().upper().replace("-", ":")


def _merge_client_data(
    arp_data: list[dict[str, Any]],
    dhcp_data: list[dict[str, Any]],
    wireless_data: list[dict[str, Any]],
) -> list[dict[str, Any]]:
    """Merge ARP, DHCP lease, and wireless registration data by MAC address.

    ARP entries are the base. DHCP enriches with hostname. Wireless enriches
    with signal/tx/rx/uptime and marks the client as wireless.
    """
    # Index DHCP leases by MAC
    dhcp_by_mac: dict[str, dict[str, Any]] = {}
    for lease in dhcp_data:
        mac_raw = lease.get("mac-address") or lease.get("active-mac-address", "")
        if mac_raw:
            dhcp_by_mac[_normalize_mac(mac_raw)] = lease

    # Index wireless registrations by MAC
    wireless_by_mac: dict[str, dict[str, Any]] = {}
    for reg in wireless_data:
        mac_raw = reg.get("mac-address", "")
        if mac_raw:
            wireless_by_mac[_normalize_mac(mac_raw)] = reg

    # Track which MACs we've already processed (from ARP)
    seen_macs: set[str] = set()
    clients: list[dict[str, Any]] = []

    # Start with ARP entries as base
    for entry in arp_data:
        mac_raw = entry.get("mac-address", "")
        if not mac_raw:
            continue
        mac = _normalize_mac(mac_raw)
        if mac in seen_macs:
            continue
        seen_macs.add(mac)

        # Determine status: ARP complete flag or dynamic flag
        is_complete = entry.get("complete", "true").lower() == "true"
        arp_status = "reachable" if is_complete else "stale"

        client: dict[str, Any] = {
            "mac": mac,
            "ip": entry.get("address", ""),
            "interface": entry.get("interface", ""),
            "hostname": None,
            "status": arp_status,
            "signal_strength": None,
            "tx_rate": None,
            "rx_rate": None,
            "uptime": None,
            "is_wireless": False,
        }

        # Enrich with DHCP data
        dhcp = dhcp_by_mac.get(mac)
        if dhcp:
            client["hostname"] = dhcp.get("host-name") or None
            dhcp_status = dhcp.get("status", "")
            if dhcp_status:
                client["dhcp_status"] = dhcp_status

        # Enrich with wireless data
        wireless = wireless_by_mac.get(mac)
        if wireless:
            client["is_wireless"] = True
            client["signal_strength"] = wireless.get("signal-strength") or None
            client["tx_rate"] = wireless.get("tx-rate") or None
            client["rx_rate"] = wireless.get("rx-rate") or None
            client["uptime"] = wireless.get("uptime") or None

        clients.append(client)

    # Also include DHCP-only entries (no ARP match -- e.g. expired leases)
    for mac, lease in dhcp_by_mac.items():
        if mac in seen_macs:
            continue
        seen_macs.add(mac)

        client = {
            "mac": mac,
            "ip": lease.get("active-address") or lease.get("address", ""),
            "interface": lease.get("active-server") or "",
            "hostname": lease.get("host-name") or None,
            "status": "stale",  # No ARP entry = not actively reachable
            "signal_strength": None,
            "tx_rate": None,
            "rx_rate": None,
            "uptime": None,
            "is_wireless": mac in wireless_by_mac,
        }

        wireless = wireless_by_mac.get(mac)
        if wireless:
            client["signal_strength"] = wireless.get("signal-strength") or None
            client["tx_rate"] = wireless.get("tx-rate") or None
            client["rx_rate"] = wireless.get("rx-rate") or None
            client["uptime"] = wireless.get("uptime") or None

        clients.append(client)

    return clients


# ---------------------------------------------------------------------------
# Endpoint
# ---------------------------------------------------------------------------


@router.get(
    "/tenants/{tenant_id}/devices/{device_id}/clients",
    summary="List connected client devices (ARP + DHCP + wireless)",
)
async def list_clients(
    tenant_id: uuid.UUID,
    device_id: uuid.UUID,
    current_user: CurrentUser = Depends(get_current_user),
    _role: CurrentUser = Depends(require_min_role("viewer")),
    db: AsyncSession = Depends(get_db),
) -> dict[str, Any]:
    """Discover all client devices connected to a MikroTik device.

    Fetches ARP table, DHCP server leases, and wireless registration table
    in parallel, then merges by MAC address into a unified client list.

    Wireless fetch failure is non-fatal (device may not have wireless interfaces).
    DHCP fetch failure is non-fatal (device may not run a DHCP server).
    ARP fetch failure is fatal (core data source).
    """
    await _check_tenant_access(current_user, tenant_id, db)
    await _check_device_online(db, device_id)

    device_id_str = str(device_id)

    # Fetch all three sources in parallel
    arp_result, dhcp_result, wireless_result = await asyncio.gather(
        routeros_proxy.execute_command(device_id_str, "/ip/arp/print"),
        routeros_proxy.execute_command(device_id_str, "/ip/dhcp-server/lease/print"),
        routeros_proxy.execute_command(
            device_id_str, "/interface/wireless/registration-table/print"
        ),
        return_exceptions=True,
    )

    # ARP is required -- if it failed, return 502
    if isinstance(arp_result, Exception):
        logger.error("ARP fetch exception", device_id=device_id_str, error=str(arp_result))
        raise HTTPException(
            status_code=status.HTTP_502_BAD_GATEWAY,
            detail=f"Failed to fetch ARP table: {arp_result}",
        )
    if not arp_result.get("success"):
        raise HTTPException(
            status_code=status.HTTP_502_BAD_GATEWAY,
            detail=arp_result.get("error", "Failed to fetch ARP table"),
        )

    arp_data: list[dict[str, Any]] = arp_result.get("data", [])

    # DHCP is optional -- log warning and continue with empty data
    dhcp_data: list[dict[str, Any]] = []
    if isinstance(dhcp_result, Exception):
        logger.warning(
            "DHCP fetch exception (continuing without DHCP data)",
            device_id=device_id_str,
            error=str(dhcp_result),
        )
    elif not dhcp_result.get("success"):
        logger.warning(
            "DHCP fetch failed (continuing without DHCP data)",
            device_id=device_id_str,
            error=dhcp_result.get("error"),
        )
    else:
        dhcp_data = dhcp_result.get("data", [])

    # Wireless is optional -- many devices have no wireless interfaces
    wireless_data: list[dict[str, Any]] = []
    if isinstance(wireless_result, Exception):
        logger.warning(
            "Wireless fetch exception (device may not have wireless interfaces)",
            device_id=device_id_str,
            error=str(wireless_result),
        )
    elif not wireless_result.get("success"):
        logger.warning(
            "Wireless fetch failed (device may not have wireless interfaces)",
            device_id=device_id_str,
            error=wireless_result.get("error"),
        )
    else:
        wireless_data = wireless_result.get("data", [])

    # Merge by MAC address
    clients = _merge_client_data(arp_data, dhcp_data, wireless_data)

    logger.info(
        "client_discovery_complete",
        device_id=device_id_str,
        tenant_id=str(tenant_id),
        arp_count=len(arp_data),
        dhcp_count=len(dhcp_data),
        wireless_count=len(wireless_data),
        merged_count=len(clients),
    )

    return {
        "clients": clients,
        "device_id": device_id_str,
        "timestamp": datetime.now(timezone.utc).isoformat(),
    }
