#!/usr/bin/env python3
from __future__ import annotations
import argparse, hashlib, json, os, re, sqlite3, sys
from pathlib import Path
from collections import defaultdict
from urllib.parse import urlparse

from matching import norm
from condition import normalize_condition
from dbutil import setting_bool, setting_int, log_event

DB=os.environ.get('FIYAT_RADAR_DB','/var/lib/fiyat-radar/fiyat-radar.sqlite')
CATALOG=Path(__file__).resolve().parents[1]/'data'/'authoritative_variants.json'
OFFICIAL_DOMAINS={
 'apple':'apple.com','samsung':'samsung.com','xiaomi':'mi.com','redmi':'mi.com','poco':'mi.com','huawei':'huawei.com',
 'honor':'honor.com','oppo':'oppo.com','realme':'realme.com','oneplus':'oneplus.com','sony':'sony.com','lg':'lg.com','dyson':'dyson.com',
 'motorola':'motorola.com','vivo':'vivo.com','asus':'asus.com','nothing':'nothing.tech','google':'google.com','zte':'zte.com.cn'
}

COMMON_COLORS={
 'siyah':'Siyah','black':'Siyah','beyaz':'Beyaz','white':'Beyaz','mavi':'Mavi','blue':'Mavi','yeşil':'Yeşil','yesil':'Yeşil','green':'Yeşil',
 'kırmızı':'Kırmızı','kirmizi':'Kırmızı','red':'Kırmızı','mor':'Mor','purple':'Mor','lavanta':'Lavanta','lavender':'Lavanta',
 'gri':'Gri','gray':'Gri','grey':'Gri','gümüş':'Gümüş','gumus':'Gümüş','silver':'Gümüş','altın':'Altın','altin':'Altın','gold':'Altın',
 'turuncu':'Turuncu','orange':'Turuncu','pembe':'Pembe','pink':'Pembe','sarı':'Sarı','sari':'Sarı','yellow':'Sarı',
 'sis mavisi':'Sis Mavisi','ada çayı':'Ada Çayı','ada cayi':'Ada Çayı','sage':'Ada Çayı','kozmik turuncu':'Kozmik Turuncu','abis':'Abis',
 'uzay siyahı':'Uzay Siyahı','uzay siyahi':'Uzay Siyahı','space black':'Uzay Siyahı','pamuk beyazı':'Pamuk Beyazı','pamuk beyazi':'Pamuk Beyazı',
 'uçuk altın rengi':'Uçuk Altın Rengi','ucuk altin rengi':'Uçuk Altın Rengi','gök mavisi':'Gök Mavisi','gok mavisi':'Gök Mavisi',
 'lacivert':'Lacivert','navy':'Lacivert','mint':'Mint','nane':'Mint','krem':'Krem','cream':'Krem','bej':'Bej','beige':'Bej',
 'grafit':'Grafit','graphite':'Grafit','titanyum gri':'Titanyum Gri','titanium gray':'Titanyum Gri','titanium grey':'Titanyum Gri',
 'titanyum siyah':'Titanyum Siyah','titanium black':'Titanyum Siyah','titanyum beyaz':'Titanyum Beyaz','titanium white':'Titanyum Beyaz',
 'buz mavisi':'Buz Mavisi','ice blue':'Buz Mavisi','açık mavi':'Açık Mavi','acik mavi':'Açık Mavi','light blue':'Açık Mavi',
 'koyu mavi':'Koyu Mavi','dark blue':'Koyu Mavi','gece siyahı':'Gece Siyahı','gece siyahi':'Gece Siyahı','midnight black':'Gece Siyahı',
 'orman yeşili':'Orman Yeşili','orman yesili':'Orman Yeşili','forest green':'Orman Yeşili','zeytin yeşili':'Zeytin Yeşili','zeytin yesili':'Zeytin Yeşili','olive green':'Zeytin Yeşili',
 'kobalt':'Kobalt','cobalt':'Kobalt','menekşe':'Menekşe','menekse':'Menekşe','violet':'Menekşe','lila':'Lila','lilac':'Lila',
 'şampanya':'Şampanya','sampanya':'Şampanya','champagne':'Şampanya','bronz':'Bronz','bronze':'Bronz','kahverengi':'Kahverengi','brown':'Kahverengi',
 'inci beyazı':'İnci Beyazı','inci beyazi':'İnci Beyazı','pearl white':'İnci Beyazı','seramik beyaz':'Seramik Beyaz','ceramic white':'Seramik Beyaz',
 'aurora mor':'Aurora Mor','aurora purple':'Aurora Mor','ay ışığı gümüş':'Ay Işığı Gümüş','ay isigi gumus':'Ay Işığı Gümüş','moonlight silver':'Ay Işığı Gümüş'
}

