"""Periodic live-status reconciliation for Billing, customers and hotspot vouchers.

Voucher lifecycle rules (independent triggers, either one is enough):
  1. Duration/jatah habis  -> hapus dari RouterOS + database.
  2. Masa berlaku habis   -> hapus dari RouterOS + database.
Status juga disinkronkan LIVE dari RouterOS (uptime / limit-uptime / active /
disabled) supaya DataTables voucher selalu mencerminkan kondisi perangkat.
"""
import asyncio
import logging
import os
import re

_log = logging.getLogger("bestweb.voucher_monitor")

from sqlalchemy import select
from datetime import datetime, timezone

from app.core.database import AsyncSessionLocal
from app.core.onboarding import refresh_customer_online_status
from app.models.models import (
    Customer,
    DeviceCredential,
    MikrotikDevice,
    Voucher,
)
from app.core.mikrotik import RealMikrotikAdapter
from app.core.secrets import decrypt_secret_strict, SecretDecryptionError


def poll_interval() -> int:
    try:
        return max(10, int(os.getenv("BESTWEB_STATUS_POLL_SECONDS", "30")))
    except ValueError:
        return 30


def voucher_poll_interval() -> int:
    try:
        return max(5, int(os.getenv("BESTWEB_VOUCHER_POLL_SECONDS", "10")))
    except ValueError:
        return 10


def parse_uptime_seconds(value: str | None) -> int:
    """Parse RouterOS uptime/limit-uptime into seconds.

    Accepts 1d02:03:04, 02:03:04, 3h04m, 2w, 5m, 45s and bare digits.
    """
    raw = str(value or "").strip().lower()
    if not raw:
        return 0

    # RouterOS uptime: [Nd]HH:MM:SS  (4 parts = days + h:m:s, 3 parts = h:m:s)
    if ":" in raw:
        days = 0
        body = raw
        day_match = re.match(r"^(\d+)d(.*)$", raw)
        if day_match:
            days = int(day_match.group(1))
            body = day_match.group(2)
        parts = body.split(":")
        try:
            nums = [int(float(p)) for p in parts]
        except ValueError:
            return 0
        while len(nums) < 3:
            nums.insert(0, 0)
        if len(nums) >= 4:
            days += nums[0]
            nums = nums[-3:]
        return days * 86400 + nums[0] * 3600 + nums[1] * 60 + nums[2]

    match = re.fullmatch(
        r"(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?", raw
    )
    if match and any(match.groups()):
        w, d, h, m, s = (int(g) if g else 0 for g in match.groups())
        return w * 604800 + d * 86400 + h * 3600 + m * 60 + s

    try:
        return int(float(raw))
    except ValueError:
        return 0


def as_utc(value: datetime | None) -> datetime | None:
    """SQLite drops tzinfo, so treat naive timestamps as UTC before comparing."""
    if value is None:
        return None
    if value.tzinfo is None:
        return value.replace(tzinfo=timezone.utc)
    return value.astimezone(timezone.utc)


async def _device_adapters(db) -> dict[int, RealMikrotikAdapter]:
    """Build one adapter per active MikroTik device that has valid credentials.

    Raises SecretDecryptionError when a stored secret cannot be decrypted with
    the active encryption key, so the caller can surface it instead of silently
    pushing ciphertext to the device.
    """
    devices = (await db.execute(
        select(MikrotikDevice).where(MikrotikDevice.is_active == True)
    )).scalars().all()
    adapters: dict[int, RealMikrotikAdapter] = {}
    for mk in devices:
        if not mk.credential_id:
            continue
        cred = await db.get(DeviceCredential, mk.credential_id)
        if not (cred and cred.is_active):
            continue
        password = decrypt_secret_strict(
            cred.encrypted_secret, label=f"Kredensial {cred.name}"
        )
        adapters[mk.id] = RealMikrotikAdapter(
            host=mk.host,
            username=cred.username,
            password=password,
            api_port=mk.api_port or 8728,
        )
    return adapters


