# -*- coding: utf-8 -*-
"""S-MDF error catalog and exception type (CDC §II.5 "Codes d'erreurs").

The cahier des charges uses two overlapping naming schemes: the numeric error
table (SEC_* / SMDF_* / SCEF_* with codes 103-614) and the symbolic names listed
per web-service. Both are captured here in a single catalog keyed by symbolic
name; ``code`` is the integer from the numeric table when one exists.

``retryable`` / ``terminal`` encode the "Traitement par le logiciel de caisse"
column so the POS layer can react uniformly:
  * retryable=True  -> the call may be retried as-is.
  * terminal=True   -> the S-MDF can no longer be used until the taxpayer acts
                       (new certificate request, resync, contact supplier, ...).
"""
from dataclasses import dataclass
from typing import Dict, Optional


@dataclass(frozen=True)
class ErrorInfo:
    name: str
    code: Optional[int]
    meaning: str
    retryable: bool = False
    terminal: bool = False


def _e(name, code, meaning, retryable=False, terminal=False):
    return ErrorInfo(name, code, meaning, retryable, terminal)


#: symbolic name -> ErrorInfo
ERROR_CATALOG: Dict[str, ErrorInfo] = {e.name: e for e in [
    # --- security / certificate & sync sub-protocol (103-116) ---
    _e("SEC_NO_PAIRING_CODE", 103, "Certificate request not initiated from the "
       "taxpayer space, or IMDF mismatch.", terminal=True),
    _e("SEC_PAIRING_FAILED", 104, "Invalid pairing code/data.", retryable=True),
    _e("SEC_INVALID_OTP", 105, "Invalid OTP.", retryable=True),
    _e("SEC_INVALID_DATA", 109, "Invalid ciphering for signature activation.",
       retryable=True),
    _e("SEC_REINIT_PIN_FAILED", 114, "PIN change failed.", retryable=True),
    _e("SEC_INVALID_OTP_EXCEED_THRESHOLD", 115,
       "Too many invalid OTP attempts.", terminal=True),
    _e("SEC_INVALID_SAD_EXCEED_THRESHOLD", 116,
       "Too many invalid signatures; S-MDF drops to NOT_SYNCHRONIZED.",
       terminal=True),
    # --- S-MDF / agent (500-523) ---
    _e("SMDF_INIT_FAILED_NOT_FACTORY", 500, "S-MDF not in FACTORY state.",
       terminal=True),
    _e("SMDF_IMDF_NOT_FOUND", 501, "IMDF not found.", terminal=True),
    _e("SMDF_NOT_REACHABLE", 503, "Server S-MDF not reachable (network).",
       retryable=True),
    _e("AGENT_FAILED_GET_HWD_INFO", 504,
       "S-MDF Agent could not read hardware info (rights).", terminal=True),
    _e("SMDF_STORAGE_FAILURE", 505, "S-MDF storage load/update failure.",
       retryable=True),
    _e("SMDF_IMDF_CAN_NOT_BE_USED", 506,
       "Current S-MDF state forbids use (suspended/maintenance?).",
       terminal=True),
    _e("SMDF_IMDF_NOT_SERVER", 507,
       "Terminal S-MDF used as a server.", terminal=True),
    _e("SEC_AGENT_SYNCHRONIZATION_ERROR", 508,
       "Error during synchronization protocol.", retryable=True),
    _e("SMDF_NOT_SYNCHRONIZED", 509,
       "S-MDF no longer synchronized; cannot sign. Run sync.", terminal=True),
    _e("SMDF_CRYPTO_ERROR", 510, "Crypto error during NACEF exchange.",
       retryable=True),
    _e("SMDF_QRCODE_GEN_ERROR", 511, "QR-code generation error.",
       terminal=True),
    _e("NACEF_NOT_REACHABLE", 512, "NACEF platform connection failed.",
       retryable=True),
    _e("SMDF_CERTIFICATE_NOT_GENERATED", 513,
       "Certificate not generated yet.", terminal=True),
    _e("AGENT_FAILED_SHOW_UI", 514,
       "S-MDF Agent could not display the input popup.", terminal=True),
    _e("SEC_SAP_PROTOCOL_ERROR", 515, "Ciphering algorithm error.",
       terminal=True),
    _e("SMDF_OFFLINE_SIGN_ERROR", 516,
       "Offline signature error; offline no longer usable, must resync.",
       terminal=True),
    _e("SMDF_LOCKED_CERT_SYNC_REQUEST", 517,
       "Cert/sync already requested from another terminal (10-min lock).",
       retryable=True),
    _e("SMDF_EXPIRED_CERTIFICATE", 518,
       "Signature certificate expired; request a new one.", terminal=True),
    _e("SMDF_REVOKED_CERTIFICATE", 519,
       "Signature certificate revoked; request a new one.", terminal=True),
    _e("SMDF_CONNECTION_REFUSED", 520,
       "Connection refused by NACEF service.", retryable=True),
    _e("AGENT_INVALID_SMDF_URL_SYNTAX", 521,
       "Invalid S-MDF URL entered at the agent.", terminal=True),
    _e("SMDF_SERVER_ERROR", 522, "Unidentified S-MDF error.", terminal=True),
    _e("SMDF_ALREADY_HAS_CERTIFICATE", 523,
       "Certificate request on an S-MDF that already has a valid certificate.",
       terminal=True),
    # --- audit (549-554) ---
    _e("SMDF_AUDIT_IO_ERROR", 549, "Audit read/write error.", terminal=True),
    _e("SMDF_AUDIT_MISSING_FILE", 550, "Audit file missing on removable disk.",
       terminal=True),
    _e("SMDF_AUDIT_CRYPTO_ERROR", 551, "Audit internal crypto error.",
       terminal=True),
    _e("SMDF_AUDIT_DEVICE_NOT_INCLUDED", 552,
       "Equipment not in the audit mission scope.", terminal=True),
    _e("SMDF_AUDIT_OUT_OF_RANGE", 553, "Audit mission order expired.",
       terminal=True),
    _e("SMDF_AUDIT_FAILED_TO_WRITE_REPORT", 554,
       "Audit report write failed.", terminal=True),
    # --- SCEF signature (600-614) ---
    _e("SCEF_CRYPTO_ERROR", 600,
       "Crypto error accessing the signature key.", retryable=True),
    _e("SCEF_CADES_INIT_ERROR", 613,
       "CAdES signature/processing error.", retryable=True),
    _e("SCEF_FAILED_OBTAIN_IMDF_CERTIFICATE", 614,
       "Failed to obtain/validate the equipment certificate.", retryable=True),
    # --- symbolic names used only in the per-endpoint error lists (no code) ---
    _e("SMDF_URL_NOT_SET", None,
       "S-MDF URL not configured on the agent.", terminal=True),
    _e("SMDF_NOT_AUTHORIZED_BY_NACEF", None,
       "S-MDF not authorized by NACEF.", terminal=True),
    _e("SMDF_CERTFICATE_NOT_GENERATED", None,  # spec typo alias of 513
       "Certificate not generated yet.", terminal=True),
    _e("SAM_INVALID_OTP", None, "Invalid OTP.", retryable=True),
    _e("SAM_INVALID_PIN", None, "Invalid PIN.", retryable=True),
    _e("SAM_NO_PAIRING_CODE", None, "No pairing code.", terminal=True),
    _e("SAM_PAIRING_FAILED", None, "Pairing failed.", retryable=True),
    _e("SAM_REINIT_PIN_FAILED", None, "PIN reinit failed.", retryable=True),
    _e("SAM_INVALID_PIN_EXCEED_THRESHOLD", None,
       "Too many invalid PIN attempts.", terminal=True),
    _e("SAM_INVALID_OTP_EXCEED_THRESHOLD", None,
       "Too many invalid OTP attempts.", terminal=True),
    _e("SAM_INVALID_SAD", None, "Invalid signature activation data.",
       retryable=True),
    _e("SAM_INVALID_SAD_EXCEED_THRESHOLD", None,
       "Too many invalid signatures.", terminal=True),
    _e("SAM_SIC_SYNCHRONIZATION_ERROR", None, "Sync protocol error.",
       retryable=True),
    _e("SIC_FAILED_GET_HWD_INFO", None, "Agent could not read hardware info.",
       terminal=True),
    _e("SIC_FAILED_SHOW_UI", None, "Agent could not show UI.", terminal=True),
]}

#: reverse lookup: numeric code -> ErrorInfo (only for coded entries)
ERROR_BY_CODE: Dict[int, ErrorInfo] = {
    e.code: e for e in ERROR_CATALOG.values() if e.code is not None
}


class NacefError(Exception):
    """Raised when the S-MDF returns an error envelope or an HTTP 400/401.

    Attributes:
        name:      symbolic error name (``errorCode`` field), when known.
        code:      numeric error code, when known.
        message:   the S-MDF ``message`` field.
        info:      the matching :class:`ErrorInfo`, when the name/code is known.
    """

    def __init__(self, name=None, code=None, message=None):
        info = None
        if name and name in ERROR_CATALOG:
            info = ERROR_CATALOG[name]
        elif code is not None and code in ERROR_BY_CODE:
            info = ERROR_BY_CODE[code]
        self.name = name or (info.name if info else None)
        self.code = code if code is not None else (info.code if info else None)
        self.message = message or (info.meaning if info else "S-MDF error")
        self.info = info
        super().__init__(f"[{self.name or self.code}] {self.message}")

    @property
    def retryable(self) -> bool:
        return bool(self.info and self.info.retryable)

    @property
    def terminal(self) -> bool:
        return bool(self.info and self.info.terminal)