def connect():
    c=sqlite3.connect(DB,timeout=15);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 load_catalogs():
    try:return json.loads(CATALOG.read_text(encoding='utf-8')).get('catalogs',[])
    except Exception:return []

def match_catalog(product):
    text_n=norm(' '.join(str(product.get(k) or '') for k in ('brand','model','name')));text=' '+text_n+' '
    qualifiers={'pro','max','plus','air','mini','ultra','fe','fold','flip'}
    product_q={q for q in qualifiers if f' {q} ' in text}
    matches=[]
    for cat in load_catalogs():
        for alias in cat.get('aliases',[]):
            a=norm(alias);alias_q={q for q in qualifiers if f' {q} ' in (' '+a+' ')}
            if a and f' {a} ' in text and alias_q==product_q:matches.append((len(a.split()),len(a),cat))
    return max(matches,key=lambda x:(x[0],x[1]))[2] if matches else None

def variant_key(p):
    brand=norm(p.get('brand') or '');model=norm(p.get('model') or '');name=norm(p.get('name') or '')
    identity=model or name
    raw='|'.join([brand,identity,normalize_condition(p.get('condition_type') or 'new'),norm(p.get('variant') or ''),str(int(p.get('capacity_gb') or 0)),norm(p.get('color') or '')])
    return hashlib.sha256(raw.encode()).hexdigest()

def normalize_color(value,cat):
    n=norm(value or '')
    if not n:return None
    for c in cat.get('colors',[]):
        if norm(c)==n:return c
    for a,c in (cat.get('color_aliases') or {}).items():
        if norm(a)==n:return c
    return None

def nearest_capacity(value,allowed):
    vals=sorted({int(v) for v in allowed if int(v)>0})
    if not vals:return None
    if not value:return vals[0]
    v=int(value)
    return v if v in vals else min(vals,key=lambda x:(abs(x-v),x))

def queue_bootstrap(c,pid,priority=55,note='Akıllı varyant motoru tarafından oluşturuldu'):
    row=c.execute("SELECT id FROM product_bootstrap_requests WHERE product_id=? AND status IN ('pending','running') LIMIT 1",(pid,)).fetchone()
    if row:return int(row['id'])
    cur=c.execute("INSERT INTO product_bootstrap_requests(product_id,status,priority,requested_by,note) VALUES(?,'pending',?,'variant-ai',?)",(pid,priority,note))
    return int(cur.lastrowid)

