
    RjC                    *   d Z ddlmZ ddlZddlZddlmZ g dZg dg dg d	g d
g ddgddgddgddgddgg ddZ	d/dZ
ddddddddddd
Zh dh dh d h d!d"Zd0d#Zd$Zd1d%Zd1d&Zd2d'Zd3d(Zd4d)Zd5d*Zd+Zd6d,Zd7d8d-Z	 d7	 	 	 	 	 	 	 d9d.Zy):a  EPG spreadsheet -> Crystal schedule frame.

The dashboard's file-upload path expects a schedule with the columns
`validate_schedule()` requires: event_name, country_name, channel_name,
telecast_type and hour. A raw EPG export has none of those directly, so this
module derives them.

Nothing here writes to tvviewers. The only DB access is three read-only
reference lookups (channel bridge, event vocabulary), which the caller
supplies as DataFrames so this module stays testable offline.

Every derived row carries a `confidence` column. Rows below "high" are meant
to land in the existing analyst-review screen rather than be trusted silently
-- a wrong telecast_type moves an estimate by an order of magnitude.
    )annotationsN   )classify)channel_namechannel_countries
prog_titleprog_st_time)r   channelepg_channel)r   country_namecountry	territory)r   description	programmeprogram_titletitle)r	   country_start_time
start_timeprog_start_time)prog_en_timecountry_end_timeend_timeprog_end_timetelecast_typesports_event
event_namesports_teamsteamsmatch_levelstagesports_category	sub_genre)country_timezonetimezonetz)r   r   r   r	   r   r   r   r   r   r!   r#   c                L   | j                         }| j                  D ci c]*  }t        |      j                         j	                         |, }}t
        j                         D ]8  \  }}||j                  v r|D ]  }|j                  |      }|| |   ||<    8 : |S c c}w )zAdd canonical column names for whichever aliases this file happens to use.

    Returns a copy. Original columns are left in place, so nothing downstream
    that reads the file's own names breaks.
    )copycolumnsstrstriplowerCOLUMN_ALIASESitemsget)dfoutcr+   canonaliasesasrcs           4/var/www/html/crystal/crystal2/engine/epg_adapter.pycanon_columnsr7   4   s     '')C02

;1SV\\^!!#Q&;E;(..0 wCKK 	A))A,CWE
		 J <s   /B!t20zt20 world cupz	world cupzcricket world cup )
t20iwt20t20wcwccwchltshlhlssplwu>   icciplodir8   testcricket>   eplfifauefalaligasoccerfootball>   nrlsixrugbynations>   atpwtarolandtennis	wimbledon)rH   rN   rQ   rV   c                4     t         fdt        D              S )zTrue if this looks like a raw EPG listings export rather than a schedule.

    Detected by signature columns, not filename, so either file type can be
    uploaded through the same box.
    c              3  L   K   | ]  }|t              j                  v   y wN)r7   r(   ).0r1   r/   s     r6   	<genexpr>z is_epg_export.<locals>.<genexpr>`   s!     I!qM"%---Is   !$)allREQUIRED_EPG_COLS)r/   s   `r6   is_epg_exportr_   Z   s     I7HIII    )epg_channel_bridgeepg_event_vocabepg_country_aliasc                    ddl m} | j                  d      }|r8 ||      t        fdt        D              rt        fdt        D              S t        |       S )a  Fetch the three reference tables the adapter needs. Read-only.

    Prefers the local snapshot written by worker/build_cache.py. The event
    vocabulary alone is a GROUP BY over 5.9M rows on a remote server; querying
    it per run cost ~85s before a single listing had been read.
    r   Pathfixtures_dirc              3  J   K   | ]  }| d z  j                           ywz.parquetN)existsr[   nr1   s     r6   r\   z%load_epg_reference.<locals>.<genexpr>r   s%     AqcN"**,As    #c              3  T   K   | ]  }t        j                  | d z         ! ywri   )pdread_parquetrk   s     r6   r\   z%load_epg_reference.<locals>.<genexpr>s   s%     Qs(^);<Qs   %()pathlibrf   r.   r]   
EPG_FRAMEStuplefetch_epg_reference)cfgrf   cacher1   s      @r6   load_epg_referencerv   f   sL     GGN#EKAjAAQjQQQs##r`   c                    ddl m}  ||       }t        j                  d|      }t        j                  d|      }t        j                  d|      }|||fS )zHAlways go to the server. build_cache.py calls this to make the snapshot.r   )reference_enginez
        SELECT gsiq_channel, real_channel, COUNT(*) AS n
        FROM gl_ratings
        WHERE real_channel IS NOT NULL AND TRIM(real_channel) <> ''
        GROUP BY gsiq_channel, real_channel
        ORDER BY n DESC
    a8  
        SELECT a.event_name AS canonical, a.event_alias AS alias,
               COALESCE(g.n, 0) AS weight
        FROM event_alias a
        LEFT JOIN (SELECT event_name, COUNT(*) n FROM global_sports
                   WHERE event_name IS NOT NULL GROUP BY event_name) g
               ON g.event_name = a.event_name
        WHERE a.event_alias IS NOT NULL AND TRIM(a.event_alias) <> ''
        UNION
        SELECT event_name, event_name, COUNT(*) FROM global_sports
        WHERE event_name IS NOT NULL AND TRIM(event_name) <> ''
        GROUP BY event_name
    z
        SELECT country, alias_country FROM country_alias
        WHERE alias_country IS NOT NULL AND TRIM(alias_country) <> ''
    )	referencerx   rn   read_sql)rt   rx   engbridgevocabaliass         r6   rs   rs   x   sj    +
