"""Single-flow PSB onboarding: MikroTik secret + supported OLT WAN."""
from dataclasses import dataclass
import asyncio
import json
import os

from app.core.customer_identity import decrypt_pppoe_password
from app.core.device_credentials import decrypt_secret
from app.core.mikrotik import RealMikrotikAdapter, MikrotikProvisioningError
from app.core.cdata_executor import CDataExecutionError, provision_cdata_onu, provision_cdata_pppoe, verify_cdata_onu
from app.models.models import Customer, DeviceCredential, MikrotikDevice, OLTDevice, ProvisioningEvent

_ONBOARDING_LOCKS: dict[int, asyncio.Lock] = {}


def classify_live_connectivity(*, olt_online: bool | None, mikrotik_reachable: bool,
                               secret_enabled: bool | None, pppoe_active: bool | None,
                               internet_reachable: bool | None, errors: list[str]) -> tuple[str, str]:
    """Classify only from observed evidence; never label an unavailable probe offline."""
    if errors or olt_online is None or not mikrotik_reachable or secret_enabled is None or pppoe_active is None:
        detail = "; ".join(errors) if errors else "Probe OLT atau MikroTik belum lengkap."
        return "unknown", f"Status internet belum bisa dipastikan. {detail}"
    if not olt_online:
        return "offline", "ONU terverifikasi offline di OLT."
    if not secret_enabled:
        return "offline", "Secret PPPoE tidak aktif atau tidak ditemukan di MikroTik."
    if not pppoe_active:
        return "waiting_session", "ONU online dan secret aktif, tetapi sesi PPPoE belum terbentuk."
    if internet_reachable is None:
        return "unknown", "Sesi PPPoE aktif, tetapi probe internet belum bisa dipastikan."
    if not internet_reachable:
        return "unknown", "Sesi PPPoE aktif, tetapi probe internet BestWeb gagal; status fisik pelanggan perlu dipastikan ulang."
    return "active", "Internet aktif; ONU, sesi PPPoE, dan probe internet terverifikasi live."


@dataclass
class OnboardingResult:
    mikrotik: str
    olt: str
    message: str


async def _mikrotik(customer: Customer, db) -> tuple[str, str]:
    if not all((customer.mikrotik_device_id, customer.pppoe_username, customer.pppoe_password)):
        return "pending_configuration", "Device MikroTik atau kredensial PPPoE belum lengkap."
    device = await db.get(MikrotikDevice, customer.mikrotik_device_id)
    if not device or not device.is_active or not device.credential_id:
        return "pending_configuration", "Device MikroTik belum aktif atau credential belum terikat."
    credential = await db.get(DeviceCredential, device.credential_id)
    if not credential or not credential.is_active:
        return "pending_configuration", "Credential MikroTik belum aktif."
    try:
        adapter = RealMikrotikAdapter(
            host=device.host,
            username=credential.username,
            password=decrypt_secret(credential.encrypted_secret),
            api_port=device.api_port or 8728,
        )
        profile, used_default = await asyncio.to_thread(adapter.resolve_profile, customer.service_profile)
        await asyncio.to_thread(
            adapter.upsert_pppoe_secret,
            customer.pppoe_username,
            decrypt_pppoe_password(customer.pppoe_password),
            profile,
        )
        enabled, active = await asyncio.to_thread(adapter.verify_pppoe, customer.pppoe_username)
        if not enabled:
            return "failed", "Secret PPPoE tidak terverifikasi aktif di MikroTik."
        if not active:
            return "waiting_session", f"Secret PPPoE aktif pada {device.name}; menunggu sesi pelanggan."
        suffix = " Profile default dipakai." if used_default else ""
        return "provisioned", f"PPPoE MikroTik aktif pada {device.name}; sesi pelanggan terverifikasi.{suffix}"
    except (MikrotikProvisioningError, ValueError, RuntimeError) as exc:
        return "failed", f"MikroTik gagal: {str(exc)[:300]}"
    except Exception:
        return "failed", "MikroTik gagal karena koneksi atau credential tidak valid."


async def _olt(customer: Customer, db) -> tuple[str, str]:
    required = (customer.olt_device_id, customer.ont_sn, customer.olt_port, customer.onu_id)
    if not all(required):
        return "pending_configuration", "OLT belum lengkap: device, SN ONT, port PON, dan ONU ID wajib."
    if customer.olt_vendor != "C-DATA" or customer.olt_model != "FD1601S-B1":
        return "pending_configuration", "Executor otomatis belum tersedia untuk model OLT ini."
    olt = await db.get(OLTDevice, customer.olt_device_id)
    if not olt or not olt.is_active or olt.management_protocol != "ssh" or not olt.credential_id:
        return "pending_configuration", "OLT SSH belum aktif atau credential belum terikat."
    credential = await db.get(DeviceCredential, olt.credential_id)
    pinned = os.getenv(f"OLT_{olt.id}_SSH_HOST_KEY_SHA256")
    if not credential or not credential.is_active or not pinned:
        return "pending_configuration", "Credential atau fingerprint host-key OLT belum siap."
    try:
        await asyncio.to_thread(
            provision_cdata_onu,
            host=olt.host, port=olt.api_port or 22, username=credential.username,
            encrypted_secret=credential.encrypted_secret, pinned_host_key=pinned,
            pon_port=customer.olt_port, onu_id=customer.onu_id, serial_number=customer.ont_sn,
            profile_name="2p",
        )
        result = await asyncio.to_thread(
            provision_cdata_pppoe,
            host=olt.host, port=olt.api_port or 22, username=credential.username,
            encrypted_secret=credential.encrypted_secret, pinned_host_key=pinned,
            pon_port=customer.olt_port, onu_id=customer.onu_id, serial_number=customer.ont_sn,
            pppoe_username=customer.pppoe_username,
            pppoe_password=decrypt_pppoe_password(customer.pppoe_password),
            vlan=customer.service_vlan or customer.pppoe_vlan or 100,
        )
        verified = await asyncio.to_thread(
            verify_cdata_onu,
            host=olt.host, port=olt.api_port or 22, username=credential.username,
            encrypted_secret=credential.encrypted_secret, pinned_host_key=pinned,
            pon_port=customer.olt_port, onu_id=customer.onu_id, serial_number=customer.ont_sn,
        )
        if not verified:
            return "failed", "OLT tidak mengonfirmasi SN/ONU setelah command diterima."
        return "provisioned", result.message
    except CDataExecutionError as exc:
        return "failed", f"OLT gagal: {str(exc)[:300]}"
    except Exception:
        return "failed", "OLT gagal karena koneksi, credential, atau host-key tidak valid."