def merge_into(c,from_id,to_id,bootstrap_req_id=None):
    if from_id==to_id:return to_id
    # running bootstrap request must survive deletion of the bad duplicate
    if bootstrap_req_id:
        c.execute('UPDATE product_bootstrap_requests SET product_id=? WHERE id=?',(to_id,bootstrap_req_id))
    old=c.execute('SELECT * FROM products WHERE id=?',(from_id,)).fetchone();new=c.execute('SELECT * FROM products WHERE id=?',(to_id,)).fetchone()
    if old and new and not new['image_path'] and old['image_path']:
        c.execute('UPDATE products SET image_path=?,image_source_url=?,image_updated_at=? WHERE id=?',(old['image_path'],old['image_source_url'],old['image_updated_at'],to_id))
    for s in c.execute('SELECT * FROM product_sources WHERE product_id=?',(from_id,)).fetchall():
        ex=c.execute('SELECT id FROM product_sources WHERE product_id=? AND url=?',(to_id,s['url'])).fetchone()
        if ex:
            target_sid=int(ex['id']);old_sid=int(s['id'])
            c.execute('UPDATE price_observations SET product_id=?,source_id=? WHERE source_id=?',(to_id,target_sid,old_sid))
            # offers can collide on seller_key; keep target copies, discard duplicate old offers
            for o in c.execute('SELECT id,seller_key FROM source_offers WHERE source_id=?',(old_sid,)).fetchall():
                if c.execute('SELECT 1 FROM source_offers WHERE source_id=? AND seller_key=?',(target_sid,o['seller_key'])).fetchone():c.execute('DELETE FROM source_offers WHERE id=?',(o['id'],))
                else:c.execute('UPDATE source_offers SET product_id=?,source_id=? WHERE id=?',(to_id,target_sid,o['id']))
            c.execute('DELETE FROM product_sources WHERE id=?',(old_sid,))
        else:
            c.execute('UPDATE product_sources SET product_id=? WHERE id=?',(to_id,s['id']))
            c.execute('UPDATE price_observations SET product_id=? WHERE source_id=?',(to_id,s['id']))
            c.execute('UPDATE source_offers SET product_id=? WHERE source_id=?',(to_id,s['id']))
    for table in ('discovery_candidates','operation_logs','manual_scan_requests'):
        try:c.execute(f'UPDATE {table} SET product_id=? WHERE product_id=?',(to_id,from_id))
        except sqlite3.IntegrityError:
            if table=='discovery_candidates':c.execute('DELETE FROM discovery_candidates WHERE product_id=?',(from_id,))
    # Dolaylı indeks fiyatları da doğru varyanta taşınır; aynı URL/engine çakışırsa hedefteki korunur.
    for io in c.execute('SELECT * FROM indirect_offers WHERE product_id=?',(from_id,)).fetchall():
        if c.execute('SELECT 1 FROM indirect_offers WHERE product_id=? AND url=? AND source_engine=?',(to_id,io['url'],io['source_engine'])).fetchone():c.execute('DELETE FROM indirect_offers WHERE id=?',(io['id'],))
        else:c.execute('UPDATE indirect_offers SET product_id=? WHERE id=?',(to_id,io['id']))
    # pending sibling bootstrap requests move if possible; duplicates may remain harmless
    c.execute('UPDATE product_bootstrap_requests SET product_id=? WHERE product_id=?',(to_id,from_id))
    c.execute('DELETE FROM products WHERE id=?',(from_id,))
    return to_id

