Ë
    ÿ�j ,  ã                   óâ   — d Z ddlZddlmZmZmZ ddlZddlmZ ddlm	Z	 ddl
mZ  ej                  e«      ZdZdZd	Zd
efd„Zd
edefd„ZdZefdededefd„Zdededefd„Zddededefd„Zy)at  A tenant's connected Microsoft Teams account: reading it, refreshing its
access token when expired, and everything below this module needs to call
Microsoft Graph.

Same shape as google_calendar.py, and the same connect flow -- written by a
different service into the tenant's own galaxiq_tenants schema, provider
value 'teams'. This module only ever reads what it wrote.
é    N)ÚdatetimeÚ	timedeltaÚtimezone)Úsql)Úsettings)Úget_db_connectionz:https://login.microsoftonline.com/common/oauth2/v2.0/tokenz https://graph.microsoft.com/v1.0ÚteamsÚ	tenant_idc                 óº  — 	 t        «       }	 |j	                  «       5 }|j                  t        j                  d«      j                  t        j                  | «      «      t        df«       |j                  «       }ddd«       |j                  «        sy|\  }}}|�5|t        j                  t        j                   «      k  rt#        | |«      }|sydd|› �i}	 t%        j&                  t(        › d�|d	¬
«      }	|	j+                  «        |	j-                  «       }
t%        j&                  t(        › d�|d	¬
«      }|j+                  «        |j-                  «       }||j'                  d«      xs |j'                  d«      xs d|
j'                  d«      xs ddœS # t        $ r%}t        j                  d| › d|› �«       Y d}~yd}~ww xY w# 1 sw Y   �ŒPxY w# t        $ r5}t        j                  d| › d|› �«       Y d}~|j                  «        yd}~ww xY w# |j                  «        w xY w# t        $ r%}t        j                  d| › d|› �«       Y d}~yd}~ww xY w)a`  This tenant's active Microsoft Teams connection, refreshed if its
    access token has expired.

    Returns {"access_token", "user_email", "timezone"} -- everything
    check_availability/create_event/cancel_event need. None covers every
    reason it cannot be used: no connection, a disconnected one, or a
    refresh failure (revoked consent).
    z(Could not read the Teams connection for ú: NzjSELECT "accessToken", "refreshToken", "expiresAt" FROM {}.integrations WHERE provider = %s AND status = %sÚACTIVEÚAuthorizationúBearer z/me/mailboxSettingsé   ©ÚheadersÚtimeoutz/mez+Could not read Microsoft Graph profile for ÚmailÚuserPrincipalNameÚ ÚtimeZoneÚUTC)Úaccess_tokenÚ
user_emailr   )r   Ú	ExceptionÚloggerÚerrorÚcursorÚexecuter   ÚSQLÚformatÚ
IdentifierÚPROVIDERÚfetchoneÚcloser   Únowr   ÚutcÚ_refreshÚhttpxÚgetÚ
GRAPH_BASEÚraise_for_statusÚjson)r
   ÚconnÚexÚcurÚrowr   Úrefresh_tokenÚ
expires_atr   Úsettings_responseÚmailbox_settingsÚme_responseÚmes                ú>/var/www/html/strategist-ai/app/services/integrations/teams.pyÚget_teams_connectionr9      s*  € ðÜ Ó"ˆð
Ø�[‰[‹]ð 	!˜cØ�K‰KÜ—‘ð Nó Oç‘œŸ™ yÓ1Ó2Ü˜8Ð$ô	&ð
 —,‘,“.ˆC÷	!ð 	�
‰
ŒáØà.1Ñ+€L�- ØÐ *´·±¼X¿\¹\Ó0JÒ"JÜ 	¨=Ó9ˆÙØà '¨,¨Ð 8Ð9€GðÜ!ŸI™IÜˆlÐ-Ð.¸ÈôMÐà×*Ñ*Ô,Ø,×1Ñ1Ó3Ðä—i‘i¤: ,¨cÐ 2¸GÈRÔPˆØ×$Ñ$Ô&Ø×ÑÓˆð %Ø—f‘f˜V“nÒI¨¯©Ð/BÓ(CÒIÀrØ$×(Ñ(¨Ó4Ò=¸ñð øôS ò Ü�‰Ð?À	¸{È"ÈRÈDÐQÔRÜûðú÷
	!ñ 	!ûô ò Ü�‰Ð?À	¸{È"ÈRÈDÐQÔRÛà�
‰
�ûð	ûð 	�
‰
�ûô* ò Ü�‰ÐBÀ9À+ÈRÐPRÈtÐTÔUÜûðúsq   ‚
F �G �AG	Á;G ÃA>H, Æ	GÆ!GÇGÇ	GÇG Ç	HÇHÇ:H ÈHÈH ÈH)È,	IÈ5IÉIr2   c           	      ó$  — 	 t        j                  t        t        j                  t        j
                  |dddœd¬«      }|j                  «        |j                  «       }|j                  d	«      }|syt        j                  t        j                  «      t!        |j                  d
