# @File: backend/services/whatsapp/devices.py
# @Desc: Device pairing service for the WhatsApp Reseller module.
#        Exposes a thin, DB-first API:
#          - list_devices(user_id)
#          - create_device(user_id, display_name)
#          - request_qr(user_id, device_id)  -> synthesises a QR payload
#          - poll_status(user_id, device_id) -> returns current status
#          - archive_device(user_id, device_id)
#
#        The upstream wasms.doiteasy.net server is the real source of truth
#        for QR payloads. Until master credentials are configured in the
#        API Hub, we fall back to a locally-generated placeholder payload so
#        the UI flow is fully testable end-to-end. When master creds ARE
#        configured, ``request_qr`` will call upstream; any upstream failure
#        is caught and the local fallback is still returned so the UX never
#        breaks during testing.
from __future__ import annotations

import logging
import secrets
from datetime import datetime, timedelta, timezone
from typing import Optional

from fastapi import HTTPException, status
from models.whatsapp import (
    WhatsappClientSubscription,
    WhatsappDevice,
    WhatsappDeviceSession,
    WhatsappPackage,
)
from sqlalchemy import and_, desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)

QR_TTL_SECONDS = 60  # Whatsapp QR codes typically refresh every ~60s.
MAX_DEVICES_HARD_CAP = 50  # Safety net regardless of package quota.


async def _require_active_subscription(
    db: AsyncSession, user_id: str
) -> WhatsappClientSubscription:
    stmt = (
        select(WhatsappClientSubscription)
        .where(
            and_(
                WhatsappClientSubscription.user_id == user_id,
                WhatsappClientSubscription.status == "active",
            )
        )
        .order_by(desc(WhatsappClientSubscription.activated_at))
        .limit(1)
    )
    row = (await db.execute(stmt)).scalar_one_or_none()
    if not row:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="No active WhatsApp subscription.",
        )
    return row


async def _get_package(db: AsyncSession, package_id: int) -> Optional[WhatsappPackage]:
    return (
        await db.execute(select(WhatsappPackage).where(WhatsappPackage.id == package_id))
    ).scalar_one_or_none()


def _serialize_device(d: WhatsappDevice) -> dict:
    return {
        "id": d.id,
        "device_slug": d.device_slug,
        "display_name": d.display_name,
        "phone_number": d.phone_number,
        "status": d.status,
        "upstream_session_id": d.upstream_session_id,
        "qr_payload": d.qr_payload,
        "qr_expires_at": d.qr_expires_at.isoformat() if d.qr_expires_at else None,
        "last_seen_at": d.last_seen_at.isoformat() if d.last_seen_at else None,
        "last_error": d.last_error,
        "archived": d.archived,
        "created_at": d.created_at.isoformat() if d.created_at else None,
    }


async def list_devices(db: AsyncSession, user_id: str) -> list[dict]:
    sub = await _require_active_subscription(db, user_id)
    stmt = (
        select(WhatsappDevice)
        .where(
            and_(
                WhatsappDevice.subscription_id == sub.id,
                WhatsappDevice.archived.is_(False),
            )
        )
        .order_by(desc(WhatsappDevice.created_at))
    )
    rows = (await db.execute(stmt)).scalars().all()
    return [_serialize_device(d) for d in rows]


async def _count_active_devices(db: AsyncSession, subscription_id: int) -> int:
    stmt = select(func.count(WhatsappDevice.id)).where(
        and_(
            WhatsappDevice.subscription_id == subscription_id,
            WhatsappDevice.archived.is_(False),
        )
    )
    return int((await db.execute(stmt)).scalar() or 0)


async def create_device(
    db: AsyncSession, user_id: str, display_name: str
) -> dict:
    sub = await _require_active_subscription(db, user_id)
    pkg = await _get_package(db, sub.package_id)

    active = await _count_active_devices(db, sub.id)
    hard_cap = MAX_DEVICES_HARD_CAP
    if pkg and not pkg.devices_unlimited:
        hard_cap = min(hard_cap, max(pkg.max_devices, 1))
    if active >= hard_cap:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Device limit reached ({hard_cap}). Upgrade your plan or archive an existing device.",
        )

    display_name = (display_name or "WhatsApp Device").strip()[:128] or "WhatsApp Device"
    slug = f"dev_{secrets.token_hex(4)}"
    device = WhatsappDevice(
        subscription_id=sub.id,
        user_id=user_id,
        device_slug=slug,
        display_name=display_name,
        status="pending",
    )
    db.add(device)
    await db.flush()
    db.add(
        WhatsappDeviceSession(
            device_id=device.id,
            event="created",
            detail=f"Device '{display_name}' created.",
        )
    )
    # Update cached usage counter on subscription.
    sub.devices_used = await _count_active_devices(db, sub.id)
    await db.commit()
    await db.refresh(device)
    return _serialize_device(device)


