import html, json, os, re, subprocess, uuid
from datetime import datetime
from pathlib import Path
from urllib.parse import quote

from flask import Blueprint, redirect, request, url_for
from plugin.api import enqueue, get_db, get_setting, manage_plugins_url, render_page, set_setting, table

bp = Blueprint("murat_audio_toolkit", __name__)
AUDIO_EXTENSIONS = {".mp3", ".flac", ".m4a", ".aac", ".ogg", ".opus", ".wav", ".wma", ".alac", ".ape"}
T_FILES = lambda: table("audio_files")
T_JOBS = lambda: table("jobs")
T_LOGS = lambda: table("job_logs")
T_RECENT = lambda: table("recent_analysis")
JSON_RE = re.compile(r'\{\s*"input_i".*?\}', re.S)

CSS = '''<style>
.mat-wrap{max-width:1400px;margin:auto}.mat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px}.mat-card{background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.12);border-radius:12px;padding:16px}.mat-num{font-size:28px;font-weight:700;margin-top:8px}.mat-actions{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0}.mat-btn{display:inline-block;padding:10px 14px;border-radius:8px;background:#2f80ed;color:white;text-decoration:none;border:0;cursor:pointer}.mat-btn.danger{background:#b84040}.mat-btn.secondary{background:#566270}.mat-tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:18px}.mat-tabs a{padding:8px 11px;border-radius:8px;background:rgba(255,255,255,.07);text-decoration:none;color:inherit}.mat-table{width:100%;border-collapse:collapse;font-size:14px}.mat-table th,.mat-table td{padding:9px;border-bottom:1px solid rgba(255,255,255,.1);text-align:left;vertical-align:top}.mat-progress{height:9px;background:rgba(255,255,255,.1);border-radius:10px;overflow:hidden}.mat-progress span{display:block;height:100%;background:#2f80ed}.mat-log{font-family:monospace;white-space:pre-wrap;background:#0b1018;padding:14px;border-radius:9px;max-height:420px;overflow:auto}.mat-badge{padding:3px 7px;border-radius:999px;background:rgba(255,255,255,.12);font-size:12px}.muted{opacity:.72}</style>'''

def esc(v): return html.escape("" if v is None else str(v))
def music_root(): return str(get_setting("music_root", "/music")).rstrip("/") or "/music"
def low_lufs():
    try: return float(get_setting("low_lufs_threshold", -19.0))
    except Exception: return -19.0
def short_seconds():
    try: return max(1.0, float(get_setting("very_short_seconds", 30.0)))
    except Exception: return 30.0
def timeout_seconds():
    try: return max(15, int(get_setting("ffmpeg_timeout", 180)))
    except Exception: return 180

def rows_dict(cur):
    cols=[d[0] for d in cur.description]
    return [dict(zip(cols,r)) for r in cur.fetchall()]
def one_dict(cur):
    r=cur.fetchone()
    if r is None:return None
    return dict(zip([d[0] for d in cur.description],r))
def scalar(cur,q,p=()):
    cur.execute(q,p);r=cur.fetchone();return r[0] if r else 0

def tabs():
    x=[("Genel Bakış","home"),("Ses / LUFS","loudness"),("Analiz Durumu","coverage"),("Son Analizler","recent"),("İş Kuyruğu","jobs")]
    return '<div class="mat-tabs">'+''.join(f'<a href="{url_for("murat_audio_toolkit."+e)}">{n}</a>' for n,e in x)+'</div>'
def page(body,title="Murat Audio Toolkit"):
    return render_page(CSS+f'<div class="mat-wrap">{tabs()}{body}</div>',title=title)