«      xs d¬«      z   }	 t#        «       }	 |j%                  «       5 }|j'                  t)        j*                  d«      j-                  t)        j.                  | «      «      ||t0        f«       ddd«       |j3                  «        |j7                  «        |S # t        $ r%}t        j                  d| › d|› �«       Y d}~yd}~ww xY w# t        $ r'}t        j                  d| › d|› �«       |cY d}~S d}~ww xY w# 1 sw Y   Œ�xY w# t        $ r5}|j5                  «        t        j                  d| › d|› �«       Y d}~Œ»d}~ww xY w# |j7                  «        w xY w)ztExchanges a refresh token for a new access token, and writes it back
    onto the same row. Returns None on failure.r2   z$https://graph.microsoft.com/.default)Ú	client_idÚclient_secretr2   Ú
grant_typeÚscoper   )Údatar   z&Could not refresh the Teams token for r   Nr   Ú
expires_ini  )Úsecondsz0Could not persist the refreshed Teams token for zSUPDATE {}.integrations SET "accessToken" = %s, "expiresAt" = %s WHERE provider = %s)r)   ÚpostÚ	TOKEN_URLr   ÚMS_TEAMS_CLIENT_IDÚMS_TEAMS_CLIENT_SECRETr,   r-   r   r   r   r*   r   r&   r   r'   r   r   r   r   r   r    r!   r"   r#   ÚcommitÚrollbackr%   )	r
   r2   ÚresponseÚpayloadr/   r   r3   r.   r0   s	            r8   r(   r(   U   sÏ  € ðÜ—:‘:Üä%×8Ñ8Ü!)×!@Ñ!@Ø!.Ø-Ø?ñð ô

ˆð 	×!Ñ!Ô#Ø—-‘-“/ˆð
 —;‘;˜~Ó.€LÙØä—‘œhŸl™lÓ+¬iØ—‘˜LÓ)Ò1¨Tô/3ñ 3€JðÜ Ó"ˆð
Ø�[‰[‹]ð 	6˜cØ�K‰KÜ—‘ð ?ó @ç‘œŸ™ yÓ1Ó2Ø˜z¬8Ð4ô	6÷	6ð 	�‰Œð
 	�
‰
ŒàÐøô? ò Ü�‰Ð=¸i¸[ÈÈ2È$ÐOÔPÜûðûô ò Ü�‰ÐGÈ	À{ÐRTÐUWÐTXÐYÔZØÕûðú÷
	6ð 	6ûô ò [Ø�‰ŒÜ�‰ÐGÈ	À{ÐRTÐUWÐTXÐY×ZÑZûð[ûð 	�
‰
�úsy   ‚AE Â8
E= ÃF< ÃAF0Ä"F< Å	E:ÅE5Å5E:Å=	F-ÆF(Æ"F-Æ(F-Æ0F9Æ5F< Æ<	G:Ç+G5Ç0G= Ç5G:Ç:G= Ç=Hé   Ú
connectionÚduration_minutesÚreturnc           	      óè  ‡‡— 	 |j                  t        j                  «      }|j                  t        j                  «      }t        j                  t
        › d�dd| d   › �i| d   g|j                  d«      ddœ|j                  d«      ddœd	œd
¬«      }|j                  «        |j                  «       d   d   j                  d«      xs g }g }|D ]˜  }	|	j                  d«      dk7  rŒ|j                  t        j                  |	d   d   «      j                  t        j                  ¬«      t        j                  |	d   d   «      j                  t        j                  ¬«      f«       Œš g }
|Št        |¬«      }‰|z   |k  r<‰|z   Št        ˆˆfd„|D «       «      s|
j                  ‰‰dœ«       ‰|z  Š‰|z   |k  rŒ<|
S # t         $ r$}t"        j%                  d|› �«       g cY d}~S d}~ww xY w)z—Open duration_minutes-long slots inside [start, end]. Returns [] on
    any API failure, same safe-failure reasoning as the Google Calendar
    module.z/me/calendar/getScheduler   r   r   r   ú%Y-%m-%dT%H:%M:%Sr   ©ÚdateTimer   )Ú	schedulesÚ	startTimeÚendTimer   ©r   r-   r   Úvaluer   ÚscheduleItemsÚstatusÚbusyÚstartrQ   )ÚtzinfoÚend)Úminutesc              3   ó<   •K  — | ]  \  }}‰|k  xr ‰|kD  –— Œ y ­w©N© )Ú.0Úb_startÚb_endr   Úslot_ends      €€r8   ú	<genexpr>z%check_availability.<locals>.<genexpr>­   s&   øè ø€ ÒXÁÀÈ%�v ‘~Ò<¨(°WÑ*<Ó<ÑXùs   ƒ)rZ   r\   z$Could not check Teams availability: N)Ú