def propose_variant(c,p,cap,color,confidence,evidence_type,source_url=None,evidence_text=None,evidence_count=1,preview_image_url=None):
    """v0.3.16: yeni varyantı doğrudan katalogda yaratma; yönetici onayına bırak."""
    if not cap or not color:return None
    cap=int(cap);color=str(color).strip()
    if not valid_storage_capacity(cap,p):return None
    family=family_key_for(p);cond=normalize_condition(p.get('condition_type') or 'new')
    v={**p,'capacity_gb':cap,'color':color};k=variant_key(v)
    if c.execute('SELECT 1 FROM products WHERE variant_key=? LIMIT 1',(k,)).fetchone():return None
    cn=norm(color)
    row=c.execute("SELECT id FROM variant_proposals WHERE family_key=? AND condition_type=? AND COALESCE(capacity_gb,0)=? AND color_normalized=? AND status='pending' LIMIT 1",(family,cond,cap,cn)).fetchone()
    if row:
        c.execute("UPDATE variant_proposals SET evidence_count=MAX(evidence_count,?),confidence=MAX(COALESCE(confidence,0),?),source_url=COALESCE(NULLIF(?,''),source_url),evidence_text=COALESCE(NULLIF(?,''),evidence_text),preview_image_url=COALESCE(NULLIF(?,''),preview_image_url),updated_at=CURRENT_TIMESTAMP WHERE id=?",(int(evidence_count or 1),float(confidence or 0),source_url or '',evidence_text or '',preview_image_url or '',int(row['id'])))
        return int(row['id'])
    cur=c.execute("""INSERT INTO variant_proposals(family_key,seed_product_id,name,category,brand,model,condition_type,capacity_gb,proposed_color,color_normalized,evidence_count,evidence_type,confidence,source_url,evidence_text,preview_image_url,status)
                     VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, 'pending')""",
                  (family,int(p.get('id') or 0) or None,p.get('name') or p.get('model') or '',p.get('category') or '',p.get('brand') or '',p.get('model') or '',cond,cap,color,cn,int(evidence_count or 1),evidence_type,float(confidence or 0),source_url,evidence_text,preview_image_url))
    pid=int(cur.lastrowid)
    log_event(c,'variant.proposal','info','Yeni varyant yönetici onayına sunuldu',run_type='variant-ai',product_id=int(p.get('id') or 0) or None,details={'proposal_id':pid,'family_key':family,'capacity_gb':cap,'color':color,'evidence_type':evidence_type,'evidence_count':evidence_count,'confidence':confidence,'source_url':source_url})
    return pid

def authoritative_apply(c,pid,bootstrap_req_id=None):
    row=c.execute('SELECT * FROM products WHERE id=?',(pid,)).fetchone()
    if not row:return pid,[],False
    p=dict(row);cat=match_catalog(p)
    if not cat:return pid,[],False
    notes=[];cap=nearest_capacity(p.get('capacity_gb'),cat.get('capacities_gb',[]));color=normalize_color(p.get('color'),cat)
    if cap!=p.get('capacity_gb'):notes.append(f"kapasite {p.get('capacity_gb') or 'boş'} → {cap} GB")
    if color is None:
        color=(cat.get('colors') or [''])[0]
        notes.append(f"renk {p.get('color') or 'boş'} → {color}")
    elif norm(color)!=norm(p.get('color') or ''):notes.append(f"renk {p.get('color')} → {color}")
    target={**p,'name':cat['canonical_name'],'brand':cat['brand'],'model':cat['model'],'variant':'','capacity_gb':cap,'color':color}
    key=variant_key(target);dup=c.execute('SELECT id FROM products WHERE variant_key=? AND id<>? LIMIT 1',(key,pid)).fetchone()
    if dup:
        target_id=merge_into(c,pid,int(dup['id']),bootstrap_req_id)
        log_event(c,'variant.ai','warning','Yanlış/mükerrer varyant mevcut doğru varyantla birleştirildi',run_type='variant-ai',product_id=target_id,details={'from_id':pid,'to_id':target_id,'catalog':cat['key'],'corrections':notes})
        pid=target_id
    else:
        c.execute('''UPDATE products SET name=?,brand=?,model=?,variant='',capacity_gb=?,color=?,variant_key=?,family_key=?,variant_ai_status='verified',variant_ai_confidence=?,variant_ai_note=?,variant_ai_checked_at=CURRENT_TIMESTAMP,updated_at=CURRENT_TIMESTAMP WHERE id=?''',
                  (cat['canonical_name'],cat['brand'],cat['model'],cap,color,key,cat['key'],float(cat.get('confidence',1)),cat.get('note','Resmî üretici kataloğu'),pid))
        if notes:log_event(c,'variant.ai','info','Ürün resmî varyant kataloğuna göre düzeltildi',run_type='variant-ai',product_id=pid,details={'catalog':cat['key'],'corrections':notes,'source_url':cat.get('source_url')})
    created=[]
    if cat.get('all_combinations') and setting_bool(c,'variant_admin_approval_required',True):
        max_create=max(1,setting_int(c,'variant_ai_max_create',30))
        seed=c.execute('SELECT * FROM products WHERE id=?',(pid,)).fetchone();seed=dict(seed) if seed else target
        for cp in cat.get('capacities_gb',[]):
            for col in cat.get('colors',[]):
                if len(created)>=max_create:break
                if int(seed.get('capacity_gb') or 0)==int(cp) and norm(seed.get('color') or '')==norm(col):continue
                prop=propose_variant(c,seed,int(cp),col,float(cat.get('confidence',1)),'official_catalog',cat.get('source_url'),cat.get('note') or 'Resmî üretici varyant matrisi',1)
                if prop and prop not in created:created.append(prop)
        if created:log_event(c,'variant.ai','info',f'{len(created)} eksik resmî varyant yönetici onayına sunuldu',run_type='variant-ai',product_id=pid,details={'proposal_ids':created,'catalog':cat['key'],'source_url':cat.get('source_url')})
    # persist authoritative catalog evidence
    c.execute('''INSERT INTO variant_catalogs(family_key,brand,model,canonical_name,capacities_json,colors_json,combinations_json,source_type,source_url,confidence,note,verified_at)
                 VALUES(?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)
                 ON CONFLICT(family_key) DO UPDATE SET capacities_json=excluded.capacities_json,colors_json=excluded.colors_json,source_type=excluded.source_type,source_url=excluded.source_url,confidence=excluded.confidence,note=excluded.note,verified_at=CURRENT_TIMESTAMP''',
              (cat['key'],cat['brand'],cat['model'],cat['canonical_name'],json.dumps(cat.get('capacities_gb',[]),ensure_ascii=False),json.dumps(cat.get('colors',[]),ensure_ascii=False),None,cat.get('source_type'),cat.get('source_url'),float(cat.get('confidence',1)),cat.get('note')))
    return pid,created,True

