#!/usr/bin/env python3
import hashlib
import json
import os
import base64
import random
import re
import secrets
import smtplib
import sqlite3
import subprocess
import tempfile
import time
from email.mime.text import MIMEText
from email.utils import formataddr, formatdate, make_msgid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse

ROOT = Path(os.environ.get("BIRDS_API_ROOT", "/opt/migrated/iis/PigeonPro/api"))
STORE = ROOT / "codes.json"
CONFIG = ROOT / "config.json"
DB = ROOT / "app.db"
TTL = 15 * 60
SESSION_TTL = 7 * 24 * 3600
ADMIN_USER = os.environ.get("BIRDS_ADMIN_USER", "admin")
ADMIN_PASS = os.environ.get("BIRDS_ADMIN_PASS", "BeeBirds@2026")

ROOT.mkdir(parents=True, exist_ok=True)
if not CONFIG.exists():
    CONFIG.write_text(json.dumps({"googleWebClientId": ""}, indent=2) + "\n", encoding="utf-8")


def load_json(path: Path, fallback):
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return fallback


def save_json(path: Path, data) -> None:
    path.write_text(json.dumps(data), encoding="utf-8")


def hash_pass(password: str, salt: str) -> str:
    return hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), 120000).hex()


def db():
    conn = sqlite3.connect(DB)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    conn = db()
    conn.executescript(
        """
        CREATE TABLE IF NOT EXISTS admins (
          user TEXT PRIMARY KEY,
          salt TEXT NOT NULL,
          pass_hash TEXT NOT NULL
        );
        CREATE TABLE IF NOT EXISTS sessions (
          token TEXT PRIMARY KEY,
          admin_user TEXT NOT NULL,
          exp REAL NOT NULL
        );
        CREATE TABLE IF NOT EXISTS kv (
          key TEXT PRIMARY KEY,
          value TEXT NOT NULL
        );
        CREATE TABLE IF NOT EXISTS notices (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          title TEXT NOT NULL,
          body TEXT NOT NULL,
          active INTEGER NOT NULL DEFAULT 1,
          created REAL NOT NULL
        );
        CREATE TABLE IF NOT EXISTS hidden_listings (
          listing_id TEXT PRIMARY KEY,
          reason TEXT,
          created REAL NOT NULL
        );
        CREATE TABLE IF NOT EXISTS blocked_emails (
          email TEXT PRIMARY KEY,
          reason TEXT,
          created REAL NOT NULL
        );
        CREATE TABLE IF NOT EXISTS events (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT NOT NULL,
          email TEXT,
          payload TEXT,
          created REAL NOT NULL
        );
        CREATE TABLE IF NOT EXISTS app_users (
          email TEXT PRIMARY KEY,
          name TEXT,
          last_seen REAL NOT NULL,
          created REAL NOT NULL,
          terms_accepted_at TEXT,
          terms_version TEXT
        );
        CREATE TABLE IF NOT EXISTS audit (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          admin_user TEXT NOT NULL,
          action TEXT NOT NULL,
          detail TEXT,
          created REAL NOT NULL
        );
        CREATE TABLE IF NOT EXISTS catalog (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          kind TEXT NOT NULL,
          label_ar TEXT NOT NULL,
          label_en TEXT NOT NULL DEFAULT '',
          extra TEXT,
          active INTEGER NOT NULL DEFAULT 1,
          sort INTEGER NOT NULL DEFAULT 0,
          created REAL NOT NULL
        );
        CREATE TABLE IF NOT EXISTS seller_profiles (
          email TEXT PRIMARY KEY,
          full_name TEXT,
          nationality TEXT,
          residence TEXT,
          phone TEXT,
          phone_verified INTEGER NOT NULL DEFAULT 0,
          id_type TEXT,
          id_number TEXT,
          id_expires TEXT,
          id_photo_path TEXT,
          experience_years TEXT,
          favorite_breed TEXT,
          bank_account TEXT,
          ship_pledge INTEGER NOT NULL DEFAULT 0,
          completed_at TEXT,
          updated REAL NOT NULL
        );
        CREATE TABLE IF NOT EXISTS bird_serials (
          prefix TEXT PRIMARY KEY,
          n INTEGER NOT NULL
        );
        CREATE TABLE IF NOT EXISTS hub_rings (
          code TEXT PRIMARY KEY,
          year INTEGER NOT NULL,
          email TEXT,
          bird_name TEXT,
          bird_id TEXT,
          city TEXT,
          created REAL NOT NULL
        );
        """
    )
    row = conn.execute("SELECT user FROM admins WHERE user=?", (ADMIN_USER,)).fetchone()
    if not row:
        salt = secrets.token_hex(16)
        conn.execute(
            "INSERT INTO admins(user, salt, pass_hash) VALUES(?,?,?)",
            (ADMIN_USER, salt, hash_pass(ADMIN_PASS, salt)),
        )
    if not conn.execute("SELECT key FROM kv WHERE key='flags'").fetchone():
        conn.execute(
            "INSERT INTO kv(key, value) VALUES('flags', ?)",
            (json.dumps({"donate": False, "googleSignIn": True, "maintenance": False}),),
        )
    if not conn.execute("SELECT key FROM kv WHERE key='banner'").fetchone():
        conn.execute(
            "INSERT INTO kv(key, value) VALUES('banner', ?)",
            (json.dumps({"active": False, "ar": "", "en": ""}),),
        )
    conn.commit()
    if conn.execute("SELECT COUNT(*) n FROM catalog").fetchone()["n"] == 0:
        now = time.time()
        seed = [
            ("shows", "معرض القاهرة 2026", "Cairo Show 2026", {"city": "القاهرة", "date": "2026-03-15", "venue": "أرض المعارض"}),
            ("shows", "معرض الإسكندرية", "Alexandria Show", {"city": "الإسكندرية", "venue": "نادي سموحة"}),
            ("breeds", "زاجل", "Racing Homer", {}),
            ("breeds", "هامر عملاق", "Giant Homer", {}),
            ("breeds", "شو هومر", "Show Homer", {}),
            ("breeds", "مودينا", "Modena", {}),
            ("breeds", "تمبلر", "Tumbler", {}),
            ("breeds", "فانتيل", "Fantail", {}),
            ("breeds", "هاي فلاير", "Highflyer", {}),
            ("colors", "أزرق", "Blue", {}),
            ("colors", "أزرق بار", "Blue bar", {}),
            ("colors", "أحمر", "Red", {}),
            ("colors", "أشقر", "Mealy", {}),
            ("colors", "أبيض", "White", {}),
            ("colors", "أسود", "Black", {}),
            ("colors", "رمادي", "Grey", {}),
            ("strains", "جانسن", "Janssen", {}),
            ("strains", "نايل", "Nile", {}),
            ("strains", "هومر", "Homer", {}),
            ("clubs", "نادي القاهرة", "Cairo Club", {}),
            ("clubs", "الاتحاد المصري", "Egyptian Federation", {}),
            ("races", "سباق الإسماعيلية", "Ismailia race", {"city": "الإسماعيلية"}),
            ("races", "سباق السويس", "Suez race", {"city": "السويس"}),
            ("medicines", "لقاح باراميوكسو", "Paramyxo vaccine", {}),
            ("medicines", "مضاد كوكسيديا", "Coccidiosis treatment", {}),
            ("feed", "علف تربية", "Breeding mix", {}),
            ("feed", "علف سباق", "Racing mix", {}),
            ("cities", "القاهرة", "Cairo", {}),
            ("cities", "الإسكندرية", "Alexandria", {}),
            ("cities", "الجيزة", "Giza", {}),
            ("cities", "الرياض", "Riyadh", {}),
            ("cities", "لندن", "London", {}),
        ]
        for kind, ar, en, extra in seed:
            conn.execute(
                "INSERT INTO catalog(kind, label_ar, label_en, extra, active, sort, created) VALUES(?,?,?,?,1,0,?)",
                (kind, ar, en, json.dumps(extra, ensure_ascii=False), now),
            )
        conn.commit()
    conn.close()