astimezoner   r'   r)   rB   r+   Ústrftimer,   r-   r*   Úappendr   ÚfromisoformatÚreplacer   Úanyr   r   r   )rK   rZ   r\   rL   Ú	start_utcÚend_utcrH   ÚitemsrY   ÚitemÚslotsÚstepr/   r   rd   s                @@r8   Úcheck_availabilityrr   ‹   sï  ù€ ð$Ø×$Ñ$¤X§\¡\Ó2ˆ	Ø—.‘.¤§¡Ó.ˆÜ—:‘:ÜˆlÐ2Ð3Ø$¨°
¸>Ñ0JÐ/KÐ&LÐMà(¨Ñ6Ð7Ø*3×*<Ñ*<Ð=PÓ*QÐ_dÑeØ(/×(8Ñ(8Ð9LÓ(MÐ[`Ñañð
 ô	
ˆð 	×!Ñ!Ô#Ø—‘“ Ñ(¨Ñ+×/Ñ/°Ó@ÒFÀBˆàˆØò 	ˆDØ�x‰x˜Ó! VÒ+ØØ�K‰KÜ×&Ñ& t¨G¡}°ZÑ'@ÓA×IÑIÔQY×Q]ÑQ]ÐIÓ^Ü×&Ñ& t¨E¡{°:Ñ'>Ó?×GÑGÌxÏ|É|ÐGÓ\ðõ ð	ð ˆØˆÜÐ!1Ô2ˆØ�t‰m˜sÒ"Ø ‘}ˆHÜÔXÐSWÔXÔXØ—‘ v°hÑ?Ô@Ø�d‰NˆFð	 �t‰m˜sÓ"ð
 ˆøÜò Ü�‰Ð;¸B¸4Ð@ÔAØ�	ûðús$   „F=G ÇG Ç	G1ÇG,Ç&G1Ç,G1Úvisitor_emailÚvisitor_namec                 óz  — 	 |j                  t        j                  «      }|j                  t        j                  «      }t        j                  t
        › d�dd| d   › �id|› �|j                  d«      ddœ|j                  d«      ddœ||d	œd
dœgdddœd¬«      }|j                  «        |j                  «       }|j                  d«      }	|	st        j                  d«       y|j                  d«      xs i }
|	|
j                  d«      dœS # t        $ r"}t        j                  d|› �«       Y d}~yd}~ww xY w)z¥Books the slot and creates the Teams meeting in one call
    (isOnlineMeeting=true) -- Graph returns the join link on the created
    event. Returns None on failure.ú
/me/eventsr   r   r   zMeeting with rO   r   rP   )ÚaddressÚnameÚrequired)ÚemailAddressÚtypeTÚteamsForBusiness)ÚsubjectrZ   r\   Ú	attendeesÚisOnlineMeetingÚonlineMeetingProviderr   rU   Úidz7Could not create the Teams event: response missing 'id'NÚonlineMeetingÚjoinUrl)Úevent_idÚmeeting_linkz"Could not create the Teams event: )rf   r   r'   r)   rB   r+   rg   r,   r-   r*   r   r   r   )rK   rZ   r\   rs   rt   rl   rm   rH   Úeventr„   Úonline_meetingr/   s               r8   Úcreate_eventrˆ   ¶   s>  € ðØ×$Ñ$¤X§\¡\Ó2ˆ	Ø—.‘.¤§¡Ó.ˆÜ—:‘:Üˆl˜*Ð%Ø$¨°
¸>Ñ0JÐ/KÐ&LÐMà*¨<¨.Ð9Ø&/×&8Ñ&8Ð9LÓ&MÐ[`ÑaØ$+×$4Ñ$4Ð5HÓ$IÐW\Ñ]à0=À|Ñ$TØ&ñð ð $(Ø);ñ
ð ô
ˆð  	×!Ñ!Ô#Ø—‘“ˆØ—9‘9˜T“?ˆÙÜ�L‰LÐRÔSØØŸ™ ?Ó3Ò9°rˆØ$°n×6HÑ6HÈÓ6SÑTÐTøÜò Ü�‰Ð9¸"¸Ð>Ô?Üûðús   ‚C#D Ã&(D Ä	D:ÄD5Ä5D:c                 óæ  — 	 |�¢|t        d¬«      z
  j                  t        j                  «      }|t        d¬«      z   j                  t        j                  «      }t	        j
                  t        › d�dd| d   › �i|j                  d«      |j                  d«      d	œd
¬«      }n't	        j
                  t        › d�dd| d   › �id
¬«      }|j                  «        |j                  «       j                  d«      xs g }|j                  «       }g }|D ]¯  }	t        |	t        «      sŒ|	j                  d«      }
t        |
t        «      sŒ6d}|
D ]_  }t        |t        «      sŒ|j                  d«      }t        |t        «      sŒ6|j                  d«      xs dj                  «       |k(  sŒ]d} n |sŒŸ|j                  |	«       Œ± 	 t%        |«      dk7  ry	 t	        j&                  t        › d|d   d   › �dd| d   › �id
¬«      }|j                  «        y# t        $ r"}t         j#                  d|› �«       Y d}~yd}~ww xY w# t        $ r"}t         j#                  d|› �«       Y d}~yd}~ww xY w)aH  Finds the event by attendee email (narrowed to a window around
    near_time when given) and deletes it. Returns False when zero or more
    than one event matches -- does not guess.

    Attendee matching is done client-side in Python, not via Graph's
    $filter -- `$filter=attendees/any(...)` is not a supported OData
    expression for the events collection and would likely 400. near_time
    narrowing uses /me/calendarView (the only endpoint that honours
    startDateTime/endDateTime as a time window) rather than /me/events,
    which silently ignores those two params.
    Né   )Úhoursz/me/calendarViewr   r   r   rO   )ÚstartDateTimeÚendDateTimer   )r   Úparamsr   rv   r   rV   r~   Frz   rw   r   Tz0Could not search Teams events for cancellation: é   z/me/events/r   r�   z"Could not delete the Teams event: )r   rf   r   r'   r)   r*   r+   rg   r,   r-   ÚlowerÚ