async def _run_onboarding_unlocked(customer: Customer, db) -> OnboardingResult:
    """Run the complete supported path and persist only sanitized status text."""
    customer.provisioning_status = "running"
    customer.status = "configuring"
    olt_status, olt_message = await _olt(customer, db)
    mk_status, mk_message = await _mikrotik(customer, db)
    customer.mikrotik_provisioning_status = mk_status
    customer.olt_provisioning_status = olt_status
    if mk_status == "provisioned" and olt_status == "provisioned":
        customer.provisioning_status = "provisioned"
        customer.status = "active"
        message = f"Internet aktif. {mk_message} {olt_message}"
    elif "failed" in (mk_status, olt_status):
        customer.provisioning_status = "failed"
        message = f"Provisioning belum selesai. {mk_message} {olt_message}"
    else:
        customer.provisioning_status = "waiting_session" if "waiting_session" in (mk_status, olt_status) else "pending_configuration"
        message = f"Menunggu sesi PPPoE pelanggan. {mk_message} {olt_message}" if mk_status == "waiting_session" else f"Menunggu konfigurasi perangkat. {mk_message} {olt_message}"
    customer.provisioning_message = message
    db.add(ProvisioningEvent(
        customer_id=customer.id, target="onboarding", status=customer.provisioning_status,
        vendor=customer.olt_vendor, olt_device_id=customer.olt_device_id,
        mikrotik_device_id=customer.mikrotik_device_id,
        adapter_key="integrated-onboarding",
        payload_summary=json.dumps({"mikrotik": mk_status, "olt": olt_status}, sort_keys=True),
        message=message,
    ))
    await db.commit()
    return OnboardingResult(mk_status, olt_status, message)


async def run_onboarding(customer: Customer, db) -> OnboardingResult:
    """Single hardware entry point; serialize duplicate clicks per customer."""
    lock = _ONBOARDING_LOCKS.setdefault(customer.id, asyncio.Lock())
    async with lock:
        return await _run_onboarding_unlocked(customer, db)


async def refresh_customer_online_status(customer: Customer, db) -> str:
    """Read-only reconciliation with explicit proof and an honest unknown state."""
    olt_online: bool | None = None
    mk_reachable = False
    mk_enabled: bool | None = None
    mk_active: bool | None = None
    internet_reachable: bool | None = None
    errors: list[str] = []

    olt = await db.get(OLTDevice, customer.olt_device_id)
    oc = await db.get(DeviceCredential, olt.credential_id) if olt and olt.credential_id else None
    pinned = os.getenv(f"OLT_{olt.id}_SSH_HOST_KEY_SHA256") if olt else None
    if not (olt and oc and pinned):
        errors.append("Konfigurasi probe OLT belum lengkap.")
    else:
        try:
            olt_online = await asyncio.to_thread(
                verify_cdata_onu, host=olt.host, port=olt.api_port or 22,
                username=oc.username, encrypted_secret=oc.encrypted_secret,
                pinned_host_key=pinned, pon_port=customer.olt_port,
                onu_id=customer.onu_id, serial_number=customer.ont_sn,
            )
        except Exception as exc:
            errors.append(f"Probe OLT gagal: {str(exc)[:160]}")

    mk = await db.get(MikrotikDevice, customer.mikrotik_device_id)
    mc = await db.get(DeviceCredential, mk.credential_id) if mk and mk.credential_id else None
    if not (mk and mc):
        errors.append("Konfigurasi probe MikroTik belum lengkap.")
    else:
        try:
            adapter = RealMikrotikAdapter(
                host=mk.host, username=mc.username,
                password=decrypt_secret(mc.encrypted_secret), api_port=mk.api_port or 8728,
            )
            mk_enabled, mk_active, internet_reachable = await asyncio.to_thread(
                adapter.verify_pppoe_internet, customer.pppoe_username
            )
            mk_reachable = True
        except Exception as exc:
            errors.append(f"Probe MikroTik gagal: {str(exc)[:160]}")

    state, message = classify_live_connectivity(
        olt_online=olt_online, mikrotik_reachable=mk_reachable,
        secret_enabled=mk_enabled, pppoe_active=mk_active,
        internet_reachable=internet_reachable, errors=errors,
    )
    customer.status = state
    customer.provisioning_status = "provisioned" if state == "active" else state
    customer.provisioning_message = message
    await db.commit()
    return customer.status
