
    ?&j@                    b   U d Z ddlmZ ddlZddlZddlZddlZddlZddl	Zddl
mZmZmZmZ ddlmZ dZdZdZd	Zd
dddZded<   dZ G d de      Z G d de      Z G d de      Z G d de      Zd%dZd&dZd'dZ d(dZ!d)dZ"ded	 	 	 	 	 	 	 	 	 d*dZ#d+d Z$d,d!Z%d-d"Z& G d# d$      Z'y).u  
alloggiati/client.py
====================
Low-level HTTP/SOAP client for the Alloggiati Web police portal.

Responsibilities:
  - Build well-formed SOAP/XML request envelopes
  - Authenticate (CODES or DIGITAL_CERTIFICATE)
  - Send payloads with timeout + retry safety
  - Parse server responses into structured Python dicts
  - Raise typed exceptions — never swallow errors silently

Security rules enforced here:
  - Credentials are accepted as in-memory strings only (never re-persisted)
  - No guest PII appears in exception messages or logs
  - Certificate/key material is written to a tempfile only for the duration
    of the SSL handshake, then immediately deleted

Alloggiati Web SOAP endpoint (Italian Police):
  https://alloggiatiweb.poliziadistato.it/PortaleAlloggiati/Service.asmx

NOTE: This client is intentionally decoupled from Django models.
      All inputs are plain Python values; no ORM objects are accepted.
    )annotationsN)AnyDictListOptional)ElementTreezFhttps://alloggiatiweb.poliziadistato.it/PortaleAlloggiati/Service.asmxzFhttps://alloggiatiweb.poliziadistato.it/PortaleAlloggiati/AuthenticatezDhttps://alloggiatiweb.poliziadistato.it/PortaleAlloggiati/SendGuestsz:https://alloggiatiweb.poliziadistato.it/PortaleAlloggiati/
PASSAPORTOzCARTA IDENTITAPATENTE)passportid_carddrivers_licensezDict[str, str]_DOCUMENT_TYPE_MAP   c                      e Zd ZdZy)AlloggiatiClientErrorz+Base exception for all client-level errors.N__name__
__module____qualname____doc__     /backend/alloggiati/client.pyr   r   D   s    5r   r   c                      e Zd ZdZy)AlloggiatiAuthErrorz5Raised when authentication with Alloggiati Web fails.Nr   r   r   r   r   r   H   s    ?r   r   c                      e Zd ZdZy)AlloggiatiNetworkErrorz(Raised on timeout or connection failure.Nr   r   r   r   r   r   L   s    2r   r   c                      e Zd ZdZy)AlloggiatiResponseErrorzERaised when the server returns a parseable but unsuccessful response.Nr   r   r   r   r   r   P   s    Or   r   c                    | t        |       nd}|j                  dd      j                  dd      j                  dd      j                  dd	      j                  d
d      S )z6Escape a value for safe inclusion in XML text content. &z&amp;<z&lt;>z&gt;"z&quot;'z&apos;)strreplace)valuetexts     r   _xml_escaper+   X   sV    *3u:D	g		f		f		h		h	r   c                ^    t        j                  dt         d|  d      j                         S )z:Wrap a SOAP body fragment in a standard SOAP 1.1 envelope.a          <?xml version="1.0" encoding="utf-8"?>
        <soap:Envelope
            xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns:tns="z%">
          <soap:Body>
            z5
          </soap:Body>
        </soap:Envelope>
    )textwrapdedent_SOAP_NSstrip)body_xmls    r   _soap_enveloper2   e   s<    ??   !z "J  	 
r   c                8    dt        |        dt        |       dS )Nz<tns:Authenticate><tns:Utente>z</tns:Utente><tns:Password>z"</tns:Password></tns:Authenticate>)r+   )usernamepasswords     r   _build_auth_bodyr6   u   s1    	"8,- .$X./ 0	r   c                T   t         j                  | j                  d      xs dj                         t        | j                  d      xs d            }dt        | j                  dd             dt        | j                  dd             dt        | j                  dd             d	t        | j                  d
d             dt        | j                  dd             dt        | j                  dd             dt        | j                  dd             dt        | j                  dd             dt        | j                  dd             dt        | j                  dd             d| dt        | j                  dd             dt        | j                  dd             dS )zh
    Serialise a single AlloggiatiPayload dict into the Alloggiati XML
    <Scheda> element format.
    document_typer!   z<Scheda><CodiceStruttura>structure_idz</CodiceStruttura><DataArrivo>arrival_datez</DataArrivo><DataPartenza>departure_datez</DataPartenza><Cognome>	last_namez</Cognome><Nome>
