"""
Katalog voucher publik + mekanisme pembelian online.

Batas tanggung jawab file ini: SAMPAI SEBELUM Midtrans.
Yang sudah berfungsi:
  - katalog paket (harga dari hotspot_profiles, tidak di-hardcode)
  - checkout: bikin order + RESERVASI voucher secara atomik
  - halaman status order untuk pembeli
  - admin (login) bisa melihat order, tandai lunas, serahkan, batalkan

Yang BELUM ada dan disengaja tidak ada:
  - panggilan API Midtrans, halaman pembayaran, webhook

Alasan: reservasi voucher adalah bagian paling rawan (bisa menyebabkan
voucher terjual dua kali atau hilang permanen). Itu harus bisa diuji sendiri
sebelum payment ditumpuk di atasnya. Titik integrasi Midtrans nanti ada di
`admin_mark_paid()`.

Anti-penjualan ganda: reservasi memakai UPDATE bersyarat
`WHERE id=? AND status='available'`, dan voucher baru dianggap terpegang kalau
`rowcount == 1`. Dua order yang membaca baris yang sama hanya akan berhasil
satu, jadi mustahil satu voucher dipegang dua order.
"""
import json
import re
import secrets
from datetime import datetime, timedelta

from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.database import get_db
from app.core.device_credentials import decrypt_secret
from app.core.security import require_roles
from app.models.models import HotspotProfile, OnlineOrder, Voucher, VoucherBatch

import os

router = APIRouter(tags=["shop"])
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"))

# Berapa lama voucher ditahan sebelum dilepas kembali ke stok.
RESERVE_MINUTES = 30
MAX_PER_ORDER = 20

_UNITS = {"s": "Detik", "m": "Menit", "h": "Jam", "d": "Hari"}

STATUS_LABEL = {
    "pending": ("Menunggu Pembayaran", "amber"),
    "reserved": ("Menunggu Pembayaran", "amber"),
    "paid": ("Lunas", "emerald"),
    "delivered": ("Selesai", "emerald"),
    "expired": ("Kedaluwarsa", "slate"),
    "cancelled": ("Dibatalkan", "rose"),
}

TONE_CLASS = {
    "amber": "bg-amber-500/10 text-amber-400 border-amber-500/25",
    "emerald": "bg-emerald-500/10 text-emerald-400 border-emerald-500/25",
    "slate": "bg-slate-500/10 text-slate-400 border-slate-500/25",
    "rose": "bg-rose-500/10 text-rose-400 border-rose-500/25",
}


def humanize_duration(value):
    """'10h' -> '10 Jam'. None kalau format tak dikenali."""
    if not value:
        return None
    m = re.fullmatch(r"\s*(\d+)\s*([smhd])\s*", str(value), re.IGNORECASE)
    if not m:
        return str(value)
    return f"{int(m.group(1))} {_UNITS[m.group(2).lower()]}"


def humanize_seconds(seconds):
    """43200 -> '12 Jam'. Masa berlaku voucher bersifat opsional."""
    if not seconds:
        return None
    seconds = int(seconds)
    if seconds <= 0:
        return None
    for size, unit in ((86400, "Hari"), (3600, "Jam"), (60, "Menit")):
        if seconds % size == 0:
            return f"{seconds // size} {unit}"
    return f"{seconds} Detik"


def rupiah(value):
    """5000 -> 'Rp5.000'."""
    return f"Rp{int(value or 0):,}".replace(",", ".")


def new_order_code():
    """Kode unik. 3 byte hex acak + stempel waktu, cukup untuk volume ini."""
    return f"ORD-{datetime.now():%y%m%d}-{secrets.token_hex(3).upper()}"


def _decode_ids(raw):
    if not raw:
        return []
    try:
        parsed = json.loads(raw)
        return [int(x) for x in parsed] if isinstance(parsed, list) else []
    except (ValueError, TypeError):
        return []


def _encode_ids(ids):
    return json.dumps([int(i) for i in ids])


async def _stock_map(db, profile_ids):
    rows = (
        await db.execute(
            select(Voucher.profile_id, func.count(Voucher.id))
            .where(
                Voucher.status == "available",
                Voucher.profile_id.in_(profile_ids or [0]),
            )
            .group_by(Voucher.profile_id)
        )
    ).all()
    return {pid: count for pid, count in rows}