def migrate(db):
    c=db.cursor()
    c.execute(f'''CREATE TABLE IF NOT EXISTS {T_FILES()}(path TEXT PRIMARY KEY,size_bytes BIGINT DEFAULT 0,mtime DOUBLE PRECISION DEFAULT 0,duration DOUBLE PRECISION,codec TEXT,sample_rate INTEGER,channels INTEGER,bitrate BIGINT,integrated_lufs DOUBLE PRECISION,true_peak_db DOUBLE PRECISION,loudness_range DOUBLE PRECISION,health_status TEXT DEFAULT 'unknown',health_detail TEXT,analyzed_at TIMESTAMP,scan_error TEXT,created_at TIMESTAMP DEFAULT now(),updated_at TIMESTAMP DEFAULT now())''')
    c.execute(f'''CREATE TABLE IF NOT EXISTS {T_JOBS()}(id TEXT PRIMARY KEY,job_type TEXT,status TEXT,total_items INTEGER DEFAULT 0,processed_items INTEGER DEFAULT 0,failed_items INTEGER DEFAULT 0,skipped_items INTEGER DEFAULT 0,current_item TEXT,stop_requested BOOLEAN DEFAULT FALSE,created_at TIMESTAMP DEFAULT now(),started_at TIMESTAMP,finished_at TIMESTAMP,error_message TEXT)''')
    c.execute(f'''CREATE TABLE IF NOT EXISTS {T_LOGS()}(id BIGSERIAL PRIMARY KEY,job_id TEXT,level TEXT DEFAULT 'INFO',message TEXT,created_at TIMESTAMP DEFAULT now())''')
    c.execute(f'''CREATE TABLE IF NOT EXISTS {T_RECENT()}(item_id TEXT PRIMARY KEY,run_id TEXT,title TEXT,artist TEXT,album TEXT,file_path TEXT,tempo DOUBLE PRECISION,musical_key TEXT,scale TEXT,energy DOUBLE PRECISION,top_moods TEXT,analyzed_at TIMESTAMP DEFAULT now())''')
    db.commit();c.close()

def create_job(kind):
    jid=str(uuid.uuid4());db=get_db();c=db.cursor();c.execute(f"INSERT INTO {T_JOBS()}(id,job_type,status) VALUES(%s,%s,'queued')",(jid,kind));db.commit();c.close();return jid
def update_job(jid,**kw):
    if not kw:return
    db=get_db();c=db.cursor();c.execute(f"UPDATE {T_JOBS()} SET "+','.join(k+'=%s' for k in kw)+" WHERE id=%s",(*kw.values(),jid));db.commit();c.close()
def log(jid,msg,level="INFO"):
    db=get_db();c=db.cursor();c.execute(f"INSERT INTO {T_LOGS()}(job_id,level,message) VALUES(%s,%s,%s)",(jid,level,msg[:4000]));db.commit();c.close()
def stop_requested(jid):
    db=get_db();c=db.cursor();c.execute(f"SELECT stop_requested FROM {T_JOBS()} WHERE id=%s",(jid,));r=c.fetchone();c.close();return bool(r and r[0])
def audio_files(root):
    if not Path(root).exists():raise RuntimeError(f"Müzik dizini bulunamadı: {root}")
    for dp,dn,fn in os.walk(root):
        dn[:]=[d for d in dn if not d.startswith('.')]
        for n in fn:
            p=Path(dp)/n
            if p.suffix.lower() in AUDIO_EXTENSIONS:yield p

def scan_inventory(jid):
    try:
        update_job(jid,status='running',started_at=datetime.utcnow());paths=list(audio_files(music_root()));update_job(jid,total_items=len(paths));log(jid,f"Envanter başladı: {len(paths)} dosya")
        db=get_db();c=db.cursor();ok=bad=0
        for p in paths:
            if stop_requested(jid):update_job(jid,status='stopped',finished_at=datetime.utcnow());log(jid,'Durduruldu');c.close();return
            try:
                st=p.stat();c.execute(f'''INSERT INTO {T_FILES()}(path,size_bytes,mtime,updated_at) VALUES(%s,%s,%s,now()) ON CONFLICT(path) DO UPDATE SET size_bytes=EXCLUDED.size_bytes,mtime=EXCLUDED.mtime,updated_at=now(),analyzed_at=CASE WHEN {T_FILES()}.size_bytes<>EXCLUDED.size_bytes OR {T_FILES()}.mtime<>EXCLUDED.mtime THEN NULL ELSE {T_FILES()}.analyzed_at END''',(str(p),st.st_size,st.st_mtime));ok+=1
            except Exception as e:bad+=1;log(jid,f"{p}: {e}",'ERROR')
            if (ok+bad)%25==0:db.commit();update_job(jid,processed_items=ok,failed_items=bad,current_item=str(p))
        db.commit();c.close();update_job(jid,status='completed',processed_items=ok,failed_items=bad,current_item=None,finished_at=datetime.utcnow());log(jid,f"Tamamlandı: {ok}, hata: {bad}")
    except Exception as e:update_job(jid,status='failed',error_message=str(e),finished_at=datetime.utcnow());log(jid,str(e),'ERROR')

