"""Al Masa Home — portable WSGI catalogue. Python 3.11+, Pillow, Waitress."""
from __future__ import annotations
import argparse, base64, contextlib, getpass, hashlib, hmac, html, io, json, math, mimetypes, os, re, secrets, sqlite3, threading, time, zipfile
from datetime import datetime, timezone
from http.cookies import SimpleCookie
from pathlib import Path
from urllib.parse import urlparse, unquote, parse_qs
import translations
import bulk_import
from PIL import Image, ImageOps

ROOT = Path(__file__).resolve().parent
DATA = Path(os.environ.get('ALMASA_DATA_DIR', str(ROOT / 'data'))).resolve()
BASE = os.environ.get('ALMASA_BASE_URL', 'http://127.0.0.1:8000').rstrip('/')
LOCK = threading.RLock()
Image.MAX_IMAGE_PIXELS = 25_000_000
CATEGORIES = [('البلاط والسيراميك',['180x90','150x75','100x100','80x80','120x60','80x40','60x60','40x40']),('الأبواب الخارجية',[]),('الأبواب الداخلية',[]),('الأدوات الصحية',[]),('المغاسل',[]),('القعدات الخارجية',[]),('درج البورسلان',['120x47']),('شجر الزينة',[]),('تجهيز صالات الأفراح',[])]
DEFAULT = dict(name='الماسة هوم', tagline='جمال التفاصيل يبدأ من هنا', intro='اختيارات راقية للبلاط والأبواب والأدوات الصحية، وتفاصيل تكمل جمال منزلك.', about='نساعدك في استكشاف خيارات التشطيب والديكور لمنزلك أو مشروعك. تواصل معنا للاستفسار عن المقاسات والتوفر والأسعار.', address='يطا، الخليل، بجانب مديرية التربية والتعليم', phones=['0569999638','0597960008'], whatsapp='970569999638', hours='', mapUrl='', facebook='', instagram='', tiktok='', heroImage='', hideEmpty=False, currency='شيكل')
SOCIAL_CONTACTS = {'whatsapp': '972569999638', 'tiktok': 'https://www.tiktok.com/@almasahome1ya', 'instagram': 'https://www.instagram.com/almas.ahome/', 'facebook': 'https://www.facebook.com/people/Almasa-Home-%D8%A7%D9%84%D9%85%D8%A7%D8%B3%D9%87-%D9%87%D9%88%D9%85/61581925581900/'}
DEFAULT.update(SOCIAL_CONTACTS)
class Problem(Exception):
    def __init__(self, message, status=400): self.message, self.status = message, status

def connect():
    db=sqlite3.connect(DATA/'catalog.sqlite3', timeout=30)
    db.row_factory=sqlite3.Row
    db.execute('PRAGMA foreign_keys=ON')
    return db

def init():
    for p in [DATA,DATA/'uploads',DATA/'backups']: p.mkdir(parents=True,exist_ok=True)
    with connect() as db:
        db.executescript('''PRAGMA journal_mode=WAL;
        CREATE TABLE IF NOT EXISTS documents(kind TEXT NOT NULL,id TEXT NOT NULL,body TEXT NOT NULL,PRIMARY KEY(kind,id));
        CREATE TABLE IF NOT EXISTS config(key TEXT PRIMARY KEY,value TEXT NOT NULL);
        CREATE TABLE IF NOT EXISTS image_imports(token TEXT PRIMARY KEY,digest TEXT NOT NULL,result TEXT NOT NULL);
        CREATE TABLE IF NOT EXISTS sessions(token TEXT PRIMARY KEY,csrf TEXT NOT NULL,expires REAL NOT NULL);
        CREATE TABLE IF NOT EXISTS attempts(ip TEXT PRIMARY KEY,count INTEGER NOT NULL,until REAL NOT NULL);
        ''')
        translations.init(db)
        if not db.execute("SELECT 1 FROM config WHERE key='settings'").fetchone():
            db.execute('INSERT INTO config VALUES (?,?)',('settings',json.dumps(DEFAULT,ensure_ascii=False)))
            for i,(name,sizes) in enumerate(CATEGORIES):
                p=dict(id='cat-'+str(i+1),name=name,sizes=sizes,order=i,visible=True)
                db.execute('INSERT INTO documents VALUES (?,?,?)',('categories',p['id'],json.dumps(p,ensure_ascii=False)))
        # Apply the owner's supplied social contacts once, including existing installations.
        marker='social_contacts_20261022'
        if not db.execute('SELECT 1 FROM config WHERE key=?',(marker,)).fetchone():
            previous=db.execute("SELECT value FROM config WHERE key='settings'").fetchone()[0]
            cfg=json.loads(previous);cfg.update(SOCIAL_CONTACTS)
            db.execute('INSERT INTO config VALUES (?,?)',(marker,previous))
            db.execute("UPDATE config SET value=? WHERE key='settings'",(json.dumps(cfg,ensure_ascii=False),))

        install_catalog_pack(db)

def records(db,kind):
    return [json.loads(r[0]) for r in db.execute('SELECT body FROM documents WHERE kind=?',(kind,))]