async def _load_packages(db):
    rows = (
        await db.execute(
            select(HotspotProfile)
            .where(HotspotProfile.is_active == True)  # noqa: E712
            .order_by(HotspotProfile.price, HotspotProfile.id)
        )
    ).scalars().all()

    stock = await _stock_map(db, [p.id for p in rows])
    packages = []
    for p in rows:
        duration = humanize_duration(p.session_timeout)
        available = stock.get(p.id, 0)
        packages.append({
            "id": p.id,
            "label": duration or p.name,
            "name": p.name,
            "price": p.price,
            "price_text": rupiah(p.price),
            "duration": duration,
            "validity": humanize_seconds(p.voucher_expiry_seconds),
            "stock": available,
            "in_stock": available > 0,
        })
    return packages


async def _release_expiring_orders(db):
    """
    Order yang lewat batas bayar dilepas; vouchernya dikembalikan ke stok.

    WAJIB mengembalikan status ke 'available'. Kalau tidak, setiap orang yang
    checkout lalu berubah pikiran akan mengikis stok permanen sampai habis.
    Hanya voucher yang masih 'reserved' yang disentuh, supaya kalau sudah
    terlanjur sold atau used tidak ada data yang ditimpa.
    """
    now = datetime.utcnow()
    stale = (
        await db.execute(
            select(OnlineOrder).where(
                OnlineOrder.status.in_(("pending", "reserved")),
                OnlineOrder.expires_at.isnot(None),
                OnlineOrder.expires_at < now,
            )
        )
    ).scalars().all()

    for order in stale:
        voucher_ids = _decode_ids(order.reserved_voucher_ids)
        if voucher_ids:
            await db.execute(
                update(Voucher)
                .where(Voucher.id.in_(voucher_ids), Voucher.status == "reserved")
                .values(status="available", comment=None)
            )
        order.status = "expired"
    if stale:
        await db.commit()
    return len(stale)


# ---------------------------------------------------------------- katalog

@router.get("/shop", response_class=HTMLResponse)
async def shop_catalog(request: Request, db: AsyncSession = Depends(get_db)):
    """Katalog paket. Publik, read-only, tanpa login."""
    await _release_expiring_orders(db)
    packages = await _load_packages(db)
    return templates.TemplateResponse(
        request=request,
        name="shop.html",
        context={
            "packages": packages,
            "total_products": len(packages),
            "in_stock_count": sum(1 for p in packages if p["in_stock"]),
        },
    )


# ---------------------------------------------------------------- checkout

@router.get("/shop/checkout", response_class=HTMLResponse)
async def shop_checkout_form(
    request: Request,
    profile_id: int = 0,
    quantity: int = 1,
    db: AsyncSession = Depends(get_db),
):
    """Formulir checkout. Paket yang tidak aktif tidak bisa dibuka langsung."""
    await _release_expiring_orders(db)
    packages = await _load_packages(db)
    by_id = {p["id"]: p for p in packages}
    if profile_id not in by_id:
        return RedirectResponse(url="/shop", status_code=303)
    return templates.TemplateResponse(
        request=request,
        name="shop_checkout.html",
        context={
            "packages": packages,
            "selected_id": profile_id,
            "quantity": max(1, min(int(quantity or 1), MAX_PER_ORDER)),
            "error": None,
            "old": {"name": "", "phone": "", "email": ""},
        },
    )


