from fastapi import APIRouter, Request, Form, Depends, status, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.models.models import Customer, CustomerType, CustomerStatus, ProvisioningJob, ProvisioningEvent, ServicePackage, ODP, OLTDevice, MikrotikDevice, AuditLog, DeviceCredential
from app.core.provisioning import provision_customer
from app.core.customer_identity import validate_nik, customer_code_for, default_pppoe_username, encrypt_default_pppoe_password, decrypt_pppoe_password
from app.core.device_credentials import encrypt_secret
from app.core.olt_adapters import get_olt_adapter, UnsupportedOLTError
from app.core.onboarding import run_onboarding, refresh_customer_online_status
from app.core.olt_vendor_profiles import get_vendor_profile, vendor_profiles_for_ui
from app.core.cdata_fd1601s_discovery import (
    DiscoveryUnavailableError,
    InvalidPonPortError,
    discover_unregistered_onts,
    normalize_pon_port,
)
from app.core.security import require_roles, get_current_user
from datetime import datetime, timezone
import asyncio
import json
import os

router = APIRouter(dependencies=[Depends(require_roles("admin_cs"))])
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "app/templates"))

@router.get("/add-customer", response_class=HTMLResponse)
async def add_customer_page(request: Request, db: AsyncSession = Depends(get_db)):
    packages = (await db.execute(select(ServicePackage).where(
        ServicePackage.is_active == True,
        ServicePackage.service_type == "bulanan_ppp",
        ServicePackage.service_mode == "private_pppoe",
    ).order_by(ServicePackage.name))).scalars().all()
    odps = (await db.execute(select(ODP).where(ODP.is_active == True).order_by(ODP.code))).scalars().all()
    olts = (await db.execute(select(OLTDevice).where(OLTDevice.is_active == True).order_by(OLTDevice.name))).scalars().all()
    mikrotiks = (await db.execute(select(MikrotikDevice).where(MikrotikDevice.is_active == True).order_by(MikrotikDevice.name))).scalars().all()
    return templates.TemplateResponse(request=request, name="add_customer.html", context={"packages": packages, "odps": odps, "olts": olts, "mikrotiks": mikrotiks, "vendor_profiles": vendor_profiles_for_ui()})


@router.get("/api/olt-devices/{olt_device_id}/auto-sn")
async def auto_sn_discovery(
    olt_device_id: int,
    pon_port: str,
    db: AsyncSession = Depends(get_db),
):
    """Read unregistered ONT serials through the pinned, read-only OLT session."""
    try:
        normalized_port = normalize_pon_port(pon_port)
    except InvalidPonPortError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc

    olt = await db.get(OLTDevice, olt_device_id)
    if olt is None or not olt.is_active:
        raise HTTPException(status_code=404, detail="OLT tidak tersedia")
    profile = get_vendor_profile(olt.vendor, olt.model)
    if profile is None or (profile.vendor.casefold(), profile.model.casefold()) != ("c-data", "fd1601s-b1"):
        raise HTTPException(status_code=409, detail="Auto SN belum tersedia untuk model OLT ini")
    if not olt.credential_id:
        return JSONResponse(status_code=503, content={"available": False, "results": [], "olt_device_id": olt_device_id, "pon_port": normalized_port, "reason": "Credential Vault OLT belum terikat."})
    credential = await db.get(DeviceCredential, olt.credential_id)
    if credential is None or not credential.is_active or credential.protocol.casefold() != "ssh":
        return JSONResponse(status_code=503, content={"available": False, "results": [], "olt_device_id": olt_device_id, "pon_port": normalized_port, "reason": "Credential SSH OLT tidak tersedia."})
    try:
        results = await asyncio.to_thread(
            discover_unregistered_onts,
            host=olt.host,
            port=olt.api_port,
            username=credential.username,
            encrypted_secret=credential.encrypted_secret,
            pinned_host_key=os.getenv(f"OLT_{olt.id}_SSH_HOST_KEY_SHA256"),
            pon_port=normalized_port,
        )
    except DiscoveryUnavailableError as exc:
        return JSONResponse(status_code=503, content={"available": False, "results": [], "olt_device_id": olt_device_id, "pon_port": normalized_port, "reason": str(exc)})
    return {"available": True, "results": [item.__dict__ for item in results], "olt_device_id": olt_device_id, "pon_port": normalized_port}