async def _load_owned_device(
    db: AsyncSession, user_id: str, device_id: int
) -> WhatsappDevice:
    sub = await _require_active_subscription(db, user_id)
    stmt = select(WhatsappDevice).where(
        and_(
            WhatsappDevice.id == device_id,
            WhatsappDevice.subscription_id == sub.id,
        )
    )
    row = (await db.execute(stmt)).scalar_one_or_none()
    if not row:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Device not found.")
    return row


async def request_qr(db: AsyncSession, user_id: str, device_id: int) -> dict:
    """Generate (or refresh) a QR payload for a device.

    Tries upstream wasms.doiteasy.net first via the API Hub credential entry
    ``whatsapp_server`` (if present) — otherwise falls back to a locally
    minted payload so the pairing UI is always demonstrable.
    """
    device = await _load_owned_device(db, user_id, device_id)
    if device.archived:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Cannot request QR for an archived device.",
        )

    upstream_detail: Optional[str] = None
    upstream_session_id: Optional[str] = None
    qr_payload: Optional[str] = None

    try:
        # Lazy import — api_hub is optional and its module path may change.
        from services.api_hub import get_provider_credentials  # type: ignore

        creds = await get_provider_credentials(db, "whatsapp_server")
        if creds and creds.get("base_url") and creds.get("api_key"):
            # Intentionally best-effort; if the upstream call fails we fall
            # back to the local QR below so the flow is still testable.
            import httpx

            base = str(creds["base_url"]).rstrip("/")
            async with httpx.AsyncClient(timeout=10.0) as cli:
                resp = await cli.post(
                    f"{base}/api/sessions",
                    json={"slug": device.device_slug, "label": device.display_name},
                    headers={"Authorization": f"Bearer {creds['api_key']}"},
                )
                if resp.status_code < 400:
                    data = resp.json() if resp.content else {}
                    upstream_session_id = (
                        data.get("session_id") or data.get("id") or None
                    )
                    qr_payload = data.get("qr") or data.get("qr_payload") or None
                else:
                    upstream_detail = f"upstream {resp.status_code}: {resp.text[:200]}"
    except Exception as exc:  # pragma: no cover - best-effort integration
        logger.info("whatsapp upstream QR request failed: %s", exc)
        upstream_detail = f"upstream error: {exc}"

    if not qr_payload:
        # Deterministic local fallback — encodes device + expiry so the UI
        # can render a QR image via <canvas> or a QR library without any
        # upstream dependency.
        nonce = secrets.token_urlsafe(18)
        qr_payload = f"wa-pair://{device.device_slug}/{nonce}"

    now = datetime.now(timezone.utc)
    device.status = "qr_ready"
    device.qr_payload = qr_payload
    device.qr_expires_at = now + timedelta(seconds=QR_TTL_SECONDS)
    if upstream_session_id:
        device.upstream_session_id = upstream_session_id
    device.last_error = upstream_detail

    db.add(
        WhatsappDeviceSession(
            device_id=device.id,
            event="qr_generated",
            detail=upstream_detail or "Local QR payload issued.",
        )
    )
    await db.commit()
    await db.refresh(device)
    return _serialize_device(device)


async def poll_status(db: AsyncSession, user_id: str, device_id: int) -> dict:
    device = await _load_owned_device(db, user_id, device_id)

    # Auto-expire stale QR codes so the UI shows the right call-to-action.
    if (
        device.status == "qr_ready"
        and device.qr_expires_at
        and device.qr_expires_at < datetime.now(timezone.utc)
    ):
        device.status = "qr_expired"
        device.qr_payload = None
        await db.commit()
        await db.refresh(device)
    return _serialize_device(device)


async def simulate_paired(
    db: AsyncSession, user_id: str, device_id: int, phone_number: Optional[str] = None
) -> dict:
    """Dev helper — marks a device as connected without an upstream handshake.

    Exposed so QA can exercise campaign / AI flows without a real WhatsApp
    scan. Kept simple: the endpoint still requires the owning user.
    """
    device = await _load_owned_device(db, user_id, device_id)
    device.status = "connected"
    device.phone_number = (phone_number or device.phone_number or "").strip()[:40] or None
    device.last_seen_at = datetime.now(timezone.utc)
    device.qr_payload = None
    device.qr_expires_at = None
    device.last_error = None
    db.add(
        WhatsappDeviceSession(
            device_id=device.id,
            event="connected",
            detail="Marked connected via client tester.",
        )
    )
    await db.commit()
    await db.refresh(device)
    return _serialize_device(device)


async def archive_device(db: AsyncSession, user_id: str, device_id: int) -> dict:
    device = await _load_owned_device(db, user_id, device_id)
    device.archived = True
    device.status = "archived"
    db.add(
        WhatsappDeviceSession(
            device_id=device.id,
            event="archived",
            detail="Device archived by owner.",
        )
    )
    sub_id = device.subscription_id
    await db.commit()

    # Refresh usage counter after archive.
    sub = (
        await db.execute(
            select(WhatsappClientSubscription).where(
                WhatsappClientSubscription.id == sub_id
            )
        )
    ).scalar_one_or_none()
    if sub:
        sub.devices_used = await _count_active_devices(db, sub.id)
        await db.commit()

    await db.refresh(device)
    return _serialize_device(device)