"""Breathing baseline from word clocks: Targ (raw ASR words of the canonical DNA), version 0/1/2 (performed passports).

Rows are the text/timing half of the new breathing table (rests, section density, hook returns, coda spacing,
fragmentation). Pitch/arrangement rows need audio and come from tools/song_gates.py --form later.
python3 preparation/measure_baselines.py   -> analysis/baseline-breathing-v01.json
"""
import json,re,statistics as st,sys
from pathlib import Path
ROOT=Path(__file__).resolve().parents[3];R=ROOT/'runs/targ-song-v3-08f0d414c5-v01'
sys.path.insert(0,str(ROOT/'tools'));import song_stress as ss
UK_V='аеєиіїоуюя'
def uk_syl(w):return sum(1 for c in w.lower() if c in UK_V)
def en_syl(w):
    w=re.sub(r"[^a-z]","",w.lower())
    if not w:return 0
    g=len(re.findall(r'[aeiouy]+',w))
    if w.endswith('e') and not w.endswith(('le','ee','ye')) and g>1:g-=1
    if w.endswith('ed') and not w.endswith(('ted','ded')) and g>1:g-=1
    return max(1,g)
REST=0.6;CODA_REST=0.5
CODA_FACTS={'brand':('ґоу','go pure','goPure','seora'),'form':('форм',),'inspect':('огля',),'payment':('плат','оплат'),'returns':('повернен',),'link':('посилан','внизу')}
def norm(t):return ss.unstress(t).lower().replace('’',"'").replace('ʼ',"'")

def uk_version(name,run,lyrics,passport,section_prefixes):
    p=json.loads((ROOT/run/passport).read_text());words=[{'text':norm(w['text']),'start':float(w['time'][0]),'end':float(w['time'][1])} for w in p['wordOccurrences']]
    lines=[l for l in (ROOT/run/lyrics).read_text().splitlines() if l.strip() and not l.startswith('[')]
    phrases=[];i=0
    for l in lines:
        n=len(ss.tokens(l));ws=words[i:i+n];i+=n
        phrases.append({'text':l,'start':ws[0]['start'],'end':ws[-1]['end'],'syl':sum(uk_syl(w['text']) for w in ws),'nwords':n,'words':[w['text'] for w in ws]})
    assert i==len(words),(name,i,len(words))
    bounds={'setup':phrases[0]['start']}
    for sec,prefix in section_prefixes.items():
        m=[x for x in phrases if x['text'].startswith(prefix)];assert m,(name,prefix);bounds[sec]=m[0]['start']
    return analyse(name,p['duration'],words,phrases,bounds,uk_syl)

def targ():
    m=json.loads((ROOT/'runs/video-dna-v1/targ/joint-audio-visual-map.json').read_text())
    raw=[w for w in m['rawASRWordOccurrences'] if re.search('[A-Za-z]',w['word'])]
    words=[{'text':w['word'].strip(),'start':float(w['start']),'end':float(w['end']),'seg':w['segmentId']} for w in raw]
    phrases=[];cur=[]
    for w in words:
        cur.append(w)
        if re.search(r"[.!?,]$",w['text']) or (words.index(w)+1<len(words) and words[words.index(w)+1]['seg']!=w['seg']):
            phrases.append({'text':' '.join(x['text'] for x in cur),'start':cur[0]['start'],'end':cur[-1]['end'],'syl':sum(en_syl(x['text']) for x in cur),'nwords':len(cur),'words':[x['text'] for x in cur]});cur=[]
    if cur:phrases.append({'text':' '.join(x['text'] for x in cur),'start':cur[0]['start'],'end':cur[-1]['end'],'syl':sum(en_syl(x['text']) for x in cur),'nwords':len(cur),'words':[x['text'] for x in cur]})
    th={t['id']:t['sourceRange'] for t in json.loads((ROOT/'runs/targ-ten-c06fb16085-v01/song/source-thoughts-v01.json').read_text())['thoughts']}
    bounds={'setup':0.0,'store':th['t10'][0],'mentor':th['t20'][0],'care':th['t25'][0],'change':th['t28'][0],'return':th['t32'][0],'selfworth':th['t36'][0],'cta':th['t38'][0]}
    return analyse('targ',194.489,words,phrases,bounds,en_syl)