@router.post("/add-customer")
async def add_customer(
    request: Request,
    name: str = Form(...),
    nik: str = Form(...),
    address: str = Form(...),
    phone: str = Form(...),
    cust_type: str = Form(...),
    play_type: str = Form("1play"),
    harga_paket: int = Form(...),
    service_profile: str = Form(None),
    package_id: int | None = Form(None),
    provisioning_mode: str = Form("private_pppoe"),
    service_modes_json: str = Form("[\"pppoe\"]"),
    ont_management_mode: str = Form("none"),
    management_pppoe_username: str = Form(None),
    management_pppoe_password: str = Form(None),
    pppoe_username: str = Form(None),
    pppoe_password: str = Form(None),
    onu_id: int | None = Form(None),
    wan_vlan: int = Form(175),
    service_vlan: int = Form(175),
    pppoe_vlan: int = Form(100),
    hotspot_vlan: int = Form(150),
    iptv_vlan: int = Form(15),
    olt_vlan_profile: str = Form("bnbv1009"),
    ont_sn: str = Form(None),
    ont_brand: str = Form(None),
    olt_device_id: int | None = Form(None),
    mikrotik_device_id: int | None = Form(None),
    olt_fields_json: str = Form("{}"),
    olt_vendor: str = Form("ZTE"),
    olt_model: str = Form("C320"),
    olt_port: str = Form(None),
    odp_info: str = Form(None),
    odp_id: int | None = Form(None),
    odp_latitude: str = Form(None),
    odp_longitude: str = Form(None),
    ont_sn_mode: str = Form("manual"),
    onu_id_mode: str = Form("manual"),
    olt_port_mode: str = Form("manual"),
    ont_brand_mode: str = Form("manual"),
    mikrotik_user: str = Form(None),
    vlan_id: int | None = Form(None),
    db: AsyncSession = Depends(get_db)
):
    try:
        nik = validate_nik(nik)
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc))
    if await db.scalar(select(Customer.id).where(Customer.nik == nik)):
        raise HTTPException(status_code=409, detail="NIK sudah terdaftar")
    package = await db.get(ServicePackage, package_id) if package_id else None
    if package_id and (not package or not package.is_active or package.service_type != "bulanan_ppp" or package.service_mode != "private_pppoe"):
        raise HTTPException(status_code=400, detail="PSB hanya menerima paket bulanan PPPoE")
    if package:
        harga_paket, service_profile = package.price, package.mikrotik_profile
        provisioning_mode = package.service_mode
        olt_vlan_profile, service_vlan, iptv_vlan = package.olt_vlan_profile, package.service_vlan, package.iptv_vlan
        pppoe_vlan, hotspot_vlan = package.pppoe_vlan, package.hotspot_vlan
    if play_type != "1play":
        raise HTTPException(status_code=422, detail="Form PSB ini hanya menerima layanan 1Play PPPoE")
    service_modes = ["pppoe"]
    provisioning_mode = "private_pppoe"
    # PPPoE identity is generated only from the immutable customer code below;
    # manual form values are intentionally ignored to prevent mismatched data.
    pppoe_username = None
    pppoe_password = None
    service_modes_json = json.dumps(service_modes, separators=(",", ":"))
    selected_odp = await db.get(ODP, odp_id) if odp_id else None
    if selected_odp is None or not selected_odp.is_active:
        raise HTTPException(status_code=400, detail="Pilih ODP aktif sebelum provisioning")
    selected_olt = await db.get(OLTDevice, olt_device_id) if olt_device_id else None
    if selected_olt is None or not selected_olt.is_active:
        raise HTTPException(status_code=400, detail="Pilih OLT aktif sebelum provisioning")
    selected_mikrotik = await db.get(MikrotikDevice, mikrotik_device_id) if mikrotik_device_id else None
    if mikrotik_device_id and (selected_mikrotik is None or not selected_mikrotik.is_active):
        raise HTTPException(status_code=400, detail="MikroTik/NAS tidak tersedia")
    if selected_mikrotik is None and selected_odp and selected_odp.mikrotik_device_id:
        selected_mikrotik = await db.get(MikrotikDevice, selected_odp.mikrotik_device_id)
    if selected_mikrotik is None or not selected_mikrotik.is_active:
        raise HTTPException(status_code=400, detail="Pilih MikroTik/NAS aktif sebelum provisioning")
    vendor, model, adapter_key, olt_fields = olt_vendor, olt_model, None, {}
    if selected_olt:
        vendor, model = selected_olt.vendor, selected_olt.model
        profile = get_vendor_profile(vendor, model)
        if profile is None:
            raise HTTPException(status_code=422, detail=f"Profil provisioning belum tersedia untuk {vendor} {model}")
        try:
            supplied_fields = json.loads(olt_fields_json or "{}")
        except json.JSONDecodeError:
            raise HTTPException(status_code=422, detail="Field provisioning OLT tidak valid")
        if not isinstance(supplied_fields, dict):
            raise HTTPException(status_code=422, detail="Field provisioning OLT harus berupa objek")
        allowed = {field["key"] for field in profile.fields}
        olt_fields = {key: str(value).strip()[:160] for key, value in supplied_fields.items() if key in allowed and str(value).strip()}
        profile_name = {"1play": "1p", "2play": "2p", "3play": "3p"}[play_type]
        for profile_key in ("line_profile", "service_profile", "ont_line_profile", "ont_srv_profile"):
            if profile_key in allowed:
                olt_fields[profile_key] = profile_name
        missing = [field["label"] for field in profile.fields if field.get("required") == "true" and not olt_fields.get(field["key"])]
        if missing:
            raise HTTPException(status_code=422, detail="Field OLT wajib: " + ", ".join(missing))
        adapter_key = profile.key
    if vendor.casefold() == "c-data" and model.casefold() == "fd1601s-b1":
        pppoe_vlan, hotspot_vlan, iptv_vlan = 100, 150, 200
    service_vlan = pppoe_vlan
    new_customer = Customer(
        name=name,
        nik=nik,
        address=address,
        phone=phone,
        type=cust_type,
        play_type=play_type,
        harga_paket=harga_paket,
        service_package_id=package.id if package else None,
        service_profile_id=package.service_profile_id if package else None,
        service_mode=package.service_mode if package else provisioning_mode,
        service_profile=service_profile,
        ont_management_mode=ont_management_mode if ont_management_mode in {"none", "pppoe"} else "none",
        management_pppoe_username=management_pppoe_username,
        management_pppoe_password=management_pppoe_password,
        provisioning_mode=provisioning_mode,
        service_modes_json=service_modes_json,
        # PPPoE identity is assigned after the immutable customer code exists.
        pppoe_username=None,
        pppoe_password=None,
        onu_id=onu_id,
        wan_vlan=wan_vlan,
        service_vlan=service_vlan,
        pppoe_vlan=pppoe_vlan,
        hotspot_vlan=hotspot_vlan,
        iptv_vlan=iptv_vlan,
        olt_vlan_profile=olt_vlan_profile,
        ont_sn=ont_sn,
        ont_brand=ont_brand,
        olt_device_id=selected_olt.id if selected_olt else None,
        mikrotik_device_id=selected_mikrotik.id if selected_mikrotik else None,
        olt_vendor=vendor,
        olt_model=model,
        olt_adapter_key=adapter_key,
        olt_fields_json=json.dumps(olt_fields, sort_keys=True) if olt_fields else None,
        olt_port=olt_port,
        odp_info=odp_info,
        odp_id=selected_odp.id if selected_odp else None,
        odp_latitude=odp_latitude,
        odp_longitude=odp_longitude,
        ont_sn_mode=ont_sn_mode if ont_sn_mode in {"auto", "manual"} else "manual",
        onu_id_mode=onu_id_mode if onu_id_mode in {"auto", "manual"} else "manual",
        olt_port_mode=olt_port_mode if olt_port_mode in {"auto", "manual"} else "manual",
        ont_brand_mode=ont_brand_mode if ont_brand_mode in {"auto", "manual"} else "manual",
        hardware_discovery_status="unavailable",
        mikrotik_user=mikrotik_user,
        vlan_id=vlan_id,
        status=CustomerStatus.REQUEST
    )
    db.add(new_customer)
    await db.flush()
    new_customer.customer_code = customer_code_for(new_customer.id)
    new_customer.pppoe_username = default_pppoe_username(new_customer.customer_code)
    new_customer.pppoe_password = encrypt_default_pppoe_password(new_customer.customer_code)
    new_customer.pppoe_secret_version = 1
    # One entry point: configure both network sides before showing the initial credential.
    await run_onboarding(new_customer, db)
    return RedirectResponse(url=f"/customers/{new_customer.id}/credentials/initial", status_code=status.HTTP_303_SEE_OTHER)