PHONE_BRANDS={
 'apple','samsung','xiaomi','redmi','poco','huawei','honor','oppo','realme','oneplus','sony','lg','motorola','vivo','asus','nothing','google','zte','tecno','infinix','nokia'
}
# Telefonlarda otomatik "depolama" varyantı olarak kabul edilen gerçekçi dahili depolama boyutları.
# 4/6/8/12/24 GB gibi RAM değerleri bu listeye özellikle alınmaz.
PHONE_STORAGE_GB={16,32,64,128,256,512,1024,2048}
RAM_HINTS=(' ram ',' bellek ',' memory ',' lpddr ',' ddr4 ',' ddr5 ',' ddr ')
STORAGE_HINTS=(' depolama ',' dahili hafiza ',' dahili hafıza ',' hafiza ',' hafıza ',' rom ',' storage ',' kapasite ',' internal ')

def _is_phone_product(product):
    if not product:return False
    b=norm(product.get('brand') or '')
    cat=norm(product.get('category') or '')
    text=' '+norm(' '.join(str(product.get(k) or '') for k in ('name','model')))+' '
    return b in PHONE_BRANDS or any(x in cat for x in ('telefon','smartphone','cep telefonu','akilli telefon')) or ' iphone ' in text or ' galaxy ' in text

def valid_storage_capacity(value,product=None):
    try:v=int(value)
    except Exception:return False
    if v<=0:return False
    if _is_phone_product(product):return v in PHONE_STORAGE_GB
    return v<=16384