def analyse(name,duration,words,phrases,bounds,syl):
    gaps=[{'after':words[i]['text'],'before':words[i+1]['text'],'at':round(words[i]['end'],2),'gap':round(words[i+1]['start']-words[i]['end'],2)} for i in range(len(words)-1)]
    rests=[g for g in gaps if g['gap']>=REST]
    ends={round(p['end'],3) for p in phrases}
    line_rests=[g for g in rests if round(g['at'],3) in ends]
    boundary_share=len([p for p in phrases[:-1] if any(abs(g['at']-p['end'])<1e-3 for g in rests)])/max(1,len(phrases)-1)
    sung=sum(p['end']-p['start'] for p in phrases);nsyl=sum(p['syl'] for p in phrases)
    order=[k for k,_ in sorted(bounds.items(),key=lambda kv:kv[1])];last=max(p['end'] for p in phrases)
    sections=[]
    for i,sec in enumerate(order):
        s=bounds[sec];e=bounds[order[i+1]] if i+1<len(order) else last+0.001
        ph=[p for p in phrases if s<=p['start']<e]
        if not ph:continue
        sp=sum(p['end']-p['start'] for p in ph);sy=sum(p['syl'] for p in ph)
        sec_rests=[g for g in rests if s<=g['at']<e]
        sections.append({'id':sec,'start':round(s,2),'end':round(e,2),'seconds':round(e-s,2),'phrases':len(ph),'syllables':sy,'sungSeconds':round(sp,2),'sylPerSungSec':round(sy/sp,2) if sp else None,'sylPerSec':round(sy/(e-s),2),'rests':len(sec_rests)})
    # hook returns: identical normalised line (>=4 words) at >=3 positions >=20 s apart
    texts={}
    for p in phrases:
        key=' '.join(re.sub(r"[^\w']","",w.lower()) for w in p['words'])
        if p['nwords']>=4:texts.setdefault(key,[]).append(p['start'])
    hook=0
    for key,ts in texts.items():
        ts=sorted(ts);sep=[ts[0]]
        for t in ts[1:]:
            if t-sep[-1]>=20:sep.append(t)
        hook=max(hook,len(sep) if len(sep)>=3 else 0)
    # coda facts and the rest after each
    coda=[]
    cta_start=bounds.get('cta',last)
    for fid,keys in CODA_FACTS.items():
        m=[p for p in phrases if p['start']>=cta_start-0.5 and any(k in norm(p['text']).lower() for k in keys)]
        if not m:coda.append({'fact':fid,'found':False});continue
        p=m[0];g=next((x['gap'] for x in gaps if abs(x['at']-p['end'])<1e-3),None)
        coda.append({'fact':fid,'found':True,'text':p['text'][:50],'end':round(p['end'],2),'restAfter':g,'restOk':g is not None and g>=CODA_REST})
    facts_found=[c for c in coda if c['found']];facts_rested=[c for c in facts_found if c['restOk']]
    rates=[p['syl']/(p['end']-p['start']) for p in phrases if p['end']-p['start']>0.3 and p['syl']>=2]
    short=[p for p in phrases if p['nwords']<=2]
    return {'id':name,'duration':duration,'words':len(words),'syllables':nsyl,'phrases':len(phrases),'sungSeconds':round(sung,2),'sungShare':round(sung/duration,3),'sylPerSungSec':round(nsyl/sung,2),'sylPerSec':round(nsyl/duration,2),
      'restsGe06':len(rests),'restsGe06AtLineEnds':len(line_rests),'lineEndRestShare':round(boundary_share,3),'medianWordGap':round(st.median(g['gap'] for g in gaps),2),'medianRest':round(st.median(g['gap'] for g in rests),2) if rests else None,
      'maxSectionSylPerSungSec':max(s['sylPerSungSec'] for s in sections if s['sylPerSungSec']),'sections':sections,'hookReturns':hook,'codaFacts':coda,'codaFactsFound':len(facts_found),'codaFactsWithRest':len(facts_rested),
      'phraseRateCV':round(st.pstdev(rates)/st.mean(rates),3),'shortPhraseShare':round(len(short)/len(phrases),3),'meanWordsPerPhrase':round(len(words)/len(phrases),2),'rests':rests,'phraseRows':[{'text':p['text'],'start':round(p['start'],2),'end':round(p['end'],2),'syl':p['syl'],'rate':round(p['syl']/max(0.01,p['end']-p['start']),2)} for p in phrases]}

def main():
    out={'schema':'adfactory.song-breathing-baseline/v1','method':'Word clocks only: Targ from the canonical Video DNA raw ASR words (faster-whisper on the original); Ukrainian versions from the accepted performed-song passports (ASR word spans on the master clock, ±0.25 s). Phrases = lyric lines (Ukrainian) or punctuation/segment splits of the ASR (Targ). Rest = gap between consecutive words ≥ 0.6 s. Section rate = syllables ÷ summed phrase durations inside the section (sung span). Pitch/arrangement rows are measured separately on audio.','restThreshold':REST,'codaRestThreshold':CODA_REST,'versions':[]}
    out['versions'].append(targ())
    out['versions'].append(uk_version('v0','runs/targ-improved-b614aa3b3e-v01','song/lyrics-plain-v07.txt','song/performed-song-passport-v01.json',{'store':'У суботу','mentor':'Того тижня','care':'Наступного дня','change':'За три тижні','return':'У червні','selfworth':'Я щиро','cta':'Крем — Ґоу'}))
    out['versions'].append(uk_version('v1','runs/targ-refined-ce99390b3b-v01','song/lyrics-plain-v02.txt','song/performed-song-passport-v01.json',{'store':'У суботу','mentor':'Того тижня','care':'Наступного дня','change':'За три тижні','return':'Червень.','selfworth':'Я щиро','cta':'Ґоу П’юр'}))
    out['versions'].append(uk_version('v2','runs/targ-ten-c06fb16085-v01','song/lyrics-plain-v04.txt','song/performed-song-passport-v01.json',{'store':'У суботу','mentor':'Того тижня','care':'Назавтра','change':'За три тижні','return':'У червні','selfworth':'Я щиро','cta':'Крем називається'}))
    (R/'analysis').mkdir(exist_ok=True);(R/'analysis/baseline-breathing-v01.json').write_text(json.dumps(out,ensure_ascii=False,indent=1)+'\n')
    for v in out['versions']:
        print(v['id'],'dur',v['duration'],'syl',v['syllables'],'phr',v['phrases'],'sung',v['sungSeconds'],'rate',v['sylPerSungSec'],'rests',v['restsGe06'],'lineEndShare',v['lineEndRestShare'],'medGap',v['medianWordGap'],'maxSec',v['maxSectionSylPerSungSec'],'hook',v['hookReturns'],'coda',v['codaFactsFound'],v['codaFactsWithRest'],'short',v['shortPhraseShare'])
        print('   ',[(s['id'],s['sylPerSungSec'],s['rests']) for s in v['sections']])
if __name__=='__main__':main()