def save_id_photo(email: str, data_url: str):
    if not data_url or "base64," not in data_url:
        return None
    try:
        raw = base64.b64decode(data_url.split("base64,", 1)[1], validate=False)
        if len(raw) > 2_500_000:
            raw = raw[:2_500_000]
        folder = ROOT / "seller_docs"
        folder.mkdir(parents=True, exist_ok=True)
        name = hashlib.sha256(email.encode("utf-8")).hexdigest()[:20] + ".jpg"
        path = folder / name
        path.write_bytes(raw)
        return str(path)
    except Exception:
        return None


def ensure_columns():
    conn = db()
    cols = {r[1] for r in conn.execute("PRAGMA table_info(app_users)")}
    if "terms_accepted_at" not in cols:
        conn.execute("ALTER TABLE app_users ADD COLUMN terms_accepted_at TEXT")
    if "terms_version" not in cols:
        conn.execute("ALTER TABLE app_users ADD COLUMN terms_version TEXT")
    conn.commit()
    conn.close()


init_db()
ensure_columns()


def kv_get(key: str, fallback):
    conn = db()
    row = conn.execute("SELECT value FROM kv WHERE key=?", (key,)).fetchone()
    conn.close()
    if not row:
        return fallback
    try:
        return json.loads(row["value"])
    except Exception:
        return fallback


def kv_set(key: str, value) -> None:
    conn = db()
    conn.execute(
        "INSERT INTO kv(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
        (key, json.dumps(value, ensure_ascii=False)),
    )
    conn.commit()
    conn.close()


DEFAULT_VIDEO_RULES = {
    "enabled": True,
    "minSec": 4,
    "maxSec": 45,
    "maxMb": 8,
    "rejectLinks": True,
    "requireMovement": True,
    "requireLight": True,
    "requireSubject": True,
    "requireRing": False,
    "minBrightness": 32,
    "maxBrightness": 230,
    "minMovement": 5.5,
    "minVariance": 16,
    "passAr": "مطابق تمام. جاري الرفع.",
    "failAr": "لسه مش مطابق. صوّر فيديو تاني حسب معايير Pigeons Hub.",
    "passEn": "This matches. Uploading now.",
    "failEn": "Not a match yet. Record another video using Pigeons Hub standards.",
    "briefAr": "صوّر الطير من الموبايل وهو بيتحرك، إضاءة واضحة، من ٤ لـ ٤٥ ثانية، من غير رابط يوتيوب.",
    "briefEn": "Film the live bird on your phone, in good light, 4–45 seconds, no YouTube link.",
}


def video_rules():
    saved = kv_get("videoRules", {})
    if not isinstance(saved, dict):
        saved = {}
    out = dict(DEFAULT_VIDEO_RULES)
    out.update({k: saved[k] for k in DEFAULT_VIDEO_RULES if k in saved})
    return out


def public_video_rules():
    rules = video_rules()
    return {
        "enabled": bool(rules.get("enabled", True)),
        "minSec": int(rules.get("minSec") or 4),
        "maxSec": int(rules.get("maxSec") or 45),
        "maxMb": int(rules.get("maxMb") or 8),
        "briefAr": str(rules.get("briefAr") or DEFAULT_VIDEO_RULES["briefAr"]),
        "briefEn": str(rules.get("briefEn") or DEFAULT_VIDEO_RULES["briefEn"]),
    }


def _write_data_url(data_url: str, suffix: str):
    raw = data_url.split(",", 1)[-1] if "," in data_url else data_url
    try:
        blob = base64.b64decode(raw)
    except Exception:
        return None
    if not blob or len(blob) > 3_500_000:
        return None
    fh = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
    fh.write(blob)
    fh.close()
    return fh.name


def _gray_pixels(path: str, size: int = 48):
    try:
        from PIL import Image

        im = Image.open(path).convert("L")
        im.thumbnail((size, size))
        w, h = im.size
        return list(im.getdata()), w, h
    except Exception:
        pass
    try:
        proc = subprocess.run(
            [
                "ffmpeg",
                "-hide_banner",
                "-loglevel",
                "error",
                "-i",
                path,
                "-vf",
                f"scale={size}:{size}",
                "-f",
                "rawvideo",
                "-pix_fmt",
                "gray",
                "pipe:1",
            ],
            capture_output=True,
            timeout=12,
        )
        if proc.returncode == 0 and proc.stdout:
            data = list(proc.stdout[: size * size])
            if data:
                return data, size, size
    except Exception:
        pass
    return None, 0, 0