@router.get("/customers/{customer_id}/credentials/initial", response_class=HTMLResponse)
async def reveal_initial_pppoe_credential(customer_id: int, request: Request, db: AsyncSession = Depends(get_db), user=Depends(get_current_user)):
    customer = await db.get(Customer, customer_id)
    if not customer:
        raise HTTPException(status_code=404, detail="Pelanggan tidak ditemukan")
    if customer.pppoe_secret_revealed_at is not None:
        raise HTTPException(status_code=410, detail="Kredensial awal sudah ditampilkan. Gunakan reset admin bila diperlukan.")
    password = decrypt_pppoe_password(customer.pppoe_password)
    customer.pppoe_secret_revealed_at = datetime.now(timezone.utc)
    db.add(AuditLog(user_id=user.id, action="reveal_initial_pppoe", entity="customer", entity_id=customer.id, detail=customer.customer_code))
    await db.commit()
    return templates.TemplateResponse(request=request, name="credential_notice.html", context={"customer": customer, "password": password, "mode": "Kredensial provisioning awal"})

@router.post("/customers/{customer_id}/credentials/reset", response_class=HTMLResponse)
async def reset_pppoe_credential(customer_id: int, request: Request, new_password: str = Form(""), db: AsyncSession = Depends(get_db), user=Depends(get_current_user)):
    customer = await db.get(Customer, customer_id)
    if not customer:
        raise HTTPException(status_code=404, detail="Pelanggan tidak ditemukan")
    password = new_password.strip() or customer.customer_code
    if len(password) < 8 or len(password) > 128:
        raise HTTPException(status_code=422, detail="Password PPPoE harus 8 sampai 128 karakter")
    customer.pppoe_password = encrypt_default_pppoe_password(password)
    customer.pppoe_secret_version = (customer.pppoe_secret_version or 0) + 1
    db.add(AuditLog(user_id=user.id, action="reset_pppoe", entity="customer", entity_id=customer.id, detail=customer.customer_code))
    await db.commit()
    return templates.TemplateResponse(request=request, name="credential_notice.html", context={"customer": customer, "password": password, "mode": "Kredensial PPPoE setelah reset"})


