"""
Jami → Cloudflare Pages + D1 API deployen.
"""

from __future__ import annotations

import json
import re
import shutil
import subprocess
from pathlib import Path

from jam.dating_site import SITE_ROOT, build_site
from jam.jami_domain import resolve_site_url, write_domain_guide

D1_ID_FILE = Path(__file__).resolve().parents[1] / "data" / "jami-d1-database-id.txt"
CF_SRC = Path(__file__).resolve().parents[1] / "cloudflare"


def _run(cmd: list[str], cwd: Path, *, stdin: str | None = None) -> tuple[int, str]:
    npx = shutil.which("npx") or shutil.which("npx.cmd")
    if not npx and cmd[0] == "npx":
        return 1, "npx fehlt"
    try:
        p = subprocess.run(
            cmd,
            cwd=str(cwd),
            capture_output=True,
            text=True,
            encoding="utf-8",
            errors="replace",
            timeout=300,
            input=stdin,
        )
        return p.returncode, (p.stdout or "") + (p.stderr or "")
    except OSError as e:
        return 1, str(e)


def _npx_base() -> list[str]:
    npx = shutil.which("npx") or shutil.which("npx.cmd")
    return [npx, "wrangler"] if npx else ["wrangler"]


def ensure_d1_database() -> str:
    if D1_ID_FILE.is_file():
        cached = D1_ID_FILE.read_text(encoding="utf-8").strip()
        if len(cached) >= 36:
            return cached
    code, out = _run(_npx_base() + ["d1", "create", "jami-db"], CF_SRC.parent)
    db_id = ""
    m = re.search(r"database_id\s*=\s*([a-f0-9-]+)", out, re.I)
    if m:
        db_id = m.group(1)
    if not db_id:
        _, lst_out = _run(_npx_base() + ["d1", "list"], CF_SRC.parent)
        m2 = re.search(
            r"([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})",
            lst_out,
            re.I,
        )
        if m2:
            db_id = m2.group(1)
    if not db_id and code != 0:
        raise RuntimeError(f"D1 create fehlgeschlagen:\n{out[-1500:]}")
    if not db_id:
        raise RuntimeError("D1 database_id nicht gefunden – Cloudflare Dashboard → D1")
    D1_ID_FILE.parent.mkdir(parents=True, exist_ok=True)
    D1_ID_FILE.write_text(db_id, encoding="utf-8")
    return db_id


def write_cloudflare_guide(site_root: Path | None = None) -> Path:
    root = site_root or SITE_ROOT
    path = root / "CLOUDFLARE_EINRICHTEN.txt"
    path.write_text(
        f"Jami läuft auf Cloudflare mit eingebautem Server (D1).\nOrdner: {root}\n",
        encoding="utf-8",
    )
    return path


# Pretty URLs on Pages already serve impressum.html at /impressum; rewrites here cause 308 loops.
_PAGES_REDIRECTS = ("/register /index.html 200",)
_REMOVE_REDIRECTS = frozenset(
    {
        "/agb /agb.html 200",
        "/impressum /impressum.html 200",
        "/datenschutz /datenschutz.html 200",
    }
)


def _ensure_pages_redirects(site_root: Path) -> None:
    """Cloudflare Pages _redirects (only rules that do not fight Pretty URLs)."""
    path = site_root / "_redirects"
    lines = path.read_text(encoding="utf-8").splitlines() if path.is_file() else []
    kept: list[str] = []
    for ln in lines:
        s = ln.strip()
        if not s or s.startswith("#") or s in _REMOVE_REDIRECTS:
            continue
        kept.append(s)
    existing = set(kept)
    for rule in _PAGES_REDIRECTS:
        if rule not in existing:
            kept.append(rule)
    path.write_text("\n".join(kept) + "\n", encoding="utf-8")