def probe(path):
    r=subprocess.run(['ffprobe','-v','error','-show_entries','format=duration,bit_rate:stream=codec_name,sample_rate,channels','-of','json',str(path)],capture_output=True,text=True,timeout=timeout_seconds())
    if r.returncode:raise RuntimeError(r.stderr.strip() or 'ffprobe başarısız')
    d=json.loads(r.stdout or '{}');f=d.get('format') or {};s=next(iter(d.get('streams') or [{}]),{})
    return (float(f['duration']) if f.get('duration') else None,s.get('codec_name'),int(s['sample_rate']) if s.get('sample_rate') else None,int(s['channels']) if s.get('channels') else None,int(f['bit_rate']) if f.get('bit_rate') else None)
def loud(path):
    r=subprocess.run(['ffmpeg','-hide_banner','-nostats','-i',str(path),'-af','loudnorm=I=-16:TP=-1.0:LRA=11:print_format=json','-f','null','-'],capture_output=True,text=True,timeout=timeout_seconds())
    m=list(JSON_RE.finditer((r.stdout or '')+'\n'+(r.stderr or '')))
    if not m:raise RuntimeError('FFmpeg loudnorm sonucu okunamadı')
    d=json.loads(m[-1].group(0))
    def n(k):
        try:return float(d.get(k))
        except Exception:return None
    return n('input_i'),n('input_tp'),n('input_lra')
def scan_lufs(jid,force=False):
    try:
        update_job(jid,status='running',started_at=datetime.utcnow());db=get_db();c=db.cursor();c.execute(f"SELECT path FROM {T_FILES()}"+(" ORDER BY path" if force else " WHERE analyzed_at IS NULL ORDER BY path"));items=[r[0] for r in c.fetchall()];update_job(jid,total_items=len(items));log(jid,f"LUFS taraması: {len(items)}")
        ok=bad=skip=0
        for path in items:
            if stop_requested(jid):update_job(jid,status='stopped',processed_items=ok,failed_items=bad,skipped_items=skip,finished_at=datetime.utcnow());log(jid,'Durduruldu');c.close();return
            p=Path(path)
            if not p.exists():skip+=1;c.execute(f"UPDATE {T_FILES()} SET health_status='missing',health_detail='Dosya bulunamadı' WHERE path=%s",(path,));continue
            try:
                duration,codec,sr,ch,br=probe(p);i,tp,lra=loud(p)
                if duration is not None and duration<short_seconds():status,detail='short',f'{duration:.1f} saniye'
                elif i is not None and i<low_lufs():status,detail='low_loudness',f'{i:.2f} LUFS'
                else:status,detail='ok',''
                c.execute(f'''UPDATE {T_FILES()} SET duration=%s,codec=%s,sample_rate=%s,channels=%s,bitrate=%s,integrated_lufs=%s,true_peak_db=%s,loudness_range=%s,health_status=%s,health_detail=%s,analyzed_at=now(),scan_error=NULL,updated_at=now() WHERE path=%s''',(duration,codec,sr,ch,br,i,tp,lra,status,detail,path));ok+=1
            except Exception as e:bad+=1;c.execute(f"UPDATE {T_FILES()} SET health_status='error',scan_error=%s,updated_at=now() WHERE path=%s",(str(e)[:2000],path));log(jid,f"{path}: {e}",'ERROR')
            if (ok+bad+skip)%5==0:db.commit();update_job(jid,processed_items=ok,failed_items=bad,skipped_items=skip,current_item=path)
        db.commit();c.close();update_job(jid,status='completed',processed_items=ok,failed_items=bad,skipped_items=skip,current_item=None,finished_at=datetime.utcnow());log(jid,f"Tamamlandı: {ok}, hata: {bad}, atlanan: {skip}")
    except Exception as e:update_job(jid,status='failed',error_message=str(e),finished_at=datetime.utcnow());log(jid,str(e),'ERROR')

