
    bMj                        U d 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 ddl	Z	ddl
Z
 ej                  d      Z ee      j                         j                  dz  ZdefdZdefdZej*                  j-                  d	d
      Zej*                  j-                  dd
      Zej*                  j-                  dd
      Zerde nd
Zej*                  j-                  dd
      Zej*                  j-                  dd      Zej*                  j-                  dd      ZdZdgZdZ dZ!dZ"dZ#i Z$eee%e&ef   f   e'd<   defdZ(dpdededz  dedz  fdZ)dedz  fdZ*dedefdZ+dede,e   dz  fd Z-ded!ed"ede.dz  fd#Z/ded$ed"ede.dz  fd%Z0ded$ed&ede,e   dz  fd'Z1	 	 dqded$ed&ed)e2d*edz  d+edz  defd,Z3	 drded$ed"ed-e.d.ededz  fd/Z4ded$ed0ede,e   dz  fd1Z5ded$ede,e   dz  fd2Z6ded$ed0ededz  fd3Z7ded$ed0ede.dz  fd4Z8ded$ed&ed5ededz  f
d6Z9dd(ddd7ded&ededz  d)e2d*edz  d+edz  defd8Z:dd9ded&ed5ededz  dedz  f
d:Z;dd9ded"ededz  de.dz  fd;Z<dd9ded0ededz  de.dz  fd<Z=	 drdd9ded"ed-e.d.ededz  dedz  fd=Z>ddddd>ded?ed@edAe,e   dBedz  dCe,e   dz  dDe,e   dz  dEe,e   dz  dedz  fdFZ?dddGded?ed@edAe,e   dBedz  dDe,e   dz  de@fdHZAddIdJdedBedz  d)e2de,e   dz  fdKZBddLdedMedNedBedz  de%e.eef   dz  f
dOZCddLdedMedBedz  de@fdPZDdQZEdQZFdedRedSe.ddTfdUZGdddVdedWe.dXedz  dYedz  de.dz  f
dZZHded!ed0ede@fd[ZIdsd\ed-e.d.edefd]ZJdtd_edz  d`ede@fdaZKeLdbk(  rddlMZMddlNZN eMj                  dcd      ZPePj                  dedfdgh       ePj                  didjk       ePj                  dld^dmn       ePj                         ZSeSj                  r7 eNj                   eKeSj                  eSj                        rd       ydo       yePj                          yy)uu8  
winston_graph.py — App-only Microsoft Graph client for Winston (Email Bot).

Provides file-read (OneDrive/Files.ReadWrite.All) and mail-draft-create
(Mail.ReadWrite scoped to sales@icastinc.com) operations using the same
MSAL client-credentials pattern as notifications.py.

Required env vars (same as Togen's existing Graph setup):
    AZURE_CLIENT_ID        App registration client/application ID
    AZURE_CLIENT_SECRET    App registration client secret
    AZURE_TENANT_ID        Azure AD tenant ID

Winston-specific env vars:
    WINSTON_ONEDRIVE_USER  UPN or object ID of the user whose OneDrive
                           holds the Takeoffs/Completed Quotes folders
                           (e.g. "administrator@icastinc.com")
    WINSTON_MAIL_FROM      Mailbox to create drafts in (default: sales@icastinc.com)
    N)Pathztogen.winston_graphzwinston_settings.jsonreturnc                      t         j                         r=	 t        j                  t         j	                               } t        | t              r| S 	 i S i S # t        j                  t        f$ r Y i S w xY w)zLoad winston_settings.json (best-effort). Saved values take precedence
    over env vars for Azure/OneDrive config so admins can update them in the UI
    without editing the systemd service file (DVI-829).)	_SETTINGS_FILEis_filejsonloads	read_text
isinstancedictJSONDecodeErrorOSError)datas    $/var/www/html/togen/winston_graph.py_winston_settingsr   %   so     	::n6689D$% & I2I $$g. 	I	s   8A A10A1c                  F    t               j                  dd      xs t        S )zGEffective OneDrive user UPN: settings file preferred, env var fallback.onedrive_user )r   getWINSTON_ONEDRIVE_USER     r   get_onedrive_userr   3   s    ""?B7P;PPr   AZURE_CLIENT_IDr   AZURE_CLIENT_SECRETAZURE_TENANT_ID"https://login.microsoftonline.com/r   WINSTON_MAIL_FROMzsales@icastinc.comWINSTON_SERVICE_USERztogen@icastinc.comzwinston-pipeline-scratchz$https://graph.microsoft.com/.defaultz https://graph.microsoft.com/v1.0z7icastinc.sharepoint.com:/sites/InfrastructurePrecastIncz[sharepoint-site]g      @_site_drive_cachec                  F    t               j                  dd      xs t        S )zAEffective SharePoint site (settings preferred, built-in default).sharepoint_siter   )r   r   _DEFAULT_SP_SITEr   r   r   get_sharepoint_siter$   V   s    ""#4b9M=MMr   tokensitec                    |xs
 t               }t        j                         }t        j                  |      }|r||d   z
  t        k  r|d   S 	 t        j                  t         d| t        |       d      }|j                  dk7  r0t        j                  d|j                  ||j                  dd	        y|j                         j                  d
      }|st        j                  d|       yt        j                  t         d| dt        |       d      }|j                  dk7  r/t        j                  d|j                  |j                  dd	        y|j                         j                  d
      }|st        j                  d|       y||ft        |<   |S # t        $ r t        j                  d       Y yw xY w)a-  Resolve the default document-library driveId for a SharePoint site.

    ``site`` is the "{host}:/sites/{name}" server-relative form; defaults to the
    InfrastructurePrecastInc site (DVI-896/DVI-947). Cached for ``_SITE_DRIVE_TTL``
    seconds to avoid a two-call resolve on every pipeline run.
    r      z/sites/   headerstimeout   z9resolve_site_drive_id: site lookup failed (%s) for %s: %sN,  idz-resolve_site_drive_id: site %s returned no idz/drivez3resolve_site_drive_id: drive lookup failed (%s): %sz6resolve_site_drive_id: site %s default drive has no idzresolve_site_drive_id error)r$   timer    r   _SITE_DRIVE_TTLrequests_GRAPH_BASE_auth_headerstatus_codelogerrortextr   	Exception	exception)r%   r&   nowcachedrsite_iddrdrive_ids           r   resolve_site_drive_idrA   [   s    (&(D
))+C""4(F3?o5ayLLK=v6U@S]_`==CIIQmmT166$3<9&&(,,t$IIEtL\\[M	@".u"5rC>>S IIKnnbggdsm5779==&IINPTU#&/$ 34s,   A(F 87F 0A(F 7F F F?>F?c                     t               } | j                  dd      xs t        }| j                  dd      xs t        }| j                  dd      xs t        }|rd| nd}|r|r|st
        j                  d       y	 t        j                  |||      }|j                  t        	      }|j                  d
      }|s&t
        j                  d|j                  d|             |S # t        $ r t
        j                  d       Y yw xY w)zAcquire an app-only Graph token via client credentials. Returns token or None.

    Saved settings take precedence over env vars (DVI-829).azure_client_idr   azure_client_secretazure_tenant_idr   zHGraph token unavailable: AZURE_CLIENT_ID/SECRET/TENANT_ID not configuredN)	authorityclient_credential)scopesaccess_tokenzGraph token request failed: %serror_descriptionzGraph token acquisition error)r   r   _AZ_CLIENT_ID