first_namez</Nome><Sesso>genderUz</Sesso><DataNascita>date_of_birthz</DataNascita><ComuneNascita>place_of_birth_cityz</ComuneNascita><StatoNascita>place_of_birth_countryz</StatoNascita><Cittadinanza>nationalityz</Cittadinanza><TipoDocumento>z!</TipoDocumento><NumeroDocumento>document_numberz*</NumeroDocumento><LuogoRilascioDocumento>document_issuing_countryz"</LuogoRilascioDocumento></Scheda>)r   getlowerr+   )payloaddoc_codes     r   _build_guest_xmlrJ   ~   s   
 "%%	_	%	+224GKK06B7H
	'NB(GHI J"7;;~r#BCD E$W[[1A2%FGH IK <=> ?W[[r:;< =gkk(C89: ;#GKK$DEF G%gkk2G&LMN O$W[[1I2%NOP Q$W[[%CDE F" $'4Er(JKL M#w{{#=rBC
D	r   c                V    dj                  d |D              }dt        |        d| dS )Nr!   c              3  2   K   | ]  }t        |        y wN)rJ   ).0gs     r   	<genexpr>z#_build_send_body.<locals>.<genexpr>   s     9&Q%a(&s   z<tns:SendGuests><tns:Token>z</tns:Token><tns:Schede>z</tns:Schede></tns:SendGuests>)joinr+   )tokenguestsschedes      r   _build_send_bodyrU      s?    WW9&99F	!%() *h 	r   )ssl_contexttimeoutc                N   | j                  d      }t        j                  j                  t        |d      }|j                  dd       |j                  dd| d       |j                  dt        t        |                   	 t        j                  j                  |||	      5 }|j                         j                  d      cd
d
d
       S # 1 sw Y   y
xY w# t        j                  j                  $ r}t        d|j                   d      |d
}~wt        j                  j                  $ rY}t        |j                         }d|j#                         v sd|j#                         v rt%        d      |t%        d|       |d
}~wt&        $ r}t%        d      |d
}~ww xY w)z
    Send a SOAP envelope via HTTP POST and return the raw response body.

    Raises:
        AlloggiatiNetworkError: On timeout or connection failure.
        AlloggiatiResponseError: On non-200 HTTP status.
    zutf-8POST)datamethodzContent-Typeztext/xml; charset=utf-8
SOAPActionr%   zContent-Length)contextrW   NzAlloggiati Web returned HTTP .z	timed outrW   z'Connection to Alloggiati Web timed out.z)Network error contacting Alloggiati Web: )encodeurllibrequestRequest_SOAP_ENDPOINT
add_headerr'   lenurlopenreaddecodeerror	HTTPErrorr   codeURLErrorreasonrG   r   TimeoutError)	envelopesoap_actionrV   rW   
body_bytesreqrespexcrm   s	            r   
_post_soapru      sp    )J
..
 
 j
 
PCNN>#<=NN<1[M!34NN#SZ%9:^^##Cg#NRV99;%%g. ONN<<!! %+CHH:Q7
	 <<   SZZ&,,.(I,G(9 %7x@
	  $5
	sO   "C 'C	C CC C F$9D F$3AFF$FF$c                   	 t        j                  |       }|j	                  d      }||j
                  xs dj                         st        d      |j
                  j                         }|j                         j                  d      rt        d      |S # t         j                  $ r}t        d      |d}~ww xY w)z
    Extract the session token from an Authenticate SOAP response.

    Raises:
        AlloggiatiAuthError: If the response indicates failure or is malformed.
    z)Malformed XML in authentication response.Nz.//{*}AuthenticateResultr!   z;Authentication failed: no token returned by Alloggiati Web.ERRORz*Authentication rejected by Alloggiati Web.)	ET
