
    ?&j;                    Z   U d Z ddlmZ ddlZddlmZmZmZmZm	Z	m
Z
 	 ddlmZ ddlmZmZmZmZ e
rddlmZ ddlmZ  G d	 d
e      Z G d de      Z G d de      Zeeef   ZdZded<   i dddddddddddddddddd d!dd"dd#dd$d%d&dd'd(d)dd*d(d+dd,d-d.dd/d-d0dd1dd2dd3dd4dd5d6d7dd8d d9dd:d;d<dd=d;d>dZd?ed@<   dAdBdZdOdCZ dPdDZ!dQdEZ"dRdFZ#dSdGZ$dTdHZ%dUdIZ&dVdJZ'	 	 	 	 	 	 	 	 	 	 dWdKZ(dXdLZ)dYdMZ*dZdNZ+y# e$ r
 ddlmZ Y w xY w)[u  
alloggiati/transformer.py
=========================
Single source of truth transformation layer between the internal Django Guest
model and the Alloggiati Web API payload format.

This module is a PURE transformation layer:
  - No database writes
  - No API calls
  - No logging of sensitive data (document numbers, names)
  - No modifications to models or serializers

Design notes
------------
- get_field_value() is used for EVERY guest-originating field that may exist
  in extra_data.  This ensures the extra_data override rule is applied
  consistently across all fields, not just some.

- Normalization (country codes, document type codes, gender) happens BEFORE
  the post-normalization validation step.  This is intentional: we must know
  the normalized value to determine whether it is valid.  Validating the raw
  value first would allow invalid raw strings to pass the presence check and
  then silently corrupt the payload.

- normalize_country_code() and normalize_document_type() return "" on failure
  (not the raw input).  This is a deliberate strict policy: leaking a human-
  readable string like "United States" or "random document" into the payload
  would cause silent rejection by the Alloggiati portal with no actionable
  error.  Returning "" causes the post-normalization validation to fire with a
  specific reason code instead.

- Identity validation (first_name + last_name) is handled separately from
  _REQUIRED_OUTPUT_FIELDS because the system intentionally allows a single-
  word name where first_name is "" and last_name is the full name.  Including
  first_name/last_name in _REQUIRED_OUTPUT_FIELDS would incorrectly reject
  valid single-name guests.

Field mapping notes (actual model vs. original spec assumptions):
  - booking.reference_code    → does not exist; mapped to booking.uid (UUID)
  - booking.structure.external_id → does not exist; mapped to
    booking.structure.istat_code (the Alloggiati/ISTAT structure identifier)

Usage:
    from alloggiati.transformer import to_alloggiati_guest, transform_booking_guests
    )annotationsN)AnyDictListOptionalTupleTYPE_CHECKING)	TypedDict)	_safe_strnormalize_gendernormalize_country_codenormalize_document_type)Guest)Bookingc                      e Zd ZU ded<   ded<   ded<   ded<   ded<   ded<   ded<   ded	<   ded