_AZ_SECRET
_AZ_TENANTr6   warningmsalConfidentialClientApplicationacquire_token_for_client_GRAPH_SCOPESr7   r9   r:   )s	client_idsecrettenantrF   clientresultr%   s           r   acquire_graph_tokenrY      s     	A',=I+R0>JF',:
FAG4VH=RI&Y^_
33fF000F

>*II6

CVX^8_` 56s   ?A(C( (D	D	c                     d|  ddS )NBearer application/jsonAuthorizationzContent-Typer   )r%   s    r   r4   r4      s    &ug.@RSSr   c                 X   	 t        j                  t         dt        |       d      }|j                  dk7  r/t
        j                  d|j                  |j                  dd        y|j                         j                  dg       S # t        $ r t
        j                  d	       Y yw xY w)
zBList all drives visible to the app (useful for initial discovery).z/drivesr)   r*   r-   zlist_drives failed (%s): %sNr.   valuezlist_drives error)r2   r   r3   r4   r5   r6   r7   r8   r   r9   r:   )r%   resps     r   list_drivesrb      s    ||{m73\%=PZ\]s"II3T5E5EtyyQURUWyy{w++ )*s   A%B (B B)(B)r@   
drive_pathc                 :   t          d| d| d}	 t        j                  |dd|  id      }|j                  dk7  r0t        j                  d	|j                  ||j                  d
d        y
|j                  S # t        $ r t        j                  d       Y y
w xY w)zDownload raw bytes of a file at drive_path inside the specified drive.

    drive_path is relative to root, e.g. "Takeoffs/2026-06-29_tasks.json".
    Returns file bytes or None on error.
    /drives//root:/	:/contentr^   r[   <   r*   r-   z&drive_read_file failed (%s) for %s: %sNr.   zdrive_read_file error
r3   r2   r   r5   r6   r7   r8   contentr9   r:   )r%   r@   rc   urlra   s        r   drive_read_filerl      s     M(7:,i
HC	||C/WUG;L)MWYZs"II>&&
DIIdsOE|| -.s   AA9 -A9 9BBuser_upnc           	      <   t          d| d| d}	 t        j                  |dd|  id      }|j                  dk7  r1t        j                  d	|j                  |||j                  d
d        y
|j                  S # t        $ r t        j                  d       Y y
w xY w)zDownload a file from a user's OneDrive by UPN.

    Requires Files.ReadWrite.All (application). drive_path is relative to root.
    /users//drive/root:/rg   r^   r[   rh   r*   r-   z.user_drive_read_file failed (%s) for %s/%s: %sNr.   zuser_drive_read_file errorri   )r%   rm   rc   rk   ra   s        r   user_drive_read_filerq      s    
 M
-
|9
MC	||C/WUG;L)MWYZs"IIF&&*diioO|| 23s   AA: .A: :BBfolder_pathc           	      l   t          d| d| d}	 t        j                  |t        |       d      }|j                  dk7  r1t
        j                  d|j                  |||j                  dd	        y|j                         j                  d
g       S # t        $ r t
        j                  d       Y yw xY w)zGList folder contents from a user's OneDrive. Returns item list or None.ro   rp   
:/childrenr)   r*   r-   z0user_drive_list_folder failed (%s) for %s/%s: %sNr.   r`   zuser_drive_list_folder errorr3   r2   r   r4   r5   r6   r7   r8   r   r9   r:   )r%   rm   rr   rk   ra   s        r   user_drive_list_folderrv      s    M
-}J
OC	||Ce)<bIs"IIH&&+tyy#Pyy{w++ 45   A B 2B B32B3r-   toporder_by	next_linkc           	         |r"|j                  t              sddddddS |d}}nt         d| d| d}|d	d
}|r||d<   	 t        j                  |t	        |       |d      }|j                  dk7  rq|r |s|j                  dk(  rt        | |||      S t        j                  d|j                  |||j                  dd        |j                  ddd|j                  dd dS |j                         }
|
j                  d      }d|
j                  dg       t        |      |ddS # t
        $ r0}	t        j                  d       ddddt        |	      dcY d}	~	S d}	~	ww xY w)a  List a OneDrive folder returning diagnostic detail (DVI-943).

    Unlike ``user_drive_list_folder`` (which collapses every failure to None),
    this surfaces the HTTP status and Graph error text so the Files views can
    show *why* the Logic pipeline's account can or cannot reach a path.

    ``order_by`` is a Graph ``$orderby`` clause (e.g. ``lastModifiedDateTime desc``)
    applied server-side so the caller gets a globally sorted result rather than a
    single unsorted page. ``next_link`` is the opaque ``@odata.nextLink`` from a
    prior call, used to fetch the next page for "Show more" (DVI-978).

    Returns ``{status, items, truncated, next_link, error}`` where ``items`` is a
    list on success or None on failure.
    r   NFinvalid paging cursorstatusitems	truncatedrz   r7   ro   rp   rt   Qid,name,size,folder,file,lastModifiedDateTime,webUrl,@microsoft.graph.downloadUrl$top$select$orderbyr)   r+   paramsr,   z#user_drive_list_folder_detail errorr-     )rx   z7user_drive_list_folder_detail failed (%s) for %s/%s: %sr.     @odata.nextLinkr`   )