def _write_static_extras(site_root: Path) -> None:
    ad_cfg = site_root / "js" / "ad-config.json"
    if not ad_cfg.exists():
        ad_cfg.write_text(
            '{"enabled":false,"adsenseClient":"","adSlot":""}\n',
            encoding="utf-8",
        )
    _ensure_pages_redirects(site_root)
    write_domain_guide(site_root)
    (site_root / "robots.txt").write_text(
        "User-agent: *\n"
        "Allow: /\n"
        "Allow: /.well-known/assetlinks.json\n"
        "Sitemap: https://www.jami-dating.de/sitemap.xml\n",
        encoding="utf-8",
    )
    headers = site_root / "_headers"
    headers.write_text(
        "/*\n"
        "  X-Content-Type-Options: nosniff\n"
        "  Referrer-Policy: strict-origin-when-cross-origin\n"
        "\n"
        "/index.html\n"
        "  Cache-Control: no-cache, must-revalidate\n"
        "\n"
        "/\n"
        "  Cache-Control: no-cache, must-revalidate\n"
        "\n"
        "/js/*\n"
        "  Cache-Control: public, max-age=3600, must-revalidate\n"
        "\n"
        "/css/*\n"
        "  Cache-Control: public, max-age=3600, must-revalidate\n"
        "\n"
        "/images/*\n"
        "  Cache-Control: public, max-age=604800\n"
        "\n"
        "/manifest.webmanifest\n"
        "  Content-Type: application/manifest+json\n"
        "  Cache-Control: public, max-age=3600\n"
        "\n"
        "/.well-known/assetlinks.json\n"
        "  Content-Type: application/json\n"
        "  Cache-Control: public, max-age=300\n"
        "\n"
        "/robots.txt\n"
        "  Content-Type: text/plain\n"
        "\n"
        "/api/media/*\n"
        "  Cache-Control: public, max-age=31536000, immutable\n",
        encoding="utf-8",
    )


def write_wrangler_toml(site_root: Path, database_id: str, *, use_r2: bool = False) -> None:
    from jam.jami_r2 import wrangler_r2_snippet

    r2_block = wrangler_r2_snippet() if use_r2 else ""
    toml = f"""name = "jami"
compatibility_date = "2024-11-01"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = "."

[[d1_databases]]
binding = "DB"
database_name = "jami-db"
database_id = "{database_id}"
{r2_block}"""
    (site_root / "wrangler.toml").write_text(toml, encoding="utf-8")


def copy_cloudflare_assets(site_root: Path) -> None:
    func_src = CF_SRC / "functions"
    func_dst = site_root / "functions"
    if not func_src.is_dir():
        return
    for path in func_src.rglob("*"):
        if path.is_file():
            rel = path.relative_to(func_src)
            out = func_dst / rel
            out.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(path, out)
    pkg = CF_SRC / "package.json"
    if pkg.is_file():
        shutil.copy2(pkg, site_root / "package.json")


def install_function_deps(site_root: Path) -> tuple[int, str]:
    """npm install für Stripe/web-push in Pages functions."""
    pkg = site_root / "package.json"
    if not pkg.is_file():
        return 0, ""
    npm = shutil.which("npm") or shutil.which("npm.cmd")
    if not npm:
        return 1, "npm fehlt – Node.js installieren"
    return _run([npm, "install", "--omit=dev"], site_root)


