# -*- coding: utf-8 -*-
"""Dataclass models mirroring the S-MDF API objects (CDC §II.4 "Objets de l'API").

Python attribute names are snake_case; the JSON wire keys are the exact camelCase
tokens from the cahier des charges (declared via field metadata ``json``).
Byte[] fields (QR codes, ticket zero) are carried as base64 ``str`` on the wire
and exposed as ``bytes`` through helper properties.
"""
from __future__ import annotations

import base64
from dataclasses import dataclass, field, fields, is_dataclass
from typing import Any, Dict, Optional


# --------------------------------------------------------------------------- #
#  (de)serialisation helpers
# --------------------------------------------------------------------------- #
def _key(f) -> str:
    return f.metadata.get("json", f.name)


def _to_wire(value: Any) -> Any:
    if is_dataclass(value):
        return value.to_dict()
    if isinstance(value, dict):
        return {k: _to_wire(v) for k, v in value.items()}
    if isinstance(value, (list, tuple)):
        return [_to_wire(v) for v in value]
    return value


class _JsonModel:
    """Mixin giving dataclasses symmetric ``to_dict`` / ``from_dict``.

    Nested dataclass fields are declared with ``metadata={"model": Cls}`` (single)
    or ``metadata={"model_map": Cls}`` (Map<String, Cls>).
    """

    def to_dict(self) -> Dict[str, Any]:
        out: Dict[str, Any] = {}
        for f in fields(self):
            val = getattr(self, f.name)
            if val is None:
                continue
            out[_key(f)] = _to_wire(val)
        return out

    @classmethod
    def from_dict(cls, data: Optional[Dict[str, Any]]):
        if data is None:
            return None
        kwargs: Dict[str, Any] = {}
        for f in fields(cls):
            wire = _key(f)
            if wire not in data:
                continue
            raw = data[wire]
            model = f.metadata.get("model")
            model_map = f.metadata.get("model_map")
            if model is not None and raw is not None:
                kwargs[f.name] = model.from_dict(raw)
            elif model_map is not None and raw is not None:
                kwargs[f.name] = {k: model_map.from_dict(v) for k, v in raw.items()}
            else:
                kwargs[f.name] = raw
        return cls(**kwargs)


# --------------------------------------------------------------------------- #
#  §4.10 EquipmentVersionsInfo
# --------------------------------------------------------------------------- #
@dataclass
class EquipmentVersionsInfo(_JsonModel):
    os: str = field(default="", metadata={"json": "os"})
    current: str = field(default="", metadata={"json": "current"})
    last: str = field(default="", metadata={"json": "last"})
    minimal: str = field(default="", metadata={"json": "minimal"})


# --------------------------------------------------------------------------- #
#  §4.9 SMDFVersionsInfo
# --------------------------------------------------------------------------- #
@dataclass
class SMDFVersionsInfo(_JsonModel):
    smdf: Optional[EquipmentVersionsInfo] = field(
        default=None, metadata={"json": "smdf", "model": EquipmentVersionsInfo})
    # keyed by the MAC address the agent used to connect
    agents: Dict[str, EquipmentVersionsInfo] = field(
        default_factory=dict,
        metadata={"json": "agents", "model_map": EquipmentVersionsInfo})


# --------------------------------------------------------------------------- #
#  §4.2 SMDFCertificateInfo
# --------------------------------------------------------------------------- #
@dataclass
class SMDFCertificateInfo(_JsonModel):
    cert_request_status: str = field(
        default="", metadata={"json": "certRequestStatus"})
    issuance_date: Optional[str] = field(
        default=None, metadata={"json": "issuanceDate"})
    expiration_date: Optional[str] = field(
        default=None, metadata={"json": "expirationDate"})
    expired: bool = field(default=False, metadata={"json": "expired"})
    revoked: bool = field(default=False, metadata={"json": "revoked"})