@router.post("/shop/checkout")
async def shop_checkout(
    request: Request,
    profile_id: int = Form(...),
    quantity: int = Form(1),
    customer_name: str = Form(...),
    customer_phone: str = Form(...),
    customer_email: str = Form(""),
    db: AsyncSession = Depends(get_db),
):
    """Buat order dan reservasi voucher dalam satu transaksi."""
    try:
        quantity = int(quantity or 1)
    except (TypeError, ValueError):
        quantity = 1
    quantity = max(1, min(quantity, MAX_PER_ORDER))

    name = (customer_name or "").strip()
    phone = (customer_phone or "").strip()
    email = (customer_email or "").strip() or None

    if len(name) < 2:
        return await _checkout_error(request, db, profile_id, quantity,
                                     "Nama minimal 2 karakter.", name, phone, email)
    if len(re.sub(r"\D", "", phone)) < 8:
        return await _checkout_error(request, db, profile_id, quantity,
                                     "Nomor HP/WA tidak valid.", name, phone, email)
    if email and not re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", email):
        return await _checkout_error(request, db, profile_id, quantity,
                                     "Format email tidak valid.", name, phone, email)

    await _release_expiring_orders(db)

    profile = await db.get(HotspotProfile, profile_id)
    if not profile or not profile.is_active:
        return RedirectResponse(url="/shop", status_code=303)

    # Kandidat diambil dengan LIMIT longgar supaya ada cadangan kalau ada
    # baris yang keburu dipegang order lain di antara SELECT dan UPDATE.
    candidates = (
        await db.execute(
            select(Voucher.id, Voucher.price)
            .where(Voucher.profile_id == profile.id, Voucher.status == "available")
            .order_by(Voucher.id)
            .limit(quantity * 3)
        )
    ).all()

    reserved_ids = []
    total = 0
    for voucher_id, voucher_price in candidates:
        if len(reserved_ids) >= quantity:
            break
        result = await db.execute(
            update(Voucher)
            .where(Voucher.id == voucher_id, Voucher.status == "available")
            .values(status="reserved", comment="dipesan online")
        )
        # rowcount == 1 berarti baris ini benar-benar berubah status, jadi
        # tidak ada order lain yang memegangnya.
        if result.rowcount == 1:
            reserved_ids.append(voucher_id)
            total += voucher_price or profile.price

    if len(reserved_ids) < quantity:
        # Stok tidak cukup: lepaskan yang sempat terambil di percobaan ini.
        if reserved_ids:
            await db.execute(
                update(Voucher)
                .where(Voucher.id.in_(reserved_ids), Voucher.status == "reserved")
                .values(status="available", comment=None)
            )
        await db.commit()
        return await _checkout_error(
            request, db, profile_id, quantity,
            f"Stok tersisa {len(reserved_ids)} voucher, tidak cukup untuk {quantity}.",
            name, phone, email,
        )

    order = OnlineOrder(
        order_code=new_order_code(),
        customer_name=name,
        customer_phone=phone,
        customer_email=email,
        profile_id=profile.id,
        profile_name=profile.name,
        duration_label=humanize_duration(profile.session_timeout),
        quantity=quantity,
        total_price=total,
        status="reserved",
        reserved_voucher_ids=_encode_ids(reserved_ids),
        expires_at=datetime.utcnow() + timedelta(minutes=RESERVE_MINUTES),
    )
    db.add(order)
    await db.commit()
    await db.refresh(order)

    # Midtrans akan mengambil alih titik ini: panggil Snap API lalu
    # redirect ke halaman pembayaran.
    return RedirectResponse(url=f"/shop/order/{order.order_code}", status_code=303)


async def _checkout_error(request, db, profile_id, quantity, message, name, phone, email):
    packages = await _load_packages(db)
    return templates.TemplateResponse(
        request=request,
        name="shop_checkout.html",
        context={
            "packages": packages,
            "selected_id": profile_id,
            "quantity": quantity,
            "error": message,
            "old": {"name": name, "phone": phone, "email": email or ""},
        },
        status_code=422,
    )


# ---------------------------------------------------------------- status order