@bp.route('/')
def home():
    db=get_db();c=db.cursor();vals=[('Envanterdeki dosya',scalar(c,f'SELECT COUNT(*) FROM {T_FILES()}')),('AudioMuse score kaydı',scalar(c,'SELECT COUNT(*) FROM score')),('LUFS taranmış',scalar(c,f'SELECT COUNT(*) FROM {T_FILES()} WHERE analyzed_at IS NOT NULL')),('Düşük sesli',scalar(c,f"SELECT COUNT(*) FROM {T_FILES()} WHERE health_status='low_loudness'")),('Sorunlu / kısa',scalar(c,f"SELECT COUNT(*) FROM {T_FILES()} WHERE health_status IN ('error','missing','short')")),('Aktif işler',scalar(c,f"SELECT COUNT(*) FROM {T_JOBS()} WHERE status IN ('queued','running')"))];c.close()
    body='<div class="mat-grid">'+''.join(f'<div class="mat-card"><div class="muted">{esc(k)}</div><div class="mat-num">{esc(v)}</div></div>' for k,v in vals)+'</div>'
    body+=f'''<div class="mat-actions"><form method="post" action="{url_for('murat_audio_toolkit.start_inventory')}"><button class="mat-btn">Dosyaların tümünü tara</button></form><form method="post" action="{url_for('murat_audio_toolkit.start_lufs')}"><button class="mat-btn">Eksik LUFS analizlerini başlat</button></form></div><div class="mat-card"><b>Salt okunur sürüm</b><p>Dosyaları değiştirmez, silmez, taşımaz veya normalize etmez.</p></div>'''
    return page(body)
@bp.post('/jobs/inventory/start')
def start_inventory():
    jid=create_job('library_inventory');enqueue(scan_inventory,jid);return redirect(url_for('murat_audio_toolkit.job_detail',job_id=jid))
@bp.post('/jobs/lufs/start')
def start_lufs():
    force=request.form.get('force')=='1';jid=create_job('loudness_scan_force' if force else 'loudness_scan');enqueue(scan_lufs,jid,force);return redirect(url_for('murat_audio_toolkit.job_detail',job_id=jid))
@bp.post('/jobs/<job_id>/stop')
def stop_job(job_id):
    db=get_db();c=db.cursor();c.execute(f"UPDATE {T_JOBS()} SET stop_requested=TRUE WHERE id=%s",(job_id,));db.commit();c.close();return redirect(url_for('murat_audio_toolkit.job_detail',job_id=job_id))
@bp.route('/jobs')
def jobs():
    db=get_db();c=db.cursor();c.execute(f'SELECT * FROM {T_JOBS()} ORDER BY created_at DESC LIMIT 100');rr=rows_dict(c);c.close();body='<div class="mat-card"><table class="mat-table"><tr><th>Tür</th><th>Durum</th><th>İlerleme</th><th></th></tr>'
    for r in rr:
        total=r['total_items'] or 0;done=(r['processed_items'] or 0)+(r['failed_items'] or 0)+(r['skipped_items'] or 0);pct=int(done*100/total) if total else 0;body+=f'<tr><td>{esc(r["job_type"])}</td><td>{esc(r["status"])}</td><td>{done}/{total}<div class="mat-progress"><span style="width:{pct}%"></span></div></td><td><a href="{url_for("murat_audio_toolkit.job_detail",job_id=r["id"])}">Aç</a></td></tr>'
    return page(body+'</table></div>','İş Kuyruğu')