async def reconcile_vouchers() -> None:
    """Sync voucher status/deletion against live RouterOS state."""
    async with AsyncSessionLocal() as db:
        now = datetime.now(timezone.utc)
        vouchers = (await db.execute(select(Voucher))).scalars().all()
        if not vouchers:
            return

        try:
            adapters = await _device_adapters(db)
        except SecretDecryptionError as exc:
            _log.error("voucher reconcile stopped: %s", exc)
            raise
        except Exception:
            return

        # Cache RouterOS state per device so we hit the API once per device.
        remote_by_device: dict[int, dict[str, dict]] = {}
        for device_id, adapter in adapters.items():
            try:
                remote_rows = await asyncio.to_thread(adapter.list_hotspot_users)
                remote_by_device[device_id] = {row["name"]: row for row in remote_rows}
            except Exception:
                # Device offline or credentials rejected: skip status sync for
                # this cycle rather than blocking the expiry rule below.
                remote_by_device[device_id] = {}
                _log.warning(
                    "voucher reconcile: RouterOS read failed for device %s", device_id
                )

        changed = False

        for v in vouchers:
            username = (v.username or v.code or "").strip()
            device_id = v.device_id
            remote = remote_by_device.get(device_id, {}).get(username) if device_id else None

            # Condition 2: absolute expiry (masa berlaku) reached.
            expires_at = as_utc(v.expires_at)
            expired = bool(expires_at and expires_at <= now)

            # Condition 1: duration/jatah reached, read live from RouterOS.
            duration_exhausted = False
            if remote:
                used_seconds = parse_uptime_seconds(remote.get("uptime"))
                limit_seconds = parse_uptime_seconds(remote.get("limit_uptime")) or parse_uptime_seconds(v.limit_uptime)
                if limit_seconds > 0 and used_seconds >= limit_seconds:
                    duration_exhausted = True

            # Live status sync from device.
            if remote:
                if remote.get("active") or parse_uptime_seconds(remote.get("uptime")) > 0:
                    if v.status == "sold":
                        v.status = "used"
                        v.used_at = v.used_at or now
                        changed = True
                elif v.status not in ("available",):
                    if v.status == "used":
                        v.status = "sold"
                        changed = True

            if expired or duration_exhausted:
                adapter = adapters.get(device_id) if device_id else None
                if adapter and username:
                    try:
                        await asyncio.to_thread(adapter.delete_hotspot_user, username)
                    except Exception:
                        pass
                await db.delete(v)
                changed = True

        if changed:
            await db.commit()


async def monitor_loop() -> None:
    """Keep billing status aligned with live OLT/MikroTik state and expire vouchers."""
    while True:
        try:
            async with AsyncSessionLocal() as db:
                customers = (await db.execute(
                    select(Customer).where(
                        Customer.is_archived == False,
                        Customer.pppoe_username.is_not(None),
                        Customer.mikrotik_device_id.is_not(None),
                        Customer.olt_device_id.is_not(None),
                    )
                )).scalars().all()
                for customer in customers:
                    try:
                        await refresh_customer_online_status(customer, db)
                    except Exception:
                        await db.rollback()
        except asyncio.CancelledError:
            raise
        except Exception:
            pass
        await asyncio.sleep(poll_interval())


async def voucher_monitor_loop() -> None:
    """Fast lane voucher reconciliation so UI reflects RouterOS state live."""
    while True:
        try:
            await reconcile_vouchers()
        except asyncio.CancelledError:
            raise
        except Exception:
            _log.exception("voucher reconcile cycle failed")
        await asyncio.sleep(voucher_poll_interval())


def voucher_reconcile_health() -> dict:
    """Report whether live voucher reconciliation can actually reach devices."""
    import asyncio as _asyncio
    from app.core.database import AsyncSessionLocal as _Session

    async def probe():
        async with _Session() as db:
            devices = (await db.execute(
                select(MikrotikDevice).where(MikrotikDevice.is_active == True)
            )).scalars().all()
            report = []
            for mk in devices:
                item = {
                    "device": mk.name,
                    "host": mk.host,
                    "port": mk.api_port or 8728,
                    "credential": None,
                    "ok": False,
                    "error": None,
                }
                try:
                    adapters = await _device_adapters(db)
                    adapter = adapters.get(mk.id)
                    if adapter is None:
                        item["error"] = "Kredensial tidak aktif / belum ditugaskan"
                    else:
                        rows = await _asyncio.to_thread(adapter.list_hotspot_users)
                        item["credential"] = adapter.username
                        item["ok"] = True
                        item["users"] = len(rows)
                except Exception as exc:
                    item["error"] = str(exc)
                report.append(item)
            return report

    try:
        devices = _asyncio.run(probe())
    except Exception as exc:
        return {"ok": False, "error": str(exc), "devices": []}
    return {
        "ok": all(d["ok"] for d in devices) if devices else False,
        "devices": devices,
    }