def settings(db): return {**DEFAULT, **json.loads(db.execute("SELECT value FROM config WHERE key='settings'").fetchone()[0])}
def payload(db,admin=False):
    cats=sorted(records(db,'categories'),key=lambda p:p['order'])
    ps=records(db,'products'); projects=records(db,'projects'); cfg=settings(db)
    if not admin:
        cats=[c for c in cats if c['visible']]; ids={c['id'] for c in cats}
        ps=[p for p in ps if p['status']=='published' and p['category'] in ids]
        projects=[p for p in projects if p['status']=='published']
        for p in ps:
            if not p['showPrice']: p['price']=0
        if cfg['hideEmpty']: cats=[c for c in cats if any(p['category']==c['id'] for p in ps)]
    return dict(settings=cfg,categories=cats,products=sorted(ps,key=lambda p:p.get('updated',''),reverse=True),projects=sorted(projects,key=lambda p:p['order']))

def txt(v,n=500,required=False):
    if not isinstance(v,str) or len(v)>n: raise Problem('قيمة نصية غير صالحة أو طويلة جدًا.')
    v=v.strip()
    if required and not v: raise Problem('يرجى تعبئة الحقول المطلوبة.')
    return v

def number(v,minimum=0,maximum=1_000_000):
    if isinstance(v,bool): raise Problem('قيمة رقمية غير صالحة.')
    try: x=float(v)
    except (TypeError,ValueError): raise Problem('قيمة رقمية غير صالحة.')
    if not math.isfinite(x) or not minimum<=x<=maximum: raise Problem('قيمة رقمية خارج النطاق.')
    return x

def boolean(v):
    if type(v) is not bool: raise Problem('قيمة خيار غير صالحة.')
    return v

def image_path(v):
    v=txt(v,200)
    if v and not re.fullmatch(r'/uploads/[a-f0-9]{32}(?:-thumb)?\.webp',v): raise Problem('يجب رفع الصورة أولًا.')
    return v

def install_catalog_pack(db):
    """Apply the owner's tile-only pack once, retaining previous records for recovery."""
    marker='owner_standing_tiles_39_v1'
    if db.execute('SELECT 1 FROM config WHERE key=?',(marker,)).fetchone(): return
    pack=ROOT/'static/catalog-pack'
    if not (pack/'catalog.json').is_file(): return
    items=json.loads((pack/'catalog.json').read_text())
    # Validate all assets before making any catalogue changes.
    for row in items:
        for key in ('image','thumb'):
            if not (pack/row[key]).is_file(): raise RuntimeError('Incomplete tile pack: '+row['code'])
    existing={p['code'].strip().casefold():p for p in records(db,'products')}
    cats=records(db,'categories')
    category=next((c['id'] for c in cats if c.get('visible') and '120x60' in c.get('sizes',[])),None)
    if category is None:
        category='cat-owner-'+secrets.token_hex(6)
        cat=validate('categories',dict(id=category,name='بلاط 120 × 60 cm',sizes=['120x60'],visible=True,order=len(cats)))
        db.execute('INSERT INTO documents VALUES (?,?,?)',('categories',category,json.dumps(cat,ensure_ascii=False)))
    previous=[];added=0;updated=0
    for row in items:
        p=existing.get(row['code'].casefold())
        if p is not None:
            previous.append(p.copy());updated+=1
        else:
            p=dict(id=secrets.token_hex(12),name=row['code'],code=row['code'],category=category,status='published',availability='ask',showPrice=False)
            added+=1
        ident=secrets.token_hex(16);url='/uploads/'+ident+'.webp'
        (DATA/'uploads'/(ident+'.webp')).write_bytes((pack/row['image']).read_bytes())
        (DATA/'uploads'/(ident+'-thumb.webp')).write_bytes((pack/row['thumb']).read_bytes())
        # Only the standing tile is shown; old uploads remain on disk and in the receipt.
        p.update(size='120x60',origin='Made in China',images=[url],imageRoles={url:'product'})
        p=validate('products',p)
        db.execute('INSERT OR REPLACE INTO documents VALUES (?,?,?)',('products',p['id'],json.dumps(p,ensure_ascii=False)))
    receipt=dict(added=added,updated=updated,previous=previous)
    db.execute('INSERT INTO config VALUES (?,?)',(marker,json.dumps(receipt,ensure_ascii=False)))


def urls(v):
    v=txt(v,2000)
    if v and (urlparse(v).scheme!='https' or not urlparse(v).netloc): raise Problem('استخدم رابط HTTPS صحيحًا.')
    return v