<   ded<   ded<   ded<   ded<   ded<   y)AlloggiatiPayloadstr
first_name	last_namegenderdate_of_birthplace_of_birth_cityplace_of_birth_countrynationalitydocument_typedocument_numberdocument_issuing_countrybooking_referenceOptional[str]arrival_datedeparture_datestructure_idN__name__
__module____qualname____annotations__     "/backend/alloggiati/transformer.pyr   r   I   sS    ONK!!!!r)   r   c                  "    e Zd ZU ded<   ded<   y)ValidResultboolvalidr   dataNr#   r(   r)   r*   r,   r,   Z   s    K
r)   r,   c                  "    e Zd ZU ded<   ded<   y)InvalidResultr-   r.   r   reasonNr#   r(   r)   r*   r1   r1   _   s    KKr)   r1   )r   r   r   r   r   r"   r   zTuple[str, ...]_REQUIRED_OUTPUT_FIELDSmissing_id_number	id_numberzDocument number is required.)fieldmessagemissing_date_of_birthr   zDate of birth is required.missing_nationalityr   zNationality is required.missing_document_typer   zDocument type is required. missing_document_issuing_countryr   z%Document issuing country is required.invalid_date_formatz8Date of birth is not a valid date (expected YYYY-MM-DD).missing_identity	full_namezBGuest name is required (first name or last name must be provided).missing_booking_relationbookingz!Guest is not linked to a booking.missing_booking_uidzBooking reference is missing.missing_structure	structurez%Booking is not linked to a structure.missing_structure_istat_codez'Structure ISTAT code is not configured.invalid_nationality_codezENationality could not be resolved to a valid Alloggiati country code.invalid_document_typezWDocument type is not recognised. Use a standard type such as Passport or Identity Card.invalid_birth_countrycountry_of_birthzJCountry of birth could not be resolved to a valid Alloggiati country code. invalid_document_issuing_countryzRDocument issuing country could not be resolved to a valid Alloggiati country code.incomplete_payloadpayloadz>One or more required fields are missing from the guest record.final_validation_failedzHGuest record failed final validation. Please review all required fields.zDict[str, Dict[str, str]]_REASON_MESSAGESunknownz(An unexpected validation error occurred.c                H    t        t        j                  | t                    S )u   
    Convert an internal reason code to a staff-readable {field, message} dict.

    Never raises — unknown codes produce a safe fallback message.
    Never exposes raw field values, credentials, or stack traces.
    )dictrM   get_FALLBACK_ERROR)r2   s    r*   reason_to_errorrS      s      $$V_=>>r)   c                    | r| j                         sy| j                         j                         }t        |      dk(  rd|d   fS |d   dj                  |dd       fS )u  
    Parse a full name string into (first_name, last_name).

    Rules:
      - Strips surrounding whitespace.
      - Two or more tokens: first token → first_name, remainder → last_name.
      - Exactly one token:  first_name = "", last_name = that token.
      - Empty / whitespace-only: returns ("", "").

    Args:
        full_name: Raw full name string from Guest.full_name.

    Returns:
        Tuple of (first_name, last_name), both stripped strings.
    ) rU      rU   r    N)stripsplitlenjoin)r>   partss     r*   split_full_namer]      s_      IOO-OO##%E
5zQE!H~!HchhuQRy)**r)   c                r    t        | dd      xs i }|j                  |      }||dk7  r|S t        | |d      S )u  
    Retrieve a guest field value applying the extra_data override rule.

    Priority:
      1. extra_data[field_name] — if present and non-empty (not None, not "").
      2. getattr(guest, field_name) — model field fallback.
      3. None — if neither source has the field.

    This function MUST be used for every guest-originating field that may
    exist in extra_data.  Using getattr() directly would bypass the override
    rule and produce inconsistent behaviour when guests submit data via the
    check-in form (which stores values in extra_data).

    Args:
        guest:      Guest model instance.
        field_name: Name of the field to retrieve.

    Returns:
        The resolved value, or None if not found in either source.
    
extra_dataNrU   )getattrrQ   )guest
field_namer_   extra_values       r*   get_field_valuerd      sG    * ")d!C!IrJ..,K;"#45*d++r)   c                   t        | dd      xs i }|j                  d      xs dj                         }|j                  d      xs dj                         }|r|r||fS t        t        | dd      xs d      \  }}|r|n|}|r|n|}||fS )ab  
    Resolve first_name and last_name using the priority chain:

      1. extra_data.first_name / extra_data.last_name  (if non-empty)
      2. Parsed Guest.full_name
      3. Empty strings as final fallback (caller must validate)

    Args:
        guest: Guest model instance.

    Returns:
        Tuple of (first_name, last_name), both stripped.
    r_   Nr   rU   r   r>   )r`   rQ   rX   r]   )ra   r_   extra_first
extra_lastparsed_firstparsed_lastr   r   s           r*   _resolve_identityrj   "  s     ")d!C!IrJ"|4:AACK!~~k28b??AJzZ(( /{B0O0USU VL+ +J(
kI	""r)   c                   | yt        | t        j                  t        j                  f      r| j                  d      dfS t        | t              r`| j                         }|syt        |      dk  ry|dd }	 t        j                  j                  |      }|j                  d      |k7  ry|dfS y# t        $ r Y yw xY w)uk  
    Strictly parse and validate a date value into "YYYY-MM-DD" format.

    Accepts:
      - datetime.date / datetime.datetime objects  → always valid
      - Strings in "YYYY-MM-DD" format with a valid calendar date

    Rejects:
      - Strings that are not valid ISO dates ("2026-99-99", "abc", "")
      - None → treated as missing, not invalid (returns None, True)

    Args:
        value: Raw date value.

    Returns:
        (formatted_str, True)  — valid date
        (None, True)           — value was None/missing (caller decides)
        (None, False)          — value present but malformed
    N)NTz%Y-%m-%dT
   )NF)	