def _stats(pixels):
    if not pixels:
        return 0.0, 0.0
    n = len(pixels)
    mean = sum(pixels) / n
    var = sum((p - mean) ** 2 for p in pixels) / n
    return mean, var ** 0.5


def review_video_payload(body: dict):
    rules = video_rules()
    reasons = []
    uri = str(body.get("uri") or body.get("url") or "")
    duration = body.get("durationSec")
    try:
        duration = float(duration) if duration is not None and duration != "" else None
    except (TypeError, ValueError):
        duration = None
    size = body.get("fileSize")
    try:
        size = float(size) if size is not None and size != "" else None
    except (TypeError, ValueError):
        size = None
    source = str(body.get("source") or "").lower()
    frames = body.get("frames") if isinstance(body.get("frames"), list) else []
    frames = [str(f) for f in frames if isinstance(f, str) and f.startswith("data:image")][:6]

    if not rules.get("enabled", True):
        return {"ok": True, "pass": True, "reasons": [], "messageAr": rules["passAr"], "messageEn": rules["passEn"]}

    if rules.get("rejectLinks", True):
        low = uri.lower()
        if low.startswith("http://") or low.startswith("https://") or "youtube.com" in low or "youtu.be" in low:
            reasons.append("link")

    min_sec = float(rules.get("minSec") or 4)
    max_sec = float(rules.get("maxSec") or 45)
    if duration is None:
        reasons.append("duration_missing")
    else:
        if duration < min_sec:
            reasons.append("too_short")
        if duration > max_sec:
            reasons.append("too_long")

    max_mb = float(rules.get("maxMb") or 8)
    if size is not None and size > max_mb * 1024 * 1024:
        reasons.append("too_heavy")

    brightnesses = []
    spreads = []
    grids = []
    paths = []
    ocr_hits = []
    try:
        for frame in frames:
            path = _write_data_url(frame, ".jpg")
            if not path:
                continue
            paths.append(path)
            pixels, _w, _h = _gray_pixels(path)
            if pixels:
                mean, spread = _stats(pixels)
                brightnesses.append(mean)
                spreads.append(spread)
                grids.append(pixels)
            text = ocr_image(frame)
            if text and RING_RE.search(text.replace(" ", "")):
                ocr_hits.append(text)
        if rules.get("requireLight", True) and brightnesses:
            avg_b = sum(brightnesses) / len(brightnesses)
            if avg_b < float(rules.get("minBrightness") or 32):
                reasons.append("too_dark")
            if avg_b > float(rules.get("maxBrightness") or 230):
                reasons.append("too_bright")
        if rules.get("requireSubject", True) and frames:
            if not spreads:
                reasons.append("no_subject")
            elif max(spreads) < float(rules.get("minVariance") or 16):
                reasons.append("empty_frame")
        if rules.get("requireMovement", True) and (len(grids) >= 2 or frames):
            if len(grids) < 2:
                reasons.append("no_movement")
            else:
                diffs = []
                for a, b in zip(grids, grids[1:]):
                    n = min(len(a), len(b))
                    if n:
                        diffs.append(sum(abs(a[i] - b[i]) for i in range(n)) / n)
                if not diffs or max(diffs) < float(rules.get("minMovement") or 5.5):
                    reasons.append("still")
        if rules.get("requireRing") and frames and not ocr_hits:
            reasons.append("no_ring")
    finally:
        for path in paths:
            try:
                os.unlink(path)
            except OSError:
                pass

    passed = not reasons
    return {
        "ok": True,
        "pass": passed,
        "reasons": reasons,
        "source": source,
        "messageAr": rules["passAr"] if passed else rules["failAr"],
        "messageEn": rules["passEn"] if passed else rules["failEn"],
        "rules": public_video_rules(),
    }


def clean_kind(raw: str) -> str:
    kind = re.sub(r"[^a-z0-9_]", "", str(raw or "").lower())[:40]
    return kind or "custom"


def normalize_hub_ring(raw: str) -> str:
    text = re.sub(r"[^A-Za-z0-9]", "", str(raw or "").upper())
    match = re.match(r"PH(\d{2})(\d+)$", text)
    if not match:
        return ""
    return f"PH-{match.group(1)}-{int(match.group(2)):06d}"


def parse_extra(raw) -> dict:
    if isinstance(raw, dict):
        out = {}
        for key in ("city", "date", "venue", "notes", "category", "phone", "country", "countryCode", "reach"):
            val = str(raw.get(key) or "").strip()[:160]
            if key == "countryCode":
                val = val.upper().replace("UK", "GB")[:8]
                if len(val) != 2:
                    continue
            if key == "reach" and val not in ("local", "worldwide"):
                continue
            if val:
                out[key] = val
        return out
    if isinstance(raw, str):
        try:
            return parse_extra(json.loads(raw))
        except Exception:
            return {}
    return {}