def whatsapp_number(value, country=''):
    value=txt(value,100,True).translate(str.maketrans('٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹','01234567890123456789'))
    value=re.sub(r'[\s()\-\u200e\u200f\u061c]', '', value)
    if value.startswith('+'): value=value[1:]
    elif value.startswith('00'): value=value[2:]
    if value.startswith('0'):
        if country not in ('970','972'): raise Problem('اختر مفتاح الرقم المحلي كما هو مسجل في واتساب: +970 أو +972، أو اكتب الرقم الدولي كاملًا.')
        if not re.fullmatch(r'05[0-9]{8}',value): raise Problem('الرقم المحلي يجب أن يكون 10 أرقام ويبدأ بـ 05.')
        value=country+value[1:]
    if not re.fullmatch(r'[1-9][0-9]{7,14}',value): raise Problem('أدخل رقم واتساب دوليًا صحيحًا، مثل +970 569 999 638.')
    if value.startswith(('970','972')) and not re.fullmatch(r'(970|972)[1-9][0-9]{7,8}',value): raise Problem('تأكد من طول الرقم واحذف الصفر بعد مفتاح الدولة.')
    return value

def validate(kind,p,cats=None):
    if not isinstance(p,dict): raise Problem('بيانات العنصر غير صالحة.')
    ident=txt(p.get('id',''),80)
    if not re.fullmatch(r'[A-Za-z0-9_-]{1,80}',ident): raise Problem('معرّف غير صالح.')
    out=dict(id=ident,name=txt(p.get('name',''),160,True))
    if kind=='categories':
        sizes=p.get('sizes',[])
        if not isinstance(sizes,list) or len(sizes)>50: raise Problem('قائمة مقاسات غير صالحة.')
        out.update(sizes=[txt(s,40,True) for s in sizes],order=number(p.get('order',0),0,9999),visible=boolean(p.get('visible',True)),image=image_path(p.get('image','')))
    else:
        imgs=p.get('images',[])
        if not isinstance(imgs,list) or len(imgs)>12: raise Problem('الحد الأقصى 12 صورة.')
        out.update(images=[image_path(v) for v in imgs if v],description=txt(p.get('description',''),5000),status=p.get('status','draft'))
        roles=p.get('imageRoles',{})
        if not isinstance(roles,dict) or any(v not in ('product','salon','bathroom','original','other') for v in roles.values()):
            raise Problem('نوع صورة غير صالح.')
        out['imageRoles']={url:roles.get(url,'other') for url in out['images']}
        if out['status'] not in ('draft','published'): raise Problem('حالة نشر غير صالحة.')
        if kind=='projects': out.update(location=txt(p.get('location',''),200),order=number(p.get('order',0),0,9999))
        else:
            cat=txt(p.get('category',''),80,True)
            if cats is not None and cat not in cats: raise Problem('القسم المحدد غير موجود.')
            out.update(category=cat,code=txt(p.get('code',''),80,True),size=txt(p.get('size',''),60),color=txt(p.get('color',''),100),finish=txt(p.get('finish',''),100),origin=txt(p.get('origin',''),100),availability=p.get('availability','ask'),unit=p.get('unit','م²'),featured=boolean(p.get('featured',False)),isNew=boolean(p.get('isNew',False)),showPrice=boolean(p.get('showPrice',False)),price=number(p.get('price',0)),coverage=number(p.get('coverage',0),0,10000),updated=txt(p.get('updated',''),80))
            if out['availability'] not in ('available','order','ask','unavailable'): raise Problem('حالة توفر غير صالحة.')
            if out['unit'] not in ('م²','قطعة','كرتونة','طقم'): raise Problem('وحدة غير صالحة.')
    return out

def validate_settings(p):
    if not isinstance(p,dict): raise Problem('إعدادات غير صالحة.')
    out={k:txt(p.get(k,DEFAULT[k]),5000 if k in ('about','intro') else 300) for k in ['name','tagline','intro','about','address','hours','currency']}
    out['whatsapp']=whatsapp_number(p.get('whatsapp',''),p.get('whatsappCountry',''))
    phones=p.get('phones',[])
    if not isinstance(phones,list) or not 1<=len(phones)<=5: raise Problem('أدخل رقم اتصال واحدًا على الأقل.')
    out['phones']=[txt(v,30,True) for v in phones]
    if any(not re.fullmatch(r'[+0-9 ()-]{5,30}',v) for v in out['phones']): raise Problem('رقم اتصال غير صالح.')
    out.update({k:urls(p.get(k,'')) for k in ('mapUrl','facebook','instagram','tiktok')})
    out['heroImage']=image_path(p.get('heroImage','')); out['hideEmpty']=boolean(p.get('hideEmpty',False))
    return out

def put(db,kind,p): db.execute('INSERT OR REPLACE INTO documents VALUES (?,?,?)',(kind,p['id'],json.dumps(p,ensure_ascii=False)))
def password_hash(password,salt): return hashlib.pbkdf2_hmac('sha256',password.encode(),bytes.fromhex(salt),600_000).hex()
def set_password(password):
    if len(password)<12: raise Problem('كلمة المرور يجب أن تكون 12 حرفًا أو أكثر.')
    salt=secrets.token_hex(16)
    with connect() as db:
        db.execute('INSERT OR REPLACE INTO config VALUES (?,?)',('admin',json.dumps(dict(salt=salt,hash=password_hash(password,salt)))))
        db.execute('DELETE FROM sessions')

