
    ?&j)                        U d Z ddlmZ ddlmZ ddlmZmZ ddddddddddddddZd	e	d
<   ddZ
ddZ ed      dd       ZddZddZ ed      dd       ZddZy)ur  
alloggiati/normalizers.py
=========================
Reusable normalization helpers for the Alloggiati Web transformer.

Design principles
-----------------
- PURE: no DB writes, no API calls, no logging of sensitive data.
- SAFE: every function returns a defined value on any input — never raises.
- STRICT: fallback on unresolvable input is "" (empty string), NOT the raw
  value.  Leaking human-readable strings like "United States" or "passport"
  into an Alloggiati payload would cause silent rejection by the portal.
- EFFICIENT: DB lookups happen at most once per process via lru_cache.
  transform_booking_guests() may process hundreds of guests; repeated queries
  for the same country/document value must never occur.

Reuse policy
------------
- Country normalization delegates entirely to the existing
  services.country_utils.resolve_istat_country_code() which already queries
  IstatCountry with its own lru_cache.  No duplicate DB queries, no duplicate
  country mapping tables.
- Document type normalization queries IstatDocumentType once and caches the
  result map for the lifetime of the process.
- Gender normalization is stateless — no DB access needed.

No ISTAT models are imported at module level.  All model imports are deferred
inside cached loader functions to avoid circular-import issues at Django
startup.

Thread safety
-------------
lru_cache is thread-safe for reads in CPython (GIL protects the dict).
The cached maps are read-only after construction, so concurrent access from
multiple threads is safe.  clear_normalizer_caches() should only be called
from single-threaded test teardown or management commands.
    )annotations)	lru_cache)AnyOptionalzidentity cardzdriving licencepassport)zid cardid_cardznational idznational id cardzcarta identitazcarta d'identitazdrivers licensedrivers_licensezdriver licensezdriver's licensezdriving licensepatente
passaportodict[str, str]_DOCUMENT_TYPE_ALIASESc                \    | yt        | t              ryt        |       j                         S )a  
    Convert any value to a stripped string safely.

    Handles None, int, bool, float, and str without raising.
    Booleans are coerced to "" because True/False are never valid field values
    in this context (bool is a subclass of int in Python, so we check it first).

    Args:
        value: Any raw field value.

    Returns:
        Stripped string, or "" for None/bool/empty.
     )
