
    -ǚjR                        d Z ddlmZ ddlZddlmZmZm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mZ  G d
 de      Zy)uJ  OutlookSyncService — outlook_phase3 체크포인트 → EntityCanon(outlook) + CharacterOutlook.

Phase 4.5: CharacterOutlook의 "해당 에피소드 character에 연결된 행 전체 DELETE 후 재INSERT" 경로를
`(character_id, outlook_id)` 키 기준 delta sync로 전환.
안정성 향상 + DELETE→INSERT 패턴 회피.
    )annotationsN)DictSetTuple)text)CANON_STATUS_ORPHANEDNULL_OUTLOOK_SHORT_ID)build_name_indexlookup_name)episode_carry)BaseSyncServiceis_cp_syncablec                  <    e Zd ZddZ	 	 d	 	 	 	 	 	 	 	 	 ddZddZy)	OutlookSyncServicec                   ddl m}m} | j                  d      }t	        |dg      s| j                  d      }d}d}d}d}|r|j                  d      dk(  nd}t	        |dg      r|j                  d	i       }	|	j                  dg       }
t        j                  |	v }| j                  j                  |      j                  |j                  | j                  k(  |j                  d
k(        j                         }|D ci c]  }|j                  s|j                  | }}|D ci c]  }|j                  | }}t!               }|
D ];  }|j                  d      xs |j                  dd      }|s+|j#                  |       = |r| j                  j                  |      j                  |j                  | j                  k(  |j                  d
k(  |j                  j%                  |            j                         }|D ]	  }d|_         |r| j                  j'                          | j                  j                  |j(                        j                  |j                  | j                  k(  |j*                  | j*                  k(        j                         D ch c]  }|d   	 }}|
D ]y  }|j                  dd      }|j                  d      xs |j                  dd      }|j                  dd      }|j                  |      }||s|j                  |      }|r3|j,                  }||_        ||_        ||_        | j0                  |_        ndt5        t7        j8                               }| j                  j#                   ||| j                  ||d
|| j0                  | j0                               ||vre| j                  j#                   |t5        t7        j8                               || j                  | j*                               |j#                  |       |dz  }| |r| j:                  j=                  d|       nt!               }|
D ]}  }|j                  d      xs |j                  dd      }|j                  dd      }|j                  |      }||s|j                  |      }|sc|j#                  |j,                          | j                  j                  |      j                  |j                  | j                  k(  |j*                  | j*                  k(  |j(                  j%                  |D cg c]  }|j,                   c}xs dg            j                         }d} |D ]1  }!|!j(                  |v r| j                  j?                  |!       | dz  } 3 | r| j:                  j=                  d|        | j                  j'                          | jA                  |
|	||      \  }}}| j:                  j=                  d||||       |r| j:                  j=                  d       d}"n| jC                         }"|||||"dS c c}w c c}w c c}w c c}w )u  outlook_phase3 (또는 legacy outlook_extraction) → outlook EntityCanon + CharacterOutlook.

        Phase 4.5: CharacterOutlook는 (character_id, outlook_id) 기준 delta sync.

        Returns: {
            "outlooks": n_canon,
            "links": n_current_pairs,
            "links_inserted": n,
            "links_deleted": n,
            "orphans_marked": n,
        }
        r   )EntityCanonEntityEpisodeLinkoutlook_phase3outlooksoutlook_extractionstatuspartialFdataoutlook
outlook_idshort_id Nnamedescription)id
project_idr   r   entity_typer   
created_at
updated_at)r    canon_idr!   
episode_id   ut   Outlook sync: partial checkpoint — upsert %d, stale outlook canon cleanup deferred (awaiting completed checkpoint)u?   Outlook sync: 이번 화 링크 %d개 해제 (canon 은 보존))
skip_staleid_onlyu>   Synced %d outlooks + %d character-outlook links (Δ +%d / -%d)uD   Outlook sync: partial checkpoint — orphan outlook cleanup deferred)r   linkslinks_insertedlinks_deletedorphans_marked)"app.models.projectr   r   _load_cpr   get_ec
LEDGER_KEYdbqueryfilterr!   r"   allr   r   setaddin_flushr%   r&   r    r   nowr$   struuiduuid4loggerinfodelete_delta_sync_character_outlook_mark_orphan_outlooks)#selfr   r   ol_cpoutlook_count
link_countr+   r,   
is_partialol_datar   _id_only_existing_olseexisting_ol_by_shortexisting_ol_by_namecp_short_idsolsconflictingrow_linked_canon_idsol_nameol_shortol_descexistingol_id_matched_ids_s_n_e_stale_links	_unlinkedlnkorphans_removeds#                                      g/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services/checkpoint_sync/outlook_sync_service.pysync_from_checkpointz'OutlookSyncService.sync_from_checkpoint   s    	F ./ej\2MM"67E
 <Aeii)Y6e
