"""Pinned, read-only Auto SN discovery for C-DATA FD1601S-B1."""
from __future__ import annotations

import base64
from dataclasses import dataclass
import hashlib
import hmac
import re
import socket
import time

from app.core.device_credentials import decrypt_secret
from app.core.ssh_identity import _normalize_sha256_fingerprint


class InvalidPonPortError(ValueError):
    pass


class DiscoveryUnavailableError(RuntimeError):
    pass


@dataclass(frozen=True)
class AutoSnResult:
    serial_number: str
    brand: str | None = None
    port: str | None = None
    onu_id: int | None = None


_PON_PORT = re.compile(r"^(?:GPON\s+)?0/0/([1-9]\d?)$", re.IGNORECASE)
_PON_ONLY = re.compile(r"^[1-9]\d?$")
_SERIAL = re.compile(r"^\s*SN\s*:\s*([A-Za-z0-9_-]{8,64})(?:\s|$)", re.MULTILINE)
_VENDOR = re.compile(r"^\s*Vendor ID\s*:\s*([A-Za-z0-9_-]{1,64})\s*$", re.MULTILINE)


def normalize_pon_port(value: str | None) -> str:
    raw = (value or "").strip()
    if _PON_ONLY.fullmatch(raw):
        return f"0/0/{raw}"
    match = _PON_PORT.fullmatch(raw)
    if not match:
        raise InvalidPonPortError("GPON/PON harus 0/0/n atau cukup nomor PON, misalnya 0/0/1 atau 1.")
    return f"0/0/{match.group(1)}"


def _fingerprint(key: object) -> str:
    return base64.b64encode(hashlib.sha256(key.asbytes()).digest()).decode("ascii").rstrip("=")


def _read(channel: object, seconds: float) -> str:
    deadline = time.monotonic() + seconds
    chunks: list[bytes] = []
    while time.monotonic() < deadline:
        if channel.recv_ready():
            chunks.append(channel.recv(65535))
            deadline = time.monotonic() + 0.25
        else:
            time.sleep(0.05)
    return b"".join(chunks).decode("utf-8", errors="replace")


def discover_unregistered_onts(*, host: str, port: int, username: str, encrypted_secret: str, pinned_host_key: str | None, pon_port: str) -> list[AutoSnResult]:
    """Run only the reviewed read-only FD1601S-B1 command sequence.

    No caller CLI fragments are accepted. The only variable, PON, is bounded
    by ``normalize_pon_port`` and converted to its numeric component.
    """
    normalized = normalize_pon_port(pon_port)
    pon = normalized.rsplit("/", 1)[1]
    expected = _normalize_sha256_fingerprint(pinned_host_key)
    if expected is None:
        raise DiscoveryUnavailableError("Auto SN diblokir: fingerprint host key SSH belum dipin.")
    if not host or not 1 <= port <= 65535:
        raise DiscoveryUnavailableError("Auto SN diblokir: endpoint management OLT tidak valid.")

    transport = None
    channel = None
    sock = None
    try:
        import paramiko
        sock = socket.create_connection((host, port), timeout=8)
        transport = paramiko.Transport(sock)
        transport.banner_timeout = 8
        transport.auth_timeout = 8
        transport.start_client(timeout=8)
        if not hmac.compare_digest(_fingerprint(transport.get_remote_server_key()), expected):
            raise DiscoveryUnavailableError("Auto SN diblokir: fingerprint server tidak cocok dengan host key yang dipin.")
        secret = decrypt_secret(encrypted_secret)
        try:
            transport.auth_password(username, secret, fallback=False)
        finally:
            secret = ""
        if not transport.is_authenticated():
            raise DiscoveryUnavailableError("Auto SN gagal: autentikasi SSH ditolak.")
        channel = transport.open_session(timeout=8)
        channel.get_pty(term="vt100", width=160, height=40)
        channel.invoke_shell()
        _read(channel, 1.2)
        transcript = ""
        for command, wait in (("enable", 1.2), ("config", 1.2), ("interface gpon 0/0", 1.2), (f"show ont autofind {pon} all", 5.0), (f"show ont info {pon} all", 5.0)):
            channel.send(command + "\r")
            transcript += _read(channel, wait)
        if any(marker in transcript.lower() for marker in ("invalid input", "incomplete command", "unknown command")):
            raise DiscoveryUnavailableError("Auto SN gagal: respons CLI OLT tidak sesuai grammar tervalidasi.")
        info_response = transcript.rsplit(f"show ont info {pon} all", 1)[-1]
        used_onu_ids = {int(match.group(1)) for match in re.finditer(r"^\s*\d+/\d+\s+\d+\s+(\d+)\s+", info_response, re.MULTILINE)}
        next_onu_id = next((candidate for candidate in range(1, 129) if candidate not in used_onu_ids), None)
        if next_onu_id is None:
            raise DiscoveryUnavailableError("Auto SN gagal: tidak ada ONU ID kosong pada PON ini.")
        blocks = re.split(r"(?=^\s*Frame/Slot\s*:)", transcript, flags=re.MULTILINE)
        results: list[AutoSnResult] = []
        seen: set[str] = set()
        for block in blocks:
            serial_match = _SERIAL.search(block)
            if serial_match is None:
                continue
            serial = serial_match.group(1)
            if serial in seen:
                continue
            seen.add(serial)
            vendor_match = _VENDOR.search(block)
            frame_match = re.search(r"^\s*Frame/Slot\s*:\s*([0-9]+/[0-9]+)\s*$", block, re.MULTILINE)
            port_match = re.search(r"^\s*Port\s*:\s*([1-9]\d*)\s*$", block, re.MULTILINE)
            discovered_port = f"{frame_match.group(1)}/{port_match.group(1)}" if frame_match and port_match else normalized
            results.append(AutoSnResult(serial, vendor_match.group(1) if vendor_match else None, discovered_port, next_onu_id))
        return results
    except DiscoveryUnavailableError:
        raise
    except Exception as exc:
        raise DiscoveryUnavailableError("Auto SN gagal saat sesi SSH read-only. Tidak ada provisioning dijalankan.") from exc
    finally:
        if channel is not None:
            channel.close()
        if transport is not None:
            transport.close()
        elif sock is not None:
            sock.close()