def read_local_secrets() -> dict:
    secrets_file = Path(__file__).resolve().parents[1] / "data" / "jami-secrets.local.json"
    if not secrets_file.is_file():
        return {}
    try:
        return json.loads(secrets_file.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        return {}


def put_pages_secret(name: str, value: str, *, project_name: str = "jami") -> tuple[int, str]:
    """Cloudflare Pages Secret setzen (Wert per stdin an wrangler)."""
    val = (value or "").strip()
    if not val:
        return 1, f"{name}: leer"
    return _run(
        _npx_base() + ["pages", "secret", "put", name, f"--project-name={project_name}"],
        CF_SRC.parent,
        stdin=val,
    )


def sync_stripe_pages_secrets(*, project_name: str = "jami", site_url: str | None = None) -> list[str]:
    """STRIPE_SECRET_KEY (+ optional Webhook) aus data/jami-secrets.local.json → Cloudflare."""
    site_url = (site_url or resolve_site_url()).rstrip("/")
    s = read_local_secrets()
    if not s:
        return ["jami-secrets.local.json fehlt"]
    logs: list[str] = []
    sk = str(s.get("stripe_secret_key", "")).strip()
    if sk.startswith("sk_") and "HIER" not in sk:
        code, out = put_pages_secret("STRIPE_SECRET_KEY", sk, project_name=project_name)
        logs.append("STRIPE_SECRET_KEY: OK" if code == 0 else f"STRIPE_SECRET_KEY fehlgeschlagen: {out[-400:]}")
    wh = str(s.get("stripe_webhook_secret", "")).strip()
    if wh.startswith("whsec_") and "HIER" not in wh:
        code, out = put_pages_secret("STRIPE_WEBHOOK_SECRET", wh, project_name=project_name)
        logs.append("STRIPE_WEBHOOK_SECRET: OK" if code == 0 else f"STRIPE_WEBHOOK_SECRET: {out[-300:]}")
    code, out = put_pages_secret("JAMI_SITE_URL", site_url, project_name=project_name)
    logs.append("JAMI_SITE_URL: OK" if code == 0 else f"JAMI_SITE_URL: {out[-300:]}")
    admin = str(s.get("jami_admin_secret", "")).strip()
    if admin and "HIER" not in admin:
        code, out = put_pages_secret("JAMI_ADMIN_SECRET", admin, project_name=project_name)
        logs.append("JAMI_ADMIN_SECRET: OK" if code == 0 else f"JAMI_ADMIN_SECRET: {out[-300:]}")
    gcid = str(s.get("google_client_id", "")).strip()
    gsec = str(s.get("google_client_secret", "")).strip()
    if gcid and "HIER" not in gcid:
        code, out = put_pages_secret("GOOGLE_CLIENT_ID", gcid, project_name=project_name)
        logs.append("GOOGLE_CLIENT_ID: OK" if code == 0 else f"GOOGLE_CLIENT_ID: {out[-300:]}")
    else:
        logs.append("GOOGLE_CLIENT_ID: fehlt (google_client_id in jami-secrets.local.json)")
    if gsec and "HIER" not in gsec:
        code, out = put_pages_secret("GOOGLE_CLIENT_SECRET", gsec, project_name=project_name)
        logs.append("GOOGLE_CLIENT_SECRET: OK" if code == 0 else f"GOOGLE_CLIENT_SECRET: {out[-300:]}")
    else:
        logs.append("GOOGLE_CLIENT_SECRET: optional (Redirect-Login)")
    fbid = str(s.get("facebook_app_id", "")).strip()
    fbsec = str(s.get("facebook_app_secret", "")).strip()
    if fbid and fbsec and "HIER" not in fbid:
        code, out = put_pages_secret("FACEBOOK_APP_ID", fbid, project_name=project_name)
        logs.append("FACEBOOK_APP_ID: OK" if code == 0 else f"FACEBOOK_APP_ID: {out[-300:]}")
        code, out = put_pages_secret("FACEBOOK_APP_SECRET", fbsec, project_name=project_name)
        logs.append("FACEBOOK_APP_SECRET: OK" if code == 0 else f"FACEBOOK_APP_SECRET: {out[-300:]}")
    else:
        logs.append("FACEBOOK_APP_ID/SECRET: fehlt (facebook_app_id + facebook_app_secret)")
    vpub = str(s.get("vapid_public_key", "")).strip()
    vpriv = str(s.get("vapid_private_key", "")).strip()
    if vpub and vpriv and "HIER" not in vpub:
        code, out = put_pages_secret("VAPID_PUBLIC_KEY", vpub, project_name=project_name)
        logs.append("VAPID_PUBLIC_KEY: OK" if code == 0 else f"VAPID_PUBLIC_KEY: {out[-300:]}")
        code, out = put_pages_secret("VAPID_PRIVATE_KEY", vpriv, project_name=project_name)
        logs.append("VAPID_PRIVATE_KEY: OK" if code == 0 else f"VAPID_PRIVATE_KEY: {out[-300:]}")
        subj = str(s.get("vapid_subject", "mailto:kontakt@ki-web-studio.de")).strip()
        if subj:
            code, out = put_pages_secret("VAPID_SUBJECT", subj, project_name=project_name)
            logs.append("VAPID_SUBJECT: OK" if code == 0 else f"VAPID_SUBJECT: {out[-300:]}")
    else:
        logs.append("VAPID_*: fehlt (python scripts/ensure_vapid_keys.py)")
    cron = str(s.get("cron_secret", "")).strip()
    if cron and "HIER" not in cron:
        code, out = put_pages_secret("CRON_SECRET", cron, project_name=project_name)
        logs.append("CRON_SECRET: OK" if code == 0 else f"CRON_SECRET: {out[-300:]}")
    else:
        logs.append("CRON_SECRET: fehlt (cron_secret in jami-secrets.local.json)")
    resend = str(s.get("resend_api_key", "")).strip()
    if resend.startswith("re_") and "HIER" not in resend:
        code, out = put_pages_secret("RESEND_API_KEY", resend, project_name=project_name)
        logs.append("RESEND_API_KEY: OK" if code == 0 else f"RESEND_API_KEY: {out[-300:]}")
    else:
        logs.append("RESEND_API_KEY: fehlt (python scripts/setup_resend_email.py)")
    mail_from = str(s.get("mail_from", "")).strip()
    if mail_from and "HIER" not in mail_from:
        code, out = put_pages_secret("MAIL_FROM", mail_from, project_name=project_name)
        logs.append("MAIL_FROM: OK" if code == 0 else f"MAIL_FROM: {out[-300:]}")
    elif resend.startswith("re_"):
        default_from = "Jami <onboarding@resend.dev>"
        code, out = put_pages_secret("MAIL_FROM", default_from, project_name=project_name)
        logs.append("MAIL_FROM: OK (onboarding@resend.dev)" if code == 0 else f"MAIL_FROM: {out[-300:]}")
    beta_url = str(s.get("beta_test_play_url", "")).strip()
    if beta_url.startswith("https://"):
        code, out = put_pages_secret("BETA_TEST_PLAY_URL", beta_url, project_name=project_name)
        logs.append("BETA_TEST_PLAY_URL: OK" if code == 0 else f"BETA_TEST_PLAY_URL: {out[-300:]}")
    else:
        logs.append("BETA_TEST_PLAY_URL: fehlt (beta_test_play_url in jami-secrets.local.json)")
    beta_notify = str(s.get("beta_test_notify_email", "david.prechtel1986@gmail.com")).strip()
    if beta_notify and "@" in beta_notify:
        code, out = put_pages_secret("BETA_TEST_NOTIFY_EMAIL", beta_notify, project_name=project_name)
        logs.append("BETA_TEST_NOTIFY_EMAIL: OK" if code == 0 else f"BETA_TEST_NOTIFY_EMAIL: {out[-300:]}")
    mkt_notify = str(s.get("marketing_notify_email", beta_notify)).strip()
    if mkt_notify and "@" in mkt_notify:
        code, out = put_pages_secret("MARKETING_NOTIFY_EMAIL", mkt_notify, project_name=project_name)
        logs.append("MARKETING_NOTIFY_EMAIL: OK" if code == 0 else f"MARKETING_NOTIFY_EMAIL: {out[-300:]}")
    return logs


def sync_vibeloop_pages_secrets(*, project_name: str = "vibeloop", site_url: str | None = None) -> list[str]:
    """VibeLoop: Stream, Stripe, Site-URLs → Cloudflare Pages Secrets."""
    s = read_local_secrets()
    if not s:
        return ["jami-secrets.local.json fehlt – Vorlage: data/jami-secrets.local.json.example"]
    resolved = (
        site_url
        or s.get("flarlo_site_url")
        or s.get("vibeup_site_url")
        or s.get("vibeloop_site_url")
        or s.get("jami_site_url")
        or "https://flarlo.de"
    )
    resolved = str(resolved).strip().rstrip("/")
    logs: list[str] = []

    account_id = str(s.get("cloudflare_account_id", "")).strip()
    if account_id and "DEINE" not in account_id.upper():
        code, out = put_pages_secret("CLOUDFLARE_ACCOUNT_ID", account_id, project_name=project_name)
        logs.append("CLOUDFLARE_ACCOUNT_ID: OK" if code == 0 else f"CLOUDFLARE_ACCOUNT_ID: {out[-300:]}")
    else:
        logs.append("CLOUDFLARE_ACCOUNT_ID: fehlt (cloudflare_account_id) – Stream-Fallback R2")

    api_token = str(s.get("cloudflare_api_token", "")).strip()
    if api_token and "DEIN" not in api_token.upper():
        code, out = put_pages_secret("CLOUDFLARE_API_TOKEN", api_token, project_name=project_name)
        logs.append("CLOUDFLARE_API_TOKEN: OK" if code == 0 else f"CLOUDFLARE_API_TOKEN: {out[-300:]}")
    else:
        logs.append("CLOUDFLARE_API_TOKEN: fehlt (cloudflare_api_token, Stream:Edit)")

    code, out = put_pages_secret("FLARLO_SITE_URL", resolved, project_name=project_name)
    logs.append("FLARLO_SITE_URL: OK" if code == 0 else f"FLARLO_SITE_URL: {out[-300:]}")
    code, out = put_pages_secret("VIBEUP_SITE_URL", resolved, project_name=project_name)
    logs.append("VIBEUP_SITE_URL: OK" if code == 0 else f"VIBEUP_SITE_URL: {out[-300:]}")
    code, out = put_pages_secret("VIBELOOP_SITE_URL", resolved, project_name=project_name)
    logs.append("VIBELOOP_SITE_URL: OK" if code == 0 else f"VIBELOOP_SITE_URL: {out[-300:]}")
    code, out = put_pages_secret("JAMI_SITE_URL", resolved, project_name=project_name)
    logs.append("JAMI_SITE_URL: OK" if code == 0 else f"JAMI_SITE_URL: {out[-300:]}")

    sk = str(s.get("stripe_secret_key", "")).strip()
    if sk.startswith("sk_") and "HIER" not in sk:
        code, out = put_pages_secret("STRIPE_SECRET_KEY", sk, project_name=project_name)
        logs.append("STRIPE_SECRET_KEY: OK" if code == 0 else f"STRIPE_SECRET_KEY: {out[-300:]}")
    wh = str(s.get("stripe_webhook_secret", "")).strip()
    if wh.startswith("whsec_") and "HIER" not in wh:
        code, out = put_pages_secret("STRIPE_WEBHOOK_SECRET", wh, project_name=project_name)
        logs.append("STRIPE_WEBHOOK_SECRET: OK" if code == 0 else f"STRIPE_WEBHOOK_SECRET: {out[-300:]}")

    return logs


def setup_vibeloop_d1_schema() -> list[str]:
    """VibeLoop D1-Migrationen (remote)."""
    logs: list[str] = []
    for mig_name in (
        "migrate_clippulse.sql",
        "migrate_clippulse_premium.sql",
        "migrate_clippulse_intro.sql",
        "migrate_clippulse_overlays.sql",
        "migrate_vibeloop_live_signal.sql",
        "migrate_vibeloop_live_segments.sql",
        "migrate_vibeloop_stream_live.sql",
        "migrate_vibeloop_live_chat.sql",
        "migrate_vibeloop_inbox.sql",
        "migrate_vibeloop_reports.sql",
        "migrate_vibeloop_live_match.sql",
        "migrate_vibeloop_video_scan.sql",
    ):
        migrate = CF_SRC / mig_name
        if not migrate.is_file():
            continue
        code, out = _run(
            _npx_base() + ["d1", "execute", "jami-db", "--file", str(migrate), "--remote"],
            CF_SRC.parent,
        )
        logs.append(f"{mig_name}: OK" if code == 0 else f"{mig_name}: {out[-200:]}")
    return logs


def deploy_vibeloop_to_cloudflare(
    *,
    project_name: str = "vibeloop",
    branch: str = "main",
    run_migrations: bool = True,
    sync_secrets: bool = True,
) -> tuple[bool, str]:
    """VibeLoop bauen, Secrets syncen, optional D1 migrieren, Pages deployen."""
    from jam.jami_r2 import ensure_r2_bucket, r2_available
    from jam.vibeloop_site import build_site

    s = read_local_secrets()
    site_url = str(
        s.get("flarlo_site_url")
        or s.get("vibeup_site_url")
        or s.get("vibeloop_site_url")
        or s.get("jami_site_url")
        or "https://flarlo.de"
    ).strip().rstrip("/")
    site = build_site(site_url=site_url)

    (site / "js" / "config.json").write_text(
        json.dumps(
            json.loads((site / "js" / "config.json").read_text(encoding="utf-8")) | {"backend": "cloudflare"},
            ensure_ascii=False,
            indent=2,
        ),
        encoding="utf-8",
    )

    try:
        db_id = ensure_d1_database()
        r2_ok, r2_msg = ensure_r2_bucket()
        write_wrangler_toml(site, db_id, use_r2=r2_available())
        copy_cloudflare_assets(site)
        code, npm_out = install_function_deps(site)
        if code != 0:
            return False, f"npm install fehlgeschlagen:\n{npm_out[-1500:]}"
        if run_migrations:
            setup_d1_schema(db_id)
    except RuntimeError as e:
        copy_cloudflare_assets(site)
        return False, f"{e}\n\nDeploy abgebrochen."

    mig_logs: list[str] = []
    if run_migrations:
        mig_logs = setup_vibeloop_d1_schema()

    secret_logs: list[str] = []
    if sync_secrets:
        secret_logs = sync_vibeloop_pages_secrets(project_name=project_name, site_url=site_url)

    npx = shutil.which("npx") or shutil.which("npx.cmd")
    if not npx:
        return False, f"Node/npx fehlt. Build-Ordner: {site}"

    cmd = [
        npx,
        "wrangler",
        "pages",
        "deploy",
        str(site),
        f"--project-name={project_name}",
        f"--branch={branch}",
        "--commit-dirty=true",
    ]
    code, out = _run(cmd, site)
    if code != 0:
        if "not authenticated" in out.lower():
            return False, "Cloudflare Login: npx wrangler login\n" + out[-800:]
        return False, out[-2000:]

    deploy_url = site_url
    for line in out.splitlines():
        if "https://" in line and ("pages.dev" in line or site_url.replace("https://", "") in line):
            for part in line.split():
                if part.startswith("https://"):
                    deploy_url = part.strip()
                    break

    parts = [f"Flarlo deploy OK: {deploy_url}", f"Projekt: {project_name}"]
    if secret_logs:
        parts.append("Secrets:\n  " + "\n  ".join(secret_logs))
    if mig_logs:
        parts.append("Migrationen:\n  " + "\n  ".join(mig_logs))
    try:
        _, r2_msg = ensure_r2_bucket()
        if r2_msg:
            parts.append(f"R2: {r2_msg}")
    except Exception:
        pass
    return True, "\n".join(parts)


def setup_d1_schema(database_id: str) -> None:
    schema = CF_SRC / "schema.sql"
    if not schema.is_file():
        return
    _run(
        _npx_base() + ["d1", "execute", "jami-db", "--file", str(schema), "--remote"],
        CF_SRC.parent,
    )
    for mig_name in (
        "migrate_profiles_v2.sql",
        "migrate_photos_friends_v3.sql",
        "migrate_profile_complete_bonus.sql",
        "migrate_last_active_at.sql",
        "migrate_profile_boost.sql",
        "migrate_activity_badges.sql",
        "migrate_chat_read_typing.sql",
        "migrate_profile_emoji.sql",
        "migrate_vip_gold.sql",
        "migrate_reactivation_email.sql",
        "migrate_gift_3d_email.sql",
        "migrate_coin_nudge.sql",
        "migrate_winback_shop.sql",
        "migrate_abandoned_checkout.sql",
        "migrate_engagement.sql",
        "migrate_like_flash.sql",
        "migrate_tool_funnel.sql",
        "migrate_tool_sales.sql",
        "migrate_worker_extra.sql",
        "migrate_push_subscriptions.sql",
        "migrate_referral.sql",
        "migrate_stripe.sql",
        "migrate_stripe_customer.sql",
        "migrate_analytics.sql",
        "migrate_remove_demo_users.sql",
        "migrate_clear_stock_photos.sql",
        "migrate_oauth.sql",
        "migrate_wm_tips.sql",
        "migrate_wm_fan_chat.sql",
        "migrate_wm_fan_reactions.sql",
        "migrate_beta_test.sql",
        "migrate_dashboard_traffic.sql",
    ):
        migrate = CF_SRC / mig_name
        if migrate.is_file():
            _run(
                _npx_base() + ["d1", "execute", "jami-db", "--file", str(migrate), "--remote"],
                CF_SRC.parent,
            )


def deploy_to_cloudflare(
    *,
    project_name: str = "jami",
    branch: str = "main",
) -> tuple[bool, str]:
    from jam.jami_stripe_setup import write_stripe_files
    from jam.jami_supabase_setup import write_supabase_files

    site = build_site(site_url=resolve_site_url())

    write_supabase_files(site)
    write_stripe_files(site)
    write_cloudflare_guide(site)
    _write_static_extras(site)

    (site / "js" / "config.json").write_text(
        json.dumps(
            json.loads((site / "js" / "config.json").read_text(encoding="utf-8"))
            | {"backend": "cloudflare"},
            ensure_ascii=False,
            indent=2,
        ),
        encoding="utf-8",
    )

    try:
        from jam.jami_r2 import ensure_r2_bucket, r2_available

        db_id = ensure_d1_database()
        r2_ok, r2_msg = ensure_r2_bucket()
        write_wrangler_toml(site, db_id, use_r2=r2_available())
        copy_cloudflare_assets(site)
        code, npm_out = install_function_deps(site)
        if code != 0:
            return False, f"npm install fehlgeschlagen:\n{npm_out[-1500:]}"
        setup_d1_schema(db_id)
    except RuntimeError as e:
        copy_cloudflare_assets(site)
        return False, f"{e}\n\nSeite ohne API deployt – nur lokal/PayPal."

    npx = shutil.which("npx") or shutil.which("npx.cmd")
    if not npx:
        return False, f"Node fehlt. Ordner: {site}"

    secret_logs = sync_stripe_pages_secrets(project_name=project_name, site_url=resolve_site_url())
    r2_note = ""
    try:
        from jam.jami_r2 import ensure_r2_bucket

        _, r2_msg = ensure_r2_bucket()
        r2_note = f"\nR2 Fotos: {r2_msg}" if r2_msg else ""
    except Exception:
        pass
    secret_msg = "\n".join(secret_logs) if secret_logs else ""

    cmd = [
        npx,
        "wrangler",
        "pages",
        "deploy",
        str(site),
        f"--project-name={project_name}",
        f"--branch={branch}",
        "--commit-dirty=true",
    ]
    code, out = _run(cmd, site)
    if code != 0:
        if "not authenticated" in out.lower():
            return False, "Cloudflare Login: npx wrangler login\n" + out[-800:]
        return False, out[-2000:]

    site_url = resolve_site_url()
    deploy_preview = ""
    for line in out.splitlines():
        if "pages.dev" in line and "https://" in line:
            for part in line.split():
                if part.startswith("https://") and "pages.dev" in part:
                    deploy_preview = part.strip()
                    break
    url = site_url
    extra = ""
    if deploy_preview and deploy_preview.rstrip("/") != site_url.rstrip("/"):
        extra = f"\nDeploy-Vorschau: {deploy_preview}"
    return True, (
        f"Live mit echtem Server: {url}\nRegistrieren → echte Nutzer weltweit.\nAPI: {url}/api/health"
        + extra
        + (f"\n\nCloudflare Secrets:\n{secret_msg}" if secret_msg else "")
        + r2_note
    )