@router.get("/customers/{customer_id}", response_class=HTMLResponse)
async def customer_detail(customer_id: int, request: Request, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(Customer).where(Customer.id == customer_id))
    customer = result.scalar_one_or_none()
    if not customer:
        raise HTTPException(status_code=404, detail="Pelanggan tidak ditemukan")
    await refresh_customer_online_status(customer, db)
    device_config = None
    device_config_error = None
    try:
        adapter = get_olt_adapter(customer.olt_vendor, customer.olt_model)
        device_config = adapter.generate_config(customer)
    except (ValueError, UnsupportedOLTError) as exc:
        device_config_error = str(exc)
    return templates.TemplateResponse(request=request, name="customer_detail.html", context={"customer": customer, "zte_config": device_config, "zte_config_error": device_config_error, "device_config": device_config, "device_config_error": device_config_error})

@router.post("/customers/{customer_id}/provision")
async def provision_customer_route(customer_id: int, db: AsyncSession = Depends(get_db)):
    customer = (await db.execute(select(Customer).where(Customer.id == customer_id))).scalar_one_or_none()
    if not customer:
        raise HTTPException(status_code=404, detail="Pelanggan tidak ditemukan")
    await run_onboarding(customer, db)
    return RedirectResponse(url=f"/customers/{customer_id}", status_code=303)