def ensure_catalog_defaults():
    conn = db()
    now = time.time()
    extras = [
        ("base_colors", "أشقر محمر", "Ash-Red", {}),
        ("base_colors", "بني", "Brown", {}),
        ("base_colors", "أسود غامق", "Intense Black", {}),
        ("base_colors", "أزرق", "Blue", {}),
        ("base_colors", "فضي", "Silver", {}),
        ("base_colors", "أشقر", "Mealy", {}),
        ("patterns", "سادة", "Barless", {}),
        ("patterns", "بار", "Bar", {}),
        ("patterns", "شطرنجي", "Checker", {}),
        ("patterns", "نقش T", "T-Pattern", {}),
        ("patterns", "منتشر", "Spread", {}),
        ("patterns", "دانتيل", "Lace", {}),
        ("patterns", "أرقط", "Mottled", {}),
        ("modifiers", "بدون", "None", {}),
        ("modifiers", "أندلسي", "Andalusian", {}),
        ("modifiers", "نيلي", "Indigo", {}),
        ("modifiers", "أوبال", "Opal", {}),
        ("modifiers", "لوزي", "Almond", {}),
        ("modifiers", "مخفف", "Reduced", {}),
        ("modifiers", "شيب", "Grizzle", {}),
        ("modifiers", "أبقع", "Pied", {}),
        ("modifiers", "سبلاش", "Splash", {}),
        ("modifiers", "فاتح", "Dilute", {}),
        ("molt", "لا يوجد", "None", {}),
        ("molt", "خفيف", "Light", {}),
        ("molt", "كثيف", "Heavy", {}),
        ("categories", "حمام سباق", "Racing Pigeons", {}),
        ("categories", "حمام زينة", "Show Pigeons", {}),
        ("categories", "حمام آسيوي", "Asiatic Pigeons", {}),
        ("categories", "هاي فلاير", "Highflyers", {}),
        ("categories", "تمبلر", "Tumblers", {}),
        ("categories", "نفعي", "Utility", {}),
        ("breeds", "غلصم سوري", "Syrian Dewlap", {"category": "Asiatic Pigeons"}),
        ("services", "توصيل الطيور الحية", "Live bird delivery", {"category": "delivery", "notes": "نقل آمن للحمام الحي بين المحافظات والدول. أضيفوا رقم التواصل من لوحة التحكم."}),
        ("services", "أقرب عيادة بيطرية مختصة", "Specialist pigeon vet", {"category": "vet", "notes": "عيادات وبياطرة متخصصون في الحمام. أضيفوا المدينة ورقم العيادة."}),
        ("services", "الأعلاف", "Pigeon feed", {"category": "feed", "notes": "علف تربية وسباق من موردين معتمدين. أضيفوا المورد والمدينة."}),
        ("services", "أدوات للحمام", "Loft tools and equipment", {"category": "tools", "notes": "حجول، سقايات، أعشاش، ومستلزمات العشة. أضيفوا تفاصيل المورد."}),
    ]
    for kind, ar, en, extra in extras:
        row = conn.execute(
            "SELECT id FROM catalog WHERE kind=? AND (label_en=? OR label_ar=?)",
            (kind, en, ar),
        ).fetchone()
        if not row:
            conn.execute(
                "INSERT INTO catalog(kind, label_ar, label_en, extra, active, sort, created) VALUES(?,?,?,?,1,0,?)",
                (kind, ar, en, json.dumps(extra, ensure_ascii=False), now),
            )
    breed_cat = {
        "Racing Homer": "Racing Pigeons",
        "زاجل": "Racing Pigeons",
        "Giant Homer": "Racing Pigeons",
        "هامر عملاق": "Racing Pigeons",
        "Show Homer": "Show Pigeons",
        "شو هومر": "Show Pigeons",
        "Modena": "Show Pigeons",
        "مودينا": "Show Pigeons",
        "Tumbler": "Tumblers",
        "تمبلر": "Tumblers",
        "Fantail": "Show Pigeons",
        "فانتيل": "Show Pigeons",
        "Highflyer": "Highflyers",
        "هاي فلاير": "Highflyers",
        "Syrian Dewlap": "Asiatic Pigeons",
        "غلصم سوري": "Asiatic Pigeons",
    }
    for row in conn.execute("SELECT id, label_ar, label_en, extra FROM catalog WHERE kind='breeds'"):
        extra = parse_extra(row["extra"])
        if extra.get("category"):
            continue
        cat = breed_cat.get(row["label_en"] or "") or breed_cat.get(row["label_ar"] or "")
        if cat:
            extra["category"] = cat
            conn.execute(
                "UPDATE catalog SET extra=? WHERE id=?",
                (json.dumps(extra, ensure_ascii=False), row["id"]),
            )
    conn.commit()
    conn.close()


ensure_catalog_defaults()


def catalog_item(row) -> dict:
    return {
        "id": row["id"],
        "kind": row["kind"],
        "labelAr": row["label_ar"],
        "labelEn": row["label_en"],
        "extra": parse_extra(row["extra"]),
        "active": bool(row["active"]),
        "sort": row["sort"],
    }


def catalog_grouped(active_only: bool = True):
    conn = db()
    query = "SELECT id, kind, label_ar, label_en, extra, active, sort FROM catalog"
    if active_only:
        query += " WHERE active=1"
    query += " ORDER BY kind, sort, id"
    groups = {}
    for row in conn.execute(query):
        item = catalog_item(row)
        groups.setdefault(item["kind"], []).append(item)
    conn.close()
    return groups


def audit(admin_user: str, action: str, detail: str = "") -> None:
    conn = db()
    conn.execute(
        "INSERT INTO audit(admin_user, action, detail, created) VALUES(?,?,?,?)",
        (admin_user, action, detail, time.time()),
    )
    conn.commit()
    conn.close()


def public_config():
    file_cfg = load_json(CONFIG, {})
    hidden = []
    blocked = []
    conn = db()
    for row in conn.execute("SELECT listing_id FROM hidden_listings"):
        hidden.append(row["listing_id"])
    for row in conn.execute("SELECT email FROM blocked_emails"):
        blocked.append(row["email"])
    notice = conn.execute(
        "SELECT title, body FROM notices WHERE active=1 ORDER BY id DESC LIMIT 1"
    ).fetchone()
    conn.close()
    return {
        "googleWebClientId": file_cfg.get("googleWebClientId", ""),
        "facebookAppId": file_cfg.get("facebookAppId", ""),
        "banner": kv_get("banner", {"active": False, "ar": "", "en": ""}),
        "flags": kv_get("flags", {"donate": False, "googleSignIn": True, "facebookSignIn": True, "maintenance": False}),
        "notice": {"title": notice["title"], "body": notice["body"]} if notice else None,
        "hiddenListingIds": hidden,
        "blockedEmails": blocked,
        "catalogs": catalog_grouped(True),
        "videoRules": public_video_rules(),
    }


def load_codes():
    now = time.time()
    data = load_json(STORE, {})
    cleaned = {k: v for k, v in data.items() if v.get("exp", 0) > now}
    if cleaned != data:
        save_json(STORE, cleaned)
    return cleaned