def optimize(raw):
    if len(raw)>12_000_000: raise Problem('حجم الصورة يتجاوز 12 ميغابايت.')
    try:
        with Image.open(io.BytesIO(raw)) as im:
            if im.width*im.height>25_000_000: raise Problem('أبعاد الصورة كبيرة جدًا.')
            im=ImageOps.exif_transpose(im).convert('RGB'); im.thumbnail((2000,2000))
            out=io.BytesIO(); im.save(out,'WEBP',quality=85)
            im.thumbnail((600,600)); thumb=io.BytesIO(); im.save(thumb,'WEBP',quality=80)
        return out.getvalue(),thumb.getvalue()
    except Problem: raise
    except Exception: raise Problem('تعذر قراءة الصورة. استخدم JPG أو PNG أو WebP.')

def store_image(raw):
    full,thumb=optimize(raw); ident=secrets.token_hex(16)
    (DATA/'uploads'/f'{ident}.webp').write_bytes(full)
    (DATA/'uploads'/f'{ident}-thumb.webp').write_bytes(thumb)
    return '/uploads/'+ident+'.webp'

def backup_bytes():
    with LOCK,connect() as db:
        db.execute('BEGIN')
        manifest=payload(db,True); manifest['translations']=translations.export(db); manifest.update(format='almasa-backup',version=2,created=datetime.now(timezone.utc).isoformat())
        buf=io.BytesIO()
        with zipfile.ZipFile(buf,'w',zipfile.ZIP_DEFLATED) as z:
            z.writestr('catalog.json',json.dumps(manifest,ensure_ascii=False))
            for p in (DATA/'uploads').glob('*.webp'): z.write(p,'uploads/'+p.name)
        return buf.getvalue()

def auto_backup(force=False):
    date=time.strftime('%Y-%m-%d'); dest=DATA/'backups'/('auto-'+date+'.zip')
    if force: dest=DATA/'backups'/('before-restore-'+str(time.time_ns())+'.zip')
    if not dest.exists():
        tmp=dest.with_suffix('.tmp'); tmp.write_bytes(backup_bytes()); tmp.replace(dest)
    for p in sorted((DATA/'backups').glob('auto-*.zip'),reverse=True)[14:]: p.unlink()

def restore(raw):
    try:
        with zipfile.ZipFile(io.BytesIO(raw)) as z:
            infos=z.infolist()
            if len(infos)>20000 or sum(i.file_size for i in infos)>350_000_000: raise Problem('النسخة كبيرة جدًا.')
            if len({i.filename for i in infos})!=len(infos): raise Problem('ملفات مكررة في النسخة.')
            m=json.loads(z.read('catalog.json'))
            if m.get('format')!='almasa-backup' or m.get('version')!=2: raise Problem('صيغة النسخة غير مدعومة.')
            cfg=validate_settings(m['settings']); groups={}
            translation_rows=translations.validate_backup(m.get('translations',[]))
            for kind in ('categories','products','projects'):
                rows=m[kind]
                if not isinstance(rows,list) or len(rows)>10000: raise Problem('قائمة غير صالحة.')
                groups[kind]=[validate(kind,p,{x['id'] for x in groups['categories']} if kind=='products' else None) for p in rows]
                if len({p['id'] for p in groups[kind]})!=len(rows): raise Problem('معرّفات مكررة.')
            codes=[p['code'].casefold() for p in groups['products']]
            if len(set(codes))!=len(codes): raise Problem('أكواد منتجات مكررة.')
            needed={v for kind in ('products','projects') for p in groups[kind] for v in p['images']}
            needed.update(p['image'] for p in groups['categories'] if p.get('image'))
            if cfg['heroImage']: needed.add(cfg['heroImage'])
            staged={}
            for path in needed:
                full,thumb=optimize(z.read(path.lstrip('/')))
                staged[path[9:]]=full; staged[path[9:].replace('.webp','-thumb.webp')]=thumb
    except Problem: raise
    except Exception: raise Problem('نسخة غير صالحة أو صور مفقودة؛ لم تُستبدل البيانات.')
    with LOCK:
        auto_backup(True)
        # New random filenames ensure existing image bytes are never overwritten before commit.
        mapping={}
        for path in needed:
            name=secrets.token_hex(16)+'.webp'; mapping[path]='/uploads/'+name
            (DATA/'uploads'/name).write_bytes(staged[path[9:]])
            (DATA/'uploads'/name.replace('.webp','-thumb.webp')).write_bytes(staged[path[9:].replace('.webp','-thumb.webp')])
        with connect() as db:
            db.execute('DELETE FROM image_imports')
            translations.restore(db,translation_rows)
            for kind,rows in groups.items():
                db.execute('DELETE FROM documents WHERE kind=?',(kind,))
                for p in rows:
                    if 'images' in p:
                        p['imageRoles']={mapping[v]:role for v,role in p.get('imageRoles',{}).items() if v in mapping}
                        p['images']=[mapping[v] for v in p['images']]
                    if p.get('image'): p['image']=mapping[p['image']]
                    put(db,kind,p)
            if cfg['heroImage']: cfg['heroImage']=mapping[cfg['heroImage']]
            db.execute("UPDATE config SET value=? WHERE key='settings'",(json.dumps(cfg,ensure_ascii=False),))

