Ë
    ÿ�jj)  ã                   óê   — 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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)aK  A tenant's connected Google Calendar account: reading it, refreshing its
access token when expired, and everything below this module needs to call
the Calendar API.

The connection is written by the dashboard's generic OAuth connect flow
into the tenant's own schema -- the same galaxiq_tenants database and
per-tenant `integrations` table mailchimp.py's connection lives beside (see
app/services/infra/database.py's get_setting). That connect flow lives in a
different service, not this one; this module only ever reads what it wrote,
and writes back a refreshed access token in place.
é    N)ÚdatetimeÚ	timedeltaÚtimezone)Úsql)Úsettings)Úget_db_connectionz#https://oauth2.googleapis.com/tokenz8https://www.googleapis.com/calendar/v3/calendars/primaryzgoogle-calendarÚ	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	 t%        j&                  t(        dd|› �id¬	«      }|j+                  «        |j-                  «       }	||	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   Œøx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)ad  This tenant's active Google Calendar connection, refreshed if its
    access token has expired.

    Returns {"access_token", "calendar_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).
    z2Could not read the Google Calendar connection for ú: NzjSELECT "accessToken", "refreshToken", "expiresAt" FROM {}.integrations WHERE provider = %s AND status = %sÚACTIVEÚAuthorizationúBearer é   ©ÚheadersÚtimeoutz'Could not read the Google Calendar for ÚidÚ ÚtimeZoneÚUTC)Úaccess_tokenÚcalendar_emailr   )r   Ú	ExceptionÚloggerÚerrorÚcursorÚexecuter   ÚSQLÚformatÚ
IdentifierÚPROVIDERÚfetchoneÚcloser   Únowr   ÚutcÚ_refreshÚhttpxÚgetÚCALENDAR_URLÚraise_for_statusÚjson)
r	   ÚconnÚexÚcurÚrowr   Úrefresh_tokenÚ
expires_atÚresponseÚcalendars
             úH/var/www/html/strategist-ai/app/services/integrations/google_calendar.pyÚget_google_calendar_connectionr5      sÝ  € ðÜ Ó"ˆð
Ø�[‰[‹]ð 	!˜cØ�K‰KÜ—‘ð Nó Oç‘œŸ™ yÓ1Ó2Ü˜8Ð$ô	&ð
 —,‘,“.ˆC÷	!ð 	�
‰
ŒáØà.1Ñ+€L�- ØÐ *´·±¼X¿\¹\Ó0JÒ"JÜ 	¨=Ó9ˆÙØð
Ü—9‘9ÜØ$¨°¨~Ð&>Ð?Øô
ˆð
 	×!Ñ!Ô#Ø—=‘=“?ˆð %Ø"Ÿ,™, tÓ,Ò2°Ø—L‘L Ó,Ò5°ñð øôO ò Ü�‰ÐIÈ)ÈÐTVÐWYÐVZÐ[Ô\Üûðú÷
	!ð 	!ûô ò Ü�‰ÐIÈ)ÈÐTVÐWYÐVZÐ[Ô\Ûà�
‰
�ûð	ûð 	�
‰
�ûô& ò Ü�‰Ð>¸y¸kÈÈBÈ4ÐPÔQÜûðúsq   ‚
E �E> �AE2Á;E> ÃAG Å	E/Å
E*Å*E/Å2E;Å7E> Å>	F<ÆF7Æ"F? Æ7F<Æ<F? Æ?GÇ	HÇG=Ç=Hr0   c                 ó"  — 	 t        j                  t        t        j                  t        j
                  |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)a  Exchanges a refresh token for a new access token, and writes the new
    token + expiry back onto the same row so the next call doesn't refresh
    again. Returns the new access token, or None on failure (a revoked
    refresh token is the same as never having connected).r0   )Ú	client_idÚclient_secretr0   Ú
grant_typer   )Údatar   z0Could not refresh the Google Calendar token for r   Nr   Ú
expires_ini  )Úsecondsz:Could not persist the refreshed Google Calendar token for zSUPDATE {}.integrations SET "accessToken" = %s, "expiresAt" = %s WHERE provider = %s)r'   ÚpostÚ	TOKEN_URLr   ÚGOOGLE_CALENDAR_CLIENT_IDÚGOOGLE_CALENDAR_CLIENT_SECRETr*   r+   r   r   r   r(   r   r$   r   r%   r   r   r   r   r   r   r   r    r!   ÚcommitÚrollbackr#   )	r	   r0   r2   Úpayloadr-   r   r1   r,   r.   s	            r4   r&   r&   V   sØ  € ð
Ü—:‘:Üä%×?Ñ?Ü!)×!GÑ!GØ!.Ø-ñ	ð ô	
ˆð 	×!Ñ!Ô#Ø—-‘-“/ˆð
 —;‘;˜~Ó.€LÙØä—‘œhŸl™lÓ+¬iØ—‘˜LÓ)Ò1¨Tô/3ñ 3€JðÜ Ó"ˆðØ�[‰[‹]ð 	6˜cØ�K‰KÜ—‘ð ?ó @ç‘œŸ™ yÓ1Ó2Ø˜z¬8Ð4ô	6÷	6ð 	�‰Œð 	�
‰
ŒàÐøôC ò Ü�‰ÐGÈ	À{ÐRTÐUWÐTXÐYÔZÜûðûô ò Ü�‰ð Ø%˜; b¨¨ð.ô 	/àÕûðú÷	6ð 	6ûô ò /Ø�‰ŒÜ�‰ð Ø%˜; b¨¨ð.÷ 	/ñ 	/ûð/ûð
 	�
‰
�úsy   ‚AE Â7
E< ÃF; ÃAF/Ä!F; Å	E9ÅE4Å4E9Å<	F,ÆF'Æ!F,Æ'F,Æ/F8Æ4F; Æ;	G9Ç+G4Ç/G< Ç4G9Ç9G< Ç<Hz/https://www.googleapis.com/calendar/v3/freeBusyz?https://www.googleapis.com/calendar/v3/calendars/primary/eventsé   Ú
connectionÚduration_minutesÚreturnc           	      ó†  ‡‡— 	 t        j                  t        dd| d   › �i|j                  «       |j                  «       d| d   igdœd¬«      }|j	                  «        |j                  «       d	   | d      d
   }|D �cg c]2  }t        j                  |d   «      t        j                  |d   «      f‘Œ4 }}g }|Št        |¬«      }	‰|	z   |k  r<‰|	z   Št        ˆˆfd„|D «       «      s|j                  ‰‰dœ«       ‰|	z  Š‰|	z   |k  rŒ<|S c c}w # t        $ r$}
t        j                  d|
› �«       g cY d}
~
S d}
~
ww xY w)a×  Open duration_minutes-long slots inside [start, end], starting at
    `start` and stepping forward every duration_minutes -- slots land on
    the half hour only when the caller passes a midnight- or
    half-hour-aligned `start`, e.g. a business-timezone day boundary.
    Returns [] on any API failure -- an empty list of slots reads to
    the model as "nothing available", which is the safe failure mode
    (never claims a slot is free when the check itself failed).r   r   r   r   r   )ÚtimeMinÚtimeMaxÚitemsr   )r   r+   r   Ú	calendarsÚbusyÚstartÚend)Úminutesc              3   ó<   •K  — | ]  \  }}‰|k  xr ‰|kD  –— Œ y ­w©N© )Ú.0Úb_startÚb_endr   Úslot_ends      €€r4   ú	<genexpr>z%check_availability.<locals>.<genexpr>°   s&   øè ø€ ÒXÁÀÈ%�v ‘~Ò<¨(°WÑ*<Ó<ÑXùs   ƒ)rN   rO   z.Could not check Google Calendar availability: N)r'   r=   ÚFREEBUSY_URLÚ	isoformatr*   r+   r   Úfromisoformatr   ÚanyÚappendr   r   r   )rE   rN   rO   rF   r2   Úbusy_blocksÚbrM   ÚslotsÚstepr-   r   rW   s              @@r4   Úcheck_availabilityrb   ’   sg  ù€ ðÜ—:‘:ÜØ$¨°
¸>Ñ0JÐ/KÐ&LÐMà Ÿ?™?Ó,ØŸ=™=›?Ø Ð,<Ñ!=Ð>Ð?ñð
 ô	
ˆð 	×!Ñ!Ô#Ø—m‘m“o kÑ2°:Ð>NÑ3OÑPÐQWÑXˆð %ö&Øô ×'Ñ'¨¨'©
Ó3´X×5KÑ5KÈAÈeÉHÓ5UÒVð &ˆð &ð ˆØˆÜÐ!1Ô2ˆØ�t‰m˜sÒ"Ø ‘}ˆHÜÔXÐSWÔXÔXØ—‘ v°hÑ?Ô@Ø�d‰NˆFð	 �t‰m˜sÓ"ð
 ˆùò&øô ò Ü�‰ÐEÀbÀTÐJÔKØ�	ûðús7   „A;D Á?7DÂ6AD ÄD ÄD Ä	E ÄD;Ä5E Ä;E Úvisitor_emailÚvisitor_namec                 óò  — 	 t        j                  t        dd| d   › �idddœd|› �d|j                  «       id|j                  «       id	|igd
d|› d|j                  «       › �iidœd¬«      }|j	                  «        |j                  «       }|j                  d«      }|st        j                  d«       y||j                  d«      dœS # t        $ r"}t        j                  d|› �«       Y d}~yd}~ww xY w)a  Books the slot and emails the visitor a calendar invite (sendUpdates
    'all' is what makes Calendar actually send it) with an auto-attached
    Meet link (conferenceDataVersion 1). Returns None on failure -- never
    raises, matching every other provider call in this module.r   r   r   Úallé   )ÚsendUpdatesÚconferenceDataVersionzMeeting with ÚdateTimeÚemailÚcreateRequestÚ	requestIdÚ-)ÚsummaryrN   rO   Ú	attendeesÚconferenceDatar   )r   Úparamsr+   r   r   zACould not create the Google Calendar event: response missing 'id'NÚhangoutLink)Úevent_idÚmeeting_linkz,Could not create the Google Calendar event: )