def send_mail(to: str, subject: str, body: str) -> None:
    msg = MIMEText(body, "plain", "utf-8")
    msg["Subject"] = subject
    msg["From"] = formataddr(("Birds App", "noreply@birds.beedevelopments.net"))
    msg["To"] = to
    msg["Date"] = formatdate(localtime=True)
    msg["Message-ID"] = make_msgid(domain="birds.beedevelopments.net")
    msg["Reply-To"] = "noreply@birds.beedevelopments.net"
    msg["Auto-Submitted"] = "auto-generated"
    with smtplib.SMTP("localhost", 25, timeout=8) as smtp:
        smtp.sendmail("noreply@birds.beedevelopments.net", [to], msg.as_string())


def read_body(handler: BaseHTTPRequestHandler, max_bytes: int = 5_000_000):
    length = int(handler.headers.get("Content-Length") or 0)
    if length > max_bytes:
        return None
    raw = handler.rfile.read(length) if length else b"{}"
    try:
        return json.loads(raw or b"{}")
    except Exception:
        return None


def bearer(handler: BaseHTTPRequestHandler):
    header = handler.headers.get("Authorization") or ""
    if header.lower().startswith("bearer "):
        return header.split(" ", 1)[1].strip()
    return ""


RING_RE = re.compile(r"[A-Za-z]{1,4}[- ]?\d{2,4}[- ]?\d{3,6}")


def ocr_image(data_url: str) -> str:
    raw = data_url.split(",", 1)[-1] if "," in data_url else data_url
    try:
        blob = base64.b64decode(raw)
    except Exception:
        return ""
    if not blob or len(blob) > 3_500_000:
        return ""
    tmp = None
    try:
        with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as fh:
            fh.write(blob)
            tmp = fh.name
        texts = []
        for psm in ("7", "6", "11"):
            proc = subprocess.run(
                ["tesseract", tmp, "stdout", "-l", "eng+ara", "--psm", psm],
                capture_output=True,
                text=True,
                timeout=20,
            )
            text = (proc.stdout or "").strip()
            if not text:
                continue
            match = RING_RE.search(text)
            if match:
                return match.group(0).replace(" ", "-")
            texts.append(text)
        return texts[0] if texts else ""
    except Exception:
        return ""
    finally:
        if tmp:
            Path(tmp).unlink(missing_ok=True)


def admin_from_token(token: str):
    if not token:
        return None
    conn = db()
    row = conn.execute("SELECT admin_user, exp FROM sessions WHERE token=?", (token,)).fetchone()
    conn.close()
    if not row or row["exp"] < time.time():
        return None
    return row["admin_user"]