3
C[[  
F KK  
E" KK  
E 5%r`   c                    ddl }ddlm} |j                   ||      j	                               }t        |      \  }}}t        | |||      S )zCConvenience wrapper: load reference tables from config, then adapt.r   Nre   )jsonrp   rf   loads	read_textrv   adapt)epgconfig_pathr   rf   rt   r|   r}   r~   s           r6   	adapt_epgr      sF    
**T+&002
3C-c2FE5feU++r`   c                z    t        j                  ddt        |       j                               j	                         S )z;Loose key for event matching: lowercase, alphanumeric only.z
[^a-z0-9]+ )resubr)   r+   r*   )ss    r6   _normr      s(    66-c!flln5;;==r`   c                    t               }t        |       j                         D ]>  }t        j	                  ||      }|j                  d |j                         D               @ |S )Nc              3  &   K   | ]	  }|s|  y wrZ    )r[   xs     r6   r\   z_tokens.<locals>.<genexpr>   s     -11-s   )setr   splitSYNONYMSr.   update)r   r0   ws      r6   _tokensr      sR    
%C1X^^ .LLA

-aggi--. Jr`   c                N    t         j                         D ]  \  }}| |z  s|c S  y rZ   )SPORT_MARKERSr-   )toksfammarkss      r6   _familyr      s/    #))+ 
U%<J r`   g{Gz?c                   g }| j                         D ]q  \  }\  }}t        |      }t        |      dk  r#|j                  ||t	        |xs d      t        t        |            t        d |D              t        |      f       s |S )a  Tokenise every alias once.

    resolve_event used to call _tokens() on all 5,378 aliases for every row it
    was given. On a 5,400-row file that is ~29 million tokenisations to answer
    33 distinct questions, and it dominated the whole pipeline at 71 seconds.
       r   c              3  <   K   | ]  }|j                           y wrZ   )isdigit)r[   ts     r6   r\   z!_prepare_vocab.<locals>.<genexpr>   s     4		4s   )r-   r   lenappendintr   anyr   )r}   r0   r~   r2   weighta_tokss         r6   _prepare_vocabr      s     C"'++- Evv;?

FE3v{#3WWU^5L4V44eElD 	E	E Jr`   c                z   t        |       }|syt        |      }g }|j                  t        |             }|r|j	                  d|d   |d   f       t        |       }|xs t        |      D ]h  \  }}	}
}}}||k  s|r0t        j                  dt        j                  |      z   dz   |      sB|r|r||k7  rL|j	                  t        |      |	|
f       j |syt        d |D              }|D cg c]  }|dk(  s|d   t        |z  k\  s| }}t        |xs |      \  }}	}|dk(  r|	dfS |	|d	k\  rdfS d
fS c c}w )aI  Map a free-text EPG title onto a canonical global_sports event_name.

    vocab maps normalised alias -> (canonical event name, evidence row count).
    Returns (event_name, confidence).

    Two rules decide it. The alias with the most tokens wins, so a specific
    event beats a generic one. But a candidate is only admissible if it carries
    a meaningful share of the evidence held by the best-evidenced candidate --
    otherwise a 4-row misspelling of an event outranks the 25,934-row real one
    purely for being longer, and every row attached to it becomes unestimable.
    )Nlowc   r   r   z\bc              3  (   K   | ]
  \  }}}|  y wrZ   r   )r[   _r   s      r6   r\   z resolve_event.<locals>.<genexpr>   s     (wq!Q(s   r   high   medium)r   r   r.   r   r   r   r   searchescaper   maxEVIDENCE_FLOOR)r   r}   preparedt_tokst_famcandsexactt_normr   r2   r   a_fam	has_digita_normbest_wr1   viablen_tokr   s                      r6   resolve_eventr      sb    U^FFOE )+EIIeEl#Eb%(E!H-.5\F<D<]W\H] 37vui RYYuryy/@'@5'H&Q Uu~c&k5&123  (%((FQA&A+1&9P1PaQFQ&/E*OE5!{f}EQJ&44H44	 Rs   0D8
