#!/usr/bin/env python3
from __future__ import annotations
import argparse, fcntl, os, sqlite3, traceback, signal
from pathlib import Path
from datetime import datetime, timezone
from discover import run as discover_run
from scanner import scan
from variant_engine import apply_product as apply_variant_intelligence
from dbutil import setting_int, log_event
from image_fetcher import queue_product as queue_image

DB=os.environ.get('FIYAT_RADAR_DB','/var/lib/fiyat-radar/fiyat-radar.sqlite')
LOCK=os.environ.get('FIYAT_RADAR_BOOTSTRAP_LOCK','/var/lib/fiyat-radar/bootstrap.lock')

def now(): return datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')


class BootstrapDeadline(RuntimeError): pass

def _deadline_handler(signum,frame):
    raise BootstrapDeadline('Bootstrap kısa tur zaman bütçesi doldu')

def connect():
    c=sqlite3.connect(DB,timeout=30);c.row_factory=sqlite3.Row
    c.execute('PRAGMA foreign_keys=ON');c.execute('PRAGMA busy_timeout=30000');c.execute('PRAGMA synchronous=NORMAL')
    return c

def _reap_expired_leases(c):
    c.execute("""UPDATE product_bootstrap_requests
                 SET status='pending',started_at=NULL,lease_until=NULL,heartbeat_at=NULL,next_attempt_at=CURRENT_TIMESTAMP,
                     note='v0.3.15 lease watchdog: süresi dolan bootstrap yeniden kuyruğa alındı'
                 WHERE status='running' AND (lease_until IS NULL OR lease_until<CURRENT_TIMESTAMP)""")

def claim_next():
    # v0.3.10: başarısız aynı birkaç ürün kuyruğun başını sonsuza dek tutmasın.
    # attempts düşük olan ve daha eski bekleyen iş önce gelir; next_attempt_at backoff'a uyar.
    c=connect()
    try:
        _reap_expired_leases(c)
        row=c.execute("""UPDATE product_bootstrap_requests
                         SET status='running',started_at=?,heartbeat_at=CURRENT_TIMESTAMP,lease_until=datetime('now','+2 minutes'),attempts=COALESCE(attempts,0)+1,
                             note='Öncelikli kaynak + fiyat hazırlığı başladı; görsel bağımsız worker'
                         WHERE id=(SELECT id FROM product_bootstrap_requests
                                   WHERE status='pending' AND (next_attempt_at IS NULL OR next_attempt_at<=CURRENT_TIMESTAMP)
                                   ORDER BY priority DESC,COALESCE(attempts,0) ASC,requested_at ASC,id ASC LIMIT 1)
                           AND status='pending'
                         RETURNING *""",(now(),)).fetchone()
        c.commit()
        return dict(row) if row else None
    finally:c.close()

def heartbeat(req,extra_seconds=120):
    c=connect()
    try:
        c.execute("UPDATE product_bootstrap_requests SET heartbeat_at=CURRENT_TIMESTAMP,lease_until=datetime('now',?) WHERE id=? AND status='running'",(f'+{int(extra_seconds)} seconds',int(req['id'])))
        c.commit()
    finally:c.close()

def finish(req,status,note):
    c=connect()
    pid=int(req['product_id'])
    src=int(c.execute("SELECT COUNT(*) FROM product_sources WHERE product_id=? AND active=1",(pid,)).fetchone()[0])
    prices=int(c.execute("SELECT COUNT(*) FROM product_sources ps JOIN products p ON p.id=ps.product_id WHERE ps.product_id=? AND ps.active=1 AND ps.condition_status='verified' AND ps.detected_condition=p.condition_type AND ps.last_price IS NOT NULL",(pid,)).fetchone()[0])
    p=c.execute('SELECT image_path FROM products WHERE id=?',(pid,)).fetchone();image=1 if p and p['image_path'] else 0
    attempts=max(1,int(req.get('attempts') or 1))
    next_at=None;last_error=None
    if status=='error':
        mins=[5,15,30,60,120,240][min(attempts-1,5)]
        next_at=c.execute("SELECT datetime('now',?)",(f'+{mins} minutes',)).fetchone()[0];last_error=note[:1600]
    elif status=='partial':
        mins=[5,10,20,45,90,180][min(attempts-1,5)]
        next_at=c.execute("SELECT datetime('now',?)",(f'+{mins} minutes',)).fetchone()[0]
    c.execute("UPDATE product_bootstrap_requests SET status=?,started_at=NULL,heartbeat_at=NULL,lease_until=NULL,finished_at=?,source_count=?,price_count=?,image_ready=?,next_attempt_at=?,last_error=?,note=? WHERE id=?",(status,now(),src,prices,image,next_at,last_error,note[:1600],req['id']))
    log_event(c,'bootstrap','info' if status in ('done','partial') else 'error',note,run_type='bootstrap',run_id=req['id'],product_id=pid,details={'sources':src,'prices':prices,'image_ready':image,'status':status,'attempts':attempts,'next_attempt_at':next_at});c.commit();c.close()