class Handler(BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        return

    def _cors(self):
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
        self.send_header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")

    def _json(self, code: int, payload: dict):
        raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
        self.send_response(code)
        self._cors()
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(raw)))
        self.end_headers()
        self.wfile.write(raw)

    def do_OPTIONS(self):
        self.send_response(204)
        self._cors()
        self.end_headers()

    def do_GET(self):
        parsed = urlparse(self.path)
        path = parsed.path.rstrip("/") or "/"
        if path.endswith("/config") and "/admin" not in path:
            self._json(200, public_config())
            return
        if path.endswith("/admin/dashboard") or path.endswith("/admin/me"):
            user = admin_from_token(bearer(self))
            if not user:
                self._json(401, {"ok": False, "error": "auth"})
                return
            if path.endswith("/me"):
                self._json(200, {"ok": True, "user": user})
                return
            conn = db()
            now = time.time()
            day = now - 86400
            week = now - 7 * 86400
            users = conn.execute("SELECT COUNT(*) n FROM app_users").fetchone()["n"]
            opens_today = conn.execute(
                "SELECT COUNT(*) n FROM events WHERE name='app_open' AND created>=?", (day,)
            ).fetchone()["n"]
            events_week = conn.execute(
                "SELECT COUNT(*) n FROM events WHERE created>=?", (week,)
            ).fetchone()["n"]
            hidden = conn.execute("SELECT COUNT(*) n FROM hidden_listings").fetchone()["n"]
            notices = conn.execute("SELECT COUNT(*) n FROM notices WHERE active=1").fetchone()["n"]
            recent_users = [
                dict(r)
                for r in conn.execute(
                    "SELECT email, name, last_seen, created, terms_accepted_at, terms_version FROM app_users ORDER BY last_seen DESC LIMIT 200"
                )
            ]
            recent_events = [
                dict(r)
                for r in conn.execute(
                    "SELECT name, email, payload, created FROM events ORDER BY id DESC LIMIT 500"
                )
            ]
            hidden_rows = [
                dict(r) for r in conn.execute("SELECT listing_id, reason, created FROM hidden_listings")
            ]
            blocked_set = {str(r["email"]).lower() for r in conn.execute("SELECT email FROM blocked_emails")}
            for u in recent_users:
                u["blocked"] = 1 if str(u.get("email") or "").lower() in blocked_set else 0
            notice_rows = [
                dict(r)
                for r in conn.execute(
                    "SELECT id, title, body, active, created FROM notices ORDER BY id DESC LIMIT 20"
                )
            ]
            event_counts = [
                dict(r)
                for r in conn.execute(
                    "SELECT name, COUNT(*) n FROM events WHERE created>=? GROUP BY name ORDER BY n DESC",
                    (week,),
                )
            ]
            seller_rows = []
            for r in conn.execute(
                "SELECT email, full_name, nationality, residence, phone, phone_verified, id_type, id_number, id_expires, id_photo_path, experience_years, favorite_breed, bank_account, ship_pledge, completed_at, updated FROM seller_profiles ORDER BY updated DESC LIMIT 200"
            ):
                item = dict(r)
                num = str(item.get("id_number") or "")
                item["id_last4"] = num[-4:] if num else ""
                item["has_photo"] = bool(item.get("id_photo_path"))
                item.pop("id_number", None)
                item.pop("id_photo_path", None)
                seller_rows.append(item)
            conn.close()
            self._json(
                200,
                {
                    "ok": True,
                    "stats": {
                        "users": users,
                        "opensToday": opens_today,
                        "eventsWeek": events_week,
                        "hiddenListings": hidden,
                        "activeNotices": notices,
                    },
                    "flags": kv_get("flags", {}),
                    "banner": kv_get("banner", {}),
                    "users": recent_users,
                    "events": recent_events,
                    "eventCounts": event_counts,
                    "hidden": hidden_rows,
                    "notices": notice_rows,
                    "catalogs": catalog_grouped(False),
                    "sellers": seller_rows,
                    "videoRules": video_rules(),
                },
            )
            return
        if path.endswith("/admin/catalog"):
            user = admin_from_token(bearer(self))
            if not user:
                self._json(401, {"ok": False, "error": "auth"})
                return
            self._json(200, {"ok": True, "catalogs": catalog_grouped(False)})
            return
        if path.endswith("/rings"):
            qs = parse_qs(parsed.query)
            code = normalize_hub_ring(str((qs.get("code") or qs.get("q") or [""])[0]))
            if not code:
                self._json(400, {"ok": False, "error": "code"})
                return
            conn = db()
            row = conn.execute(
                "SELECT code, year, bird_name, city, created FROM hub_rings WHERE code=?",
                (code,),
            ).fetchone()
            conn.close()
            if not row:
                self._json(200, {"ok": True, "ring": None})
                return
            self._json(
                200,
                {
                    "ok": True,
                    "ring": {
                        "code": row["code"],
                        "year": row["year"],
                        "name": row["bird_name"] or "",
                        "city": row["city"] or "",
                        "created": row["created"],
                    },
                },
            )
            return
        self._json(404, {"ok": False})

    def do_PUT(self):
        path = urlparse(self.path).path.rstrip("/")
        user = admin_from_token(bearer(self))
        if not user:
            self._json(401, {"ok": False, "error": "auth"})
            return
        body = read_body(self)
        if body is None:
            self._json(400, {"ok": False, "error": "bad_json"})
            return
        if path.endswith("/admin/flags"):
            flags = kv_get("flags", {})
            for key in ("donate", "googleSignIn", "maintenance"):
                if key in body:
                    flags[key] = bool(body[key])
            kv_set("flags", flags)
            audit(user, "flags", json.dumps(flags))
            self._json(200, {"ok": True, "flags": flags})
            return
        if path.endswith("/admin/banner"):
            banner = {
                "active": bool(body.get("active")),
                "ar": str(body.get("ar") or ""),
                "en": str(body.get("en") or ""),
            }
            kv_set("banner", banner)
            audit(user, "banner", json.dumps(banner, ensure_ascii=False))
            self._json(200, {"ok": True, "banner": banner})
            return
        if path.endswith("/admin/video-rules"):
            current = video_rules()
            for key, default in DEFAULT_VIDEO_RULES.items():
                if key not in body:
                    continue
                val = body[key]
                if isinstance(default, bool):
                    current[key] = bool(val)
                elif isinstance(default, (int, float)):
                    try:
                        current[key] = type(default)(val)
                    except (TypeError, ValueError):
                        continue
                else:
                    current[key] = str(val)[:240]
            kv_set("videoRules", current)
            audit(user, "video_rules", json.dumps(current, ensure_ascii=False)[:500])
            self._json(200, {"ok": True, "videoRules": current})
            return
        if path.endswith("/admin/catalog"):
            cid = int(body.get("id") or 0)
            if not cid:
                self._json(400, {"ok": False})
                return
            kind = clean_kind(body.get("kind") or "")
            label_ar = str(body.get("labelAr") or "").strip()[:120]
            label_en = str(body.get("labelEn") or "").strip()[:120]
            extra = parse_extra(body.get("extra") or {})
            if not kind or not label_ar:
                self._json(400, {"ok": False})
                return
            conn = db()
            conn.execute(
                "UPDATE catalog SET kind=?, label_ar=?, label_en=?, extra=?, active=?, sort=? WHERE id=?",
                (
                    kind,
                    label_ar,
                    label_en,
                    json.dumps(extra, ensure_ascii=False),
                    1 if body.get("active", True) else 0,
                    int(body.get("sort") or 0),
                    cid,
                ),
            )
            conn.commit()
            conn.close()
            audit(user, "catalog_edit", f"{kind}:{label_ar}")
            self._json(200, {"ok": True, "catalogs": catalog_grouped(False)})
            return
        self._json(404, {"ok": False})

    def do_DELETE(self):
        parsed = urlparse(self.path)
        path = parsed.path.rstrip("/")
        user = admin_from_token(bearer(self))
        if not user:
            self._json(401, {"ok": False, "error": "auth"})
            return
        qs = parse_qs(parsed.query)
        conn = db()
        if path.endswith("/admin/notice"):
            nid = int((qs.get("id") or ["0"])[0])
            conn.execute("DELETE FROM notices WHERE id=?", (nid,))
            conn.commit()
            audit(user, "notice_delete", str(nid))
            conn.close()
            self._json(200, {"ok": True})
            return
        if path.endswith("/admin/hidden"):
            listing_id = str((qs.get("id") or [""])[0])
            conn.execute("DELETE FROM hidden_listings WHERE listing_id=?", (listing_id,))
            conn.commit()
            audit(user, "unhide", listing_id)
            conn.close()
            self._json(200, {"ok": True})
            return
        if path.endswith("/admin/block"):
            email = str((qs.get("email") or [""])[0]).strip().lower()
            conn.execute("DELETE FROM blocked_emails WHERE email=?", (email,))
            conn.commit()
            audit(user, "unblock", email)
            conn.close()
            self._json(200, {"ok": True})
            return
        if path.endswith("/admin/catalog"):
            cid = int((qs.get("id") or ["0"])[0])
            conn.execute("DELETE FROM catalog WHERE id=?", (cid,))
            conn.commit()
            audit(user, "catalog_delete", str(cid))
            conn.close()
            self._json(200, {"ok": True})
            return
        conn.close()
        self._json(404, {"ok": False})

    def do_POST(self):
        path = urlparse(self.path).path.rstrip("/")
        body = read_body(self, 8_000_000 if path.endswith("/video/review") else 5_000_000)
        if body is None:
            self._json(400, {"ok": False, "error": "bad_json"})
            return

        if path.endswith("/video/review"):
            result = review_video_payload(body)
            email = str(body.get("email") or "").strip().lower()[:120]
            conn = db()
            conn.execute(
                "INSERT INTO events(name, email, payload, created) VALUES(?,?,?,?)",
                (
                    "video_review_pass" if result.get("pass") else "video_review_fail",
                    email,
                    json.dumps({"reasons": result.get("reasons") or []}, ensure_ascii=False)[:2000],
                    time.time(),
                ),
            )
            conn.commit()
            conn.close()
            self._json(200, result)
            return

        if path.endswith("/admin/login"):
            user = str(body.get("user") or "").strip()
            password = str(body.get("pass") or "")
            conn = db()
            row = conn.execute("SELECT salt, pass_hash FROM admins WHERE user=?", (user,)).fetchone()
            if not row or hash_pass(password, row["salt"]) != row["pass_hash"]:
                conn.close()
                self._json(401, {"ok": False, "error": "creds"})
                return
            token = secrets.token_urlsafe(32)
            conn.execute(
                "INSERT INTO sessions(token, admin_user, exp) VALUES(?,?,?)",
                (token, user, time.time() + SESSION_TTL),
            )
            conn.commit()
            conn.close()
            audit(user, "login", "")
            self._json(200, {"ok": True, "token": token, "user": user})
            return

        if path.endswith("/events"):
            name = str(body.get("name") or "").strip()[:80]
            if not name:
                self._json(400, {"ok": False})
                return
            email = str(body.get("email") or "").strip().lower()[:120]
            payload = json.dumps(body.get("meta") or {}, ensure_ascii=False)[:2000]
            conn = db()
            conn.execute(
                "INSERT INTO events(name, email, payload, created) VALUES(?,?,?,?)",
                (name, email, payload, time.time()),
            )
            conn.commit()
            conn.close()
            self._json(200, {"ok": True})
            return

        if path.endswith("/users/ping"):
            email = str(body.get("email") or "").strip().lower()
            name = str(body.get("name") or "").strip()[:80]
            terms_at = str(body.get("termsAcceptedAt") or "").strip()[:40]
            terms_ver = str(body.get("termsVersion") or "").strip()[:32]
            if "@" not in email:
                self._json(400, {"ok": False})
                return
            now = time.time()
            conn = db()
            row = conn.execute(
                "SELECT email, name, terms_accepted_at, terms_version FROM app_users WHERE email=?",
                (email,),
            ).fetchone()
            if row:
                next_name = name or row["name"] or email.split("@")[0]
                next_at = row["terms_accepted_at"]
                next_ver = row["terms_version"]
                if terms_at and terms_ver and (not next_at or next_ver != terms_ver):
                    next_at = terms_at
                    next_ver = terms_ver
                conn.execute(
                    "UPDATE app_users SET name=?, last_seen=?, terms_accepted_at=?, terms_version=? WHERE email=?",
                    (next_name, now, next_at, next_ver, email),
                )
            else:
                conn.execute(
                    "INSERT INTO app_users(email, name, last_seen, created, terms_accepted_at, terms_version) VALUES(?,?,?,?,?,?)",
                    (email, name or email.split("@")[0], now, now, terms_at or None, terms_ver or None),
                )
            conn.commit()
            conn.close()
            self._json(200, {"ok": True})
            return

        if path.endswith("/users/seller"):
            email = str(body.get("email") or "").strip().lower()
            if "@" not in email:
                self._json(400, {"ok": False})
                return
            photo_path = save_id_photo(email, str(body.get("idPhoto") or ""))
            now = time.time()
            conn = db()
            conn.execute(
                """
                INSERT INTO seller_profiles(
                  email, full_name, nationality, residence, phone, phone_verified,
                  id_type, id_number, id_expires, id_photo_path, experience_years,
                  favorite_breed, bank_account, ship_pledge, completed_at, updated
                ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
                ON CONFLICT(email) DO UPDATE SET
                  full_name=excluded.full_name,
                  nationality=excluded.nationality,
                  residence=excluded.residence,
                  phone=excluded.phone,
                  phone_verified=excluded.phone_verified,
                  id_type=excluded.id_type,
                  id_number=excluded.id_number,
                  id_expires=excluded.id_expires,
                  id_photo_path=COALESCE(excluded.id_photo_path, seller_profiles.id_photo_path),
                  experience_years=excluded.experience_years,
                  favorite_breed=excluded.favorite_breed,
                  bank_account=excluded.bank_account,
                  ship_pledge=excluded.ship_pledge,
                  completed_at=excluded.completed_at,
                  updated=excluded.updated
                """,
                (
                    email,
                    str(body.get("fullName") or "")[:80],
                    str(body.get("nationality") or "")[:80],
                    str(body.get("residence") or "")[:120],
                    str(body.get("phone") or "")[:40],
                    1 if body.get("phoneVerified") else 0,
                    str(body.get("idType") or "passport")[:20],
                    str(body.get("idNumber") or "")[:80],
                    str(body.get("idExpiresAt") or "")[:20],
                    photo_path,
                    str(body.get("experienceYears") or "")[:12],
                    str(body.get("favoriteBreed") or "")[:80],
                    str(body.get("bankAccount") or "")[:120],
                    1 if body.get("shipWorldwidePledge") else 0,
                    str(body.get("completedAt") or "")[:40],
                    now,
                ),
            )
            conn.commit()
            conn.close()
            self._json(200, {"ok": True})
            return

        if path.endswith("/ocr"):
            text = ocr_image(str(body.get("image") or ""))
            self._json(200, {"ok": True, "text": text})
            return

        if path.endswith("/serial"):
            raw_prefix = re.sub(r"[^A-Za-z]", "", str(body.get("prefix") or "BA")).upper()[:4]
            prefix = raw_prefix or "BA"
            conn = db()
            row = conn.execute("SELECT n FROM bird_serials WHERE prefix=?", (prefix,)).fetchone()
            n = int(row["n"]) + 1 if row else 1
            conn.execute(
                "INSERT INTO bird_serials(prefix, n) VALUES(?,?) ON CONFLICT(prefix) DO UPDATE SET n=excluded.n",
                (prefix, n),
            )
            conn.commit()
            conn.close()
            self._json(200, {"ok": True, "serial": f"{prefix}-{n:06d}"})
            return

        if path.endswith("/rings"):
            email = str(body.get("email") or "").strip().lower()[:120]
            name = str(body.get("name") or "").strip()[:80]
            city = str(body.get("city") or "").strip()[:80]
            bird_id = str(body.get("birdId") or "").strip()[:80]
            year = time.localtime().tm_year % 100
            prefix = f"PH{year:02d}"
            conn = db()
            row = conn.execute("SELECT n FROM bird_serials WHERE prefix=?", (prefix,)).fetchone()
            n = int(row["n"]) + 1 if row else 1
            conn.execute(
                "INSERT INTO bird_serials(prefix, n) VALUES(?,?) ON CONFLICT(prefix) DO UPDATE SET n=excluded.n",
                (prefix, n),
            )
            code = f"PH-{year:02d}-{n:06d}"
            conn.execute(
                "INSERT INTO hub_rings(code, year, email, bird_name, bird_id, city, created) VALUES(?,?,?,?,?,?,?)",
                (code, 2000 + year, email, name, bird_id, city, time.time()),
            )
            conn.commit()
            conn.close()
            self._json(200, {"ok": True, "code": code, "year": 2000 + year})
            return

        admin_user = admin_from_token(bearer(self))
        if path.endswith("/admin/notice"):
            if not admin_user:
                self._json(401, {"ok": False, "error": "auth"})
                return
            title = str(body.get("title") or "").strip()
            text = str(body.get("body") or "").strip()
            if not title or not text:
                self._json(400, {"ok": False})
                return
            conn = db()
            conn.execute(
                "INSERT INTO notices(title, body, active, created) VALUES(?,?,1,?)",
                (title, text, time.time()),
            )
            conn.commit()
            conn.close()
            audit(admin_user, "notice", title)
            self._json(200, {"ok": True})
            return

        if path.endswith("/admin/hidden"):
            if not admin_user:
                self._json(401, {"ok": False, "error": "auth"})
                return
            listing_id = str(body.get("listingId") or "").strip()
            reason = str(body.get("reason") or "").strip()
            if not listing_id:
                self._json(400, {"ok": False})
                return
            conn = db()
            conn.execute(
                "INSERT INTO hidden_listings(listing_id, reason, created) VALUES(?,?,?) ON CONFLICT(listing_id) DO UPDATE SET reason=excluded.reason",
                (listing_id, reason, time.time()),
            )
            conn.commit()
            conn.close()
            audit(admin_user, "hide", listing_id)
            self._json(200, {"ok": True})
            return

        if path.endswith("/admin/block"):
            if not admin_user:
                self._json(401, {"ok": False, "error": "auth"})
                return
            email = str(body.get("email") or "").strip().lower()
            reason = str(body.get("reason") or "").strip()
            if "@" not in email:
                self._json(400, {"ok": False})
                return
            conn = db()
            conn.execute(
                "INSERT INTO blocked_emails(email, reason, created) VALUES(?,?,?) ON CONFLICT(email) DO UPDATE SET reason=excluded.reason",
                (email, reason, time.time()),
            )
            conn.commit()
            conn.close()
            audit(admin_user, "block", email)
            self._json(200, {"ok": True})
            return

        if path.endswith("/admin/catalog"):
            if not admin_user:
                self._json(401, {"ok": False, "error": "auth"})
                return
            kind = clean_kind(body.get("kind") or "")
            label_ar = str(body.get("labelAr") or "").strip()[:120]
            label_en = str(body.get("labelEn") or "").strip()[:120]
            extra = parse_extra(body.get("extra") or {})
            if not kind or not label_ar:
                self._json(400, {"ok": False})
                return
            conn = db()
            conn.execute(
                "INSERT INTO catalog(kind, label_ar, label_en, extra, active, sort, created) VALUES(?,?,?,?,1,?,?)",
                (
                    kind,
                    label_ar,
                    label_en,
                    json.dumps(extra, ensure_ascii=False),
                    int(body.get("sort") or 0),
                    time.time(),
                ),
            )
            conn.commit()
            conn.close()
            audit(admin_user, "catalog_add", f"{kind}:{label_ar}")
            self._json(200, {"ok": True, "catalogs": catalog_grouped(False)})
            return

        email = str(body.get("email") or "").strip().lower()
        purpose = str(body.get("purpose") or "verify")
        if purpose not in ("verify", "reset", "phone"):
            purpose = "verify"
        if "@" not in email:
            self._json(400, {"ok": False, "error": "email"})
            return
        if path.endswith("/send"):
            code = f"{random.randint(0, 999999):06d}"
            codes = load_codes()
            codes[f"{purpose}:{email}"] = {"code": code, "exp": time.time() + TTL}
            save_json(STORE, codes)
            if purpose == "reset":
                subject = "Pigeons Hub password reset"
            elif purpose == "phone":
                subject = "Pigeons Hub phone verification"
            else:
                subject = "Pigeons Hub verification code"
            text = (
                f"رمز Pigeons Hub هو {code}. صالح لمدة ١٥ دقيقة.\n\n"
                f"Your Pigeons Hub code is {code}. It expires in 15 minutes."
            )
            try:
                send_mail(email, subject, text)
            except Exception as err:
                print(f"mail_fail {email}: {err}", flush=True)
                self._json(502, {"ok": False, "error": "mail"})
                return
            print(f"mail_ok {purpose}:{email}", flush=True)
            self._json(200, {"ok": True})
            return
        if path.endswith("/confirm"):
            code = str(body.get("code") or "").strip()
            codes = load_codes()
            item = codes.get(f"{purpose}:{email}")
            if not item or item.get("code") != code:
                self._json(400, {"ok": False})
                return
            del codes[f"{purpose}:{email}"]
            save_json(STORE, codes)
            self._json(200, {"ok": True})
            return
        self._json(404, {"ok": False})


if __name__ == "__main__":
    port = int(os.environ.get("PORT", "8788"))
    hosts = [os.environ.get("HOST", "172.17.0.1"), "0.0.0.0"]
    last = None
    for host in hosts:
        try:
            print(f"birds-auth listening on {host}:{port}", flush=True)
            ThreadingHTTPServer((host, port), Handler).serve_forever()
            break
        except OSError as err:
            last = err
    else:
        raise last