D8c                d   !" t        |       }t        D cg c]  }||j                  vs| }}|r9t        ddj	                  |      z   dz   dj	                  d |D              z         t        |      }|j                  g d      }|t        |      z
  }|j                  dg      j                  d	g      }	|j                  |	d	dg   d
d	d      }|d   |d<   |t        |      rt        |d   |d         D 
ci c]M  \  }
}t        |
t              r8|
j                         r(t        |
      j                         j                         |O c}}
"|d   j                  "fd      |d<   t        j                   |d   d      }|j"                  j$                  |d<   |j"                  j&                  |d<   ||d<   d|j                  v rt        j                   |d   d      nt        j(                  |d<   d|j                  v r0|d   j+                  t              j                  j                         n t        j,                  d|j.                        }|j1                  dddd      }|d   j3                  d      j+                  t              }|j5                         D ci c]  }|t7        |       c} t        j8                  |j                   fd      |j                   fd       d!|j.                        }|j;                  |dk7  |d"         |d<   t        j,                  d#|j.                        j;                  |dk7  |d$         |d%<   d&|j                  v rEt        |d'   |d(   |d&         D 
ci c]!  \  }
}}t=        |
      |t?        |xs d)      f# }}}
}n0t        |d'   |d(         D 
ci c]  \  }
}t=        |
      |d*f }}
}tA        |      }|j5                         D ci c]  }|tC        |||       c}!t        j8                  |j                  !fd+      |j                  !fd,      d-|j.                        }t        jD                  ||gd*.      }d/|j                  v r|d0   jG                         }|jI                         r|jJ                  |d/f   j3                  d      j+                  t              }|j5                         D ci c]  }|tC        |||       c}|j                  fd1      |jJ                  |d0f<   |j                  fd2      |jJ                  |d3f<   d4D ]  }||j                  vsd||<    d5d*d)d6}t        |jL                  j                  |      |jN                  j                  |      |jP                        D cg c]+  \  }}}tS        ||t        |t              r|rd5nd)gd7 8      - c}}}|d9<   |jU                         D ci c]  \  }}||
 }}}|d9   j                  |      |d9<   |jW                  d9d:i;      }|d   |d<<   |g d=   }||jX                  d><   ||jX                  d?<   |j[                  d@A      S c c}w c c}}
