"""
Sondeo DHCP en LAN: quien responde a DISCOVER (quien 'manda' las IPs).

No altera la red: solo envia un DISCOVER broadcast y lee OFFER.
Si Windows no deja bindear el puerto 68 (sin admin), se informa y se usa
ipconfig como evidencia parcial.
"""
from __future__ import annotations

import os
import random
import socket
import struct
import time
from typing import Dict, List, Optional, Tuple

from .models import normalize_mac
from .netinfo import _run


DHCP_MAGIC = b"\x63\x82\x53\x63"
DHCP_DISCOVER = 1
DHCP_OFFER = 2


def _local_mac_bytes() -> bytes:
    """MAC del adaptador activo (best effort)."""
    out = _run(["getmac", "/fo", "csv", "/nh"])
    for line in out.splitlines():
        # "AA-BB-CC-DD-EE-FF","Nombre"
        if "Media disconnected" in line or "desconect" in line.lower():
            continue
        m = None
        import re

        m = re.search(r"([0-9A-Fa-f]{2}[-:]){5}[0-9A-Fa-f]{2}", line)
        if m:
            mac = normalize_mac(m.group(0))
            parts = mac.split(":")
            if len(parts) == 6:
                try:
                    return bytes(int(p, 16) for p in parts)
                except Exception:
                    pass
    return os.urandom(6)


def _build_discover(xid: int, chaddr: bytes) -> bytes:
    chaddr16 = (chaddr + b"\x00" * 16)[:16]
    # op, htype, hlen, hops, xid, secs, flags (broadcast), ci, yi, si, gi
    pkt = struct.pack(
        "!BBBBIHH4s4s4s4s",
        1, 1, 6, 0,
        xid & 0xFFFFFFFF,
        0,
        0x8000,
        b"\x00" * 4,
        b"\x00" * 4,
        b"\x00" * 4,
        b"\x00" * 4,
    )
    pkt += chaddr16
    pkt += b"\x00" * 64  # sname
    pkt += b"\x00" * 128  # file
    pkt += DHCP_MAGIC
    # options: 53=discover, 55=param request (1,3,6,28,51,54), 255
    pkt += bytes([53, 1, DHCP_DISCOVER])
    pkt += bytes([55, 6, 1, 3, 6, 28, 51, 54])
    pkt += bytes([255])
    return pkt


def _parse_options(raw: bytes) -> Dict[int, bytes]:
    opts: Dict[int, bytes] = {}
    i = 0
    while i < len(raw):
        tag = raw[i]
        if tag == 255:
            break
        if tag == 0:
            i += 1
            continue
        if i + 1 >= len(raw):
            break
        ln = raw[i + 1]
        val = raw[i + 2 : i + 2 + ln]
        opts[tag] = val
        i += 2 + ln
    return opts


def _ip_from_bytes(b: bytes) -> str:
    if len(b) >= 4:
        return ".".join(str(x) for x in b[:4])
    return ""


def parse_dhcp_offer(data: bytes) -> Optional[Dict]:
    if len(data) < 240:
        return None
    if data[236:240] != DHCP_MAGIC:
        return None
    op = data[0]
    yiaddr = _ip_from_bytes(data[16:20])
    siaddr = _ip_from_bytes(data[20:24])
    opts = _parse_options(data[240:])
    msg_type = opts.get(53, b"\x00")[:1]
    if msg_type != bytes([DHCP_OFFER]) and op != 2:
        # aceptar OFFER o BOOTREPLY
        if op != 2:
            return None
    server = _ip_from_bytes(opts.get(54, b"")) or siaddr
    mask = _ip_from_bytes(opts.get(1, b""))
    router = _ip_from_bytes(opts.get(3, b""))
    lease = 0
    if 51 in opts and len(opts[51]) >= 4:
        lease = struct.unpack("!I", opts[51][:4])[0]
    return {
        "offered_ip": yiaddr,
        "server_ip": server,
        "siaddr": siaddr,
        "gateway_offered": router,
        "mask_offered": mask,
        "lease_sec": lease,
    }


def dhcp_discover(timeout_sec: float = 3.0) -> Dict:
    """
    Broadcast DHCP DISCOVER. Devuelve lista de servidores que OFFER.
    """
    result = {
        "ok": False,
        "servers": [],
        "error": "",
        "bind_port": 68,
        "note": (
            "Un DHCP Discover pregunta 'quien da IPs en esta LAN'. "
            "Si responden 2 equipos (p.ej. Askey y Tenda), hay doble DHCP."
        ),
    }
    chaddr = _local_mac_bytes()
    xid = random.randint(1, 0xFFFFFFFE)
    payload = _build_discover(xid, chaddr)
    sock: Optional[socket.socket] = None
    servers: List[Dict] = []
    seen = set()
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            sock.bind(("0.0.0.0", 68))
        except OSError:
            # sin privilegio / puerto ocupado (cliente DHCP de Windows)
            sock.bind(("0.0.0.0", 0))
            result["bind_port"] = sock.getsockname()[1]
            result["error"] = (
                "No se pudo usar el puerto 68 (Windows DHCP lo ocupa o falta admin). "
                "El sondeo puede no recibir OFFER; se usa ipconfig como evidencia."
            )
        sock.settimeout(0.4)
        sock.sendto(payload, ("255.255.255.255", 67))
        deadline = time.time() + timeout_sec
        while time.time() < deadline:
            try:
                data, addr = sock.recvfrom(2048)
            except socket.timeout:
                continue
            except OSError:
                break
            parsed = parse_dhcp_offer(data)
            if not parsed:
                continue
            sip = parsed.get("server_ip") or (addr[0] if addr else "")
            if not sip or sip in seen:
                continue
            seen.add(sip)
            parsed["from_addr"] = addr[0] if addr else sip
            servers.append(parsed)
        result["servers"] = servers
        result["ok"] = True
        if servers:
            result["error"] = ""
    except Exception as e:
        result["error"] = str(e)
    finally:
        if sock:
            try:
                sock.close()
            except Exception:
                pass
    return result