def current_stats(pid):
    c=connect()
    try:
        src=int(c.execute("SELECT COUNT(*) FROM product_sources WHERE product_id=? AND active=1",(pid,)).fetchone()[0])
        priced=int(c.execute("SELECT COUNT(*) FROM product_sources ps JOIN products p ON p.id=ps.product_id WHERE ps.product_id=? AND ps.active=1 AND ps.condition_status='verified' AND ps.detected_condition=p.condition_type AND ps.last_price IS NOT NULL",(pid,)).fetchone()[0])
        p=c.execute('SELECT image_path FROM products WHERE id=?',(pid,)).fetchone();image=bool(p and p['image_path'])
        pending=int(c.execute("SELECT COUNT(*) FROM discovery_candidates WHERE product_id=? AND status='pending'",(pid,)).fetchone()[0])
        indirect=int(c.execute("SELECT COUNT(*) FROM indirect_offers io JOIN products p ON p.id=io.product_id WHERE io.product_id=? AND io.active=1 AND io.condition_status='verified' AND io.detected_condition=p.condition_type AND (io.expires_at IS NULL OR io.expires_at>=CURRENT_TIMESTAMP)",(pid,)).fetchone()[0])
        return src,priced,image,pending,indirect
    finally:c.close()

def ensure_image_job(pid,priority):
    try:
        ic=connect();queue_image(ic,pid,max(100,priority),'Bootstrap: görsel bağımsız kuyruğa alındı');ic.commit();ic.close()
        try:Path('/var/lib/fiyat-radar/image.trigger').touch()
        except OSError:pass
        return None
    except Exception as e:
        return str(e)