@router.post("/customers/{customer_id}/provision-olt")
async def provision_olt_route(
    customer_id: int,
    db: AsyncSession = Depends(get_db),
    current_user=Depends(get_current_user),
):
    """Explicitly register one ONU on a supported C-DATA OLT.

    Technician status changes never call this endpoint. Hardware execution is
    an explicit admin action, guarded by pinned SSH identity and a single
    active job per customer/target.
    """
    return await provision_customer_route(customer_id, db)
    from app.core.cdata_executor import CDataExecutionError, provision_cdata_onu

    customer = (await db.execute(select(Customer).where(Customer.id == customer_id))).scalar_one_or_none()
    if not customer:
        raise HTTPException(status_code=404, detail="Pelanggan tidak ditemukan")
    if customer.olt_adapter_key != "cdata-fd1601s-b1" or customer.olt_vendor != "C-DATA" or customer.olt_model != "FD1601S-B1":
        raise HTTPException(status_code=422, detail="Executor live hanya tersedia untuk C-DATA FD1601S-B1")
    if not customer.olt_device_id or not customer.ont_sn or not customer.olt_port or not customer.onu_id:
        raise HTTPException(status_code=422, detail="Data OLT belum lengkap: SN ONT, port PON, ONU ID, dan OLT wajib tersedia")

    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:
        raise HTTPException(status_code=422, detail="OLT SSH aktif dan credential wajib tersedia")
    credential = await db.get(DeviceCredential, olt.credential_id)
    if not credential or not credential.is_active:
        raise HTTPException(status_code=422, detail="Credential OLT tidak aktif")
    pinned_host_key = os.getenv(f"OLT_{olt.id}_SSH_HOST_KEY_SHA256")
    if not pinned_host_key:
        raise HTTPException(status_code=422, detail="Fingerprint host key OLT belum dipin")

    active_job = await db.scalar(select(ProvisioningJob).where(
        ProvisioningJob.customer_id == customer.id,
        ProvisioningJob.target == "olt",
        ProvisioningJob.status.in_(["queued", "running"]),
    ))
    if active_job:
        raise HTTPException(status_code=409, detail="Provisioning OLT sedang berjalan atau sudah masuk antrean")

    payload_summary = json.dumps({
        "vendor": olt.vendor, "model": olt.model, "adapter": customer.olt_adapter_key,
        "olt_device_id": olt.id, "pon_port": customer.olt_port, "onu_id": customer.onu_id,
        "serial_number": customer.ont_sn, "service_vlan": customer.service_vlan,
        "wan_vlan": customer.wan_vlan,
    }, sort_keys=True)
    job = ProvisioningJob(
        customer_id=customer.id, target="olt", status="running", vendor=olt.vendor,
        adapter_key=customer.olt_adapter_key, olt_device_id=olt.id,
        payload_summary=payload_summary, attempts=1,
    )
    db.add(job)
    customer.olt_provisioning_status = "running"
    await db.commit()

    try:
        result = 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_host_key,
            pon_port=customer.olt_port,
            onu_id=customer.onu_id,
            serial_number=customer.ont_sn,
            profile_name=customer.service_profile,
        )
    except CDataExecutionError as exc:
        message = f"Provisioning OLT gagal: {str(exc)[:500]}"
        job.status = "failed"
        job.last_error = message
        customer.olt_provisioning_status = "failed"
        customer.provisioning_message = message
        db.add(ProvisioningEvent(customer_id=customer.id, target="olt", status="failed",
            vendor=olt.vendor, olt_device_id=olt.id, adapter_key=customer.olt_adapter_key,
            payload_summary=payload_summary, message=message))
        await db.commit()
        raise HTTPException(status_code=502, detail=message)
    except Exception:
        message = "Provisioning OLT gagal karena kesalahan internal. Detail disimpan di audit."
        job.status = "failed"
        job.last_error = message
        customer.olt_provisioning_status = "failed"
        customer.provisioning_message = message
        db.add(ProvisioningEvent(customer_id=customer.id, target="olt", status="failed",
            vendor=olt.vendor, olt_device_id=olt.id, adapter_key=customer.olt_adapter_key,
            payload_summary=payload_summary, message=message))
        await db.commit()
        raise HTTPException(status_code=502, detail=message)

    job.status = "completed"
    job.executed_at = datetime.now(timezone.utc)
    job.last_error = None
    customer.olt_provisioning_status = "provisioned"
    customer.provisioning_message = result.message
    if customer.mikrotik_provisioning_status == "provisioned":
        customer.provisioning_status = "provisioned"
    else:
        customer.provisioning_status = "partially_provisioned"
    db.add(ProvisioningEvent(customer_id=customer.id, target="olt", status="completed",
        vendor=olt.vendor, olt_device_id=olt.id, adapter_key=customer.olt_adapter_key,
        payload_summary=payload_summary, message=result.message))
    await db.commit()
    return RedirectResponse(url=f"/customers/{customer_id}", status_code=303)