startswithr3   r2   r   r4   r9   r6   r:   strr5   user_drive_list_folder_detailr7   r8   r   bool)r%   rm   rr   rx   ry   rz   rk   r   ra   excr   nls               r   r   r      s   $ ##K0$U!%0GI IVWXJmK=
Sj
 !)F:f||Ce)<VUWX 3 I$*:*:c*A0+SVWW		K""Hk499Tc?	L**T!DIIdsO= 	=99;D	#	$B'2&"X   f;<dT\_`c\deefs   "D% %	E.%EEErj   content_typec           	      F   t          d| d| d}	 t        j                  |d|  |d|d      }|j                  dvr1t        j                  d	|j                  |||j                  d
d        y
|j                         S # t        $ r t        j                  d       Y y
w xY w)zPUpload/overwrite a file in a user's OneDrive. Returns the item metadata or None.ro   rp   rg   r[   r]   x   r+   r   r,   r-      z0user_drive_upload_file failed (%s) for %s/%s: %sNr.   zuser_drive_upload_file error)
r3   r2   putr5   r6   r7   r8   r   r9   r:   )r%   rm   rc   rj   r   rk   ra   s          r   user_drive_upload_filer     s     M
-
|9
MC||(/w&7V	
 :-IIH&&*diioOyy{ 45s   AA? /A? ?B B item_idc           	      l   t          d| d| d}	 t        j                  |t        |       d      }|j                  dk7  r1t
        j                  d|j                  |||j                  dd	        y|j                         j                  d
g       S # t        $ r t
        j                  d       Y yw xY w)zCList children of a drive folder by item ID (for folder navigation).ro   /drive/items/z	/childrenr)   r*   r-   z7user_drive_list_children failed (%s) for %s item %s: %sNr.   r`   zuser_drive_list_children errorru   r%   rm   r   rk   ra   s        r   user_drive_list_childrenr   4  s    M
-y	
JC	||Ce)<bIs"IIO&&'499Tc?Lyy{w++ 67rw   c                 d   t          d| d}	 t        j                  |t        |       d      }|j                  dk7  r0t
        j                  d|j                  ||j                  dd        y|j                         j                  d	g       S # t        $ r t
        j                  d
       Y yw xY w)z#List the root of a user's OneDrive.ro   z/drive/root/childrenr)   r*   r-   z+user_drive_list_root failed (%s) for %s: %sNr.   r`   zuser_drive_list_root errorru   )r%   rm   rk   ra   s       r   user_drive_list_rootr   C  s    M
*>
?C	||Ce)<bIs"IIC&&$))DS/Cyy{w++ 23s   AB .B B/.B/c           	      J   t          d| d| }	 t        j                  |t        |       d      }|j                  dk7  r1t
        j                  d|j                  |||j                  dd        y|j                         S # t        $ r t
        j                  d	       Y yw xY w)