# --------------------------------------------------------------------------- #
#  §4.1 SMDFManifest
# --------------------------------------------------------------------------- #
@dataclass
class SMDFManifest(_JsonModel):
    imdf: str = field(default="", metadata={"json": "imdf"})
    status: str = field(default="", metadata={"json": "status"})
    version: str = field(default="", metadata={"json": "version"})
    type: str = field(default="", metadata={"json": "type"})
    certificate_info: Optional[SMDFCertificateInfo] = field(
        default=None,
        metadata={"json": "certificateInfo", "model": SMDFCertificateInfo})
    maintenance_enabled: bool = field(
        default=False, metadata={"json": "maintenanceEnabled"})
    state: str = field(default="", metadata={"json": "state"})
    # Byte[] on the wire -> base64 str here
    ticket_zero_qrcode: Optional[str] = field(
        default=None, metadata={"json": "ticketZeroQRCode"})
    synchronization_rate: int = field(
        default=0, metadata={"json": "synchronizationRate"})
    version_info: Optional[SMDFVersionsInfo] = field(
        default=None, metadata={"json": "versionsInfo", "model": SMDFVersionsInfo})
    available_offline_tickets: int = field(
        default=0, metadata={"json": "availableOfflineTickets"})

    @property
    def ticket_zero_bytes(self) -> Optional[bytes]:
        if self.ticket_zero_qrcode is None:
            return None
        return base64.b64decode(self.ticket_zero_qrcode)


# --------------------------------------------------------------------------- #
#  §4.4 SICCashRegisterInfo  /  §4.3 SICCertificateRequest
# --------------------------------------------------------------------------- #
@dataclass
class SICCashRegisterInfo(_JsonModel):
    model: str = field(default="", metadata={"json": "model"})
    serial_number: str = field(default="", metadata={"json": "serialNumber"})
    version: str = field(default="", metadata={"json": "version"})


@dataclass
class SICCertificateRequest(_JsonModel):
    cash_register_info: Optional[SICCashRegisterInfo] = field(
        default=None,
        metadata={"json": "cashRegisterInfo", "model": SICCashRegisterInfo})


# --------------------------------------------------------------------------- #
#  §4.5 SMDFTicketInfo  /  §4.6 SMDFSignedTicket
# --------------------------------------------------------------------------- #
@dataclass
class SMDFTicketInfo(_JsonModel):
    #: base64 of the JSON ticket (Annexe A3 schema)
    base64_ticket: str = field(default="", metadata={"json": "base64Ticket"})
    total_ht: int = field(default=0, metadata={"json": "totalHT"})
    total_tax: int = field(default=0, metadata={"json": "totalTax"})
    operation_type: str = field(default="", metadata={"json": "operationType"})
    transaction_type: str = field(
        default="", metadata={"json": "transactionType"})


@dataclass
class SMDFSignedTicket(_JsonModel):
    ticket_identifier: str = field(
        default="", metadata={"json": "ticketIdentifier"})
    #: Byte[] on the wire -> base64 str here
    qrcode_image: Optional[str] = field(
        default=None, metadata={"json": "qrcodeImage"})

    @property
    def qrcode_bytes(self) -> Optional[bytes]:
        if self.qrcode_image is None:
            return None
        return base64.b64decode(self.qrcode_image)


# --------------------------------------------------------------------------- #
#  §4.7 SMDFSyncRequest  /  §4.8 SICLogEntry
# --------------------------------------------------------------------------- #
@dataclass
class SMDFSyncRequest(_JsonModel):
    request_pin_update: bool = field(
        default=False, metadata={"json": "requestPINupdate"})
    update_smdf_url: bool = field(
        default=False, metadata={"json": "updateSMDFURL"})


@dataclass
class SICLogEntry(_JsonModel):
    module: str = field(default="", metadata={"json": "module"})
    operation: str = field(default="", metadata={"json": "operation"})
    level: str = field(default="INFO", metadata={"json": "level"})
    message: str = field(default="", metadata={"json": "message"})