@bp.route('/jobs/<job_id>')
def job_detail(job_id):
    db=get_db();c=db.cursor();c.execute(f'SELECT * FROM {T_JOBS()} WHERE id=%s',(job_id,));j=one_dict(c);c.execute(f'SELECT level,message,created_at FROM {T_LOGS()} WHERE job_id=%s ORDER BY id DESC LIMIT 150',(job_id,));logs=list(reversed(rows_dict(c)));c.close()
    if not j:return page('<div class="mat-card">İş bulunamadı</div>'),404
    total=j['total_items'] or 0;done=(j['processed_items'] or 0)+(j['failed_items'] or 0)+(j['skipped_items'] or 0);pct=int(done*100/total) if total else 0;button=f'<form method="post" action="{url_for("murat_audio_toolkit.stop_job",job_id=job_id)}"><button class="mat-btn danger">Durdur</button></form>' if j['status'] in ('queued','running') else ''
    text='\n'.join(f'{r["created_at"]} [{r["level"]}] {r["message"]}' for r in logs);body=f'<div class="mat-card"><h3>{esc(j["job_type"])}</h3><p>{done}/{total}</p><div class="mat-progress"><span style="width:{pct}%"></span></div><p>{esc(j["current_item"])}</p>{button}</div><h3>Akış</h3><div class="mat-log">{esc(text)}</div><script>setTimeout(()=>location.reload(),3000)</script>';return page(body,'İş Akışı')
@bp.route('/loudness')
def loudness():
    status=request.args.get('status','');pg=max(1,request.args.get('page',1,type=int));limit=50;off=(pg-1)*limit;where='WHERE health_status=%s' if status else '';params=[status] if status else [];db=get_db();c=db.cursor();total=scalar(c,f'SELECT COUNT(*) FROM {T_FILES()} {where}',params);c.execute(f'SELECT path,duration,codec,bitrate,integrated_lufs,true_peak_db,loudness_range,health_status,health_detail,scan_error FROM {T_FILES()} {where} ORDER BY path LIMIT %s OFFSET %s',(*params,limit,off));rr=rows_dict(c);c.close()
    filters=' '.join(f'<a class="mat-btn secondary" href="?status={s}">{n}</a>' for s,n in [('', 'Tümü'),('low_loudness','Düşük ses'),('short','Kısa'),('error','Hata'),('ok','Normal')]);body=f'<div class="mat-actions">{filters}<form method="post" action="{url_for("murat_audio_toolkit.start_lufs")}"><button class="mat-btn">Eksikleri tara</button></form><form method="post" action="{url_for("murat_audio_toolkit.start_lufs")}"><input type="hidden" name="force" value="1"><button class="mat-btn danger">Tümünü yeniden tara</button></form></div><div class="mat-card"><p>Toplam: {total}</p><table class="mat-table"><tr><th>Dosya</th><th>LUFS</th><th>Peak</th><th>LRA</th><th>Süre</th><th>Codec</th><th>Durum</th></tr>'
    for r in rr:body+=f'<tr><td>{esc(r["path"])}</td><td>{esc(r["integrated_lufs"])}</td><td>{esc(r["true_peak_db"])}</td><td>{esc(r["loudness_range"])}</td><td>{esc(r["duration"])}</td><td>{esc(r["codec"])}</td><td>{esc(r["health_status"])}<br>{esc(r["health_detail"] or r["scan_error"])}</td></tr>'
    body+='</table></div>';prev=f'<a class="mat-btn secondary" href="?status={quote(status)}&page={pg-1}">Önceki</a>' if pg>1 else '';nxt=f'<a class="mat-btn secondary" href="?status={quote(status)}&page={pg+1}">Sonraki</a>' if off+limit<total else '';return page(body+f'<div class="mat-actions">{prev}{nxt}</div>','Ses / LUFS')
@bp.route('/coverage')
def coverage():
    db=get_db();c=db.cursor();c.execute("SELECT column_name FROM information_schema.columns WHERE table_schema=current_schema() AND table_name='score'");cols={r[0] for r in c.fetchall()};pc=next((x for x in ('file_path','path','filepath') if x in cols),None);score=scalar(c,'SELECT COUNT(*) FROM score');files=scalar(c,f'SELECT COUNT(*) FROM {T_FILES()}')
    if pc:matched=scalar(c,f'SELECT COUNT(*) FROM {T_FILES()} f WHERE EXISTS(SELECT 1 FROM score s WHERE s.{pc}=f.path)');body=f'<div class="mat-grid"><div class="mat-card">Score<div class="mat-num">{score}</div></div><div class="mat-card">Envanter<div class="mat-num">{files}</div></div><div class="mat-card">Yol eşleşen<div class="mat-num">{matched}</div></div><div class="mat-card">Eşleşmeyen<div class="mat-num">{max(0,files-matched)}</div></div></div>'
    else:body=f'<div class="mat-card"><p>score tablosunda dosya yolu sütunu bulunamadı.</p><p>Score: <b>{score}</b> — Envanter: <b>{files}</b></p></div>'
    c.close();return page(body,'Analiz Durumu')