%*.ii+G{{:r2H ~~0H GGMM+6==&&$//9''94 ce  <I#W=aAJJAJJM= #W6C"Dm16619m"D !$LFF<(BBFF:r,B $$Q'  "ggmmK8??**doo=++y8((,,\: #%	 
 'C#'CL 'GGMMO #'''--0A0J0J"K"R"R%00DOOC%00DOOC# #%#+#3A #  + &&,66,/I266*b3I&&3/33H=#H266w?H$KKE(0H%$+HM+2H(*.((H'

-EGGKK T__x$)$+#'88	!   11GGKK 1tzz|,u#'??t!  &))%0"I N   G!"  #u"B-G
B1GB+B-11"5Bz(044R8$((/ #  $ww}}->?FF%00DOOC%00DOOC%..22'45}!}5="? 
 #%  	'C|||3  GGNN3'NI ( KK$$Y!
 GGMMO 22'j( 3 	
 KKP KKV  O"88:O%,*-
 	
{ $X"D@+\ 6s   =YY$YY Y c                	   ddl m} | j                  j                  |      j	                  |j
                  | j
                  k(  |j                  dk(        j                         }|D ci c]&  }|j                  s|j                  |j                  ( }}t        |d d       }	| j                  j                  |      j	                  |j
                  | j
                  k(  |j                  dk(        j                         }
|
D ci c]&  }|j                  s|j                  |j                  ( }}t        |
d d	       }t               }|j                  d
g       D ]  }|j                  d|j                  dg             }|D ]  }|j                  dd      }d|v r%|j                  d      r|j                  d      d   n|}|j                  dd      }|j                  dd      }|j                  dd      }|j                  |      }|j                  |      }|s |xs t        ||      }|xs t        |	|      }|s|s|j!                  ||f         | j                  j#                  t%        d      | j
                  | j&                  d      j)                         }i }|D ]F  }|d   |d   |d   }}}||f}||v r(| j                  j#                  t%        d      d|i       B|||<   H t        |j+                               }| j                  j#                  t%        d      d| j
                  i      j)                         }i }|D ]  }|j-                  |d   |d   f|d          ! ||z
  } d}!t/        | t        |      z        D ]D  }| j                  j#                  t%        d      | j&                  ||   d       ||   ||<   |!dz  }!F | t        |      z
  } |r
t               n||z
  }"|"D ],  }| j                  j#                  t%        d      d||   i       . | D ]j  \  }}| j                  j#                  t%        d      t1        t3        j4                               | j
                  | j&                  ||| j6                  d       l | s|"s|!r| j                  j9                          |!r| j:                  j=                  d |!       t?        |      t?        |       t?        |"      fS c c}w c c}w )!u  character_outlook을 (character_id, outlook_id) 쌍 기준 delta sync.

        기대 쌍을 scene_assignments에서 파싱 → 현재 DB 쌍과 diff →
        추가 INSERT / 제거 DELETE. UPSERT는 PK(id)가 임의 uuid이므로 불필요.

        **name_matcher**: outlook/character 모두 원본 + 괄호·공백 정규화 키로 인덱스 구축.
        out-param을 쓰지 않고 local에서만 빌드해 caller dict 오염 없음.

        Args:
            skip_stale: True면 missing 쌍의 DELETE skip (Codex P1-2 — partial 시
                        부분 cp가 옛 link 정상 누락하지 않도록 보호).
        r   )r   r   c                    | j                   S Nr   rL   s    rb   <lambda>zBOutlookSyncService._delta_sync_character_outlook.<locals>.<lambda>  s        c                    | j                   S rf   r    rh   s    rb   ri   zBOutlookSyncService._delta_sync_character_outlook.<locals>.<lambda>  s    ADDrj   )key_fnvalue_fn	characterc                    | j                   S rf   rg   rh   s    rb   ri   zBOutlookSyncService._delta_sync_character_outlook.<locals>.<lambda>  s    166rj   c                    | j                   S rf   rl   rh   s    rb   ri   zBOutlookSyncService._delta_sync_character_outlook.<locals>.<lambda>  s    addrj   scene_assignmentsassignments
characterscharacter_idr   OCcharacter_namer   outlook_namezzSELECT co.id, co.character_id, co.outlook_id FROM character_outlook co WHERE co.project_id = :pid AND co.episode_id = :eid)pideidr'      z,DELETE FROM character_outlook WHERE id = :idr    z{SELECT co.id, co.character_id, co.outlook_id FROM character_outlook co WHERE co.project_id = :pid AND co.episode_id IS NULLrz   z=UPDATE character_outlook SET episode_id = :eid WHERE id = :id)r{   r    zINSERT INTO character_outlook (id, project_id, episode_id, character_id, outlook_id, created_at) VALUES (:id, :pid, :eid, :cid, :oid, :now))r    rz   r{   cidoidr;   uM   character_outlook: 화 칸 없는 legacy 행 %d개를 이 화로 이어받음) r.   r   r3   r4   r5   r!   r"   r6   r   r    r
   r7   r0   