def parse_capacity(title,product=None):
    """Extract storage, never RAM.

    Retail titles frequently contain both e.g. "6 GB RAM 128 GB". The old parser
    returned the first number and created a false 6 GB storage variant. We now
    classify every GB/TB token by context and, for phones, only accept realistic
    storage capacities.
    """
    raw=norm(title or '')
    if not raw:return None
    padded=' '+raw+' '
    candidates=[]
    for m in re.finditer(r'\b(\d{1,4})\s*(gb|tb)\b',raw):
        n=int(m.group(1));v=n*1024 if m.group(2)=='tb' else n
        start=max(0,m.start()-28);end=min(len(raw),m.end()+28);ctx=' '+raw[start:end]+' '
        suffix=raw[m.end():m.end()+14]
        # RAM labels conventionally follow the RAM amount ("6 GB RAM"). Only
        # inspect the immediate suffix so "6 GB RAM 128 GB" does not taint 128 GB.
        if re.match(r'\s*(ram|bellek|memory|lpddr|ddr\d*)\b',suffix):
            continue
        if not valid_storage_capacity(v,product):
            continue
        strong=1 if any(h in ctx for h in STORAGE_HINTS) else 0
        candidates.append((strong,v,m.start()))
    if not candidates:return None
    # Explicit storage marker first. If a title has "8+256 GB" or multiple plausible
    # tokens without labels, the larger capacity is the safer storage interpretation.
    candidates.sort(key=lambda x:(x[0],x[1],-x[2]),reverse=True)
    return candidates[0][1]

def parse_color(title):
    s=' '+norm(title or '')+' '
    # longest aliases first
    for alias,canonical in sorted(COMMON_COLORS.items(),key=lambda kv:len(norm(kv[0])),reverse=True):
        if f' {norm(alias)} ' in s:return canonical
    return None

def family_key_for(p):
    b=norm(p.get('brand') or '');m=norm(p.get('model') or p.get('name') or '')
    cond=normalize_condition(p.get('condition_type') or 'new')
    base=f'{b}|{m}'
    return base if cond=='new' else f'{base}|condition:{cond}'

def family_product_ids(c,p):
    """Return every legacy variant already mapped to the same brand/model/condition family."""
    family=(p.get('family_key') or '').strip() or family_key_for(p)
    cond=normalize_condition(p.get('condition_type') or 'new')
    rows=c.execute("SELECT id FROM products WHERE active=1 AND condition_type=? AND (family_key=? OR id=? OR (LOWER(TRIM(COALESCE(brand,'')))=LOWER(TRIM(?)) AND LOWER(TRIM(COALESCE(model,'')))=LOWER(TRIM(?)))) ORDER BY id",(cond,family,int(p.get('id') or 0),p.get('brand') or '',p.get('model') or '')).fetchall()
    ids=[int(r['id']) for r in rows]
    return ids or [int(p['id'])]

def _pair_from_text(family_product,text):
    text=(text or '').strip()
    if not text:return None
    try:
        from matching import validate_product_match
        validate_product_match(family_product,text,enforce_condition=False)
    except Exception:return None
    cap=parse_capacity(text,family_product);col=parse_color(text)
    return (cap,col) if cap and col else None