def process_one(req):
    pid=int(req['product_id'])
    priority=int(req.get('priority') or 100)
    # v0.3.12: Bir bootstrap turunun görevi katalog doldurmak değil, bu ürüne EN AZ
    # bir doğrulanmış fiyat kazandırmaktır. Derinlik sonraki backoff turlarında artar.
    deep_mode=priority>=500
    target=1; browser_pages=2; browser_timeout_ms=7000; web_limit=1; discover_limit=10 if deep_mode else 6
    # İlk turlar cooldown'a saygılıdır. Ürün uzun süre eksik kalıp L4/420 önceliğe
    # ulaştıysa ve en az üç kez başarısız olduysa engelli mağazalara tek kontrollü
    # probe izni verilir; browser/deadline bütçeleri yine geçerlidir.
    force_access_probe=(priority>=420 and int(req.get('attempts') or 0)>=3)
    c=connect();log_event(c,'bootstrap','info','Kısa hazırlama turu başladı',run_type='bootstrap',run_id=req['id'],product_id=pid,details={'target_sources':target,'browser_max_pages':browser_pages,'browser_timeout_ms':browser_timeout_ms,'priority':priority,'web_first':True,'exact_merchant_budget':discover_limit,'force_access_probe':force_access_probe,'deep_mode':deep_mode,'sitemap_first':deep_mode},commit=True);c.close()
    discover_error=None;scan_error=None;image_error=None

    # Önce model/varyant zekâsı. Bu ağ işi değildir ve yanlış varyantların taranmasını engeller.
    try:
        canonical_pid,new_variants=apply_variant_intelligence(pid,bootstrap_req_id=int(req['id']))
        if canonical_pid!=pid:
            pid=canonical_pid;req=dict(req);req['product_id']=pid
        if new_variants:
            c=connect();log_event(c,'bootstrap.variant-ai','info',f'{len(new_variants)} yeni varyant yönetici onayına sunuldu',run_type='bootstrap',run_id=req['id'],product_id=pid,details={'proposal_ids':new_variants},commit=True);c.close()
    except Exception as e:
        c=connect();log_event(c,'bootstrap.variant-ai','warning',f'Varyant zekâsı tamamlanamadı: {e}',run_type='bootstrap',run_id=req['id'],product_id=pid,commit=True);c.close()

    heartbeat(req)
    image_error=ensure_image_job(pid,priority)

    # v0.3.12: Kaynak zaten varsa önce onu tara. Önceden her tur discovery ile başlayıp
    # Trendyol/n11/Teknosa 403'lerinde dakikalar harcanıyordu.
    heartbeat(req)
    src,priced,image,pending,indirect=current_stats(pid)
    if src>0 and priced==0:
        try:
            scan(limit=3,force=True,product_id=pid,note=f'Bootstrap mevcut kaynak hızlı tarama #{req["id"]}',image_force=False,manual=True,browser_max_pages=1,browser_timeout_ms=7000)
        except Exception as e:
            scan_error=str(e)
        heartbeat(req)
        src,priced,image,pending,indirect=current_stats(pid)
    if priced>0:
        finish(req,'done',f'Fiyat hazır: {src} kaynak, {priced} doğrulanmış fiyat.' + (' Görsel de hazır.' if image else ' Görsel bağımsız worker tarafından tamamlanacak.'))
        return

    # Kaynak/fiyat yoksa aile kaynakları, sitemap/indeks ve mağaza discovery çalışır. Cooldown'daki mağaza arama
    # sayfaları zorlanmaz; Chromium yalnız bir sayfalık son çaredir.
    try:
        discover_run(limit=discover_limit,web_limit=web_limit,product_id=pid,target_sources=target,browser_max_pages=browser_pages,force_access_probe=force_access_probe,browser_timeout_ms=browser_timeout_ms,web_first=True,sitemap_first=deep_mode)
    except Exception as e:
        discover_error=str(e)

    heartbeat(req)
    src,priced,image,pending,indirect=current_stats(pid)
    if src>0 and priced==0:
        try:
            scan(limit=3,force=True,product_id=pid,note=f'Bootstrap keşif sonrası hızlı tarama #{req["id"]}',image_force=False,manual=True,browser_max_pages=1,browser_timeout_ms=7000)
        except Exception as e:
            scan_error=str(e)
    src,priced,image,pending,indirect=current_stats(pid)

    if priced>0:
        finish(req,'done',f'Fiyat hazır: {src} kaynak, {priced} doğrulanmış fiyat.' + (' Görsel de hazır.' if image else ' Görsel bağımsız worker tarafından tamamlanacak.'))
    elif indirect>0:
        finish(req,'partial',f'{indirect} arama-indeksi fiyat sinyali bulundu; doğrudan mağaza doğrulaması sonraki kısa turda tekrar denenecek.')
    elif pending>0:
        finish(req,'partial',f'{pending} kaynak adayı bulundu ancak güven eşiğini geçmedi; sonraki kısa turda yeniden incelenecek.')
    elif src>0:
        finish(req,'partial',f'{src} kaynak var ancak fiyat doğrulanamadı; kaynak backoff sonrası tekrar taranacak.' + (f' Tarama: {scan_error}' if scan_error else ''))
    else:
        detail='; '.join(x for x in (discover_error,scan_error,image_error) if x)
        finish(req,'error','Kaynak/fiyat bulunamadı; kısa tur sonlandırıldı ve backoff uygulandı.' + (f' {detail}' if detail else ''))

def process_queue(limit=5):
    # Önceki worker SIGKILL/reboot ile yarıda kaldıysa sonsuza dek running kalmasın.
    c=connect()
    try:
        c.execute("""UPDATE product_bootstrap_requests
                     SET status='pending',started_at=NULL,lease_until=NULL,heartbeat_at=NULL,next_attempt_at=CURRENT_TIMESTAMP,
                         note='v0.3.10 watchdog: sahipsiz/yarım hızlı hazırlama yeniden kuyruğa alındı'
                     WHERE status='running' AND (lease_until IS NULL OR lease_until < CURRENT_TIMESTAMP OR started_at IS NULL OR started_at < datetime('now','-4 minutes'))""")
        c.commit()
    finally:
        c.close()
    Path(LOCK).parent.mkdir(parents=True,exist_ok=True)
    with open(LOCK,'a+',encoding='utf-8') as fh:
        try:fcntl.flock(fh.fileno(),fcntl.LOCK_EX|fcntl.LOCK_NB)
        except BlockingIOError:return 0
        n=0
        while n<limit:
            req=claim_next()
            if not req:break
            old_handler=signal.signal(signal.SIGALRM,_deadline_handler)
            signal.alarm(75)
            try:
                process_one(req)
            except BootstrapDeadline as e:
                finish(req,'error',f'{e}; işlem kontrollü biçimde durduruldu ve backoff uygulanacak.')
            except Exception as e:
                finish(req,'error',f'Bootstrap hatası: {e}')
                traceback.print_exc()
            finally:
                signal.alarm(0)
                signal.signal(signal.SIGALRM,old_handler)
            n+=1
        return n

if __name__=='__main__':
    ap=argparse.ArgumentParser();ap.add_argument('--limit',type=int,default=5);a=ap.parse_args();process_queue(a.limit)