isinstancedatetimedatestrftimer   rX   rZ   fromisoformat
ValueError)valuestripped	date_partparseds       r*   _parse_date_strictrw   D  s    ( }%(--):):;<z*D11%;;=x=2 SbM		!]]00;F ??:&)3 4    	! 	!s   9B2 2	B>=B>c                    g d}|D ]8  \  }}t        | |      }|"t        |t              s%|j                         r6|c S  t        | d      }t	        |      \  }}|syy)uJ  
    Validate presence of required guest-level fields BEFORE normalization.

    Uses get_field_value() for all fields so that extra_data overrides are
    respected during validation — the same source used during payload assembly.
    This prevents the mismatch where validation passes on the model field but
    payload assembly uses a different (extra_data) value.

    Required fields:
      document_number  → "missing_id_number"
      date_of_birth    → "missing_date_of_birth"
      nationality      → "missing_nationality"
      document_type    → "missing_document_type"

    Date format is also validated here:
      date_of_birth must be a valid ISO date → "invalid_date_format"

    Args:
        guest: Guest model instance.

    Returns:
        None if all fields pass, or a reason string on the first failure.
    )r   r4   )r   r8   )r   r9   )r   r:   )r   r;   Nr   r<   )rd   rm   r   rX   rw   )ra   scalar_requiredr6   r2   rawdob_raw_