zSGet metadata for a single drive item by ID (includes @microsoft.graph.downloadUrl).ro   r   r)   r*   r-   z2user_drive_get_item failed (%s) for %s item %s: %sNr.   zuser_drive_get_item errorru   r   s        r   user_drive_get_itemr   R  s    M
-y
AC	||Ce)<bIs"IIJ&&'499Tc?Lyy{ 12s   A B 1B B"!B"c           	      D   t          d| d| d}	 t        j                  |t        |       d      }|j                  dk7  r1t
        j                  d|j                  |||j                  dd	        y|j                  S # t        $ r t
        j                  d
       Y yw xY w)a  Fetch a drive item's raw content by ID from a user's OneDrive (DVI-975).

    Used by the Winston Files preview proxy so file previews are served
    same-origin (mirrors the File Browser tool's behaviour). Returns the file
    bytes or None on any failure.
    ro   r   /contentrh   r*   r-   z3user_drive_read_item failed (%s) for %s item %s: %sNr.   zuser_drive_read_item error)r3   r2   r   r4   r5   r6   r7   r8   rj   r9   r:   r   s        r   user_drive_read_itemr   a  s     M
-y
IC	||Ce)<bIs"IIK&&'499Tc?L|| 23s   A A> 2A> >BBquote_numberc                    t          d| d| d}ddd}d}|r	 t        j                  |t        |       d|vr|nd	d
      }d	}|j                  dk7  r/t
        j                  d|j                  |j                  d	d        y	|j                         }|j                  dg       D ]X  }	|	j                  dd      }
|dz  }||
v s|
j                         j                  d      s?t
        j                  d|
|       |	c S  |j                  d      }|rt
        j                  d|||       y	# t        $ r t
        j                  d       Y y	w xY w)zFind the xlsm workbook whose filename contains quote_number in a OneDrive folder.

    Scans up to ~4,700 .xlsm files. Returns the first matching item dict or None.
    Supports paged results ($top=500) to handle large folders efficiently.
    ro   rp   rt   r   Uid,name,size,lastModifiedDateTime,webUrl,parentReference,@microsoft.graph.downloadUrlr   r   ?Nrh   r   r-   z0find_quote_workbook folder fetch failed (%s): %sr.   r`   namer   r(   .xlsmz5find_quote_workbook: found %s after scanning %d itemsr   zfind_quote_workbook errorz@find_quote_workbook: quote %s not found in %s (scanned %d items))r3   r2   r   r4   r5   r6   r7   r8   r   lowerendswithinfor9   r:   rN   )r%   rm   rr   r   rk   r   seenra   r   itemr   s              r   find_quote_workbookr   u  sZ    M
-}J
OC&}~FD
	<<\%-@SV^aSagkuwxDF3&		LdN^N^`d`i`ijnkn`op99;D"-  xx+	4'DJJL,A,A',JHHTVZ\`aK  ((,-C $ KKRT`bmost	  	MM56	s+   A'D, A D, D, #D, >D, ,EE)r&   rx   ry   rz   c                   |r"|j                  t              sddddddS |d}}nAt        | |      }|sddddd|xs
 t                ddS t         d| d	| d
}|dd}|r||d<   	 t	        j
                  |t        |       |d      }	|	j                  dk7  rp|r |s|	j                  dk(  rt        | |||      S t        j                  d|	j                  ||	j                  dd        |	j                  ddd|	j                  dd dS |	j                         }|j                  d      }d|j                  dg       t!        |      |ddS # t        $ r0}
t        j                  d       ddddt        |
      dcY d}
~
S d}
~
ww xY w)u  Diagnostic folder listing on the SharePoint site drive (mirror of
    ``user_drive_list_folder_detail`` for the shared Files views — DVI-947).

    Supports ``order_by`` ($orderby) + ``next_link`` (paging cursor) for the
    sort/Show-more Files controls (DVI-978).r   NFr|   r}   z)could not resolve SharePoint site drive ()re   rf   rt   r   r   r   r)   r   z#site_drive_list_folder_detail errorr-   r   )r&   rx   z4site_drive_list_folder_detail failed (%s) for %s: %sr.   r   r   r`   )r   r3   rA   r$   r2   r   r4   r9   r6   r:   r   r5   site_drive_list_folder_detailr7   r8   r   r   )r%   rr   r&   rx   ry   rz   rk   r   r@   ra   r   r   r   s                r   r   r     s    ##K0$U!%0GI IV(5$UQUHIfQdQfHgghik kXhZw{m:Nj
 !)F:f||Ce)<VUWX 3I$*:*:c*A0$TWXX		H""K4C	B**T!DIIdsO= 	=99;D	#	$B'2&"X   f;<dT\_`c\deefs   '"E
 
	F%E>8F>F)r&   c                   t        | |      }|st        j                  d       yt         d| d| d}ddd}d	}|r	 t	        j
                  |t        |       d