w c c}w c c}}}
w c c}}
w c c}w c c}w c c}}}w c c}}w )Ba   Convert a raw EPG frame into a Crystal schedule frame.

    channel_bridge: columns gsiq_channel, real_channel (from gl_ratings)
    event_vocab:    columns canonical, alias, weight (event_alias + global_sports)
    country_alias:  columns country, alias_country (from country_alias)
    z&EPG file is missing required columns: z, z. Accepted names for each: z; c              3  V   K   | ]!  }| d dj                  t        |           # yw)z = /N)joinr,   )r[   r1   s     r6   r\   zadapt.<locals>.<genexpr>  s*     N3sxxq(9:;<Ns   '))r   r   r	   )subsetreal_channelgsiq_channelr   left)left_onright_onhowr   r   alias_countryr   c                r    j                  t        |       j                         j                         |       S rZ   )r.   r)   r*   r+   )vamaps    r6   <lambda>zadapt.<locals>.<lambda>.  s%    dhhs1v||~335q9 r`   r	   coerce)errors	prog_datehourstart_dtr   end_dtr   r9   )index)nanNone-r   c                    |    d   S Nr   r   r   _clss    r6   r   zadapt.<locals>.<lambda>C  s    a r`   c                    |    d   S Nr   r   r   s    r6   r   zadapt.<locals>.<lambda>D      $q'!* r`   )_tt_ttcr   r   r   tt_confidencer   r~   	canonicalr   r   c                    |    d   S r   r   r   _ress    r6   r   zadapt.<locals>.<lambda>U  s    471: r`   c                    |    d   S r   r   r   s    r6   r   zadapt.<locals>.<lambda>V  s    d1gaj r`   )r   ev_confidence)axisr   r   c                    |    d   S r   r   r   _alts    r6   r   zadapt.<locals>.<lambda>]  r   r`   c                    |    d   S r   r   r   s    r6   r   zadapt.<locals>.<lambda>^  s    T!WQZ r`   r   )r   r   r!   r#   r   )r   r   r   c                    | S rZ   r   )r   s    r6   r   zadapt.<locals>.<lambda>i  s    PQ r`   )key
confidenceepg_confidence)r(   broadcast_date)r   r   r   r   r   r   r   r   r   r   r   r   r   r   r!   r#   r   r   n_rawn_duplicates_removedT)drop).r7   r^   r(   
ValueErrorr   r   drop_duplicatesdropnamergezip
isinstancer)   r*   r+   maprn   to_datetimedtdater   NaTastypeSeriesr   replacefillnauniquer   	DataFramewherer   r   r   r   concatisnar   locr   r   r   minr-   renameattrsreset_index)#r   channel_bridgeevent_vocabcountry_aliasr/   r1   missingr   n_dedupbrr4   tsstated_titlesr   derivedr   r}   r   evneedsecolrankr_ttr_evrckr   invr0   r   r   r   r   s#                                  @@@@r6   r   r     sn    
s	B+CQq

/BqCGC4tyy7II)*IINgNNOP 	P GE 
		#Q		RBc"goG 

'7

8(0@A 	"nn56(>v 
 
OB /0B~ S%7o >i@XY5Aqa%!'') A$$&) 5  /339;>	>*8	<BeejjB{OBvJBzN &3 NN2n#5hG9; xL !BJJ. !((-1177946IIb4Q ^^B<=F %%b)005G$+NN$45qAx{N5Dll7;;/C#D$+KK0D$EGNPhhXG ,,v|WU^DB))F"((;AA"gfo'B ;&&&[);{+C[QYEZ[] ]1aqAs16{++ ] ] [);{+CDFdaqAq6! F F
 e$H:A..:JKQA}Qx00KD	W[[1E%F(/4H(IKRTRZRZ
\B	B8!	$B#,$$&88:n,-44R8??DBBD))+NQA}Qx88ND)+0D)EBFF4%&,.FF3G,HBFF4() V bjj BsG
 1-D """2"2"6"6t"<"$"2"2"6"6t"<"$//3 D$ 	T4jS1ba@kRB| !JJL
)DAq1a4
)C
),'++C0B| 
L*:;	<Bk?B
 $ %C
 CIIg(/CII$%???%%Y D250 6]F L O *s;   Y>Y>AZ+Z	&ZZ>Z-Z 90Z%Z,)r/   pd.DataFramereturnr&  )r/   r&  r'  bool)rt   dictr'  z/tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame])r   r&  r'  r&  )r   r)   r'  r)   )r   r)   r'  set[str])r   r*  r'  z
str | None)r}   dict[str, tuple[str, int]]r'  listrZ   )r   r)   r}   r+  r'  ztuple[str | None, str])
r   r&  r  r&  r  r&  r  zpd.DataFrame | Noner'  r&  )__doc__
__future__r   r   pandasrn   	epg_rulesr   r^   r,   r7   r   r   r_   rq   rv   rs   r   r   r   r   r   r   r   r   r   r`   r6   <module>r1     s   # 	  %  DV]`Z **(,7('2'1+[9?$0 5?
12Rb ?G/=	J L
$$( V,>
 "/5j 04w&&w&#w& -w& 9Ew&r`   