@bp.route('/recent')
def recent():
    db=get_db();c=db.cursor();c.execute(f'SELECT * FROM {T_RECENT()} ORDER BY analyzed_at DESC LIMIT 200');rr=rows_dict(c);c.close();body='<div class="mat-card"><table class="mat-table"><tr><th>Zaman</th><th>Parça</th><th>Sanatçı</th><th>Tempo</th><th>Enerji</th><th>Mood</th></tr>'+''.join(f'<tr><td>{esc(r["analyzed_at"])}</td><td>{esc(r["title"])}</td><td>{esc(r["artist"])}</td><td>{esc(r["tempo"])}</td><td>{esc(r["energy"])}</td><td>{esc(r["top_moods"])}</td></tr>' for r in rr)+'</table></div>';return page(body,'Son Analizler')
@bp.route('/settings',methods=['GET','POST'])
def settings():
    if request.method=='POST':
        for k,d in [('music_root','/music'),('low_lufs_threshold','-19'),('very_short_seconds','30'),('ffmpeg_timeout','180')]:set_setting(k,request.form.get(k,d).strip())
        return redirect(manage_plugins_url())
    body=f'''<div class="mat-card"><form method="post"><label>Müzik dizini</label><input name="music_root" value="{esc(get_setting('music_root','/music'))}" style="width:100%;padding:9px;margin:5px 0 14px"><label>Düşük ses eşiği (LUFS)</label><input name="low_lufs_threshold" value="{esc(get_setting('low_lufs_threshold',-19))}" style="width:100%;padding:9px;margin:5px 0 14px"><label>Kısa dosya sınırı (sn)</label><input name="very_short_seconds" value="{esc(get_setting('very_short_seconds',30))}" style="width:100%;padding:9px;margin:5px 0 14px"><label>FFmpeg zaman aşımı (sn)</label><input name="ffmpeg_timeout" value="{esc(get_setting('ffmpeg_timeout',180))}" style="width:100%;padding:9px;margin:5px 0 14px"><button class="mat-btn">Kaydet</button></form></div>''';return page(body,'Ayarlar')

def on_song_analyzed(song):
    m=song.get('metadata') or {};a=song.get('analysis') or {};db=get_db();c=db.cursor();c.execute(f'''INSERT INTO {T_RECENT()}(item_id,run_id,title,artist,album,file_path,tempo,musical_key,scale,energy,top_moods,analyzed_at) VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,now()) ON CONFLICT(item_id) DO UPDATE SET run_id=EXCLUDED.run_id,title=EXCLUDED.title,artist=EXCLUDED.artist,album=EXCLUDED.album,file_path=EXCLUDED.file_path,tempo=EXCLUDED.tempo,musical_key=EXCLUDED.musical_key,scale=EXCLUDED.scale,energy=EXCLUDED.energy,top_moods=EXCLUDED.top_moods,analyzed_at=now()''',(str(song.get('item_id')),str(song.get('run_id') or ''),m.get('title'),m.get('artist') or m.get('album_artist'),m.get('album') or m.get('album_name'),m.get('file_path'),a.get('tempo'),a.get('key'),a.get('scale'),a.get('energy'),json.dumps(song.get('top_moods') or {},ensure_ascii=False)));db.commit();c.close()
def register(ctx):
    ctx.add_blueprint(bp);ctx.add_menu_item('Murat Audio Toolkit','murat_audio_toolkit.home',admin_only=True);ctx.on_install(migrate);ctx.on_song_analyzed(on_song_analyzed)