|vr|ndd      }d}|j                  dk7  r/t        j                  d|j                  |j                  dd        y|j                         }	|	j                  dg       D ]X  }
|
j                  dd      }|dz  }||v s|j                         j                  d      s?t        j                  d||       |
c S  |	j                  d      }|rt        j                  d|||       y# t        $ r t        j                  d       Y yw xY w)z;Site-drive equivalent of ``find_quote_workbook`` (DVI-947).z6site_find_quote_workbook: could not resolve site driveNre   rf   rt   r   r   r   r   r   rh   r   r-   z5site_find_quote_workbook folder fetch failed (%s): %sr.   r`   r   r   r(   r   z:site_find_quote_workbook: found %s after scanning %d itemsr   zsite_find_quote_workbook errorzEsite_find_quote_workbook: quote %s not found in %s (scanned %d items))rA   r6   r7   r3   r2   r   r4   r5   r8   r   r   r   r   r9   r:   rN   )r%   rr   r   r&   r@   rk   r   r   ra   r   r   r   s               r   site_find_quote_workbookr     sv    %UD1H		JKM(7;-z
JC&}~FD
	<<\%-@14CvTSUWDF3&		Q**DIIdsO=99;D"-  xx+	4'DJJL,A,A',JHHY[_aefK  ((,-C! ( KKWk41  	MM:;	s+   A'E &A E 'E E "E E10E1c                :    t        | |      }|syt        | ||      S )z=Read a file from the SharePoint site drive by path (DVI-947).N)rA   rl   )r%   rc   r&   r@   s       r   site_drive_read_filer     s#    $UD1H5(J77r   c                `   t        | |      }|syt         d| d| d}	 t        j                  |t	        |       d      }|j
                  dk7  r0t        j                  d|j
                  ||j                  dd	        y|j                  S # t        $ r t        j                  d
       Y yw xY w)u   Fetch a drive item's raw content by ID from the SharePoint site drive
    (site-drive equivalent of ``user_drive_read_item`` — DVI-975). Returns the
    file bytes or None on any failure.Nre   /items/r   rh   r*   r-   z0site_drive_read_item failed (%s) for item %s: %sr.   zsite_drive_read_item error)rA   r3   r2   r   r4   r5   r6   r7   r8   rj   r9   r:   )r%   r   r&   r@   rk   ra   s         r   site_drive_read_itemr     s     %UD1HM(77)8
DC	||Ce)<bIs"IIH&&4CB|| 23s   AB  B B-,B-c                b   t        | |      }|syt         d| d| d}	 t        j                  |d|  |d|d      }|j                  d	vr0t
        j                  d
|j                  ||j                  dd        y|j                         S # t        $ r t
        j                  d       Y yw xY w)zUpload/overwrite a file on the SharePoint site drive (DVI-947).

    Requires Sites.ReadWrite.All (confirmed granted). Returns item metadata or None.
    Nre   rf   rg   r[   r]   r   r   r   z-site_drive_upload_file failed (%s) for %s: %sr.   zsite_drive_upload_file error)rA   r3   r2   r   r5   r6   r7   r8   r   r9   r:   )r%   rc   rj   r   r&   r@   rk   ra   s           r   site_drive_upload_filer     s     %UD1HM(7:,i
HC||(/w&7V	
 :-IIE&&
DIIdsOEyy{ 45s   AB =B B.-B.)mailboxattachmentscc_addressesbcc_addressessubject	body_htmlto_addressesr   r   r   r   c          	         |xs t         }|st        j                  d       y|d|d|D cg c]  }dd|ii
 c}d}	|r|D cg c]  }dd|ii
 c}|	d<   |r|D cg c]  }dd|ii
 c}|	d	<   |r.|D 
cg c]  }
d
|
d   |
j                  dd      |
d   d! c}
|	d<   t         d| d}	 t        j                  |t        |       |	d      }|j                  dvr0t        j                  d|j                  ||j                  dd        y|j                         S c c}w c c}w c c}w c c}
w # t        $ r t        j                  d       Y yw xY w)aQ  Create a draft message in the Winston mailbox.

    mailbox defaults to WINSTON_MAIL_FROM env var (sales@icastinc.com).
    attachments is a list of {"name", "contentType", "contentBytes"} dicts
    where contentBytes is already base64-encoded (Graph fileAttachment shape).
    Returns the created message metadata or None on error.
    zDmail_create_draft: no mailbox configured (WINSTON_MAIL_FROM not set)NHTMLcontentTyperj   emailAddressaddressr   bodytoRecipientsccRecipientsbccRecipientsz#microsoft.graph.fileAttachmentr   r   application/octet-streamcontentBytes)@odata.typer   r   r   r   ro   z	/messagesrh   r+   r   r,   r   z0mail_create_draft failed (%s) for mailbox %s: %sr.   zmail_create_draft error)r   r6   r7   r   r3   r2   postr4   r5   r8   r   r9   r:   )r%   r   r   r   r   r   r   r   amessageattrk   ra   s                r   mail_create_draftr   -  s   $ **G		XY  &9=COPa.9a.9PG
 MY"ZNYN#C"ZN[#\^i^$D#\  #"
   AF"ww}6PQ #N 3	"
 M	
3C	}}S,u*=GUWX:-IIH&&4CByy{1 Q #[#\"
$  /0s0   DD"D'6$D,-AD1 D1 1EE)r   r   c                   |xs t         }|st        j                  d       y|xs g D cg c]  }|s|	 }}|xs g D cg c]  }|s|	 }}|s|st        j                  d       y|d|d|D cg c]  }dd|ii
 c}d}|r|D cg c]  }dd|ii
 c}|d	<   t         d