def learn_from_web_search(c,pid):
    """Brand-agnostic family search; v0.3.17'de harici arama kapalıysa ağ işi yapmaz."""
    if setting_bool(c,'external_search_disabled',True):return []
    if not setting_bool(c,'variant_ai_web_learning',True):return []
    p=c.execute('SELECT * FROM products WHERE id=?',(pid,)).fetchone()
    if not p:return []
    p=dict(p);family=family_key_for(p);brand=norm(p.get('brand') or '')
    try:
        import requests
        from bs4 import BeautifulSoup
        from discover import web_search, safe_get, host_domain, UA
        family_product={**p,'capacity_gb':None,'color':'','ean':''}
        sess=requests.Session();sess.headers.update({'User-Agent':UA,'Accept-Language':'tr-TR,tr;q=0.9,en;q=0.6'})
        results,engine=web_search(sess,family_product,limit=20)
    except Exception as e:
        log_event(c,'variant.ai.web','warning',f'Geniş varyant araması başlatılamadı: {e}',run_type='variant-ai',product_id=pid)
        return []
    pairs=defaultdict(set);official_pairs=set();samples={};official=OFFICIAL_DOMAINS.get(brand)
    def add_evidence(text,url,confidence,evidence_type):
        pair=_pair_from_text(family_product,text)
        if not pair:return
        cap,col=pair;domain=(urlparse(url or '').hostname or '').lower().removeprefix('www.')
        if not domain:return
        pairs[(cap,col)].add(domain);samples.setdefault((cap,col),(text,url))
        is_official=bool(official and (domain==official or domain.endswith('.'+official)))
        if is_official:official_pairs.add((cap,col))
        c.execute('INSERT INTO variant_evidence(family_key,product_id,source_url,capacity_gb,color,evidence_type,confidence) VALUES(?,?,?,?,?,?,?)',(family,pid,url,cap,col,evidence_type,0.96 if is_official else confidence))
    # Search result titles/snippets are lightweight and work even when retailer pages block HTTP.
    for result in results[:20]:
        u=result.get('url') or '';txt=' '.join([result.get('title') or '',result.get('description') or '',*list(result.get('extra_snippets') or [])])
        add_evidence(txt,u,0.72,'search_result_'+engine)
    # Fetch only a bounded subset for stronger page-title evidence.
    for result in results[:12]:
        u=result.get('url') or ''
        try:
            html,final=safe_get(sess,u,headers={'User-Agent':UA,'Accept-Language':'tr-TR,tr;q=0.9,en;q=0.6'})
            soup=BeautifulSoup(html,'lxml');title=soup.title.get_text(' ',strip=True) if soup.title else ''
            meta=soup.select_one('meta[name="description"],meta[property="og:title"]');extra=meta.get('content','') if meta else ''
            add_evidence((title+' '+extra).strip(),final,0.82,'page_metadata_'+engine)
        except Exception:continue
    min_sources=max(2,setting_int(c,'variant_ai_min_sources',2));created=[];max_create=max(1,setting_int(c,'variant_ai_max_create',30))
    for (cap,col),domains in sorted(pairs.items()):
        if len(created)>=max_create:break
        is_official=(cap,col) in official_pairs
        if not is_official and len(domains)<min_sources:continue
        conf=.98 if is_official else min(.95,.80+.06*len(domains));title,url=samples.get((cap,col),('',None))
        prop=propose_variant(c,p,cap,col,conf,'web_multi_source_official' if is_official else 'web_multi_source',url,title,len(domains))
        if prop and prop not in created:created.append(prop)
    return created