date_valids           r*   _validate_guest_fieldsr   y  se    0.O )veU+;:c3/		M ) e_5G&w/MAz$r)   c                   g }t        |       \  }}|j                         s*|j                         s|j                  t        d             g d}d}|D ]W  \  }}t	        | |      }|"t        |t              s%|j                         r6|j                  t        |             |dk(  sVd}Y |s6t	        | d      }	t        |	      \  }
}|s|j                  t        d             t        | dd      }t        |      }|r|j                  t        |             |S t        t	        | d	            xs d}|r't        |      }|s|j                  t        d
             t        t	        | d            xs d}|r't        |      }|s|j                  t        d             t        t	        | d            xs d}|r't        |      }|s|j                  t        d             |S )a  
    Collect ALL validation errors for a guest in a single pass.

    Unlike _validate_guest_fields() which stops at the first failure,
    this function checks every required field and returns the complete
    list of problems.  This is used by the service layer to build
    structured, staff-readable error reports.

    Validation order mirrors the transformer pipeline:
      1. Identity (name)
      2. Required scalar fields (document_number, date_of_birth,
         nationality, document_type, document_issuing_country)
      3. Date format for date_of_birth
      4. Booking + structure references
      5. Post-normalization checks (nationality code, document type code,
         document_issuing_country code)

    Args:
        guest: Guest model instance.

    Returns:
        List of {"field": str, "message": str} dicts.
        Empty list means the guest is fully valid.
        Never raises.
    r=   ry   FNr   Tr<   r@   r   rE   r   rF   r   rI   )rj   rX   appendrS   rd   rm   r   rw   r`   _validate_booking_fieldsr   r   r   )ra   errorsr   r   scalar_checksmissing_dobrb   reason_coder{   r|   r}   r~   r@   booking_reasonraw_nationalitynorm_natraw_doc_typenorm_docraw_issuingnorm_issuings                       r*   collect_guest_validation_errorsr     s   4 $&F .e4J	ioo&7o&89:,M K#0
KeZ0;:c3/		MM/+67_," $1 !%9*73:MM/*?@A eY-G-g6Non56  } EFN$O)/:MM/*DEF_UODEML*<8MM/*ABCOE3MNOWSWK-k:MM/*LMNMr)   c                    | yt        | dd      }|t        |      j                         dk(  ryt        | dd      }|yt        |dd      }|t        |      j                         dk(  ry	y)
u"  
    Validate required booking- and structure-level fields.

    Checks (fails on first error):
      booking exists                         → "missing_booking_relation"
      booking.uid non-empty                  → "missing_booking_uid"
      booking.structure exists               → "missing_structure"
      booking.structure.istat_code non-empty → "missing_structure_istat_code"

    Args:
        booking: Booking model instance (or None).

    Returns:
        None if all fields pass, or a reason string on the first failure.
    Nr?   uidrU   rA   rC   rB   
istat_coderD   )r`   r   rX   )r@   r   rC   r   s       r*   r   r     sy      )
'5$
'C
{c#hnn&",$d3I"L$7JS_224:-r)   c                    | sy|syy)u  
    Validate that normalization produced non-empty coded values for required
    fields.

    This step runs AFTER normalization.  It is necessary because a field can
    pass the presence check in _validate_guest_fields() (e.g. "Mars" is a
    non-empty string) but then fail normalization (normalize_country_code
    returns "" for an unrecognised value).  Without this step, an invalid
    raw value would silently produce an empty field in the payload.

    Required to be non-empty after normalization:
      nationality              → "invalid_nationality_code"
      document_type            → "invalid_document_type"

    Optional fields (empty is allowed — not all guests have these):
      place_of_birth_country   → "invalid_birth_country"   (only if non-empty raw)
      document_issuing_country → "invalid_document_issuing_country" (only if non-empty raw)

    Note: place_of_birth_country and document_issuing_country are validated
    only when the caller passes a non-empty string, indicating the raw value
    was present but failed normalization.

    Args:
        nationality:              Normalized nationality code (or "").
        document_type:            Normalized document type code (or "").
        place_of_birth_country:   Normalized birth country code (or "").
        document_issuing_country: Normalized issuing country code (or "").

    Returns:
        None if all checks pass, or a reason string on the first failure.
    rE   rF   Nr(   r   r   r   r   s       r*   _validate_normalized_fieldsr   -  s    J )& r)   c                   t        | j                  d            }t        | j                  d            }|s|syt        D ]   }| j                  |      }t        |      r  y d}|D ]  }t        | j                  |            r y y)u
  
    Final safety gate: ensure every required output field is non-empty.

    Checks _REQUIRED_OUTPUT_FIELDS (which intentionally excludes first_name
    and last_name — identity is validated separately).

    Also enforces that at least one of first_name / last_name is non-empty.

    Args:
        payload: The assembled AlloggiatiPayload dict.

    Returns:
        None if the payload passes all checks.
        "missing_identity"        — both first_name and last_name are empty.
        "incomplete_payload"      — a required field is empty/missing.
        "final_validation_failed" — a critical field subset is empty
                                    (document_number, document_type,
                                     structure_id, booking_reference).
    r   r   r=   rJ   )r   r   r"   r   rL   N)r   rQ   r3   )rK   firstlastr6   rs   criticals         r*   _final_payload_guardr   g  s    * gkk,/0EW[[-.D! )E"' ) YHU+,,  r)   c                   t        |       }|rd|dS t        |       \  }}|j                         s|j                         sdddS t        | dd      }t	        |      }|rd|dS |j
                  }t        | d      }t        |      \  }}	|	r|dddS t        t        |dd            \  }
}|sdddS t        t        |d	d            \  }}|sdddS t        t        t        | d
            xs d      }t        t        t        | d            xs d      }t        t        t        | d            xs d      }t        t        t        | d            xs d      }t        t        | d            }t        ||||      }|rd|dS ||||t        t        | d            |||t        t        | d            |t        |j                        |
|t        |j                        d}t        |      }|rd|dS d|dS )u)  
    Transform a single Guest instance into an Alloggiati Web payload dict.

    Validation + transformation pipeline (fails fast on first error):

      STEP 1 — Pre-normalization field presence check
               Ensures required raw fields exist before we attempt normalization.
               Uses get_field_value() so extra_data overrides are respected.

      STEP 2 — Identity resolution
               Resolves first_name / last_name from extra_data or full_name.

      STEP 3 — Booking + structure validation
               Checks uid, structure, istat_code.

      STEP 4 — Strict date parsing
               Validates date_of_birth, check_in_date, check_out_date.
               Uses get_field_value() for date_of_birth to match STEP 1.

      STEP 5 — Normalization
               Converts raw values to ISTAT coded values.
               All guest fields use get_field_value() for consistent
               extra_data override behaviour.

      STEP 6 — Post-normalization validation
               Ensures normalization produced non-empty coded values for
               required fields.  Catches cases where a field was present
               but unrecognised (e.g. nationality="Mars").

      STEP 7 — Final payload guard
               Last-resort check that no required output field is empty.

    Args:
        guest: Guest model instance with a related Booking and Structure.

    Returns:
        {"valid": True,  "data": AlloggiatiPayload}  on success.
        {"valid": False, "reason": str}               on any validation failure.
    F)r.   r2   r=   r@   Nr   r<   check_in_datecheck_out_dater   rH   r   r   r   r   place_of_birthr   )r   r   r   r   r   r   r   r   r   r   r   r    r!   r"   T)r.   r/   )r   rj   rX   r`   r   rC   rd   rw   r   r   r   r   r   r   r   r   )ra   field_errorr   r   r@   booking_errorrC   r|   dob_str	dob_validarrival_strarrival_validdeparture_strdeparture_validnorm_nationalitynorm_country_of_birthnorm_doc_issuing_countrynorm_document_typenorm_gender