isinstanceboolstrstripvalues    "/backend/alloggiati/normalizers.py	_safe_strr   L   s,     }%u:    c                >    | j                         j                         S )z4Lowercase + strip for case-insensitive dict lookups.)r   lowerr   s    r   _normalize_keyr   b   s    ;;=  r      )maxsizec                    ddl m}  i }| j                  j                  dd      D ]_  }|j                  xs dj                         }|j                  xs dj                         }|sB|r||t        |      <   ||t        |      <   a |S )u  
    Build a lookup dict from IstatDocumentType (read-only, cached for process lifetime).

    Keys added per DB row:
      - normalised description  e.g. "passport"      → "PASS"
      - normalised code         e.g. "pass"           → "PASS"

    Called at most once per process; subsequent calls return the cached dict.
    The cache is populated lazily on first normalization call, not at import time.
    r   )IstatDocumentTypecodedescriptionr   )istat.modelsr   objectsonlyr    r   r!   r   )r   mappingdocr    r!   s        r   _load_document_type_mapr'   g   s     / G ((--fmDB%%',"33537GN;/0(,t$% E Nr   c                    t        | t              ryt        | t              r| dk(  ry| dk(  ryyt        |       }|sy|j	                         }|dv ry|dv ryy)uD  
    Normalize any gender representation to the Alloggiati Web accepted codes.

    Accepted inputs (case-insensitive, leading/trailing whitespace ignored):
      "male", "m", "maschio", "1"   → "M"
      "female", "f", "femmina", "2" → "F"
      anything else / None / bool   → "U"

    Integers are accepted: 1 → "M", 2 → "F".
    Booleans always map to "U" (True/False are not valid gender values).
    Never raises.

    Args:
        value: Raw gender value from Guest.gender or extra_data.

    Returns:
        "M", "F", or "U" — always a non-empty string.
    Ur   M   F>   1mmalemaschio>   2ffemalefemmina)r   r   intr   r   )r   rawkeys      r   normalize_genderr8      sh    ( %%A:A:
E
C
))+C
++
--r   c                J    | sy| j                         }|syddlm}  ||      S )u  
    Convert any country representation to IstatCountry.code (canonical ISO3).

    Delegates directly to services.country_snapshot.resolve_istat_country_code()
    — a pure O(1) dict lookup against the immutable in-memory snapshot loaded
    at Django startup.  Zero ORM, zero DB access.

    No lru_cache is applied here: the snapshot is already O(1) and immutable,
    so an additional caching layer would only add memory overhead and hide
    correctness issues without providing any performance benefit.

    Resolution order (handled inside the snapshot resolver):
      1. Empty / None                → COUNTRY_NOT_FOUND ("")
      2. Exact match on code (ISO3)  → return code  ("USA" → "USA")
      3. Match on iso_code (ISO2)    → return code  ("US"  → "USA")
      4. Match on country name       → return code  ("Italy" → "ITA")
      5. Unresolvable                → COUNTRY_NOT_FOUND ("")

    Returning "" on failure is intentional: the transformer's post-normalization
    validation will produce "invalid_nationality_code" rather than silently
    sending a human-readable string like "United States" to the portal.

    Args:
        value: Any country representation — ISO2 ("IT"), ISO3/canonical ("ITA"),
               or full name ("Italy").

    Returns:
        IstatCountry.code string if resolved, otherwise "".
        Never None, never raises (SnapshotNotLoadedError propagates if startup
        was incomplete — this is intentional fail-fast behavior).
    r   r   )resolve_istat_country_code)r   services.country_snapshotr:   )r   stripped_snap_resolves      r   normalize_country_coder>      s*    @ {{}HU""r      c                    | sy| j                         }|syt               }t        |      }||v r||   S t        j	                  |      }|r	||v r||   S y)u  
    Map a user-friendly document type string to the official ISTAT code.

    Resolution order:
      1. Empty / None input                                → ""
      2. Direct match against IstatDocumentType.code       → return code
      3. Direct match against IstatDocumentType.description → return code
      4. Match via _DOCUMENT_TYPE_ALIASES preprocessing    → return code
      5. Unresolvable                                      → ""

    Returning "" on failure is intentional: the transformer's post-normalization
    validation will produce "invalid_document_type" rather than sending a raw
    string like "random document" to the Alloggiati portal.

    The IstatDocumentType table is loaded once and cached for the process
    lifetime (_load_document_type_map).

    Args:
        value: Raw document type (e.g. "passport", "PASS", "id card").

    Returns:
        Official ISTAT document type code if resolved, otherwise "".
        Never None, never raises.
    r   )r   r'   r   r   get)r   r<   doc_mapr7   aliased_keys        r   normalize_document_typerD      sl    4 {{}H%'G

"C g~s| ),,S1K{g-{## r   c                 n    t         j                          t        j                          ddlm}   |         y)a  
    Clear document-type cache and reset the country snapshot.

    Use in tests after installing mock data to force a fresh load on the
    next normalization call.  Should only be called from single-threaded
    test setup/teardown.

    normalize_country_code() has no lru_cache (the snapshot is already O(1)),
    so only the document type map and snapshot state need resetting.
    r   reset_country_snapshotN)r'   cache_clearrD   r;   rG   rF   s    r   clear_normalizer_cachesrI     s(     '')'') Ar   N)r   r   returnr   )r   r   rJ   r   )rJ   r   )r   zOptional[str]rJ   r   )rJ   None)__doc__
__future__r   	functoolsr   typingr   r   r   __annotations__r   r   r'   r8   r>   rD   rI    r   r   <module>rR      s   $L #    &5%4%4%4%4%4%6%6%6%6%6%6%/*  ,,!
 1 >'T)#X 3- -`r   