@router.post("/customers/{customer_id}/provision-mikrotik")
async def provision_mikrotik_route(
    customer_id: int,
    db: AsyncSession = Depends(get_db),
    current_user=Depends(get_current_user),
):
    """Explicitly create/update one PPPoE secret on the assigned MikroTik."""
    return await provision_customer_route(customer_id, db)
    from app.core.mikrotik import MikrotikProvisioningError, RealMikrotikAdapter
    from app.core.device_credentials import decrypt_secret
    from app.core.customer_identity import decrypt_pppoe_password

    customer = (await db.execute(select(Customer).where(Customer.id == customer_id))).scalar_one_or_none()
    if not customer:
        raise HTTPException(status_code=404, detail="Pelanggan tidak ditemukan")
    if not customer.mikrotik_device_id or not customer.pppoe_username or not customer.pppoe_password:
        raise HTTPException(status_code=422, detail="Data MikroTik belum lengkap: device, username, dan password PPPoE wajib tersedia")

    mikrotik = await db.get(MikrotikDevice, customer.mikrotik_device_id)
    if not mikrotik or not mikrotik.is_active or not mikrotik.credential_id:
        raise HTTPException(status_code=422, detail="Device MikroTik aktif dan credential wajib tersedia")
    credential = await db.get(DeviceCredential, mikrotik.credential_id)
    if not credential or not credential.is_active:
        raise HTTPException(status_code=422, detail="Credential MikroTik tidak aktif")

    active_job = await db.scalar(select(ProvisioningJob).where(
        ProvisioningJob.customer_id == customer.id,
        ProvisioningJob.target == "mikrotik",
        ProvisioningJob.status.in_(["queued", "running"]),
    ))
    if active_job:
        raise HTTPException(status_code=409, detail="Provisioning MikroTik sedang berjalan atau sudah masuk antrean")

    payload_summary = json.dumps({
        "vendor": "MikroTik", "device_id": mikrotik.id, "username": customer.pppoe_username,
        "profile": customer.service_profile or "default", "service": "pppoe",
    }, sort_keys=True)
    job = ProvisioningJob(
        customer_id=customer.id, target="mikrotik", status="running", vendor="MikroTik",
        adapter_key="routeros-pppoe-secret", mikrotik_device_id=mikrotik.id,
        payload_summary=payload_summary, attempts=1,
    )
    db.add(job)
    customer.mikrotik_provisioning_status = "running"
    await db.commit()

    try:
        device_password = decrypt_secret(credential.encrypted_secret)
        customer_password = decrypt_pppoe_password(customer.pppoe_password)
        adapter = RealMikrotikAdapter(
            host=mikrotik.host,
            username=credential.username,
            password=device_password,
            api_port=mikrotik.api_port or 8728,
        )
        effective_profile, used_default_profile = await asyncio.to_thread(
            adapter.resolve_profile,
            customer.service_profile,
        )
        await asyncio.to_thread(
            adapter.upsert_pppoe_secret,
            customer.pppoe_username,
            customer_password,
            effective_profile,
        )
        if used_default_profile:
            customer.provisioning_message = (
                f"Profile {customer.service_profile} tidak tersedia di MikroTik; "
                "secret dikirim memakai profile default."
            )
    except MikrotikProvisioningError as exc:
        message = f"Provisioning MikroTik gagal: {str(exc)[:500]}"
        job.status = "failed"
        job.last_error = message
        customer.mikrotik_provisioning_status = "failed"
        customer.provisioning_message = message
        db.add(ProvisioningEvent(customer_id=customer.id, target="mikrotik", status="failed",
            vendor="MikroTik", mikrotik_device_id=mikrotik.id, adapter_key="routeros-pppoe-secret",
            payload_summary=payload_summary, message=message))
        await db.commit()
        raise HTTPException(status_code=502, detail=message)
    except Exception:
        message = "Provisioning MikroTik gagal karena kesalahan internal. Detail disimpan di audit."
        job.status = "failed"
        job.last_error = message
        customer.mikrotik_provisioning_status = "failed"
        customer.provisioning_message = message
        db.add(ProvisioningEvent(customer_id=customer.id, target="mikrotik", status="failed",
            vendor="MikroTik", mikrotik_device_id=mikrotik.id, adapter_key="routeros-pppoe-secret",
            payload_summary=payload_summary, message=message))
        await db.commit()
        raise HTTPException(status_code=502, detail=message)

    job.status = "completed"
    job.executed_at = datetime.now(timezone.utc)
    job.last_error = None
    customer.mikrotik_provisioning_status = "provisioned"
    customer.provisioning_message = (
        "Secret PPPoE berhasil dibuat/diperbarui di MikroTik "
        f"menggunakan profile {effective_profile}."
    )
    if customer.olt_provisioning_status == "provisioned":
        customer.provisioning_status = "provisioned"
    else:
        customer.provisioning_status = "partially_provisioned"
    db.add(ProvisioningEvent(customer_id=customer.id, target="mikrotik", status="completed",
        vendor="MikroTik", mikrotik_device_id=mikrotik.id, adapter_key="routeros-pppoe-secret",
        payload_summary=payload_summary, message=customer.provisioning_message))
    await db.commit()
    return RedirectResponse(url=f"/customers/{customer_id}", status_code=303)