def legacy_import(rows):
    if not isinstance(rows,list) or len(rows)>10000: raise Problem('ملف المنتجات القديم غير صالح.')
    prepared=[]; warnings=0
    with connect() as db:
        cats={c['name']:c['id'] for c in records(db,'categories')}; ids=set(cats.values())
        existing={p['code'].casefold() for p in records(db,'products')}
    for i,row in enumerate(rows):
        if not isinstance(row,dict): raise Problem('منتج قديم غير صالح.')
        if row.get('category') not in cats: raise Problem('أضف القسم أولًا: '+str(row.get('category','')))
        p=dict(id=secrets.token_hex(12),name=row.get('name',''),code='OLD-'+secrets.token_hex(4).upper(),category=cats[row['category']],size=row.get('size',''),finish=row.get('finish',''),description=row.get('description',''),images=[],status='draft')
        p=validate('products',p,ids)
        raw=row.get('imageData','')
        if raw:
            try:
                if not isinstance(raw,str) or len(raw)>18_000_000 or not raw.startswith('data:image/'): raise ValueError()
                p['images']=[store_image(base64.b64decode(raw.split(',',1)[1],validate=True))]
            except Exception: raise Problem('صورة غير صالحة في المنتج '+str(i+1))
        elif row.get('imageUrl'): warnings+=1
        prepared.append(p)
    with LOCK:
        auto_backup(True)
        with connect() as db:
            for p in prepared: put(db,'products',p)
    return dict(count=len(prepared),externalImages=warnings)

