#!/usr/bin/env python3
"""Single local Chatterbox worker. Run manually first; optional audio, not agent control."""
import argparse, json, os, shutil, subprocess, sys, time
from pathlib import Path
from voice_hook import connect

def lock_worker(state):
    handle = open(state / 'worker.lock', 'a+b')
    handle.seek(0); handle.write(b'0'); handle.flush(); handle.seek(0)
    if os.name == 'nt':
        import msvcrt
        msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
    else:
        import fcntl
        fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
    return handle  # Keep open; OS releases lock if worker dies.

def play(path):
    if sys.platform == 'darwin':
        subprocess.run(['afplay', str(path)], check=True, timeout=90)
    elif os.name == 'nt':
        import winsound
        winsound.PlaySound(str(path), winsound.SND_FILENAME)
    elif shutil.which('aplay'):
        subprocess.run(['aplay', str(path)], check=True, timeout=90)
    else:
        raise RuntimeError('No player; use --no-play and inspect the WAV manually')

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--config', type=Path, default=Path(__file__).with_name('config.json'))
    ap.add_argument('--once', action='store_true')
    ap.add_argument('--dry-run', action='store_true', help='Queue test only: no model, download, audio, or playback')
    ap.add_argument('--no-play', action='store_true', help='Generate and retain a WAV without playing it')
    args = ap.parse_args()
    config_path = args.config.resolve()
    cfg = json.loads(config_path.read_text(encoding='utf-8'))
    state = Path(cfg['state_dir']).expanduser().resolve()
    db = connect(state)
    lock = lock_worker(state)
    model = None
    try:
        while True:
            cfg = json.loads(config_path.read_text(encoding='utf-8'))
            if cfg.get('mute', False): return
            with db:
                db.execute("UPDATE jobs SET status='expired', text=NULL WHERE status='queued' AND created < ?", (time.time()-300,))
                row = db.execute("SELECT id,voice,text FROM jobs WHERE status='queued' ORDER BY created,id LIMIT 1").fetchone()
                if row: db.execute("UPDATE jobs SET status='started' WHERE id=?", (row[0],))
            if not row:
                if args.once: return
                time.sleep(1); continue
            job_id, role, text = row
            wav_path = state / (job_id + '.wav')
            try:
                reference = Path(cfg['voices'][role]).expanduser()
                if not reference.is_absolute(): reference = config_path.parent / reference
                reference = reference.resolve(strict=True)
                if args.dry_run:
                    print(json.dumps({'voice': role, 'characters': len(text), 'mode': 'dry-run'}))
                else:
                    if model is None:
                        from chatterbox.tts import ChatterboxTTS
                        model = ChatterboxTTS.from_pretrained(device=cfg.get('device', 'cpu'))
                    import torchaudio
                    audio = model.generate(text, audio_prompt_path=str(reference))
                    torchaudio.save(str(wav_path), audio, model.sr)
                    if not args.no_play:
                        play(wav_path)
                        wav_path.unlink(missing_ok=True)
                    print(json.dumps({'voice':role, 'status':'generated' if args.no_play else 'played'}))
                status = 'dry-run' if args.dry_run else 'generated' if args.no_play else 'played'
            except Exception as exc:
                print('Narration failed: ' + type(exc).__name__, file=sys.stderr)
                status = 'failed'  # No retries or provider fallback.
            with db:
                # Keep hashes/status for dedupe; remove spoken text after processing.
                db.execute('UPDATE jobs SET status=?, text=NULL WHERE id=?', (status, job_id))
            if args.once: return
    finally:
        db.close(); lock.close()

if __name__ == '__main__':
    try: main()
    except KeyboardInterrupt: pass