| d}	 t        j                  |t        |       |ddd      }	|	j                  dvr0t        j                  d|	j                  ||	j                  dd        yyc c}w c c}w c c}w c c}w # t        $ r t        j                  d       Y yw xY w)aj  Send an HTML message via Graph sendMail from the Winston mailbox.

    Unlike ``mail_create_draft`` this delivers immediately (used for the
    per-run summary report, DVI-976). mailbox defaults to WINSTON_MAIL_FROM
    (sales@icastinc.com, the mailbox the Application Access Policy scopes the
    app to). Returns True on a 200/202 accept, False otherwise.
    z<mail_send: no mailbox configured (WINSTON_MAIL_FROM not set)Fu,   mail_send: no recipients — nothing to sendr   r   r   r   r   r   ro   z	/sendMailfalse)r   saveToSentItemsrh   r   )r-      z(mail_send failed (%s) for mailbox %s: %sNr.   Tzmail_send error)r   r6   r7   r   r3   r2   r   r4   r5   r8   r9   r:   )
r%   r   r   r   r   r   r   r   rk   ra   s
             r   	mail_sendr   e  sf     **G		PQ , 29!qA9L9 , 29!qA9L9?@  &9=COPa.9a.9PG
 MY"ZNYN#C"ZM	
3C}} '$A	
 :-II@&&4CB5 :9 Q #[  '(s5   DDD	D2D
D*A"D" "EE2   )r   rx   c          	         |xs t         }|st        j                  d       yt         d| d}t	        dt        t        |xs d      d            dd	d
d}	 t        j                  |t        |       |d      }|j                  dk7  r0t        j                  d|j                  ||j                  dd        y|j                         j                  dg       S # t        $ r t        j                  d       Y yw xY w)ul  List messages in the Drafts folder of the Winston mailbox.

    Returns a list of message dicts (subject, toRecipients, bodyPreview,
    lastModifiedDateTime, hasAttachments) with attachment metadata expanded
    (id/name/contentType/size only — NOT contentBytes, to keep the payload
    small). mailbox defaults to WINSTON_MAIL_FROM. Returns None on error.
    zCmail_list_drafts: no mailbox configured (WINSTON_MAIL_FROM not set)Nro   z/mailFolders/drafts/messagesr(   r   r-   zlastModifiedDateTime desczWid,subject,bodyPreview,toRecipients,lastModifiedDateTime,createdDateTime,hasAttachmentsz6attachments($select=id,name,contentType,size,isInline))r   r   r   z$expandrh   r   z/mail_list_drafts failed (%s) for mailbox %s: %sr.   r`   zmail_list_drafts error)r   r6   r7   r3   maxminintr2   r   r4   r5   r8   r   r9   r:   )r%   r   rx   rk   r   ra   s         r   mail_list_draftsr     s     **G		WXM	)E
FCAs3syb>3/0/lK	F	||Ce)<VUWXs"IIG&&4CByy{w++ ./s   A C 9C C:9C:)r   
message_idattachment_idc          	      x   |xs t         }|st        j                  d       yt         d| d| d| }	 t	        j
                  |t        |       d      }|j                  dk7  r1t        j                  d	|j                  |||j                  dd
        y|j                         }|j                  d      }|s't        j                  d|j                  d      |       yt        j                  |      |j                  dd      |j                  dd      fS # t        $ r t        j                  d       Y yw xY w)zDownload a single mail attachment's raw bytes.

    Returns (content_bytes, content_type, name) or None on error.
    mailbox defaults to WINSTON_MAIL_FROM.
    z*mail_get_attachment: no mailbox configuredNro   
/messages/z/attachments/rh   r*   r-   z5mail_get_attachment failed (%s) for msg %s att %s: %sr.   r   z9mail_get_attachment: no contentBytes (type=%s) for att %sr   r   r   r   
attachmentzmail_get_attachment error)r   r6   r7   r3   r2   r   r4   r5   r8   r   base64	b64decoder9   r:   )r%   r   r   r   rk   ra   r   b64s           r   mail_get_attachmentr     s&    **G		>?M	J<}]O
\C||Ce)<bIs"IIM&&
M499Tc?Tyy{hh~&IIQhh}-}>S!HH]$>?HHV\*
 	

  12s   A D A	D  7D D98D9c                    |xs t         }t         d| d| }	 t        j                  |dd|  id      }|j                  dk(  S # t
        $ r t        j                  d       Y y	w xY w)
z6Delete a draft message by ID. Returns True on success.ro   r   r^   r[   r)   r*      zmail_delete_message errorF)r   r3   r2   deleter5   r9   r6   r:   )r%   r   r   rk   ra   s        r   mail_delete_messager     sw    **GM	J<
@Cs_w>O,PZ\]3&& 12s   +A A'&A'i  @ item_base_urlr   zdict | Nonec           
         	 t        j                  | dd|  dddddiid	      }|j
                  dvr/t        j                  d|j
                  |j                  dd        y|j                         j                  d      }|st        j                  d       yt        |      }d}t        d|t              D ]  }|||t        z    }|t        |      z   dz
  }		 t        j                  |d| d|	 d| t        t        |            d|d      }
|
j
                  dv r|
j                         }w|
j
                  dk(  rt        j                  d|
j
                  |||
j                  dd         y |S # t        $ r t        j	                  d
       Y yw xY w# t        $ r t        j	                  d||       Y  yw xY w)a  Upload bytes to a Graph item path via an upload session (for files >4 MB).

    ``item_base_url`` is the OneDrive/SharePoint path URL ending with ``:`` but
    without the trailing ``/content``, e.g.
    ``https://graph.microsoft.com/v1.0/drives/{id}/root:/{folder}/{file}:``.

    Returns the completed item JSON (containing ``id`` and ``parentReference``)
    on success, or ``None`` on error.
    z/createUploadSessionr[   r\   r]   r   z!@microsoft.graph.conflictBehaviorreplacer)   r   z._upload_via_session: createUploadSession errorNr   z8_upload_via_session: createUploadSession failed (%s): %sr.   	uploadUrlzC_upload_via_session: createUploadSession response missing uploadUrlr   r(   zbytes -/)zContent-RangezContent-Lengthr   r   z._upload_via_session: chunk error at byte %d/%dr   z8_upload_via_session: chunk failed (%s) at byte %d/%d: %sr-   )r2   r   r9   r6   r:   r5   r7   r8   r   r   lenrange_UPLOAD_CHUNK_SIZEr   r   )r%   r   r   	sess_resp
upload_urltotal	item_datastartchunkendput_resps              r   _upload_via_sessionr    s   	MMo12(/w&7I[\>	JK	
	 J.		L'')=	?!%%k2J		WXIE#Iq%!34 U5#556c%j 1$	||'-eWAcU!E7%C&)#e*o H :- I!!S(IIP**E5(--:MO14 U  FG:  	MMJESXY	s#   &E6 7F6FF F>=F>)scratch_filenameservice_user
xlsx_bytesr  r  c                @   |xs" dt        j                         j                   d}t         d| }|rt         d| d| d}n2t        |       }|st        j                  d       yt         d	| d
| d}t        |      dz  }t        |      t        kD  r=t        j                  d||       t        | ||      }	|	t        j                  d|       y	 t        j                  | dd|  dd|d      }
|
j                   dvr0t        j                  d|
j                   ||
j"                  dd        y|
j%                         }	|	j'                  dd      }|	j'                  di       j'                  dd      }|r|st        j                  d|       y	 t        j&                  t         d	| d| ddd|  id d!      }|j                   d#k7  r=t        j                  d$|j                   ||j"                  dd        t)        | ||       y|j*                  }t)        | ||       t        j                  d%|t        |      dz         |S # t        $ r t        j                  d|       Y yw xY w# t        $ r& t        j                  d"|       t)        | ||       Y yw xY w)&u  Upload xlsx bytes to a scratch location, convert to PDF via Graph, delete scratch.

    Follows the B1 recipe proven in DVI-807:
      1. Upload xlsx (simple PUT for ≤4 MB; upload session for larger files)
      2. GET /drives/{drive_id}/items/{item_id}/content?format=pdf  → PDF bytes
      3. DELETE the scratch item

    service_user: when given, use that user's OneDrive for the scratch file
                  (legacy behavior). When omitted (the default), the scratch
                  file goes to the SharePoint site drive (DVI-947) because the
                  pipeline account (togen@) has no personal OneDrive.
    scratch_filename: filename for the temp xlsx (default: auto-generated UUID name)
    Returns PDF bytes on success, None on error.
    winston_scratch_z.xlsxr   ro   rp   :zGxlsx_to_pdf: could not resolve SharePoint site drive for scratch uploadNre   rf   i   u1   xlsx_to_pdf: %s is %d KB → using upload sessionz)xlsx_to_pdf: upload session failed for %sr   r[   zAapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetr]   r   r   z xlsx_to_pdf: upload error for %sr   z*xlsx_to_pdf: upload failed (%s) for %s: %sr.   r/   r   parentReferencedriveIdz9xlsx_to_pdf: upload response missing id or driveId for %sr   z/content?format=pdfr^   T)r+   allow_redirectsr,   z)xlsx_to_pdf: convert request error for %sr-   z+xlsx_to_pdf: convert failed (%s) for %s: %su   xlsx_to_pdf: %s → %d KB PDF)uuiduuid4hex_SCRATCH_FOLDERr3   rA   r6   r7   r   _SIMPLE_UPLOAD_LIMITr   r  r2   r   r9   r:   r5   r8   r   r   _delete_drive_itemrj   )r%   r  r  r  filenamescratch_pathr   scratch_drive_idsize_kbr   up_respr   r@   pdf_resp	pdf_bytess                  r   xlsx_to_pdfr  5  s   *  M%5djjl6F6F5Gu#MH%&az2L &-w|nM,WXY07II_`&-x0@/AVWX*o%G
