Ë
    b’Àj=9  ã                  ó`  — d dl mZ d dlZd dlZd dlmZ d dlmZmZ d dlm	Z	m
Z
 d dlmZ d dlmZ dZd	Zd
ZdZdZddddœZ ed¬«       G d„ d«      «       Zd$d„Zd%d„Z	 	 	 	 	 	 	 	 	 	 d&d„Zd'd„Zd(d„Zd)d„Zd*d„Zd+d„Zd,d„Zd-d„Zeedœ	 	 	 	 	 	 	 	 	 d.d „Z d/d!„Z!d0d"„Z"e#d#k(  r e"«        yy)1é    )ÚannotationsN)Ú	dataclass)ÚdatetimeÚ	timedelta)Úcreate_engineÚtext)Ú
Connection)Údatabase_urlÚbf4swÚbf4sw_reconcileéd   é   é   ÚpcÚps4Úxboxone)ÚPCzPS4/5ÚXBoxT)Úfrozenc                  óJ   — e Zd ZU ded<   ded<   ded<   ded<   ded<   ded	<   y
)ÚAliasRowÚintÚalias_idÚstrÚplatformÚ
persona_idÚplayer_namer   Ú
first_seenÚ	last_seenN)Ú__name__Ú
__module__Ú__qualname__Ú__annotations__© ó    úbf4ps/discovery.pyr   r      s#   … àƒMØƒMØƒOØÓØÓØÔr%   r   c                 ó^   — t         j                  j                  d«      } | st        d«      ‚| S )NÚBF4PS_BF4SW_DATABASE_URLz$BF4PS_BF4SW_DATABASE_URL is required)ÚosÚenvironÚgetÚRuntimeError)Úvalues    r&   Úbf4sw_database_urlr.   #   s)   € Ü�J‰J�N‰NÐ5Ó6€EÙÜÐAÓBÐBØ€Lr%   c           	     óH   — t        | d   | d   | d   | d   | d   | d   ¬«      S )NÚidr   r   r   r   r   )r   r   r   r   r   r   )r   )Úrows    r&   Ú_alias_from_mappingr2   *   s=   € ÜØ�T‘Ø�Z‘Ø�|Ñ$Ø˜Ñ&Ø�|Ñ$Ø�kÑ"ôð r%   c               ó‚   — |dk  rt        d«      ‚|dk  rt        d«      ‚| €|t        |¬«      z
  S | t        |¬«      z
  S )Né   z'initial_window_hours must be at least 1r   z"overlap_minutes cannot be negative)Úhours)Úminutes)Ú
ValueErrorr   )Úprevious_successÚ
source_nowÚinitial_window_hoursÚoverlap_minutess       r&   Úreconciliation_sincer<   5   sS   € ð ˜aÒÜÐBÓCÐCØ˜ÒÜÐ=Ó>Ð>ØÐØœIÐ,@ÔAÑAÐAØœi°Ô@Ñ@Ð@r%   c               óØ   — | j                  t        d«      ||dœ«      j                  «       j                  «       }|st	        d|›d|› �«      ‚|D �cg c]  }t        |«      ‘Œ c}S c c}w )Nz÷
            SELECT id, platform, persona_id, player_name, first_seen, last_seen
            FROM bf4_player_aliases
            WHERE persona_id = :persona_id
              AND platform = :platform
            ORDER BY first_seen, id
            ©r   r   z%BF4SW aliases not found for platform=z, persona_id=)Úexecuter   ÚmappingsÚallÚLookupErrorr2   )Úsourcer   r   Úrowsr1   s        r&   Úload_aliasesrE   E   ss   € Ø�>‰>Üðó	
ð "¨xÑ8ó÷ �hƒj—‘“ð 	ñ ÜÐAÀ(ÀÈ]Ð[eÐZfÐgÓhÐhØ04Ö5¨Ô Õ$Ò5Ð5ùÒ5s   ÁA'c               ó²   — | j                  t        d«      ||dœ«      j                  «       j                  «       }|D �cg c]  }t	        |«      ‘Œ c}S c c}w )NzÞ
            SELECT id, platform, persona_id, player_name, first_seen, last_seen
            FROM bf4_player_aliases
            WHERE id > :after_alias_id
            ORDER BY id
            LIMIT :batch_size
            ©Úafter_alias_idÚ
batch_size©r?   r   r@   rA   r2   )rC   rH   rI   rD   r1   s        r&   Úload_discovery_batchrK   W   sU   € Ø�>‰>Üðó	
ð *¸ÑDó÷ �hƒj—‘“ð 	ð 15Ö5¨Ô Õ$Ò5Ð5ùÒ5ó   ¿Ac               ó²   — | j                  t        d«      ||dœ«      j                  «       j                  «       }|D �cg c]  }t	        |«      ‘Œ c}S c c}w )Na  
            SELECT id, platform, persona_id, player_name, first_seen, last_seen
            FROM bf4_player_aliases
            WHERE persona_id IS NOT NULL
              AND last_seen >= :since
              AND last_seen <= :through
            ORDER BY last_seen, id
            ©ÚsinceÚthroughrJ   )rC   rO   rP   rD   r1   s        r&   Úload_reconciliation_rowsrQ   g   sU   € Ø�>‰>Üðó		
ð  GÑ,ó÷ �hƒj—‘“ð 	ð 15Ö5¨Ô Õ$Ò5Ð5ùÒ5rL   c           	     óö  — |st        d«      ‚|D �ch c]  }|j                  |j                  f’Œ }}t        |«      dk7  rt        d«      ‚t	        t        |«      «      \  }}t        j                  |«      }|€t        d|›�«      ‚t        d„ |D «       «      }t        d„ |D «       «      }t        |d„ ¬«      }	| j                  t        d	«      |||	j                  ||d
œ«      j                  «       }
|D ]@  }| j                  t        d«      |
|j                  |j                  |j                  dœ«       ŒB | j                  t        d«      |
||dœ«       | j                  t        d«      d|
i«       |
S c c}w )Nz$At least one BF4SW alias is requiredr4   z=All aliases must belong to the same platform/persona identityzUnsupported BF4SW platform c              3  ó4   K  — | ]  }|j                   –— Œ y ­w©N)r   ©Ú.0Úaliass     r&   ú	<genexpr>z!import_aliases.<locals>.<genexpr>ƒ   s   è ø€ Ò>¨U˜×(Õ(Ñ>ùó   ‚c              3  ó4   K  — | ]  }|j                   –— Œ y ­wrT   )r   rU   s     r&   rX   z!import_aliases.<locals>.<genexpr>„   s   è ø€ Ò;¨%�e—o•oÑ;ùrY   c                ó2   — | j                   | j                  fS rT   ©r   r   ©rW   s    r&   ú<lambda>z import_aliases.<locals>.<lambda>…   s   € °E·O±OÀUÇ^Á^Ð3T€ r%   ©Úkeya¶  
            INSERT INTO soldiers (persona_id, platform, current_name, first_seen_at, last_seen_at)
            VALUES (:persona_id, :platform, :name, :first_seen, :last_seen)
            ON CONFLICT (persona_id, platform) DO UPDATE SET
                current_name = CASE
                    WHEN EXCLUDED.last_seen_at >= soldiers.last_seen_at THEN EXCLUDED.current_name
                    ELSE soldiers.current_name
                END,
                first_seen_at = LEAST(soldiers.first_seen_at, EXCLUDED.first_seen_at),
                last_seen_at = GREATEST(soldiers.last_seen_at, EXCLUDED.last_seen_at),
                updated_at = now()
            RETURNING soldier_id
            )r   r   Únamer   r   a¬  
                INSERT INTO soldier_names (soldier_id, name, first_seen_at, last_seen_at)
                VALUES (:soldier_id, :name, :first_seen, :last_seen)
                ON CONFLICT (soldier_id, name) DO UPDATE SET
                    first_seen_at = LEAST(soldier_names.first_seen_at, EXCLUDED.first_seen_at),
                    last_seen_at = GREATEST(soldier_names.last_seen_at, EXCLUDED.last_seen_at)
                )Ú
soldier_idra   r   r   aª  
            INSERT INTO soldier_sources (soldier_id, source_type, first_seen_at, last_seen_at)
            VALUES (:soldier_id, 'bf4sw', :first_seen, :last_seen)
            ON CONFLICT (soldier_id, source_type) DO UPDATE SET
                first_seen_at = LEAST(soldier_sources.first_seen_at, EXCLUDED.first_seen_at),
                last_seen_at = GREATEST(soldier_sources.last_seen_at, EXCLUDED.last_seen_at)
            )rb   r   r   zbINSERT INTO collection_state (soldier_id) VALUES (:soldier_id) ON CONFLICT (soldier_id) DO NOTHINGrb   )r7   r   r   ÚlenÚnextÚiterÚPLATFORM_MAPr+   ÚminÚmaxr?   r   r   Ú
scalar_oner   r   )ÚdestinationÚaliasesrW   ÚidentityÚsource_platformr   r   Úearliest_seenÚlatest_seenÚcurrent_aliasrb   s              r&   Úimport_aliasesrq   x   s¤  € ÙÜÐ?Ó@Ð@Ø@GÖH°u�—‘ ×!1Ñ!1Ò2ÐH€HÐHÜ
ˆ8ƒ}˜ÒÜÐXÓYÐYÜ"&¤t¨H£~Ó"6Ñ€O�ZÜ×Ñ Ó0€HØÐÜÐ6°Ð6IÐJÓKÐKäÑ>°gÔ>Ó>€MÜÑ;°7Ô;Ó;€KÜ˜Ñ%TÔU€MØ×$Ñ$Üðó	
ð "¨xÀ×AZÑAZÐjwð  GRñ  	Só!÷" �jƒlð# ð& ò 
ˆØ×ÑÜðóð &¨u×/@Ñ/@ÐPU×P`ÑP`Ðot×o~Ño~Ñõ	
ð
ð ×ÑÜðó	
ð "°È[ÑYôð ×ÑÜÐqÓrØ	�zÐ"ôð Ðùòy Is   ’E6c                ó¢   — | j                  t        d«      dt        i«       | j                  t        d«      dt        i«      j                  «       S )NúvINSERT INTO discovery_state (source_name, last_alias_id) VALUES (:source_name, 0) ON CONFLICT (source_name) DO NOTHINGÚsource_namezJSELECT last_alias_id FROM discovery_state WHERE source_name = :source_name)r?   r   ÚSOURCE_NAMEri   ©rj   s    r&   Úensure_discovery_staterw   º   sV   € Ø×ÑÜð  Fó  	GØ	œÐ$ôð ×ÑÜÐYÓZØ	œÐ$ó÷ �jƒlðr%   c                ó¢   — | j                  t        d«      dt        i«       | j                  t        d«      dt        i«      j                  «       S )Nrs   rt   zLSELECT last_success_at FROM discovery_state WHERE source_name = :source_name)r?   r   ÚRECONCILE_SOURCE_NAMEri   rv   s    r&   Úensure_reconciliation_staterz   Å   sX   € Ø×ÑÜð  Fó  	GØ	Ô-Ð.ôð ×ÑÜÐ[Ó\Ø	Ô-Ð.ó÷ �jƒlðr%   c          	     ó&  — |dk  rt        d«      ‚t        |«      }|j                  t        d«      dt        i«       t        | ||¬«      }|s'|j                  t        d«      dt        i«       ||ddfS t        |D �ch c]  }|j                  |j                  f’Œ c}«      }|D ]  \  }}t        |t        | ||¬«      «       Œ |d	   j                  }	|j                  t        d
«      t        |	dœ«       ||	t        |«      t        |«      fS c c}w )Nr4   zbatch_size must be at least 1údUPDATE discovery_state SET last_poll_at = now(), updated_at = now() WHERE source_name = :source_namert   rG   zzUPDATE discovery_state SET last_success_at = now(), last_error = NULL, updated_at = now() WHERE source_name = :source_namer   r>   éÿÿÿÿzšUPDATE discovery_state SET last_alias_id = :last_alias_id, last_success_at = now(), last_error = NULL, updated_at = now() WHERE source_name = :source_name)rt   Úlast_alias_id)r7   rw   r?   r   ru   rK   Úsortedr   r   rq   rE   r   rc   )
rC   rj   rI   r~   ÚbatchrW   Ú
identitiesr   r   Únew_last_alias_ids
             r&   Úrun_discovery_batchrƒ   Ð   s.  € Ø�A‚~ÜÐ8Ó9Ð9Ü*¨;Ó7€MØ×ÑÜÐsÓtØ	œÐ$ôô ! ¸ÐR\Ô]€EÙØ×ÑÜð  Nó  OØœKÐ(ô	
ð ˜m¨Q°Ð1Ð1äÈÖOÀ˜%Ÿ.™.¨%×*:Ñ*:Ò;ÒOÓP€JØ *ò dÑˆ�*Ü�{¤L°ÀJÐYaÔ$bÕcðdð ˜b™	×*Ñ*ÐØ×ÑÜð  jó  	kÜ#Ð6GÑHôð Ð+¬S°«Z¼¸Z»ÐHÐHùò Ps   Á=D©r:   r;   c               ó  — t        |«      }| j                  t        d«      «      j                  «       }t	        ||||¬«      }|j                  t        d«      dt
        i«       t        | ||¬«      }i }|D ]9  }	|j                  |	j                  |	j                  fg «      j                  |	«       Œ; |j                  «       D ]  }
t        ||
«       Œ |j                  t        d«      t
        |dœ«       ||t        |«      t        |«      fS )NzSELECT clock_timestamp()r„   r|   rt   rN   z†UPDATE discovery_state SET last_success_at = :source_watermark, last_error = NULL, updated_at = now() WHERE source_name = :source_name)rt   Úsource_watermark)rz   r?   r   ri   r<   ry   rQ   Ú
setdefaultr   r   ÚappendÚvaluesrq   rc   )rC   rj   r:   r;   r8   r9   rO   rD   ÚgroupedrW   rk   s              r&   Úrun_reconciliationr‹   ì   s  € ô 3°;Ó?ÐØ—‘¤Ð%?Ó @ÓA×MÑMÓO€JÜ ØØØ1Ø'ô	€Eð ×ÑÜÐsÓtØ	Ô-Ð.ôô
 $ F°%ÀÔL€DØ57€GØò QˆØ×Ñ˜EŸN™N¨E×,<Ñ,<Ð=¸rÓB×IÑIÈ%ÕPðQà—>‘>Ó#ò -ˆÜ�{ GÕ,ð-ð ×ÑÜð  Vó  	WÜ-À:ÑNôð �*œc $›i¬¨W«Ð5Ð5r%   c               óX  — d}d}d}	 | j                  «       5 }|j                  «       5 }t        |||¬«      \  }}	}
}d d d «       d d d «       
dk(  rt        d	› d|› d|› d|› �«       y |dz  }||
z  }|z  }t        d|› d	› d
	› d|
› d|› �
«       ŒŒ# 1 sw Y   ŒXxY w# 1 sw Y   Œ\xY w)Nr   ©rI   z$BF4SW discovery caught up at cursor=z
: batches=ú alias_rows=z identity_visits=r4   zBatch z	: cursor=ú->ú identities=)ÚconnectÚbeginrƒ   Úprint)Úsource_engineÚdestination_enginerI   Úbatch_numberÚtotal_alias_rowsÚtotal_identitiesrC   rj   Ú
old_cursorÚ
new_cursorÚ
alias_rowsr�   s               r&   Úrun_catch_uprœ     s  € Ø€LØÐØÐØ
Ø×"Ñ"Ó$ð 	A¨Ø#×)Ñ)Ó+ð A¨{ÜATÐU[Ð]hÐuô  BAÑ>�
˜J¨
°J÷A÷	Að ˜Š?ÜÐ8¸¸ÀJÈ|ÈnÐ\hÐiyÐhzð  {Lð  M]ð  L^ð  _ô  `ØØ˜ÑˆØ˜JÑ&ÐØ˜JÑ&ÐÜ��|�n I¨j¨\¸¸J¸<À|ÐT^ÐS_Ð_kÐlvÐkwÐxÔyð ÷Að Aú÷	Að 	Aús!   ˜B ©B½B ÂB	ÂB Â B)c                 óR  — t        j                  d¬«      } | j                  d¬«      }|j                  dt        d¬«       |j                  dd	d
¬«       |j                  dd	d¬«       |j                  dd	d¬«       | j                  dt        t        «      d¬«       | j                  dt        t        dt        › d�¬«       | j                  dt        t        dt        › d�¬«       | j                  dt        t        dt        › d�¬«       | j                  «       }t        t        «       «      }t        t        «       «      }|j                  �Í|j                  €| j!                  d«       |j#                  «       5 }t%        ||j                  |j                  ¬«      }d d d «       |j'                  «       5 }t)        |«      }d d d «       t+        d„ ¬«      }	t-        d|	j                  › d |	j                  › d!t/        |«      › d"|	j0                  ›d#› �
«       y |j                  �| j!                  d$«       |j2                  rt5        |||j6                  ¬%«       y |j8                  rŽ|j#                  «       5 }|j'                  «       5 }t;        |||j<                  |j>                  ¬&«      \  }
}}}d d d «       d d d «       t-        d'
jA                  «       › d(jA                  «       › d)› d*› �«       y |j#                  «       5 }|j'                  «       5 }tC        |||j6                  ¬%«      \  }}}}d d d «       d d d «       t-        d+› d,› d)› d*› �«       y # 1 sw Y   �ŒÂxY w# 1 sw Y   �ŒªxY w# 1 sw Y   ŒÆxY w# 1 sw Y   ŒÊxY w# 1 sw Y   Œ[xY w# 1 sw Y   Œ_xY w)-Nz!Import BF4SW soldiers into BF4PS.)ÚdescriptionT)Úrequiredz--persona-idz:Import one known BF4SW persona and complete alias history.)ÚtypeÚhelpz
--discoverÚ
store_truez&Run one bounded BF4SW discovery batch.)Úactionr¡   z--discover-until-caught-upzHRun bounded discovery batches until BF4SW has no rows beyond the cursor.z--reconcilezGRefresh mutable BF4SW alias observations using a source-time watermark.z
--platformz+BF4SW platform; required with --persona-id.)Úchoicesr¡   z--batch-sizez)Alias rows per discovery batch (default: z).)r    Údefaultr¡   z--reconcile-window-hoursz:Initial reconciliation activity window in hours (default: z--reconcile-overlap-minutesz6Reconciliation watermark overlap in minutes (default: z(--platform is required with --persona-idr>   c                ó2   — | j                   | j                  fS rT   r\   r]   s    r&   r^   zmain.<locals>.<lambda>5  s   € ¸¿¹ÈÏÉÐ7X€ r%   r_   zImported BF4SW persona_id=z
 platform=z	 aliases=z current_name=z as BF4PS soldier_id=z*--platform is only valid with --persona-idr�   r„   z%BF4SW reconciliation complete: since=z	 through=rŽ   r�   z'BF4SW discovery batch complete: cursor=r�   )"ÚargparseÚArgumentParserÚadd_mutually_exclusive_groupÚadd_argumentr   r   rf   ÚDEFAULT_BATCH_SIZEÚDEFAULT_RECONCILE_WINDOW_HOURSÚ!DEFAULT_RECONCILE_OVERLAP_MINUTESÚ
parse_argsr   r.   r
   r   r   Úerrorr‘   rE   r’   rq   rh   r“   rc   r   Údiscover_until_caught_uprœ   rI   Ú	reconciler‹   Úreconcile_window_hoursÚreconcile_overlap_minutesÚ	isoformatrƒ   )ÚparserÚmodeÚargsr”   r•   rC   rk   rj   rb   rp   rO   rP   r›   r�   r™   rš   s                   r&   Úmainr¸     sP  € Ü×$Ñ$Ð1TÔU€FØ×.Ñ.¸Ð.Ó=€DØ×Ñ�n¬3Ð5qÐÔrØ×Ñ�l¨<Ð>fÐÔgØ×ÑÐ2¸<ð  OYÐô  ZØ×Ñ�m¨Lð  @IÐô  JØ
×Ñ˜¬f´\Ó.BÐIvÐÔwØ
×Ñ˜¬SÔ:LÐU~ô  @Rð  Sð  SUð  TVÐô  WØ
×ÑÐ2¼ÔFdð  nhô  iGð  hHð  HJð  lKÐô  LØ
×ÑÐ5¼CÔIjð  tjô  kLð  jMð  MOð  rPÐô  QØ×ÑÓ€Dä!Ô"4Ó"6Ó7€MÜ&¤|£~Ó6ÐØ‡�Ð"Ø�=‰=Ð Ø�L‰LÐCÔDØ×"Ñ"Ó$ð 	_¨Ü" 6°d·o±oÐPT×P]ÑP]Ô^ˆG÷	_à×%Ñ%Ó'ð 	>¨;Ü'¨°WÓ=ˆJ÷	>ä˜GÑ)XÔYˆÜÐ*¨=×+CÑ+CÐ*DÀJÈ}×OeÑOeÐNfÐfoÔpsÐt{Óp|Ðo}ð  ~Lð  MZ÷  Mfñ  Mfð  Lið  i~ð  Ið  ~Jð  Kô  	LØØ‡}�}Ð Ø�‰ÐAÔBØ×$Ò$Ü�]Ð$6À4Ç?Á?ÕSØØ‡~‚~Ø×"Ñ"Ó$ð 	C¨Ø#×)Ñ)Ó+ð C¨{Ü9KÈFÐT_Ðvz÷  wRñ  wRð  dh÷  dBñ  dBô  :CÑ6��w 
¨J÷C÷	Cô 	Ð5°e·o±oÓ6GÐ5HÈ	ÐRY×RcÑRcÓReÐQfÐfrÐs}Ðr~ð  Kð  LVð  KWð  Xô  	YØà	×	Ñ	Ó	 ð B FØ×%Ñ%Ó'ð 	B¨;Ü=PÐQWÐYdÐqu÷  rAñ  rAô  >BÑ:ˆJ˜
 J°