@router.get("/shop/order/{order_code}", response_class=HTMLResponse)
async def shop_order_status(order_code: str, request: Request, db: AsyncSession = Depends(get_db)):
    """
    Halaman status untuk pembeli.

    Kode voucher HANYA ditampilkan setelah order lunas. Sebelum itu sengaja
    disembunyikan supaya tidak bisa dibaca dari URL.
    """
    await _release_expiring_orders(db)

    order = (
        await db.execute(select(OnlineOrder).where(OnlineOrder.order_code == order_code))
    ).scalar_one_or_none()

    if not order:
        return templates.TemplateResponse(
            request=request,
            name="shop_order.html",
            context={"found": False, "order_code": order_code},
            status_code=404,
        )

    vouchers = []
    if order.status in ("paid", "delivered"):
        vouchers = await _decrypt_vouchers(db, order)

    label, tone = STATUS_LABEL.get(order.status, (order.status, "slate"))
    minutes_left = None
    if order.status == "reserved" and order.expires_at:
        expires_naive = order.expires_at.replace(tzinfo=None)
        delta = (expires_naive - datetime.utcnow()).total_seconds()
        minutes_left = max(0, int(delta // 60))

    return templates.TemplateResponse(
        request=request,
        name="shop_order.html",
        context={
            "found": True,
            "order": order,
            "status_label": label,
            "status_tone": tone,
            "tone_class": TONE_CLASS.get(tone, TONE_CLASS["slate"]),
            "vouchers": vouchers,
            "price_text": rupiah(order.total_price),
            "minutes_left": minutes_left,
            "reserve_minutes": RESERVE_MINUTES,
        },
    )


async def _decrypt_vouchers(db, order):
    """
    Bentuk daftar voucher siap tampil untuk pembeli.

    Password disimpan terenkripsi hanya untuk mode 'separate'; mode 'same'
    berarti password sama dengan username. Kegagalan decrypt satu voucher
    tidak boleh menggagalkan seluruh halaman, jadi ditangani per baris.
    """
    voucher_ids = _decode_ids(order.reserved_voucher_ids)
    if not voucher_ids:
        return []

    rows = (
        await db.execute(
            select(Voucher.id).where(Voucher.id.in_(voucher_ids))
        )
    ).scalars().all()

    batch_modes = {}
    if rows:
        batch_ids = (
            await db.execute(
                select(Voucher.batch_id).where(Voucher.id.in_(voucher_ids))
            )
        ).scalars().all()
        if batch_ids:
            found = (
                await db.execute(
                    select(VoucherBatch.id, VoucherBatch.user_mode)
                    .where(VoucherBatch.id.in_(batch_ids))
                )
            ).all()
            batch_modes = {bid: mode for bid, mode in found}

    records = (
        await db.execute(
            select(
                Voucher.code, Voucher.username, Voucher.password_ciphertext,
                Voucher.credential_mode, Voucher.status, Voucher.limit_uptime,
                Voucher.batch_id,
            ).where(Voucher.id.in_(voucher_ids))
        )
    ).all()

    prepared = []
    for code, username, cipher, mode, vstatus, uptime, batch_id in records:
        password = None
        if cipher:
            try:
                password = decrypt_secret(cipher)
            except Exception:
                password = None
        username_or_code = username or code
        if (
            password is None
            or password == username_or_code
            or mode in ("legacy_same", "same")
            or batch_modes.get(batch_id) == "same"
        ):
            password = username_or_code
        prepared.append({
            "code": code,
            "username": username_or_code,
            "password": password,
            "status": vstatus,
            "uptime": uptime,
        })
    return prepared


# ---------------------------------------------------------------- admin

admin_router = APIRouter(dependencies=[Depends(require_roles("admin_cs"))], tags=["shop-admin"])


@admin_router.get("/shop/orders", response_class=HTMLResponse)
async def admin_orders(request: Request, db: AsyncSession = Depends(get_db)):
    """Daftar order online untuk admin. Butuh login admin_cs."""
    await _release_expiring_orders(db)
    orders = (
        await db.execute(select(OnlineOrder).order_by(OnlineOrder.created_at.desc()).limit(100))
    ).scalars().all()
    counts = {}
    for status in ("reserved", "paid", "delivered", "expired", "cancelled"):
        counts[status] = sum(1 for o in orders if o.status == status)
    return templates.TemplateResponse(
        request=request,
        name="shop_orders.html",
        context={
            "orders": orders,
            "status_label": STATUS_LABEL,
            "tone_class": TONE_CLASS,
            "counts": counts,
        },
    )


@admin_router.post("/shop/orders/{order_id}/pay")
async def admin_mark_paid(order_id: int, db: AsyncSession = Depends(get_db)):
    """
    Tandai lunas manual. Untuk pembayaran di luar Midtrans (transfer, tuner)
    dan jalur pemulihan kalau webhook gagal.

    Integrasi Midtrans nanti memanggil logika yang sama lewat webhook, bukan
    lewat endpoint ini.
    """
    order = await db.get(OnlineOrder, order_id)
    if not order:
        raise HTTPException(status_code=404, detail="Order tidak ditemukan")
    if order.status not in ("paid", "delivered"):
        order.status = "paid"
        order.paid_at = datetime.utcnow()
        await db.commit()
    return RedirectResponse(url="/shop/orders", status_code=303)


@admin_router.post("/shop/orders/{order_id}/deliver")
async def admin_deliver(order_id: int, db: AsyncSession = Depends(get_db)):
    """Tandai voucher sudah diserahkan ke pembeli; status voucher jadi 'sold'."""
    order = await db.get(OnlineOrder, order_id)
    if not order:
        raise HTTPException(status_code=404, detail="Order tidak ditemukan")
    if order.status == "paid":
        voucher_ids = _decode_ids(order.reserved_voucher_ids)
        if voucher_ids:
            await db.execute(
                update(Voucher)
                .where(Voucher.id.in_(voucher_ids), Voucher.status == "reserved")
                .values(status="sold", sold_at=datetime.utcnow())
            )
        order.status = "delivered"
        order.delivered_at = datetime.utcnow()
        await db.commit()
    return RedirectResponse(url="/shop/orders", status_code=303)


@admin_router.post("/shop/orders/{order_id}/cancel")
async def admin_cancel(order_id: int, db: AsyncSession = Depends(get_db)):
    """Batalkan order; voucher dilepas kembali ke stok."""
    order = await db.get(OnlineOrder, order_id)
    if not order:
        raise HTTPException(status_code=404, detail="Order tidak ditemukan")
    if order.status in ("pending", "reserved"):
        voucher_ids = _decode_ids(order.reserved_voucher_ids)
        if voucher_ids:
            await db.execute(
                update(Voucher)
                .where(Voucher.id.in_(voucher_ids), Voucher.status == "reserved")
                .values(status="available", comment=None)
            )
        order.status = "cancelled"
        await db.commit()
    return RedirectResponse(url="/shop/orders", status_code=303)