fromstring
ParseErrorr   findr*   r0   upper
startswith)xml_bodyrootrt   	result_elrR   s        r   _parse_auth_responser      s    }}X& 		45I!52 < < >!I
 	
 NN  "E{{}(!8
 	
 L! == !7
	s   B B=,B88B=c                F   	 t        j                  |       }|j	                  d      }||j
                  xs dj                         nd}g }|j                  d      D ]o  }|j	                  d      }|j	                  d      }|j                  |t        |j
                        nd||j
                  xs dj                         nd	d
       q |j	                  d      }	|j	                  d      }
|	!|	j
                  rt        |	j
                        nd}|
!|
j
                  rt        |
j
                        n
t        |      }|j                         j                  d       xr t        |      dk(  }|||||dS # t         j                  $ r}t        d      |d}~ww xY w)az  
    Parse a SendGuests SOAP response into a structured result dict.

    Returns:
        {
            "success": bool,
            "accepted": int,
            "rejected": int,
            "rejected_details": [{"index": int, "reason": str}, ...],
            "raw_message": str,
        }

    Raises:
        AlloggiatiResponseError: If the response XML is unparseable.
    z%Malformed XML in SendGuests response.Nz.//{*}SendGuestsResultr!   z.//{*}GuestErrorz{*}Indexz
{*}Messageunknown)indexrm   z.//{*}AcceptedCountz.//{*}RejectedCountr   rw   successacceptedrejectedrejected_detailsraw_message)rx   ry   rz   r   r{   r*   r0   findallappendintre   r|   r}   )r~   r   rt   r   r   r   err_elidx_elmsg_elaccepted_elrejected_elr   r   r   s                 r   _parse_send_responser      s    }}X& 		23I4=4I9>>'R..0rK-/,,12Z(\*-3-?V[[)R9?9K6;;,"335QZ	
 3 ))12K))12K(3(?KDTDTs;##$Z[H "{'7'7 	K!"  **733 	' !Q&  ," C == %3
	s   E< <F FF c                   t        j                  t         j                        }d|_        t         j                  |_        t        j                  d      \  }}t        j                  d      \  }}	 t        j                  |d      5 }|j                  |        ddd       t        j                  |d      5 }|j                  |       ddd       	 |j                  ||       	 ||fD ]  }		 t        j                  |	        |S # 1 sw Y   oxY w# 1 sw Y   KxY w# t         j                  $ r}t        d      |d}~ww xY w# t        $ r Y gw xY w# ||fD ]'  }		 t        j                  |	       # t        $ r Y %w xY w w xY w)u  
    Build an SSL context for mTLS using in-memory PEM strings.

    PEM data is written to a tempfile only for context creation, then
    immediately deleted — even on exception.

    Raises:
        AlloggiatiAuthError: If the certificate/key material is invalid.
    Tz.pem)suffixwN)certfilekeyfilez;Invalid certificate or private key for Alloggiati Web mTLS.)ssl
SSLContextPROTOCOL_TLS_CLIENTcheck_hostnameCERT_REQUIREDverify_modetempfilemkstemposfdopenwriteload_cert_chainSSLErrorr   unlinkOSError)
certificate_pemprivate_key_pemctxcert_fd	cert_pathkey_fdkey_pathfrt   paths
             r   _build_mtls_contextr   4  s]    ..00
