# app/modules/loyalty/services/terminal_device_service.py
"""Terminal device service.

Pairs Android (or other) POS tablets to a single store. Issues a long-lived
JWT at pair time — the token itself is shown once and never persisted; only
the `jti` lives on the device row, which remains the source of truth for
revocation.

Two failure modes render a token unusable:
- Explicit `revoked_at` (merchant action, row stays for audit)
- `expires_at < now` (1y default, no auto-renewal — re-pair the tablet)
"""

import base64
import io
import json
import logging
import uuid
from datetime import UTC, datetime, timedelta

import qrcode
from sqlalchemy.orm import Session

from app.core.config import settings
from app.modules.loyalty.exceptions import TerminalDeviceNotFoundException
from app.modules.loyalty.models import TerminalDevice
from app.modules.loyalty.schemas.terminal_device import (
    TerminalDeviceCreate,
    TerminalDeviceUpdate,
)
from middleware.auth import AuthManager

logger = logging.getLogger(__name__)


DEFAULT_TOKEN_LIFETIME = timedelta(days=365)


class TerminalDeviceService:
    """Service for pairing and managing terminal devices."""

    def __init__(self):
        self._auth = AuthManager()

    # =========================================================================
    # Reads
    # =========================================================================

    def get_device(self, db: Session, device_id: int) -> TerminalDevice | None:
        return (
            db.query(TerminalDevice)
            .filter(TerminalDevice.id == device_id)
            .first()
        )

    def require_device(self, db: Session, device_id: int) -> TerminalDevice:
        device = self.get_device(db, device_id)
        if not device:
            raise TerminalDeviceNotFoundException(str(device_id))
        return device

    def require_device_for_merchant(
        self, db: Session, device_id: int, merchant_id: int
    ) -> TerminalDevice:
        """Fetch a device but enforce it belongs to the given merchant."""
        device = self.require_device(db, device_id)
        if device.merchant_id != merchant_id:
            raise TerminalDeviceNotFoundException(str(device_id))
        return device

    def get_device_by_jti(self, db: Session, jti: str) -> TerminalDevice | None:
        return db.query(TerminalDevice).filter(TerminalDevice.jti == jti).first()

    def list_devices(
        self,
        db: Session,
        merchant_id: int,
        *,
        store_id: int | None = None,
        include_revoked: bool = False,
    ) -> list[TerminalDevice]:
        query = db.query(TerminalDevice).filter(
            TerminalDevice.merchant_id == merchant_id
        )
        if store_id is not None:
            query = query.filter(TerminalDevice.store_id == store_id)
        if not include_revoked:
            query = query.filter(TerminalDevice.revoked_at.is_(None))
        return query.order_by(
            TerminalDevice.store_id, TerminalDevice.created_at.desc()
        ).all()

    # =========================================================================
    # Writes
    # =========================================================================

    def create_device(
        self,
        db: Session,
        *,
        merchant_id: int,
        data: TerminalDeviceCreate,
        created_by_user_id: int | None,
    ) -> tuple[TerminalDevice, str, dict, str]:
        """Pair a new device and issue its setup token.

        Returns ``(device, setup_token, setup_payload, qr_png_base64)``.
        The caller must NOT persist ``setup_token`` — only the device row's
        ``jti`` is kept.
        """
        from app.modules.tenancy.models import Store

        store = (
            db.query(Store)
            .filter(Store.id == data.store_id, Store.merchant_id == merchant_id)
            .first()
        )
        if not store:
            raise TerminalDeviceNotFoundException(f"store:{data.store_id}")

        jti = str(uuid.uuid4())
        expires_at = datetime.now(UTC) + DEFAULT_TOKEN_LIFETIME

        device = TerminalDevice(
            merchant_id=merchant_id,
            store_id=store.id,
            label=data.label,
            jti=jti,
            expires_at=expires_at,
            created_by_id=created_by_user_id,
        )
        db.add(device)
        db.commit()
        db.refresh(device)

        token = self._auth.create_device_token(
            device_id=device.id,
            jti=jti,
            merchant_id=merchant_id,
            store_id=store.id,
            store_code=store.store_code,
            expires_at=expires_at,
        )

        payload = {
            "api_url": settings.app_base_url,
            "store_id": store.id,
            "store_code": store.store_code,
            "store_name": store.name,
            "auth_token": token,
        }
        qr_png_base64 = self._render_qr_png_base64(json.dumps(payload))

        logger.info(
            f"Paired terminal device {device.id} ('{device.label}') for "
            f"merchant {merchant_id}, store {store.id} ({store.store_code})"
        )
        return device, token, payload, qr_png_base64

    def update_device(
        self,
        db: Session,
        device: TerminalDevice,
        data: TerminalDeviceUpdate,
    ) -> TerminalDevice:
        if data.label is not None:
            device.label = data.label
        db.commit()
        db.refresh(device)
        return device

    def revoke_device(
        self,
        db: Session,
        device: TerminalDevice,
        *,
        revoked_by_user_id: int | None,
        reason: str | None = None,
    ) -> TerminalDevice:
        if device.revoked_at is not None:
            return device
        device.revoked_at = datetime.now(UTC)
        device.revoked_by_id = revoked_by_user_id
        device.revoked_reason = reason
        db.commit()
        db.refresh(device)
        logger.info(
            f"Revoked terminal device {device.id} by user {revoked_by_user_id}"
        )
        return device

    def delete_device(self, db: Session, device: TerminalDevice) -> None:
        device_id = device.id
        db.delete(device)
        db.commit()
        logger.info(f"Deleted terminal device {device_id}")

    def touch_last_seen(
        self,
        db: Session,
        device: TerminalDevice,
        ip: str | None = None,
    ) -> None:
        """Bump last_seen_at/ip on each authenticated request. Best-effort."""
        device.last_seen_at = datetime.now(UTC)
        if ip is not None:
            device.last_seen_ip = ip
        try:
            db.commit()
        except Exception:  # noqa: EXC003 - best-effort heartbeat, must not break the request
            db.rollback()
            logger.warning(
                f"Failed to update last_seen for device {device.id}", exc_info=True
            )

    # =========================================================================
    # Helpers
    # =========================================================================

    @staticmethod
    def _render_qr_png_base64(data: str) -> str:
        qr = qrcode.QRCode(
            version=None,
            error_correction=qrcode.constants.ERROR_CORRECT_M,
            box_size=8,
            border=2,
        )
        qr.add_data(data)
        qr.make(fit=True)
        img = qr.make_image(fill_color="black", back_color="white")
        buffer = io.BytesIO()
        img.save(buffer, format="PNG")
        encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
        return f"data:image/png;base64,{encoded}"

    @staticmethod
    def build_response(device: TerminalDevice) -> dict:
        """Hydrate a response dict including derived status + store name."""
        return {
            "id": device.id,
            "merchant_id": device.merchant_id,
            "store_id": device.store_id,
            "store_name": device.store.name if device.store else None,
            "label": device.label,
            "status": device.status,
            "expires_at": device.expires_at,
            "last_seen_at": device.last_seen_at,
            "last_seen_ip": device.last_seen_ip,
            "revoked_at": device.revoked_at,
            "revoked_by_id": device.revoked_by_id,
            "revoked_reason": device.revoked_reason,
            "created_by_id": device.created_by_id,
            "created_at": device.created_at,
        }


terminal_device_service = TerminalDeviceService()