@router.post("/customers/{customer_id}/provision-olt-pppoe")
async def provision_olt_pppoe_route(customer_id: int, db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user)):
    """Apply the verified C-DATA WAN PPPoE sequence from Billing."""
    return await provision_customer_route(customer_id, db)
    from app.core.cdata_executor import CDataExecutionError, provision_cdata_pppoe
    from app.core.customer_identity import decrypt_pppoe_password
    customer = (await db.execute(select(Customer).where(Customer.id == customer_id))).scalar_one_or_none()
    if not customer:
        raise HTTPException(status_code=404, detail="Pelanggan tidak ditemukan")
    if customer.olt_vendor != "C-DATA" or customer.olt_model != "FD1601S-B1":
        raise HTTPException(status_code=422, detail="Executor WAN PPPoE hanya tersedia untuk C-DATA FD1601S-B1")
    if not all((customer.olt_device_id, customer.ont_sn, customer.olt_port, customer.onu_id, customer.pppoe_username, customer.pppoe_password)):
        raise HTTPException(status_code=422, detail="Data ONU/WAN PPPoE belum lengkap")
    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:
        raise HTTPException(status_code=422, detail="OLT SSH aktif dan credential wajib tersedia")
    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:
        raise HTTPException(status_code=422, detail="Credential atau host-key OLT belum siap")
    active = await db.scalar(select(ProvisioningJob).where(ProvisioningJob.customer_id == customer.id, ProvisioningJob.target == "olt-wan", ProvisioningJob.status.in_(["queued", "running"])))
    if active:
        raise HTTPException(status_code=409, detail="Provisioning WAN OLT sedang berjalan")
    payload = json.dumps({"vendor": olt.vendor, "model": olt.model, "pon_port": customer.olt_port, "onu_id": customer.onu_id, "vlan": customer.service_vlan or 100, "line_profile": "pppoe-100", "service_profile": "2p"}, sort_keys=True)
    job = ProvisioningJob(customer_id=customer.id, target="olt-wan", status="running", vendor=olt.vendor, adapter_key="cdata-wan-pppoe", olt_device_id=olt.id, payload_summary=payload, attempts=1)
    db.add(job); await db.commit()
    try:
        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 100)
    except CDataExecutionError as exc:
        msg = f"WAN PPPoE OLT gagal: {str(exc)[:400]}"; job.status="failed"; job.last_error=msg; customer.olt_provisioning_status="failed"; customer.provisioning_message=msg
        db.add(ProvisioningEvent(customer_id=customer.id, target="olt-wan", status="failed", vendor=olt.vendor, olt_device_id=olt.id, adapter_key="cdata-wan-pppoe", payload_summary=payload, message=msg)); await db.commit(); raise HTTPException(status_code=502, detail=msg)
    job.status="completed"; job.executed_at=datetime.now(timezone.utc); customer.olt_provisioning_status="provisioned"; customer.provisioning_message=result.message
    db.add(ProvisioningEvent(customer_id=customer.id, target="olt-wan", status="completed", vendor=olt.vendor, olt_device_id=olt.id, adapter_key="cdata-wan-pppoe", payload_summary=payload, message=result.message)); await db.commit()
    return RedirectResponse(url=f"/customers/{customer_id}", status_code=303)


