#!/usr/bin/env python3
from __future__ import annotations
import argparse, fcntl, hashlib, ipaddress, os, socket, sqlite3, sys, time
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from pathlib import Path
from urllib.parse import urljoin, urlparse
import requests
from extractors.generic import extract
from matching import confidence, validate_product_match

DB=os.environ.get('FIYAT_RADAR_DB','/var/lib/fiyat-radar/fiyat-radar.sqlite')
IMAGE_DIR=Path(os.environ.get('FIYAT_RADAR_IMAGE_DIR','/var/lib/fiyat-radar/images'))
UA=os.environ.get('FIYAT_RADAR_UA','Mozilla/5.0 (compatible; FiyatRadar/0.1.1; price-monitor)')
TIMEOUT=int(os.environ.get('FIYAT_RADAR_TIMEOUT','20'))
MAX_IMAGE_BYTES=int(os.environ.get('FIYAT_RADAR_MAX_IMAGE_BYTES',str(8*1024*1024)))
IMAGE_REFRESH_DAYS=int(os.environ.get('FIYAT_RADAR_IMAGE_REFRESH_DAYS','7'))
LOCK_FILE=os.environ.get('FIYAT_RADAR_LOCK_FILE','/var/lib/fiyat-radar/scan.lock')


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

@contextmanager
def scan_lock():
    Path(LOCK_FILE).parent.mkdir(parents=True,exist_ok=True)
    with open(LOCK_FILE,'a+',encoding='utf-8') as fh:
        fcntl.flock(fh.fileno(),fcntl.LOCK_EX)
        try:
            yield
        finally:
            fcntl.flock(fh.fileno(),fcntl.LOCK_UN)
def connect():
    c=sqlite3.connect(DB,timeout=10); c.row_factory=sqlite3.Row
    c.execute('PRAGMA foreign_keys=ON'); c.execute('PRAGMA journal_mode=WAL'); c.execute('PRAGMA busy_timeout=5000')
    return c

def _public_host(host: str) -> bool:
    try:
        infos=socket.getaddrinfo(host,None,type=socket.SOCK_STREAM)
    except socket.gaierror:
        return False
    ips={x[4][0] for x in infos}
    if not ips: return False
    for raw in ips:
        try:
            if not ipaddress.ip_address(raw).is_global: return False
        except ValueError:
            return False
    return True

def _safe_image_url(url: str) -> str:
    p=urlparse(url)
    if p.scheme not in ('http','https') or not p.hostname:
        raise ValueError('Görsel URL şeması geçersiz')
    if not _public_host(p.hostname):
        raise ValueError('Görsel URL herkese açık bir adrese çözülmüyor')
    return url

def _safe_image_get(session: requests.Session, url: str):
    current=url
    for _ in range(4):
        _safe_image_url(current)
        r=session.get(current,timeout=TIMEOUT,headers={'User-Agent':UA,'Accept':'image/avif,image/webp,image/apng,image/*,*/*;q=0.8'},stream=True,allow_redirects=False)
        if r.status_code in (301,302,303,307,308):
            loc=r.headers.get('location')
            r.close()
            if not loc: raise ValueError('Görsel yönlendirmesinde Location yok')
            current=urljoin(current,loc)
            continue
        r.raise_for_status()
        return r,current
    raise ValueError('Görsel çok fazla yönlendirme yaptı')

def _image_ext(content_type: str) -> str:
    ct=(content_type or '').split(';',1)[0].strip().lower()
    return {
        'image/jpeg':'jpg','image/jpg':'jpg','image/png':'png','image/webp':'webp',
        'image/gif':'gif','image/avif':'avif'
    }.get(ct,'')