:--DhPWX'}jI	IIA8L	ll /*'.ug%6[  
G j0II<##Tc"	 LLN	mmD"%G}}.377	2FH(		MxX
<<m8H:WWI=PQ$w&78 	
 s"		9  MM$3		
 	5(G4  Iuh0HH,hI$8NO_  	MM<hG	:  A8L5(G4s$   "I	 +I. 	I+*I+.,JJc                    	 t        j                  t         d| d| dd|  id      }|j                  dv }|s!t        j                  d|j                  |       |S # t        $ r t        j                  d	||       Y y
w xY w)zIDelete a drive item by drive ID + item ID. Returns True on success (204).re   r   r^   r[   r)   r*   )r-   r   z4_delete_drive_item: unexpected status %s for item %sz*_delete_drive_item error: drive=%s item=%sF)r2   r   r3   r5   r6   rN   r9   r:   )r%   r@   r   r=   oks        r   r  r    s    OOm8H:WWI>$w&78

 ]]j(KKFW^ 	 BHgVs   AA  B Br   c                 R    | |t        j                  |      j                  d      dS )zIPackage raw bytes into a Graph fileAttachment dict for mail_create_draft.ascii)r   r   r   )r   	b64encodedecode)r   rj   r   s      r   encode_attachmentr    s-     #((188A r   Takeoffsr   folderc                    t        j                  t         j                  d       d}t        d       t        t        rdt        dd  dnd	       t        d