isinstanceÚdictÚlistrh   r   r   r   ÚlenÚdelete)rK   rs   Ú	near_timeÚwindow_startÚ
window_endrH   Ú
candidatesÚvisitor_email_lowerrn   r†   r~   ÚmatchedÚattendeeÚemail_addressr/   s                  r8   Úcancel_eventrž   Ú   sr  € ð0ØÐ Ø%¬	¸Ô(:Ñ:×FÑFÄxÇ|Á|ÓTˆLØ#¤i°aÔ&8Ñ8×DÑDÄXÇ\Á\ÓRˆJÜ—y‘yÜ�,Ð.Ð/Ø(¨G°J¸~Ñ4NÐ3OÐ*PÐQà%1×%:Ñ%:Ð;NÓ%OØ#-×#6Ñ#6Ð7JÓ#Kñð ô‰Hô —y‘yÜ�,˜jÐ)Ø(¨G°J¸~Ñ4NÐ3OÐ*PÐQØôˆHð
 	×!Ñ!Ô#Ø—]‘]“_×(Ñ(¨Ó1Ò7°Rˆ
ð ,×1Ñ1Ó3ÐØˆØò 	$ˆEÜ˜e¤TÔ*ØØŸ	™	 +Ó.ˆIÜ˜i¬Ô.ØØˆGØ%ò �Ü! (¬DÔ1ØØ (§¡¨^Ó <�Ü! -´Ô6ØØ!×%Ñ% iÓ0Ò6°B×=Ñ=Ó?ÐCVÓVØ"�GÙðò Ø—‘˜UÕ#ñ#	$ô, ˆ5ƒz�Q‚Øð
Ü—<‘<Üˆl˜+ e¨A¡h¨t¡nÐ%5Ð6Ø$¨°
¸>Ñ0JÐ/KÐ&LÐMØô
ˆð
 	×!Ñ!Ô#Øøô ò Ü�‰ÐGÈÀtÐLÔMÜûðûô ò Ü�‰Ð9¸"¸Ð>Ô?Üûðús<   ‚F(H Æ+H Æ3H Ç?I È	IÈ H=È=IÉ	I0ÉI+É+I0r_   )Ú__doc__Úloggingr   r   r   r)   Úpsycopg2r   Úapp.core.configr   Úapp.services.infra.databaser   Ú	getLoggerÚ__name__r   rC   r+   r#   Ústrr9   r(   ÚSLOT_MINUTESr’   Úintr“   rr   rˆ   Úboolrž   r`   ó    r8   ú<module>r«      sÇ   ðñó ß 2Ñ 2ã Ý å $Ý 9à	ˆ×	Ñ	˜8Ó	$€àH€	Ø/€
à€ð8 Có 8ðv0˜ð 0¨Có 0ðf €ð NZñ ( 4ð (Àsð (Ð^bó (ðV!˜Tð !¸cð !ÐQTó !ñHK˜Tð K°#ð KÈ$ô Krª   