r'   r=   Ú
EVENTS_URLrZ   r*   r+   r(   r   r   r   )	rE   rN   rO   rc   rd   r2   Úeventrt   r-   s	            r4   Úcreate_eventrx   ¹   s  € ð
Ü—:‘:ÜØ$¨°
¸>Ñ0JÐ/KÐ&LÐMØ#(À1ÑEà*¨<¨.Ð9Ø$ e§o¡oÓ&7Ð8Ø" C§M¡M£OÐ4Ø&¨Ð6Ð7à# k°m°_ÀAÀeÇoÁoÓFWÐEXÐ3YÐ%Zð#\ñð ô
ˆð 	×!Ñ!Ô#Ø—‘“ˆØ—9‘9˜T“?ˆÙÜ�L‰LÐ\Ô]ØØ$°e·i±iÀÓ6NÑOÐOøÜò Ü�‰ÐCÀBÀ4ÐHÔIÜûðús   ‚B4C Â7C Ã	C6ÃC1Ã1C6c                 ó€  ‡
— d|i}|�@|t        d¬«      z
  j                  «       |d<   |t        d¬«      z   j                  «       |d<   	 t        j                  t        dd| d	   › �i|d
¬«      }|j                  «        |j                  «       j                  d«      xs g }|j                  «       Š
g }|D ][  }t        |t        «      sŒ|j                  d«      }t        |t        «      sŒ6t        ˆ
fd„|D «       «      sŒK|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)aæ  Finds the event by attendee email (narrowed to a window around
    near_time when given) and deletes it. Returns False -- does not guess
    -- when zero or more than one event matches.

    The `q` full-text search only narrows candidates -- it also matches an
    event that merely mentions the email in its summary/description text
    without the visitor actually being an attendee. The final match is
    always confirmed client-side against each candidate's `attendees` list.
    ÚqNé   )ÚhoursrI   rJ   r   r   r   r   )r   rr   r   rK   rp   c              3   óŽ   •K  — | ]<  }t        |t        «      xr& |j                  d «      xs dj                  «       ‰k(  –— Œ> y­w)rk   r   N)Ú
isinstanceÚdictr(   Úlower)rT   ÚattendeeÚvisitor_email_lowers     €r4   rX   zcancel_event.<locals>.<genexpr>þ   sL   øè ø€ ò .àô ˜h¬Ó-ò TØ Ÿ™ WÓ-Ò3°×:Ñ:Ó<Ð@SÑSóTñ .ùs   ƒAAz7Could not search the Google Calendar for cancellation: Frg   ú/r   r   r   Tz,Could not delete the Google Calendar event: )r   rZ   r'   r(   rv   r*   r+   r€   r~   r   Úlistr\   r]   r   r   r   ÚlenÚdelete)rE   rc   Ú	near_timerr   r2   Ú
candidatesrK   rw   rp   r-   r‚   s             @r4   Úcancel_eventr‰   Ù   sÍ  ø€ ð �=Ð!€FØÐØ&¬¸Ô);Ñ;×FÑFÓHˆˆyÑØ&¬¸Ô);Ñ;×FÑFÓHˆˆyÑðÜ—9‘9ÜØ$¨°
¸>Ñ0JÐ/KÐ&LÐMØØô	
ˆð 	×!Ñ!Ô#Ø—]‘]“_×(Ñ(¨Ó1Ò7°Rˆ
ð ,×1Ñ1Ó3ÐØˆØò 		$ˆEÜ˜e¤TÔ*ØØŸ	™	 +Ó.ˆIÜ˜i¬Ô.ØÜó .à#,ô.õ .ð —‘˜UÕ#ñ		$ô ˆ5ƒz�Q‚Øð
Ü—<‘<Üˆl˜!˜E !™H T™NÐ+Ð,Ø$¨°
¸>Ñ0JÐ/KÐ&LÐMØô
ˆð
 	×!Ñ!Ô#Øøô ò Ü�‰ÐNÈrÈdÐSÔTÜûðûô ò Ü�‰ÐCÀBÀ4ÐHÔIÜûðús7   Á	B6E$ Ä E$ Ä$?F Å$	FÅ-F
Æ
FÆ	F=ÆF8Æ8F=rR   )Ú__doc__Úloggingr   r   r   r'   Úpsycopg2r   Úapp.core.configr   Úapp.services.infra.databaser   Ú	getLoggerÚ__name__r   r>   r)   r!   Ústrr5   r&   rY   rv   ÚSLOT_MINUTESr   Úintr„   rb   rx   Úboolr‰   rS   ó    r4   ú<module>r–      sÎ   ðñ
ó ß 2Ñ 2ã Ý å $Ý 9à	ˆ×	Ñ	˜8Ó	$€à1€	ØI€à€ð6¨có 6ðr3˜ð 3¨Có 3ðl A€ØN€
à€ð NZñ $ 4ð $Àsð $Ð^bó $ðN˜Tð ¸cð ÐQTó ñ@:˜Tð :°#ð :È$ô :r•   