def _needs_image_refresh(row, image_url: str | None, force: bool) -> bool:
    if not image_url: return False
    if force or not row['image_path'] or row['image_source_url'] != image_url: return True
    stamp=row['image_updated_at']
    if not stamp: return True
    try:
        dt=datetime.strptime(stamp,'%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
        return datetime.now(timezone.utc)-dt >= timedelta(days=max(1,IMAGE_REFRESH_DAYS))
    except Exception:
        return True

def cache_product_image(c, session, row, image_url: str | None, force=False):
    if not _needs_image_refresh(row,image_url,force): return None
    if not image_url: return None
    r,final_url=_safe_image_get(session,image_url)
    try:
        ext=_image_ext(r.headers.get('content-type',''))
        if not ext: raise ValueError('Görsel yanıtının Content-Type alanı desteklenmiyor')
        length=r.headers.get('content-length')
        if length and int(length) > MAX_IMAGE_BYTES: raise ValueError('Ürün görseli boyut sınırını aşıyor')
        IMAGE_DIR.mkdir(parents=True,exist_ok=True)
        digest=hashlib.sha256(final_url.encode('utf-8')).hexdigest()[:14]
        filename=f"product-{int(row['product_id'])}-{digest}.{ext}"
        tmp=IMAGE_DIR/(filename+'.tmp')
        total=0
        with open(tmp,'wb') as f:
            for chunk in r.iter_content(65536):
                if not chunk: continue
                total += len(chunk)
                if total > MAX_IMAGE_BYTES:
                    raise ValueError('Ürün görseli boyut sınırını aşıyor')
                f.write(chunk)
        if total < 128: raise ValueError('Ürün görseli boş veya geçersiz')
        dest=IMAGE_DIR/filename
        os.replace(tmp,dest)
        old=row['image_path']
        if old and old != filename:
            try:
                oldp=IMAGE_DIR/Path(old).name
                if oldp.is_file(): oldp.unlink()
            except OSError:
                pass
        c.execute('UPDATE products SET image_path=?,image_source_url=?,image_updated_at=? WHERE id=?',(filename,final_url,now(),row['product_id']))
        return filename
    finally:
        r.close()
        try:
            if 'tmp' in locals() and tmp.exists(): tmp.unlink()
        except OSError:
            pass

def fetch_source(session,row):
    domain=(urlparse(row['url']).hostname or '').lower()
    merchant_domain=(row['domain'] or '').lower()
    if merchant_domain and not (domain==merchant_domain or domain.endswith('.'+merchant_domain)):
        raise ValueError(f'URL domaini mağaza domainiyle eşleşmiyor: {domain}')
    r=session.get(row['url'],timeout=TIMEOUT,headers={'User-Agent':UA,'Accept-Language':'tr-TR,tr;q=0.9,en;q=0.7'})
    r.raise_for_status()
    ctype=r.headers.get('content-type','')
    if 'html' not in ctype and '<html' not in r.text[:1000].lower(): raise ValueError('Kaynak HTML sayfası döndürmedi')
    offer=extract(r.text,row['url'])
    product={k:row[k] for k in ('brand','model','variant','ean')}
    validate_product_match(product,offer.title)
    conf=confidence(product,offer.title,r.text)
    if (row['model'] or row['variant']) and conf < 0.45:
        raise ValueError(f'Ürün eşleşme güveni düşük ({conf:.2f})')
    return offer,conf

def scan(limit=50,force=False,product_id=None,note=None,image_force=False):
    c=connect(); start=now(); cur=c.execute('INSERT INTO scan_log(started_at,note) VALUES(?,?)',(start,note,)); log_id=cur.lastrowid;c.commit()
    q='''SELECT s.*,p.name product_name,p.brand,p.model,p.variant,p.ean,p.image_path,p.image_source_url,p.image_updated_at,
                m.name merchant,m.domain,m.trust_score
         FROM product_sources s JOIN products p ON p.id=s.product_id JOIN merchants m ON m.id=s.merchant_id
         WHERE s.active=1 AND p.active=1 AND m.active=1'''
    args=[]
    if product_id is not None:
        q += ' AND p.id=?'; args.append(int(product_id))
    if not force: q += " AND (s.next_check_at IS NULL OR s.next_check_at <= datetime('now'))"
    q += ' ORDER BY COALESCE(s.next_check_at,\'1970-01-01\'),s.id LIMIT ?'; args.append(limit)
    rows=c.execute(q,args).fetchall(); ok=fail=0
    session=requests.Session()
    image_done_products=set()
    for row in rows:
        checked=now()
        next_at=(datetime.now(timezone.utc)+timedelta(minutes=max(15,row['check_interval_minutes']))).strftime('%Y-%m-%d %H:%M:%S')
        try:
            offer,conf=fetch_source(session,row)
            effective=conf*(float(row['trust_score'])/100.0)
            if effective < 0.40: raise ValueError(f'Etkin güven düşük ({effective:.2f})')
            c.execute('''UPDATE product_sources SET last_price=?,last_currency=?,last_stock=?,last_title=?,last_confidence=?,last_status='ok',last_error=NULL,last_checked_at=?,next_check_at=?,consecutive_failures=0 WHERE id=?''',
                      (offer.price,offer.currency, None if offer.in_stock is None else int(offer.in_stock),offer.title,conf,checked,next_at,row['id']))
            c.execute('''INSERT INTO price_observations(product_id,source_id,merchant_id,price,currency,in_stock,confidence,raw_title,observed_at) VALUES(?,?,?,?,?,?,?,?,?)''',
                      (row['product_id'],row['id'],row['merchant_id'],offer.price,offer.currency,None if offer.in_stock is None else int(offer.in_stock),conf,offer.title,checked))
            if row['product_id'] not in image_done_products:
                try:
                    cached=cache_product_image(c,session,row,offer.image_url,force=image_force)
                    if cached:
                        image_done_products.add(row['product_id'])
                        print(f"IMG {row['product_name']} | {cached}")
                    elif row['image_path'] and not _needs_image_refresh(row,offer.image_url,image_force):
                        image_done_products.add(row['product_id'])
                except Exception as img_err:
                    print(f"IMG-WARN {row['product_name']} | {img_err}",file=sys.stderr)
            c.execute('UPDATE products SET updated_at=CURRENT_TIMESTAMP WHERE id=?',(row['product_id'],)); ok+=1
            print(f"OK {row['merchant']} | {row['product_name']} | {offer.price} {offer.currency} | güven={conf:.2f}")
        except Exception as e:
            fails=row['consecutive_failures']+1
            backoff=min(1440,max(row['check_interval_minutes'],30)*(2 if fails>=3 else 1))
            next_at=(datetime.now(timezone.utc)+timedelta(minutes=backoff)).strftime('%Y-%m-%d %H:%M:%S')
            c.execute('''UPDATE product_sources SET last_status='error',last_error=?,last_checked_at=?,next_check_at=?,consecutive_failures=? WHERE id=?''',(str(e)[:1000],checked,next_at,fails,row['id']))
            fail+=1; print(f"ERR {row['merchant']} | {row['product_name']} | {e}",file=sys.stderr)
        c.commit(); time.sleep(0.25)
    c.execute('UPDATE scan_log SET finished_at=?,checked_count=?,success_count=?,failure_count=? WHERE id=?',(now(),len(rows),ok,fail,log_id)); c.commit(); c.close()
    print(f'Tamamlandı: {len(rows)} kontrol, {ok} başarılı, {fail} hata')
    return len(rows),ok,fail

def process_manual_queue(max_requests=10):
    processed=0
    while processed < max_requests:
        c=connect()
        row=c.execute("SELECT * FROM manual_scan_requests WHERE status='pending' ORDER BY id LIMIT 1").fetchone()
        if not row:
            c.close(); break
        cur=c.execute("UPDATE manual_scan_requests SET status='running',started_at=? WHERE id=? AND status='pending'",(now(),row['id']))
        c.commit(); c.close()
        if cur.rowcount != 1: continue
        try:
            label='tüm ürünler' if row['product_id'] is None else f"ürün #{row['product_id']}"
            checked,ok,fail=scan(limit=5000,force=True,product_id=row['product_id'],note=f'Manuel tarama: {label}',image_force=True)
            c=connect(); c.execute("UPDATE manual_scan_requests SET status='done',finished_at=?,checked_count=?,success_count=?,failure_count=?,note=? WHERE id=?",
                (now(),checked,ok,fail,'Manuel tarama tamamlandı.',row['id'])); c.commit(); c.close()
        except Exception as e:
            c=connect(); c.execute("UPDATE manual_scan_requests SET status='error',finished_at=?,note=? WHERE id=?",(now(),str(e)[:1000],row['id'])); c.commit(); c.close()
            print(f"MANUAL ERR request={row['id']} | {e}",file=sys.stderr)
        processed+=1
    print(f'Manuel kuyruk işlendi: {processed} istek')
    return processed

if __name__=='__main__':
    ap=argparse.ArgumentParser()
    ap.add_argument('--limit',type=int,default=50)
    ap.add_argument('--force',action='store_true')
    ap.add_argument('--product-id',type=int)
    ap.add_argument('--process-manual-queue',action='store_true')
    args=ap.parse_args()
    with scan_lock():
        if args.process_manual_queue:
            process_manual_queue(args.limit)
            sys.exit(0)
        checked,ok,fail=scan(args.limit,args.force,args.product_id,image_force=args.force)
        sys.exit(0 if fail==0 else 2)