startswithsplitr   r8   executesql_textr&   fetchallkeys
setdefaultsortedr<   r=   r>   r;   r:   r?   r@   len)#rD   r   rI   r(   r)   r   
ol_entriesrL   ol_short_to_idol_name_to_idchar_entrieschar_short_to_idchar_name_iddesired_pairssacharsccsid_rawcsidcnameosidonamer}   r~   existing_rowsexisting_maprS   row_idkeyexisting_pairslegacy_rows
legacy_map	to_insertclaimed	to_deletes#                                      rb   rB   z0OutlookSyncService._delta_sync_character_outlook   s   & 	3 WW]];/66""doo5##y0
 #% 	 5?MJq!**!**add*JM(/.

 ww}}[188""doo5##{2
 #% 	
 '3,
&2ajjAJJl 	 ,
 (!1N

 /2e++126BFF="&&r*BCE554 h8+>+>s+C NN3'*! 
 .3uu\2.nb1 '**40$((.A\5!ACB]E!BC3!%%sCj1%  78 F
 OODOO<
 (* 	 46 C"1vs1vs1vCF*Cl"KL6N
 %+S! ! \..01
 ggooG
 DOO$
 (* 	 24
C!!3q63q6"2CF;  "N2	)c*o56CGGOO * +z#?
 !+3LqLG 7 J/	
 (CEn}.L	CGGOOGH|C()  $JS#GGOOA djjl+????88 $" 	WGGMMOKK_
 =!3y>3y>AAM N,
s   (S4:S4S9S9c           
        | j                   j                  t        d      | j                  t        d      j                         }d}|D ]N  }|d   t        k(  r| j                   j                  t        d      t        | j                  |d   d       |dz  }P |rP| j                   j                          | j                  j                  d|t        |D cg c]  }|d   	 c}d	d
        |S c c}w )u  어디에도 안 붙은 아웃룩을 **표시만** 한다. ★지우지 않는다.

        ★★★종전 이름은 `_remove_orphan_outlooks` 였고 실제로 `entity_canon`
         까지 DELETE 했다. 이 파이프라인에서 아웃룩 canon 은 **프로젝트 자산**
         이라 한 화의 sync 가 지울 것이 아니다. 실측(골목 끝 da049582)에서
         1화 아웃룩 셋 중 하나가 이 경로로 사라졌다.

        ★★참조를 **넷 다** 본다 (Codex 2026-09-04). `character_outlook` 만
         보면 「관계는 없는데 링크·관계사실·이미지가 붙어 있는」 행을 고아로
         잘못 읽는다. 그런 행을 지우던 것이 앞 판의 손실이었다.
        a7  SELECT ec.id, ec.name, ec.short_id, ec.status FROM entity_canon ec WHERE ec.project_id = :pid AND ec.entity_type = 'outlook'   AND ec.short_id IS DISTINCT FROM :null_outlook   AND NOT EXISTS (SELECT 1 FROM character_outlook co                   WHERE co.outlook_id = ec.id)   AND NOT EXISTS (SELECT 1 FROM entity_episode_link el                   WHERE el.canon_id = ec.id)   AND NOT EXISTS (SELECT 1 FROM relation_participant rp                   WHERE rp.canon_id = ec.id)   AND NOT EXISTS (SELECT 1 FROM image_asset ia                   WHERE ia.entity_id = ec.id))rz   null_outlookr      zGUPDATE entity_canon SET status = :st, updated_at = :now WHERE id = :oid)str;   r~   r'   uN   아무 데도 안 붙은 아웃룩 %d개를 %s 로 표시 (삭제 안 함): %sN   )r3   r   r   r!   r	   r   r   r;   r:   r?   r@   )rD   rowsmarkedrS   rs        rb   rC   z(OutlookSyncService._mark_orphan_outlooks  s     wwx
< 
 ??4IJL MUHJ 	 C1v..GGOOHY+DHHSVLN aKF  GGMMOKK`-d/Cd!d/CBQ/G  0Ds   C.N)returnzDict[str, int])FF)
r   listrI   dictr(   boolr)   r   r   zTuple[int, int, int])r   int)__name__
__module____qualname__rc   rB   rC    rj   rb   r   r      sX    P
l !`B`B `B 	`B
 `B 
`BD'rj   r   )__doc__
__future__r   r=   typingr   r   r   
sqlalchemyr   r   app.core.entity_identityr   r	   app.core.name_matcherr
   r   app.modules.pipeliner   r1   "app.services.checkpoint_sync._baser   r   r   r   rj   rb   <module>r      s7    #  # # '= ? 5 N\ \rj   