t
                t        dt         d       t        d       t               }|st        d       yt        d       | xs t        }|st        d       t        d       nt        d| d| d       t        |||      }|t        d| d       d}nat        dt        |       d|        |dd D ]5  }|j                  dd      }t        d|j                  d       d| d       7 t                t        d t         d       t        |d!d"d#g$      }|st        d%       d}nR|j                  d&d      }	t        d'|	dd(  d)       t        d*       t        ||	      rt        d+       nt        d,       |rd-nd.}
t        d/|
 d0       |S )1zRun a two-part verification: (a) list a OneDrive folder, (b) create+delete a draft.

    Returns True only when both steps succeed. Safe to run in production
    because the draft is created and immediately deleted.
    z$%(levelname)s  %(name)s  %(message)s)levelformatTz#=== Winston Graph verification ===
z  AZURE_CLIENT_ID : N   z...z  AZURE_CLIENT_ID : NOT SETz  AZURE_TENANT_ID : z  WINSTON_MAIL_FROM: 
z![1/4] Acquiring app-only token...z  FAIL: could not acquire tokenFz  OK: token acquired
uG   [2/4] SKIP: WINSTON_ONEDRIVE_USER not set — cannot verify file accessz<       Set WINSTON_ONEDRIVE_USER env var to run this check.
z[2/4] Listing 'z' folder for user z  FAIL: could not list u4    — check Files.ReadWrite.All permission + user UPNz  OK: z item(s) in    sizer   z    - r   z (z bytes)z[3/4] Creating test draft in uD   [Winston verify] Graph permission check — please ignore and deletez=<p>Winston Graph verification draft. This can be deleted.</p>zandy@voyagetech.com)r   r   r   u^     FAIL: could not create draft — check Mail.ReadWrite permission + Application Access Policyr/   z  OK: draft created (id:    z...)
z[4/4] Deleting test draft...u,     OK: draft deleted — verification PASSED
uG     WARN: draft created but cleanup delete failed — verify it manually
PASSEDFAILEDz=== Result: z ===)loggingbasicConfigINFOprintrK   rM   r   rY   r   rv   r   r   r   r   )r   r!  r  r%   userr   itr(  draftmsg_idrX   s              r   _verifyr4    s    gllEG	B	
01	= r!2 33
7Nkl	 
-.	!"3!4B
78 

-.!E/0	
"# 11DWXMN 	x'9$sCD&udF;=+F83ghiBF3u:,l6(;<BQi @vvfb)rvvf~.bg>?@ G 
)*;)<C
@AVQ+,	E np4$)&"+f=> 	,-uf-AB\]XF	L
%&Ir   __main__zWinston Graph verification)descriptionz--verify
store_truezRun permission verification)actionhelpz--onedrive-userz!UPN of the OneDrive owner to test)r9  z--folderz+OneDrive folder to list (default: Takeoffs))defaultr9  r(   )N)r-   NN)r   )zapplication/pdf)Nr   )Y__doc__r   r   r,  osr0   r  pathlibr   rO   r2   	getLoggerr6   __file__resolveparentr   r   r   r   r   environr   rK   rL   rM   _AZ_AUTHORITYr   r   r   r  rR   r3   r#   SITE_DRIVE_UPNr1   r    tuplefloat__annotations__r$   rA   rY   r4   listrb   bytesrl   rq   rv   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r   r  r  r  r  r4  __name__argparsesysArgumentParserparseradd_argument
parse_argsargsverifyexitr   r!  
print_helpr   r   r   <module>rU     s  &    	     g-.h'')003JJ4 Q3 Q 

0"5

4b9


0"5
EO4ZLAUW

'>C 

':<PQ 

'=?ST ,782 M  %24 4U5#:../ 4NS N
$ $C$J $#* $TS4Z 2T T T
s 
tDzD0 
3 # 3 54< & s  PT $#  3 4PT:X\K\   =@9=555,/5695Dj5,/$J5 
5r Uo+.9>NQ	D[,C 3  dVZIZ  s tDzD7H s c C D4K  s S UT\ (s c  SV [_bf[f L 9=// /+.:/CF/Dj/,/$J/ 
/f LP!! !03!>ADj!	D[!H MQ 8 8 8sTz 8UZ]aUa 8 JN  c C$J RWZ^R^ , 3JN*/=@4Z 
D[N %)%)&*555 5 s)	5 4Z5 dd"5 s)d"5 9t#5 
D[5| %)111 1 s)	1 4Z1 s)d"1 
1n 	!! 4Z! 
	!
 
$Z$!R $$$ $
 4Z$ 5#s?d"$N OS 	s 	 	t 	W[ 	 ' % ;s ;3 ;e ; ;D $(#fff Dj	f
 *f T\fRc S 3 4 ,C % s [_ B3: Bc B4 BJ z$X$$1MNF

<>[\
)0ST

J=jkD{{gd00$++>FAF r   