"""Design Notification's fictional voice and read only the source poem verbatim."""
import argparse, base64, concurrent.futures, hashlib, json, os, re, subprocess, tempfile
from pathlib import Path

ROOT=Path(__file__).resolve().parent
OUT=ROOT/'production/audio'
OUT.mkdir(parents=True,exist_ok=True)
DESCRIPTION='A fictional female AI phone assistant, adult voice with a clear neutral American accent. Warm, intimate, softly persuasive and impeccably articulate. Medium pitch, smooth clean tone, calm conversational pace, gentle smile in the voice with a faint unsettling composure. Natural human speech rather than robotic effects. Dry close microphone, no background music, no reverb, no whispering. Read poetry precisely with restrained expression.'

def key():
    if os.environ.get('ELEVENLABS_API_KEY'):return os.environ['ELEVENLABS_API_KEY']
    for filename in ['/home/cdxker/.config/poetry-studio/dev.vars','/home/cdxker/.env','/home/cdxker/work/tominister/startracks-ai-music/.env']:
        p=Path(filename)
        if not p.exists():continue
        m=re.search(r'^\s*(?:export\s+)?ELEVENLABS_API_KEY\s*=\s*(.*?)\s*$',p.read_text(),re.M)
        if m:return m[1].strip().strip('\"\'')
    raise RuntimeError('ELEVENLABS_API_KEY is unavailable')

def api(method,path,payload=None):
    # Secret passed over stdin, never command arguments or saved request files.
    config='header = "xi-api-key: '+key()+'"\n'
    command=['curl','--silent','--show-error','--max-time','180','--config','-','--request',method,'--header','Content-Type: application/json','--write-out','\n%{http_code}','https://api.elevenlabs.io/v1'+path]
    with tempfile.TemporaryDirectory() as d:
        if payload is not None:
            p=Path(d)/'body.json';p.write_text(json.dumps(payload));command+=['--data-binary','@'+str(p)]
        result=subprocess.run(command,input=config,text=True,capture_output=True)
    if result.returncode:raise RuntimeError(f'ElevenLabs transport failed ({result.returncode}); do not blindly retry a paid request')
    body,status=result.stdout.rsplit('\n',1)
    response=json.loads(body)
    if not 200<=int(status)<300:raise RuntimeError(f'ElevenLabs HTTP {status}: '+json.dumps(response)[:600])
    return response

def save(path,data):
    tmp=path.with_suffix('.tmp');tmp.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n');tmp.replace(path)

def source():
    frames=json.loads((ROOT/'editor-data.json').read_text())
    body=json.loads((ROOT/'source.json').read_text())['body']
    tokens=lambda s:re.findall(r"[\w]+(?:['’][\w]+)*",s)
    assert tokens('\n\n'.join(f['poem'] for f in frames))==tokens(body),'Storyboard must cover every poem word in order'
    for f in frames:assert f['poem'] in body
    return frames,body

def design():
    path=ROOT/'production/notification-voice.json'
    if path.exists():return json.loads(path.read_text())
    frames,_=source()
    text='\n\n'.join(f['poem'] for f in frames[:2])
    preview_path=OUT/'voice-design.json'
    if preview_path.exists():result=json.loads(preview_path.read_text())
    else:
        result=api('POST','/text-to-voice/design',{'voice_description':DESCRIPTION,'model_id':'eleven_multilingual_ttv_v2','text':text,'auto_generate_text':False,'seed':12052026,'guidance_scale':5})
        for i,preview in enumerate(result['previews']):
            (OUT/f'voice-preview-{i+1}.mp3').write_bytes(base64.b64decode(preview.pop('audio_base_64')))
        save(preview_path,result)
    assert result['text']==text
    chosen=result['previews'][0]['generated_voice_id']
    response=api('POST','/text-to-voice',{'voice_name':'Notification - the phone','voice_description':DESCRIPTION,'generated_voice_id':chosen})
    record={'voice_id':response['voice_id'],'name':response.get('name'),'description':DESCRIPTION,'preview':'production/audio/voice-preview-1.mp3','preview_text':text,'provider':'ElevenLabs','model':'eleven_multilingual_v2','fictional':True}
    save(path,record);print('Created fictional Notification voice.',flush=True)
    return record

def generate(frame,voice):
    n=frame['number'];text=frame['poem'];audio=OUT/f'frame-{n:02d}.mp3';meta=OUT/f'frame-{n:02d}.json'
    if meta.exists() and audio.exists():
        previous=json.loads(meta.read_text());assert previous['text']==text and previous['voice_id']==voice['voice_id'];return previous
    payload={'text':text,'model_id':'eleven_multilingual_v2','apply_text_normalization':'off','seed':20261005+n,'voice_settings':{'stability':0.65,'similarity_boost':0.8,'style':0.12,'use_speaker_boost':True,'speed':1.0}}
    result=api('POST',f"/text-to-speech/{voice['voice_id']}/with-timestamps?output_format=mp3_44100_128",payload)
    audio.write_bytes(base64.b64decode(result.pop('audio_base64')))
    alignment=result.get('alignment') or {}
    chars=''.join(alignment.get('characters',[]))
    # Whitespace can differ in alignments; the submitted text remains byte-exact.
    words=lambda s:re.findall(r"[\w]+(?:['’][\w]+)*",s)
    assert words(chars)==words(text),f'Alignment wording differs in frame {n}'
    duration=float(subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration','-of','default=nw=1:nk=1',str(audio)]))
    record={'frame':n,'file':str(audio.relative_to(ROOT)),'durationSeconds':duration,'text':text,'voice_id':voice['voice_id'],'input_sha256':hashlib.sha256(text.encode()).hexdigest(),'alignment':alignment,'normalized_alignment':result.get('normalized_alignment'),'alignment_verbatim':True}
    save(meta,record);print(f'Frame {n:02d}: {duration:.2f}s, exact poem input and alignment verified.',flush=True)
    return record

if __name__=='__main__':
    p=argparse.ArgumentParser();p.add_argument('action',choices=['check','design','generate']);args=p.parse_args()
    frames,body=source()
    if args.action=='check':
        info=api('GET','/user/subscription');print(json.dumps({k:info.get(k) for k in ['tier','character_count','character_limit','can_extend_character_limit']}));print('Exact source coverage verified:',len(frames),'frames,',sum(len(f['poem']) for f in frames),'characters')
    elif args.action=='design':design()
    else:
        voice=design()
        with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(lambda f:generate(f,voice),frames))
        save(ROOT/'production/voiceover-manifest.json',{'provider':'ElevenLabs','voice_id':voice['voice_id'],'source_sha256':hashlib.sha256(body.encode()).hexdigest(),'text_policy':'Every spoken word comes verbatim from source.json. PING is read. No additional dialogue.','frames':sorted(results,key=lambda x:x['frame'])})
        print('All 18 voiceovers ready.',flush=True)