def learn_from_existing_evidence(c,pid):
    """Generic family-wide learner: Samsung/Xiaomi/Honor/Oppo/etc. use the same evidence rules as Apple."""
    if not setting_bool(c,'variant_ai_web_learning',True):return []
    p=c.execute('SELECT * FROM products WHERE id=?',(pid,)).fetchone()
    if not p:return []
    p=dict(p);family=family_key_for(p);ids=family_product_ids(c,p);ph=','.join('?'*len(ids));cond=normalize_condition(p.get('condition_type') or 'new')
    family_product={**p,'capacity_gb':None,'color':'','ean':''};rows=[]
    queries=[
      (f"SELECT po.raw_title title,COALESCE(po.merchant_id,ps.merchant_id) merchant_id,ps.url url FROM price_observations po JOIN product_sources ps ON ps.id=po.source_id WHERE po.product_id IN ({ph}) AND po.raw_title IS NOT NULL AND po.condition_type=? AND ps.condition_status='verified'",ids+[cond]),
      (f"SELECT ps.last_title title,ps.merchant_id,ps.url FROM product_sources ps JOIN products px ON px.id=ps.product_id WHERE ps.product_id IN ({ph}) AND ps.last_title IS NOT NULL AND ps.active=1 AND ps.condition_status='verified' AND ps.detected_condition=px.condition_type",ids),
      (f"SELECT so.raw_title title,so.merchant_id,ps.url FROM source_offers so JOIN product_sources ps ON ps.id=so.source_id JOIN products px ON px.id=so.product_id WHERE so.product_id IN ({ph}) AND so.raw_title IS NOT NULL AND so.active=1 AND ps.condition_status='verified' AND so.condition_type=px.condition_type",ids),
      (f"SELECT dc.title title,dc.merchant_id,dc.url FROM discovery_candidates dc WHERE dc.product_id IN ({ph}) AND dc.title IS NOT NULL AND dc.condition_status='verified' AND dc.detected_condition=?",ids+[cond])
    ]
    for sql,args in queries:
        try:rows.extend(c.execute(sql,args).fetchall())
        except Exception:pass
    pairs=defaultdict(set);samples={}
    for r in rows:
        pair=_pair_from_text(family_product,r['title'])
        if not pair:continue
        cap,col=pair;host=(urlparse(r['url'] or '').hostname or '').lower().removeprefix('www.');source=('m:'+str(int(r['merchant_id']))) if r['merchant_id'] else ('d:'+host)
        if source in ('m:0','d:'):continue
        pairs[(cap,col)].add(source);samples.setdefault((cap,col),(r['title'],r['url']))
        c.execute('INSERT INTO variant_evidence(family_key,product_id,merchant_id,source_url,capacity_gb,color,evidence_type,confidence) VALUES(?,?,?,?,?,?,?,?)',(family,pid,r['merchant_id'],r['url'],cap,col,'family_observed_offer',0.76))
    min_sources=max(2,setting_int(c,'variant_ai_min_sources',2));created=[];max_create=max(1,setting_int(c,'variant_ai_max_create',30))
    for (cap,col),sources in sorted(pairs.items()):
        if len(created)>=max_create:break
        if len(sources)<min_sources:continue
        conf=min(.97,.80+.06*len(sources));title,url=samples.get((cap,col),('',None))
        prop=propose_variant(c,p,cap,col,conf,'family_multi_source',url,title,len(sources))
        if prop and prop not in created:created.append(prop)
    return created

def apply_product(pid,bootstrap_req_id=None,allow_web=False):
    c=connect()
    try:
        if not setting_bool(c,'variant_ai_enabled',True):return pid,[]
        c.execute('BEGIN')
        canonical,created,known=authoritative_apply(c,pid,bootstrap_req_id)
        if not known:
            created += learn_from_existing_evidence(c,canonical)
            if allow_web:created += learn_from_web_search(c,canonical)
        c.commit()
        # created listesi v0.3.16'da ürün ID'si değil onay bekleyen proposal ID'leridir.
        return canonical,created
    except Exception:
        c.rollback();raise
    finally:c.close()

def sweep(limit=50):
    c=connect();ids=[int(r['id']) for r in c.execute("SELECT id FROM products WHERE active=1 ORDER BY CASE WHEN variant_ai_checked_at IS NULL THEN 0 ELSE 1 END,variant_ai_checked_at ASC,id DESC LIMIT ?",(limit,)).fetchall()];c.close()
    corrected=created=errors=0
    for pid in ids:
        try:
            newpid,newids=apply_product(pid,allow_web=True)
            corrected += int(newpid!=pid);created += len(newids)
        except Exception as e:
            errors+=1;c=connect();log_event(c,'variant.ai','error',f'Varyant zekâsı hatası: {e}',run_type='variant-ai',product_id=pid,commit=True);c.close()
    print(f'Varyant zekâsı: {len(ids)} kontrol, {corrected} birleştirme/düzeltme, {created} yönetici onay adayı, {errors} hata')
    return len(ids),corrected,created,errors

if __name__=='__main__':
    ap=argparse.ArgumentParser();ap.add_argument('--product-id',type=int);ap.add_argument('--bootstrap-request-id',type=int);ap.add_argument('--sweep',action='store_true');ap.add_argument('--limit',type=int,default=50);a=ap.parse_args()
    if a.product_id:print(apply_product(a.product_id,a.bootstrap_request_id))
    else:sweep(a.limit)