norm_errorrK   guard_errors                         r*   to_alloggiati_guestr     sR   Z )/K+66
 .e4J	ioo&7*<==
 eY-G,W5M-88!!I e_5G+G4GY*?@@!3$/"K *?@@%7)40&"M? *?@@ ./%78@D 3/%);<=E  6/%)CDEM  1/%9:Bd #?5(#CDK -$(4!9	J *55 $.#,#.#*#,_UDT-U#V#8#3#5#,_UDU-V#W$<#,W[[#9#.#0#,Y-A-A#BG( 'w/K+667++r)   c           
     |   g }g }t        | dd      }| j                  j                         D ]  }t        |      }|j	                  d      r|j                  |d          4t        |      }t        t        |dd            xs d}|j                  t        |dd      |||j	                  dd	      |d
        ||dS )um  
    Transform all guests associated with a booking.

    Iterates booking.guests.all(), applies to_alloggiati_guest() to each,
    and separates results into valid and invalid buckets.

    For invalid guests, collect_guest_validation_errors() is called to
    produce the full structured error list (all fields, not just the first).

    Country normalization is O(1) per call — the snapshot is an immutable
    in-memory dict loaded once at startup.  Document type lookups are cached
    via lru_cache for the process lifetime.  No N+1 DB queries occur.

    Args:
        booking: Booking model instance with a prefetchable guests relation.

    Returns:
        {
            "valid":   [AlloggiatiPayload, ...],
            "invalid": [
                {
                    "guest_id":    int,
                    "booking_id":  int,
                    "guest_name":  str,   # display name only, no doc numbers
                    "reason":      str,   # legacy single-reason code
                    "errors":      [{"field": str, "message": str}, ...],
                },
                ...
            ],
        }
    idNr.   r/   r>   rU   zUnknown Guestr2   unknown_error)guest_id
booking_id
guest_namer2   r   )r.   invalid)r`   guestsallr   rQ   r   r   r   )r@   valid_payloadsinvalid_recordsr   ra   resultstructured_errorsr   s           r*   transform_booking_guestsr   0  s    @ /1N,.O$-J##%$U+::g!!&.1 !@ F #75+r#BCVJ""")%t"<",","(**X"G"3 &. "" r)   )r2   r   returnzDict[str, str])r>   r   r   Tuple[str, str])ra   'Guest'rb   r   r   r   )ra   r   r   r   )rs   r   r   zTuple[Optional[str], bool])ra   r   r   r   )ra   r   r   zList[Dict[str, str]])r@   r   r   r   )
r   r   r   r   r   r   r   r   r   r   )rK   zDict[str, Any]r   r   )ra   r   r   TransformResult)r@   z	'Booking'r   zDict[str, List[Any]]),__doc__
__future__r   rn   typingr   r   r   r   r   r	   r
   ImportErrortyping_extensionsalloggiati.normalizersr   r   r   r   guests.modelsr   bookings.modelsr   r   r,   r1   r   r   r3   r'   rM   rR   rS   r]   rd   rj   rw   r   r   r   r   r   r   r   r(   r)   r*   <module>r      s  ,\ #  B B,   #'	 ") 
I  sCx.,  &K/1K/ "/K/  -K/ "/K/$ '+:)%K/. "M/K/8 W9K/B 6!CK/J 2KK/R :SK/Z #<%[K/d  Z!eK/l "lmK/t %_uK/| '-g)}K/F SGK/N ] OK/ + KZ &2\]?+>,D#D.j+dUxL333  3 "	3
 3t&ZU,x>u  ,++,s   D D*)D*