1CC''CO!))8GY''v6FHYYw$GGO$ %YYvs#qGGO$ $	HE )D		$ * J# %$## || 	%M	   )D		$  *s   3E 	D	E 9DE D! 0E	DE DE !E4E  EE 	EEF E65F6	F	?FF	Fc                      e Zd ZdZddded	 	 	 	 	 	 	 	 	 	 	 ddZeed	 	 	 	 	 	 	 dd       Zeed	 	 	 	 	 	 	 dd       Zdd	Z	dd
Z
y)AlloggiatiClienta  
    Stateless client for the Alloggiati Web SOAP API.

    Instantiate with decrypted credential values (plain strings).
    The client does not interact with the database.

    Usage (CODES mode):
        client = AlloggiatiClient.from_codes(username="...", password="...")
        result = client.send_guests(guests=[...])

    Usage (DIGITAL_CERTIFICATE mode):
        client = AlloggiatiClient.from_certificate(
            certificate_pem="...", private_key_pem="..."
        )
        result = client.send_guests(guests=[...])
    r!   N)r4   r5   rV   rW   c               J    || _         || _        || _        || _        || _        y rM   )_mode	_username	_password_ssl_context_timeout)selfmoder4   r5   rV   rW   s         r   __init__zAlloggiatiClient.__init__o  s(     
!!'r   )rW   c                    | d|||      S )z=Create a client for CODES (username/password) authentication.CODES)r   r4   r5   rW   r   )clsr4   r5   rW   s       r   
from_codeszAlloggiatiClient.from_codes~  s     (XwWWr   c               0    t        ||      } | d||      S )z>Create a client for DIGITAL_CERTIFICATE (mTLS) authentication.DIGITAL_CERTIFICATE)r   rV   rW   )r   )r   r   r   rW   rV   s        r   from_certificatez!AlloggiatiClient.from_certificate  s!     */?K-;PWXXr   c                    | j                   dk(  ryt        t        | j                  | j                              }t        |t        | j                        }t        |      S )a	  
        Obtain a session token from Alloggiati Web.

        For CODES mode: performs a SOAP Authenticate call.
        For DIGITAL_CERTIFICATE mode: the mTLS handshake acts as auth;
        returns a synthetic token placeholder (the portal may return one
        automatically on the first SendGuests call).

        Returns:
            Session token string.

        Raises:
            AlloggiatiAuthError: On credential rejection or missing token.
            AlloggiatiNetworkError: On connection failure.
        r   __MTLS_SESSION__)ro   rp   rW   )	r   r2   r6   r   r   ru   _SOAP_ACTION_AUTHr   r   )r   ro   r~   s      r   authenticatezAlloggiatiClient.authenticate  sV      ::.. & ""24>>4>>"RS)MM

 $H--r   c                    |sdddg ddS | j                         }| j                  dk(  r| j                  nd}t        t	        ||            }t        |t        || j                        }t        |      S )u  
        Authenticate and send a list of guest payloads to Alloggiati Web.

        Args:
            guests: List of AlloggiatiPayload dicts (from transformer).

        Returns:
            Parsed response dict:
            {
                "success": bool,
                "accepted": int,
                "rejected": int,
                "rejected_details": [...],
                "raw_message": str,
            }

        Raises:
            AlloggiatiAuthError: If authentication fails — caller must stop sync.
            AlloggiatiNetworkError: On timeout or connection failure.
            AlloggiatiResponseError: On malformed server response.
        Tr   zNo guests to send.r   r   N)ro   rp   rV   rW   )	r   r   r   r2   rU   ru   _SOAP_ACTION_SENDr   r   )r   rS   rR   ssl_ctxro   r~   s         r   send_guestszAlloggiatiClient.send_guests  s    , $&3  !!#'+zz5J'J$##PT!"25&"AB)MM	
 $H--r   )r   r'   r4   r'   r5   r'   rV   Optional[ssl.SSLContext]rW   r   returnNone)r4   r'   r5   r'   rW   r   r   'AlloggiatiClient')r   r'   r   r'   rW   r   r   r   )r   r'   )rS   List[Dict[str, Any]]r   Dict[str, Any])r   r   r   r   _DEFAULT_TIMEOUTr   classmethodr   r   r   r   r   r   r   r   r   ]  s    * 04'    	 
   .    
   (X X 	X
 X 
X X  (	Y 	Y 		Y
 	Y 
	Y 	Y.F).r   r   )r)   r   r   r'   )r1   r'   r   r'   )r4   r'   r5   r'   r   r'   )rH   r   r   r'   )rR   r'   rS   r   r   r'   )
ro   r'   rp   r'   rV   r   rW   r   r   r'   )r~   r'   r   r'   )r~   r'   r   r   )r   r'   r   r'   r   zssl.SSLContext)(r   
__future__r   r   r   r   r-   urllib.requestr`   urllib.errortypingr   r   r   r   	xml.etreer   rx   rc   r   r   r/   r   __annotations__r   	Exceptionr   r   r   r   r+   r2   r6   rJ   rU   ru   r   r   r   r   r   r   r   <module>r      s  2 # 	 
     , , '& 
 M  K  H  & N   6I 6@/ @32 3P3 P
 <$ -1#'' ' *	'
 ' 	'\89@"RH. H.r   