÷	B÷Bô 
Ð3°J°<¸rÀ*ÀÈ\ÐZdÐYeÐeqÐr|Ðq}Ð
~Õ÷-	_ñ 	_ú÷	>ñ 	>ú÷Cð Cú÷	Cð 	Cú÷	Bð 	Bú÷Bð Bús`   Å<#MÆ7M,É:NÊ)M9Ê4NÌNÌNÌ7NÍM)Í,M6Í9N	Í>NÎNÎN	ÎNÎN&Ú__main__)Úreturnr   )rº   r   )
r8   údatetime | Noner9   r   r:   r   r;   r   rº   r   )rC   r	   r   r   r   r   rº   úlist[AliasRow])rC   r	   rH   r   rI   r   rº   r¼   )rC   r	   rO   r   rP   r   rº   r¼   )rj   r	   rk   r¼   rº   r   )rj   r	   rº   r   )rj   r	   rº   r»   )rC   r	   rj   r	   rI   r   rº   ztuple[int, int, int, int])
rC   r	   rj   r	   r:   r   r;   r   rº   z#tuple[datetime, datetime, int, int])rI   r   rº   ÚNone)rº   r½   )$Ú
__future__r   r§   r)   Údataclassesr   r   r   Ú
sqlalchemyr   r   Úsqlalchemy.enginer	   Úbf4ps.configr
   ru   ry   r«   r¬   r­   rf   r   r.   r2   r<   rE   rK   rQ   rq   rw   rz   rƒ   r‹   rœ   r¸   r    r$   r%   r&   ú<module>rÃ      s.  ðÝ "ã Û 	Ý !ß (ç *Ý (å %à€Ø)Ð ØÐ Ø!#Ð Ø$&Ð !à
ØØñ€ñ �$Ô÷ð ó ðóóðAØ%ðAàðAð ð	Að
 ðAð óAó 6ó$6ó 6ó"?óDóóIð@ !?Ø<ñ6Øð6àð6ð ð	6ð
 ð6ð )ó6óDzó"(@ðV ˆzÒÙ…Fð r%   