class Request:
    def __init__(self,e):
        self.e=e; self.method=e['REQUEST_METHOD']; self.path=unquote(e.get('PATH_INFO','/')); self.status=200; self.headers=[]
    def body(self,limit=20_000_000):
        try: n=int(self.e.get('CONTENT_LENGTH') or 0)
        except ValueError: raise Problem('حجم الطلب غير صالح.')
        if n<0 or n>limit: raise Problem('الملف يتجاوز الحجم المسموح.',413)
        return self.e['wsgi.input'].read(n)
    def json(self):
        try: return json.loads(self.body())
        except Problem: raise
        except Exception: raise Problem('بيانات JSON غير صالحة.')
    def session(self):
        try:
            c=SimpleCookie(); c.load(self.e.get('HTTP_COOKIE','')); token=c['almasa_session'].value
        except Exception: return None
        hashed=hashlib.sha256(token.encode()).hexdigest()
        with connect() as db: row=db.execute('SELECT * FROM sessions WHERE token=? AND expires>?',(hashed,time.time())).fetchone()
        return dict(row) if row else None
    def admin(self):
        session=self.session()
        if not session: raise Problem('يرجى تسجيل الدخول.',401)
        if self.method not in ('GET','HEAD') and not hmac.compare_digest(self.e.get('HTTP_X_CSRF_TOKEN',''),session['csrf']): raise Problem('انتهت صلاحية الطلب. حدّث الصفحة.',403)
        return session
    def route(self):
        method,path=self.method,self.path
        if method not in ('GET','HEAD'):
            if self.e.get('HTTP_ORIGIN')!=BASE: raise Problem('مصدر الطلب غير مسموح.',403)
        if path=='/api/health' and method=='GET':
            return dict(application='almasa-home',version='2.8.0')
        if path=='/api/catalog' and method=='GET':
            lang=parse_qs(self.e.get('QUERY_STRING','')).get('lang',['ar'])[0]
            if lang not in ('ar',*translations.LANGUAGES): raise Problem('لغة غير مدعومة.')
            with connect() as db:
                catalog=payload(db)
                if lang!='ar': catalog['translation']=translations.snapshot(db,catalog,lang)
            if lang!='ar': translations.enqueue(connect,catalog,lang)
            return catalog
        if path=='/api/session' and method=='GET':
            s=self.session()
            with connect() as db: configured=bool(db.execute("SELECT 1 FROM config WHERE key='admin'").fetchone())
            return dict(authenticated=bool(s),csrf=s['csrf'] if s else '',configured=configured)
        if path=='/api/login' and method=='POST':
            p=self.json(); password=txt(p.get('password',''),512); ip=self.e.get('REMOTE_ADDR','unknown'); now=time.time()
            with LOCK,connect() as db:
                row=db.execute('SELECT * FROM attempts WHERE ip=?',(ip,)).fetchone()
                if row and row['until']>now and row['count']>=5: raise Problem('محاولات كثيرة. حاول بعد 15 دقيقة.',429)
                admin=db.execute("SELECT value FROM config WHERE key='admin'").fetchone()
                if not admin: raise Problem('أنشئ كلمة مرور المدير بواسطة SETUP.bat أولًا.',503)
                stored=json.loads(admin[0]); good=hmac.compare_digest(password_hash(password,stored['salt']),stored['hash'])
                if not good:
                    count=row['count']+1 if row and row['until']>now else 1
                    db.execute('INSERT OR REPLACE INTO attempts VALUES (?,?,?)',(ip,count,now+900)); db.commit()
                    raise Problem('كلمة المرور غير صحيحة.',401)
                db.execute('DELETE FROM attempts WHERE ip=?',(ip,)); db.execute('DELETE FROM sessions WHERE expires<?',(now,))
                token=secrets.token_urlsafe(32); csrf=secrets.token_urlsafe(32)
                db.execute('INSERT INTO sessions VALUES (?,?,?)',(hashlib.sha256(token.encode()).hexdigest(),csrf,now+8*3600))
            self.headers.append(('Set-Cookie','almasa_session='+token+'; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800'+('; Secure' if BASE.startswith('https://') else '')))
            return dict(ok=True,csrf=csrf)
        if path.startswith('/api/admin') or path=='/api/logout':
            session=self.admin()
            if path=='/api/logout' and method=='POST':
                with connect() as db: db.execute('DELETE FROM sessions WHERE token=?',(session['token'],))
                self.headers.append(('Set-Cookie','almasa_session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0'))
                return dict(ok=True)
            if path=='/api/admin/catalog' and method=='GET':
                with connect() as db: return payload(db,True)
            if path=='/api/admin/import-images' and method=='POST':
                body=self.json(); rows=body.get('products'); token=txt(body.get('token',''),80,True)
                if not re.fullmatch(r'[a-zA-Z0-9_-]{16,80}',token):raise Problem('معرّف دفعة غير صالح.')
                if not isinstance(rows,list) or not 1<=len(rows)<=100:raise Problem('اختر من صورة واحدة إلى 100 صورة في الدفعة.')
                digest=hashlib.sha256(json.dumps(rows,sort_keys=True,ensure_ascii=False).encode()).hexdigest()
                with LOCK:
                    with connect() as db:
                        receipt=db.execute('SELECT * FROM image_imports WHERE token=?',(token,)).fetchone()
                        if receipt:
                            if receipt['digest']!=digest:raise Problem('هذه الدفعة محفوظة بالفعل؛ ابدأ دفعة جديدة.')
                            return json.loads(receipt['result'])
                    auto_backup()
                    with connect() as db:
                        db.execute('BEGIN IMMEDIATE')
                        cats={c['id'] for c in records(db,'categories')};codes={p['code'].casefold() for p in records(db,'products')};prepared=[];errors=[]
                        for i,raw in enumerate(rows):
                            try:
                                if not isinstance(raw,dict):raise Problem('بيانات صنف غير صالحة.')
                                item=dict(raw,id=secrets.token_hex(12),updated=datetime.now(timezone.utc).isoformat())
                                product=validate('products',item,cats)
                                if len(product['images'])!=1:raise Problem('يجب رفع صورة واحدة لهذا الصنف.')
                                if not (DATA/product['images'][0].lstrip('/')).is_file():raise Problem('الصورة غير موجودة، أعد رفعها.')
                                if product['code'].casefold() in codes:raise Problem('كود الصنف مكرر في الموقع أو الدفعة.')
                                codes.add(product['code'].casefold());prepared.append(product)
                            except Problem as exc:errors.append(dict(row=i+1,message=exc.message))
                        if errors:return dict(imported=0,errors=errors)
                        for product in prepared:put(db,'products',product)
                        result=dict(imported=len(prepared),errors=[],ids=[p['id'] for p in prepared])
                        db.execute('INSERT INTO image_imports VALUES (?,?,?)',(token,digest,json.dumps(result)))
                        return result
            if path=='/api/admin/import-products' and method=='POST':
                body=self.json();commit=boolean(body.get('commit',False))
                with LOCK:
                    if commit: auto_backup()
                    with connect() as db:
                        if commit: db.execute('BEGIN IMMEDIATE')
                        prepared,report=bulk_import.parse(body.get('text',''),body.get('category',''),body.get('status','draft'),records(db,'categories'),records(db,'products'),validate,Problem)
                        if commit and report['errors']: return dict(**report,imported=0)
                        if commit:
                            for product in prepared: put(db,'products',product)
                        return dict(**report,imported=len(prepared) if commit else 0)
            if path=='/api/admin/translations' and method=='GET':
                with connect() as db:
                    catalog=payload(db); cfg=translations.configuration(db)
                    rows=translations.export(db); present={(r['source'],r['lang']) for r in rows}
                    for lang in translations.LANGUAGES:
                        rows.extend(dict(source=source,lang=lang,value=value,manual=0) for source,value in translations.snapshot(db,catalog,lang)['texts'].items() if (source,lang) not in present)
                    return dict(configured=bool(cfg.get('key')),enabled=cfg.get('enabled',False),dailyLimit=cfg.get('dailyLimit',100000),sources=translations.sources(catalog),rows=rows,errors=translations._errors.copy(),running=list(translations._running))
            if path=='/api/admin/translations/config' and method=='PUT':
                p=self.json()
                with connect() as db:
                    cfg=translations.configuration(db)
                    if p.get('key'): cfg['key']=txt(p['key'],300,True)
                    if p.get('removeKey'): cfg['key']=''
                    cfg['enabled']=boolean(p.get('enabled',False));cfg['dailyLimit']=int(number(p.get('dailyLimit',100000),1000,1000000))
                    db.execute('INSERT OR REPLACE INTO config VALUES (?,?)',('translation_service',json.dumps(cfg)))
                return dict(ok=True)
            if path=='/api/admin/translations/run' and method=='POST':
                with connect() as db: catalog=payload(db)
                for lang in translations.LANGUAGES: translations.enqueue(connect,catalog,lang)
                return dict(ok=True)
            if path=='/api/admin/translations' and method=='PUT':
                p=self.json();lang=p.get('lang');source=txt(p.get('source',''),5000,True);value=txt(p.get('value',''),15000)
                if lang not in translations.LANGUAGES: raise Problem('لغة غير مدعومة.')
                with LOCK:
                    auto_backup()
                    with connect() as db:
                        if source not in translations.sources(payload(db)): raise Problem('النص الأصلي تغيّر؛ حدّث القائمة.')
                        if value: db.execute('INSERT OR REPLACE INTO translations VALUES (?,?,?,1)',(source,lang,value))
                        else: db.execute('DELETE FROM translations WHERE source=? AND lang=?',(source,lang))
                return dict(ok=True)
            if path=='/api/admin/image' and method=='POST':
                with LOCK: return dict(url=store_image(self.body(12_000_000)))
            if path=='/api/admin/backup' and method=='GET':
                self.headers.extend([('Content-Type','application/zip'),('Content-Disposition','attachment; filename="almasa-backup-'+time.strftime('%Y%m%d')+'.zip"')]); return backup_bytes()
            if path=='/api/admin/backups' and method=='GET':
                return [dict(name=p.name,size=p.stat().st_size) for p in sorted((DATA/'backups').glob('*.zip'),reverse=True)]
            if path.startswith('/api/admin/backups/') and method=='GET':
                name=path.rsplit('/',1)[1]
                if not re.fullmatch(r'(auto-[0-9-]+|before-restore-[0-9]+)\.zip',name): raise Problem('نسخة غير موجودة.',404)
                p=DATA/'backups'/name
                if not p.exists(): raise Problem('نسخة غير موجودة.',404)
                self.headers.extend([('Content-Type','application/zip'),('Content-Disposition',f'attachment; filename="{name}"')]); return p.read_bytes()
            if path=='/api/admin/restore' and method=='POST': restore(self.body(150_000_000)); return dict(ok=True)
            if path=='/api/admin/import-legacy' and method=='POST': return legacy_import(self.json())
            if path=='/api/admin/settings' and method=='PUT':
                cfg=validate_settings(self.json())
                with LOCK:
                    auto_backup()
                    with connect() as db: db.execute("UPDATE config SET value=? WHERE key='settings'",(json.dumps(cfg,ensure_ascii=False),))
                return dict(ok=True)
            if path=='/api/admin/password' and method=='POST':
                p=self.json()
                with connect() as db: current=json.loads(db.execute("SELECT value FROM config WHERE key='admin'").fetchone()[0])
                if not hmac.compare_digest(password_hash(txt(p.get('current',''),512),current['salt']),current['hash']): raise Problem('كلمة المرور الحالية غير صحيحة.')
                set_password(txt(p.get('password',''),512)); return dict(ok=True)
            m=re.fullmatch('/api/admin/(products|categories|projects)(?:/([A-Za-z0-9_-]+))?',path)
            if m and method in ('POST','PUT','DELETE'):
                kind,ident=m.groups()
                with LOCK:
                    auto_backup()
                    with connect() as db:
                        if method=='DELETE':
                            if kind=='categories' and any(p['category']==ident for p in records(db,'products')): raise Problem('انقل منتجات القسم أو احذفها قبل حذف القسم.')
                            db.execute('DELETE FROM documents WHERE kind=? AND id=?',(kind,ident)); return dict(ok=True)
                        raw=self.json()
                        if not isinstance(raw,dict): raise Problem('عنصر غير صالح.')
                        if method=='POST': raw['id']=secrets.token_hex(12)
                        else:
                            if not ident or not db.execute('SELECT 1 FROM documents WHERE kind=? AND id=?',(kind,ident)).fetchone(): raise Problem('العنصر غير موجود.',404)
                            raw['id']=ident
                        if kind=='products': raw['updated']=datetime.now(timezone.utc).isoformat()
                        p=validate(kind,raw,{c['id'] for c in records(db,'categories')})
                        if kind=='products' and any(x['id']!=p['id'] and x['code'].casefold()==p['code'].casefold() for x in records(db,kind)): raise Problem('كود المنتج مستخدم بالفعل.')
                        for img in p.get('images',[])+([p['image']] if p.get('image') else []):
                            if not (DATA/img.lstrip('/')).is_file(): raise Problem('إحدى الصور غير موجودة، أعد رفعها.')
                        put(db,kind,p); return p
            raise Problem('المسار غير موجود.',404)
        if method not in ('GET','HEAD'): raise Problem('الطريقة غير مدعومة.',405)
        if path=='/robots.txt':
            self.headers.append(('Content-Type','text/plain; charset=utf-8')); return f'User-agent: *\nAllow: /\nDisallow: /admin\nDisallow: /api/\nSitemap: {BASE}/sitemap.xml'.encode()
        if path=='/sitemap.xml':
            with connect() as db: data=payload(db)
            links=[BASE+'/']+[BASE+'/product/'+p['id'] for p in data['products']]
            self.headers.append(('Content-Type','application/xml; charset=utf-8'))
            return ('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'+''.join('<url><loc>'+html.escape(v)+'</loc></url>' for v in links)+'</urlset>').encode()
        if path.startswith('/static/') or path.startswith('/uploads/'):
            parent=ROOT/'static' if path.startswith('/static/') else DATA/'uploads'
            target=(parent/path.split('/',2)[2]).resolve()
            if not target.is_relative_to(parent.resolve()) or not target.is_file(): raise Problem('ملف غير موجود.',404)
            self.headers.append(('Content-Type',mimetypes.guess_type(str(target))[0] or 'application/octet-stream'))
            self.headers.append(('Cache-Control','public, max-age=86400')); return target.read_bytes()
        if path=='/admin':
            self.headers.append(('Content-Type','text/html; charset=utf-8')); return (ROOT/'static/admin.html').read_bytes()
        if path=='/' or path.startswith('/product/'):
            with connect() as db: data=payload(db)
            cfg=data['settings']; title=cfg['name']; description=cfg['intro']; pic=cfg['heroImage'] or '/static/logo.jpg'; fallback=''
            if path.startswith('/product/'):
                product=next((p for p in data['products'] if p['id']==path[9:]),None)
                if not product: raise Problem('المنتج غير منشور أو غير موجود.',404)
                title=product['name']+' | '+title; description=product['description'] or product['name']; pic=(product['images'] or [pic])[0]
                fallback='<h1>'+html.escape(product['name'])+'</h1><p>'+html.escape(description)+'</p>'
            else:
                fallback='<h1>'+html.escape(title)+'</h1><p>'+html.escape(description)+'</p>'+''.join('<p><a href="/product/'+p['id']+'">'+html.escape(p['name'])+'</a></p>' for p in data['products'])
            source=(ROOT/'static/index.html').read_text()
            for k,v in dict(TITLE=title,DESCRIPTION=description[:300],CANONICAL=BASE+path,OGIMAGE=BASE+pic).items(): source=source.replace('{{'+k+'}}',html.escape(v,quote=True))
            source=source.replace('{{FALLBACK}}',fallback)
            self.headers.append(('Content-Type','text/html; charset=utf-8')); return source.encode()
        raise Problem('الصفحة غير موجودة.',404)

