"""C-DATA FD1601S-B1 Hardware Provisioning Executor with Host Key Pinning."""
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
from app.core.cdata_fd1601s_discovery import normalize_pon_port, _fingerprint, _read


class CDataExecutionError(RuntimeError):
    pass


@dataclass(frozen=True)
class CDataProvisionResult:
    success: bool
    pon_port: str
    onu_id: int
    serial_number: str
    message: str
    raw_transcript: str = ""


def provision_cdata_onu(
    *,
    host: str,
    port: int,
    username: str,
    encrypted_secret: str,
    pinned_host_key: str | None,
    pon_port: str,
    onu_id: int,
    serial_number: str,
    profile_name: str | None = None,
) -> CDataProvisionResult:
    """Execute bounded, safe ONU registration on C-DATA FD1601S-B1 OLT.
    
    Commands sent:
    - enable
    - config
    - interface gpon 0/0
    - ont add {pon} {onu_id} sn-auth {sn}
    """
    normalized_pon = normalize_pon_port(pon_port)
    pon = normalized_pon.rsplit("/", 1)[1]
    
    if not (1 <= onu_id <= 128):
        raise CDataExecutionError(f"ONU ID {onu_id} tidak valid (harus 1-128).")
    
    clean_sn = serial_number.strip().upper()
    if not re.fullmatch(r"[A-Z0-9_-]{8,64}", clean_sn):
        raise CDataExecutionError(f"Serial number {serial_number} tidak valid.")

    expected = _normalize_sha256_fingerprint(pinned_host_key)
    if expected is None:
        raise CDataExecutionError("Provisioning diblokir: fingerprint host key SSH belum dipin.")
    if not host or not (1 <= port <= 65535):
        raise CDataExecutionError("Provisioning diblokir: endpoint management OLT tidak valid.")

    transport = None
    channel = None
    sock = None
    transcript = ""
    try:
        import paramiko
        sock = socket.create_connection((host, port), timeout=10)
        transport = paramiko.Transport(sock)
        transport.banner_timeout = 10
        transport.auth_timeout = 10
        transport.start_client(timeout=10)

        if not hmac.compare_digest(_fingerprint(transport.get_remote_server_key()), expected):
            raise CDataExecutionError("Provisioning 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 CDataExecutionError("Provisioning gagal: autentikasi SSH ditolak.")

        channel = transport.open_session(timeout=10)
        channel.get_pty(term="vt100", width=160, height=40)
        channel.invoke_shell()
        _read(channel, 1.2)

        # Standard C-DATA command sequence for ONU registration
        commands = [
            ("enable", 1.2),
            ("config", 1.2),
            ("interface gpon 0/0", 1.2),
            (f"ont add {pon} {onu_id} sn-auth {clean_sn} ont-lineprofile-name 2p ont-srvprofile-name 2p", 3.0),
        ]

        for cmd, wait in commands:
            channel.send(cmd + "\r")
            out = _read(channel, wait)
            transcript += out
            # Check for immediate CLI error responses
            if any(err in out.lower() for err in ("invalid input", "incomplete command", "unknown command", "error", "failed")):
                raise CDataExecutionError(f"OLT CLI menolak command '{cmd}': {out.strip()}")

        # Verify registration via show ont info
        channel.send(f"show ont info {pon} {onu_id}\r")
        verify_out = _read(channel, 3.0)
        transcript += verify_out

        return CDataProvisionResult(
            success=True,
            pon_port=normalized_pon,
            onu_id=onu_id,
            serial_number=clean_sn,
            message=f"ONU ID {onu_id} berhasil didaftarkan di PON {normalized_pon} (SN: {clean_sn}).",
            raw_transcript=transcript,
        )

    except Exception as exc:
        if isinstance(exc, CDataExecutionError):
            raise
        raise CDataExecutionError(f"Koneksi/eksekusi OLT gagal: {exc}") from exc
    finally:
        if channel is not None:
            try:
                channel.close()
            except Exception:
                pass
        if transport is not None:
            try:
                transport.close()
            except Exception:
                pass
        if sock is not None:
            try:
                sock.close()
            except Exception:
                pass


def provision_cdata_pppoe(
    *,
    host: str,
    port: int,
    username: str,
    encrypted_secret: str,
    pinned_host_key: str | None,
    pon_port: str,
    onu_id: int,
    serial_number: str,
    pppoe_username: str,
    pppoe_password: str,
    vlan: int = 100,
    line_profile_name: str = "pppoe-100",
    service_profile_name: str = "2p",
) -> CDataProvisionResult:
    """Apply the verified C-DATA WAN PPPoE sequence to one existing ONU."""
    normalized_pon = normalize_pon_port(pon_port)
    pon = normalized_pon.rsplit("/", 1)[1]
    clean_sn = serial_number.strip().upper()
    if not (1 <= onu_id <= 128) or not re.fullmatch(r"[A-Z0-9_-]{8,64}", clean_sn):
        raise CDataExecutionError("Data ONU tidak valid.")
    if not re.fullmatch(r"[A-Za-z0-9_.@_-]{1,64}", pppoe_username):
        raise CDataExecutionError("Username PPPoE tidak valid.")
    if not 1 <= vlan <= 4094:
        raise CDataExecutionError("VLAN tidak valid.")
    expected = _normalize_sha256_fingerprint(pinned_host_key)
    if expected is None:
        raise CDataExecutionError("Fingerprint host key SSH belum dipin.")
    transport = channel = sock = None
    transcript = ""
    try:
        import paramiko
        sock = socket.create_connection((host, port), timeout=10)
        transport = paramiko.Transport(sock)
        transport.banner_timeout = transport.auth_timeout = 10
        transport.start_client(timeout=10)
        if not hmac.compare_digest(_fingerprint(transport.get_remote_server_key()), expected):
            raise CDataExecutionError("Fingerprint server OLT tidak cocok.")
        secret = decrypt_secret(encrypted_secret)
        try:
            transport.auth_password(username, secret, fallback=False)
        finally:
            secret = ""
        if not transport.is_authenticated():
            raise CDataExecutionError("Autentikasi SSH OLT ditolak.")
        channel = transport.open_session(timeout=10)
        channel.get_pty(term="vt100", width=180, height=60)
        channel.invoke_shell()
        _read(channel, 1.0)
        commands = [
            ("enable", 1.0),
            ("conf", 1.0),
            ("interface gpon 0/0", 1.0),
            (f"ont modify {pon} {onu_id} ont-lineprofile-name {line_profile_name} ont-srvprofile-name {service_profile_name}", 2.0),
            # legacy WAN syntax is not accepted by the live firmware.
            (f"ont ipconfig {pon} {onu_id} ip-index 0 pppoe username {pppoe_username} password {pppoe_password} vlan {vlan} priority 1", 2.5),
            (f"ont ipconfig {pon} {onu_id} ip-index 0 connection-type route", 1.5),
            ("exit", 1.0),
            ("save", 3.0),
            ("interface gpon 0/0", 1.0),
            (f"ont reboot {pon} {onu_id}", 10.0),
        ]
        for cmd, wait in commands:
            channel.send(cmd + "\r")
            out = _read(channel, wait)
            transcript += out
            low = out.lower()
            if any(err in low for err in ("unknown command", "invalid", "error", "failed", "incorrect")):
                if "already exists" not in low:
                    raise CDataExecutionError(f"OLT menolak tahap {cmd.split()[0]}.")
        return CDataProvisionResult(True, normalized_pon, onu_id, clean_sn,
            f"WAN PPPoE ONU {clean_sn} diterapkan pada VLAN {vlan} dan ONU direboot.", transcript)
    except CDataExecutionError:
        raise
    except Exception as exc:
        raise CDataExecutionError(f"Eksekusi WAN PPPoE OLT gagal: {exc}") from exc
    finally:
        for obj in (channel, transport, sock):
            try:
                if obj is not None:
                    obj.close()
            except Exception:
                pass


def verify_cdata_onu(
    *, host: str, port: int, username: str, encrypted_secret: str,
    pinned_host_key: str | None, pon_port: str, onu_id: int, serial_number: str,
) -> bool:
    """Read back one ONU and require the expected serial to be present."""
    normalized_pon = normalize_pon_port(pon_port)
    pon = normalized_pon.rsplit("/", 1)[1]
    expected = _normalize_sha256_fingerprint(pinned_host_key)
    clean_sn = serial_number.strip().upper()
    if expected is None or not clean_sn:
        raise CDataExecutionError("Verifikasi ONU diblokir: host-key atau SN belum siap.")
    import paramiko
    sock = socket.create_connection((host, port), timeout=10)
    transport = paramiko.Transport(sock)
    channel = None
    try:
        transport.start_client(timeout=10)
        if not hmac.compare_digest(_fingerprint(transport.get_remote_server_key()), expected):
            raise CDataExecutionError("Fingerprint server OLT tidak cocok saat verifikasi.")
        secret = decrypt_secret(encrypted_secret)
        try:
            transport.auth_password(username, secret, fallback=False)
        finally:
            secret = ""
        if not transport.is_authenticated():
            raise CDataExecutionError("Autentikasi SSH OLT ditolak saat verifikasi.")
        channel = transport.open_session(timeout=10)
        channel.get_pty(term="vt100", width=180, height=60)
        channel.invoke_shell()
        _read(channel, 1.0)
        for command, wait in (("enable", 1.0), ("config", 1.0), ("interface gpon 0/0", 1.0), (f"show ont info {pon} {onu_id}", 3.0)):
            channel.send(command + "\r")
            output = _read(channel, wait)
        upper = output.upper()
        online = bool(re.search(r"RUN\s+STATE\s*:\s*ONLINE", upper))
        return clean_sn in upper and online
    finally:
        for obj in (channel, transport, sock):
            try:
                if obj is not None:
                    obj.close()
            except Exception:
                pass


def deprovision_cdata_onu(
    *,
    host: str,
    port: int,
    username: str,
    encrypted_secret: str,
    pinned_host_key: str | None,
    pon_port: str,
    onu_id: int,
) -> CDataProvisionResult:
    """Safe rollback/deprovisioning on C-DATA OLT."""
    normalized_pon = normalize_pon_port(pon_port)
    pon = normalized_pon.rsplit("/", 1)[1]
    
    expected = _normalize_sha256_fingerprint(pinned_host_key)
    if expected is None:
        raise CDataExecutionError("Rollback diblokir: fingerprint host key SSH belum dipin.")

    transport = None
    channel = None
    sock = None
    transcript = ""
    try:
        import paramiko
        sock = socket.create_connection((host, port), timeout=10)
        transport = paramiko.Transport(sock)
        transport.start_client(timeout=10)

        if not hmac.compare_digest(_fingerprint(transport.get_remote_server_key()), expected):
            raise CDataExecutionError("Rollback diblokir: fingerprint server tidak cocok.")

        secret = decrypt_secret(encrypted_secret)
        try:
            transport.auth_password(username, secret, fallback=False)
        finally:
            secret = ""

        channel = transport.open_session(timeout=10)
        channel.get_pty(term="vt100", width=160, height=40)
        channel.invoke_shell()
        _read(channel, 1.2)

        commands = [
            ("enable", 1.2),
            ("config", 1.2),
            ("interface gpon 0/0", 1.2),
            (f"ont delete {pon} {onu_id}", 3.0),
        ]

        for cmd, wait in commands:
            channel.send(cmd + "\r")
            transcript += _read(channel, wait)

        return CDataProvisionResult(
            success=True,
            pon_port=normalized_pon,
            onu_id=onu_id,
            serial_number="",
            message=f"ONU ID {onu_id} pada PON {normalized_pon} berhasil dihapus (rollback).",
            raw_transcript=transcript,
        )
    except Exception as exc:
        raise CDataExecutionError(f"Rollback C-DATA gagal: {exc}") from exc
    finally:
        if transport is not None:
            transport.close()