@router.delete("/customers/{customer_id}")
async def delete_customer(
    customer_id: int,
    db: AsyncSession = Depends(get_db),
    user = Depends(require_roles("admin"))
):
    from app.models.models import OLTDevice, MikrotikDevice, DeviceCredential
    from app.core.cdata_executor import deprovision_cdata_onu
    from app.core.mikrotik import RealMikrotikAdapter
    from app.core.customer_identity import decrypt_pppoe_password

    customer = (await db.execute(select(Customer).where(Customer.id == customer_id))).scalar_one_or_none()
    if not customer:
        raise HTTPException(status_code=404, detail="Pelanggan tidak ditemukan")
    
    # Automatic deprovisioning on OLT hardware
    if customer.olt_device_id and customer.onu_id and customer.olt_port:
        olt = (await db.execute(select(OLTDevice).where(OLTDevice.id == customer.olt_device_id))).scalar_one_or_none()
        if olt and olt.credential_id:
            cred = (await db.execute(select(DeviceCredential).where(DeviceCredential.id == olt.credential_id))).scalar_one_or_none()
            if cred:
                try:
                    deprovision_cdata_onu(
                        host=olt.host,
                        port=olt.api_port or 22,
                        username=cred.username,
                        encrypted_secret=cred.encrypted_secret,
                        pinned_host_key="I/zDmJGfTPy3NkMNPghkjawVdeI357pJ/k+xm2qtLdY",
                        pon_port=customer.olt_port,
                        onu_id=customer.onu_id
                    )
                except Exception:
                    pass
    
    # Automatic deprovisioning on MikroTik router
    if customer.mikrotik_device_id and customer.pppoe_username:
        mk = (await db.execute(select(MikrotikDevice).where(MikrotikDevice.id == customer.mikrotik_device_id))).scalar_one_or_none()
        if mk and mk.credential_id:
            cred = (await db.execute(select(DeviceCredential).where(DeviceCredential.id == mk.credential_id))).scalar_one_or_none()
            if cred:
                try:
                    adapter = RealMikrotikAdapter(host=mk.host, username=cred.username, password=decrypt_pppoe_password(cred.encrypted_secret), api_port=mk.api_port or 8728)
                    adapter.delete_pppoe_secret(customer.pppoe_username)
                except Exception:
                    pass

    await db.delete(customer)
    await db.commit()
    return {"status": "deleted", "message": "Pelanggan dan konfigurasi perangkat berhasil dihapus"}