def application(environ,start_response):
    req=Request(environ)
    try:
        if environ.get('HTTP_HOST')!=urlparse(BASE).netloc: raise Problem('اسم المضيف غير مسموح.',400)
        result=req.route()
    except Problem as e: req.status=e.status; result=dict(error=e.message)
    except Exception:
        import traceback; traceback.print_exc(); req.status=500; result=dict(error='حدث خطأ بالخادم. لم يكتمل الطلب؛ راجع سجل التشغيل.')
    if not isinstance(result,bytes):
        result=json.dumps(result,ensure_ascii=False).encode(); req.headers.append(('Content-Type','application/json; charset=utf-8'))
    req.headers.extend([('X-Content-Type-Options','nosniff'),('Referrer-Policy','strict-origin-when-cross-origin'),('X-Frame-Options','DENY'),('Content-Security-Policy',"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' blob:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"),('Content-Length',str(len(result)))])
    if not any(k=='Cache-Control' for k,v in req.headers): req.headers.append(('Cache-Control','no-store'))
    if BASE.startswith('https://'): req.headers.append(('Strict-Transport-Security','max-age=31536000'))
    reasons={200:'OK',400:'Bad Request',401:'Unauthorized',403:'Forbidden',404:'Not Found',405:'Method Not Allowed',413:'Payload Too Large',429:'Too Many Requests',500:'Internal Server Error',503:'Service Unavailable'}
    start_response(str(req.status)+' '+reasons.get(req.status,'Error'),req.headers)
    return [b'' if req.method=='HEAD' else result]

if __name__=='__main__':
    parser=argparse.ArgumentParser(); parser.add_argument('--setup',action='store_true'); parser.add_argument('--backup',action='store_true'); parser.add_argument('--dev',action='store_true'); args=parser.parse_args(); init()
    if args.setup:
        p=getpass.getpass('New admin password (12+ characters): ')
        if p!=getpass.getpass('Repeat password: '): raise SystemExit('Passwords do not match.')
        set_password(p); print('Admin password saved. All existing sessions signed out.')
    elif args.backup: auto_backup(True); print('Backup saved in data/backups.')
    else:
        port=int(os.environ.get('PORT','8000')); host=os.environ.get('ALMASA_BIND','127.0.0.1')
        print('Al Masa Home: '+BASE+' | Admin: '+BASE+'/admin',flush=True)
        if args.dev:
            from wsgiref.simple_server import make_server
            make_server(host,port,application).serve_forever()
        else:
            from waitress import serve
            serve(application,host=host,port=port,max_request_body_size=150_000_000,threads=4)
else: init()
