/* =====================================================================
   DOGFATHER UNIVERSE — Interner Bereich (Postfach, Team-Verwaltung,
   DogiCrew-Supporter-Abo). Node.js/Express-Server als 1:1-Ersatz für den
   Cloudflare Worker "dogfather-universe-postfach"
   (siehe ../cloudflare-worker/src/worker.js für das Original).

   Architektur (unverändert zum Original):
     db.js                    SQLite-Datenbank (better-sqlite3), Migrationen
     lib/crypto.js            Hashing, AES-GCM (Code-Verschlüsselung), IDs
     lib/auth.js               Login-Prüfung, Sessions, Rate-Limiting (Team)
     lib/supporter-auth.js     Login/Sessions für den öffentlichen Bereich
     lib/permissions.js        Vollständiger Permission-Katalog
     lib/audit.js               Aktivitätsprotokoll schreiben
     lib/http.js                 JSON-Response-Helfer
     lib/live-status.js          TikTok-Live-Erkennung (ersetzt Cloudflare-Cron)
     routes/*.js                  Endpoint-Handler pro Bereich

   Unterschiede zum Original (rein technisch, gleiches Verhalten):
     - D1 (async) → better-sqlite3 (synchron)
     - KV BEWERBUNGEN wurde nie für mehr als den Live-Status-Schalter
       genutzt, der jetzt komplett in app_settings (SQLite) lebt.
     - ctx.waitUntil() → einfaches "nicht abwarten" (Node-Prozess läuft
       durchgehend weiter, kein Cold-Start-Risiko wie bei Workers).
     - scheduled()-Cron → node-cron, gleicher Rhythmus ("* * * * *").
   ===================================================================== */
import "dotenv/config";
import express from "express";
import cors from "cors";
import cron from "node-cron";

import { initDb } from "./db.js";
import { json } from "./lib/http.js";
import { requireSession } from "./lib/auth.js";
import { requireSupporterSession } from "./lib/supporter-auth.js";
import { handleLiveStatusGet, handleLiveStatusSet, refreshLiveFromTikTok } from "./lib/live-status.js";

import { login, logout, setOwnerCode } from "./routes/auth.js";
import { listRoles, createRole, updateRole, deleteRole } from "./routes/roles.js";
import {
  listUsers, prepareUser, finalizeUser, setUserCode, revealUserCode,
  lockUser, unlockUser, deleteUser, setUserRole, setUserPermissions,
  endUserSessions, updateUserMeta,
} from "./routes/users.js";
import {
  submitApplication, listApplications, changeApplicationStatus,
  takeOverApplication, assignApplication, archiveApplication,
  restoreApplication, deleteApplication, markAnswered,
  listNotes, addNote, deleteNote, listDrafts, saveDraft,
} from "./routes/applications.js";
import { listAuditLog } from "./routes/audit.js";
import {
  registerSupporter, verifySupporterEmail, requestSupporterLogin, verifySupporterLogin, logoutSupporter,
  googleLoginSupporter, getPublicConfig,
  getSupporterMe, updateSupporterProfile, requestSupporterEmailChange, confirmSupporterEmailChange,
  redeemPremium, deleteSupporterAccount,
} from "./routes/supporter.js";
import {
  startPaypalSubscription, confirmPaypalSubscription, cancelPaypalSubscription, handlePaypalWebhook,
} from "./routes/supporter-paypal.js";
import {
  listSupporters, getSupporterDetail, updatePremium, listCollections, upsertCollection, getAdminOverview,
} from "./routes/supporter-admin.js";

initDb();

const PORT = Number(process.env.PORT || 4200);
const app = express();
app.set("trust proxy", 1);
app.disable("x-powered-by"); // verrät sonst unnötig den eingesetzten Technik-Stack

app.use((req, res, next) => {
  res.setHeader("X-Content-Type-Options", "nosniff");
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
  res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
  next();
});

app.use(cors());
app.use(
  express.json({
    limit: "2mb",
    // Roh-Body wird zusätzlich als String aufbewahrt — die PayPal-Webhook-
    // Signaturprüfung braucht den unveränderten Original-Text (siehe
    // routes/supporter-paypal.js), nicht das nachträglich neu serialisierte
    // req.body.
    verify: (req, res, buf) => {
      req.rawBody = buf.toString("utf8");
    },
  })
);

/* ---------- Absturzsicherheit ----------
   Express 4 fängt Fehler aus async-Handlern NICHT selbst ab: eine abgelehnte Promise wird zu einer
   "unhandled rejection", und Node beendet daraufhin den ganzen Prozess — im Cloudflare Worker war
   ein Fehler dagegen immer auf die eine betroffene Anfrage begrenzt. Ohne die folgende Absicherung
   könnte z.B. eine Anfrage mit fehlender ID (better-sqlite3 wirft bei `undefined` als Parameter)
   den kompletten internen Bereich offline nehmen. `wrap()` leitet jeden Fehler stattdessen an den
   Fehler-Handler am Ende der Datei weiter. */
const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

/* ---------- Middlewares: interne Team-Session bzw. Supporter-Session ---------- */

const requireTeamSession = wrap(async (req, res, next) => {
  const session = await requireSession(req);
  if (!session) return json(res, { ok: false, error: "Sitzung abgelaufen oder ungültig. Bitte erneut anmelden." }, 401);
  req.session = session;
  next();
});

const requireSupporterAuth = wrap(async (req, res, next) => {
  const supporter = await requireSupporterSession(req);
  if (!supporter) return json(res, { ok: false, error: "Sitzung abgelaufen oder ungültig. Bitte erneut einloggen." }, 401);
  req.supporter = supporter;
  next();
});

/** Reicht die injizierte session/supporter als drittes Argument an den Handler weiter (wie im Cloudflare-Original). */
const withSession = (handler) => wrap((req, res) => handler(req, res, req.session));
const withSupporter = (handler) => wrap((req, res) => handler(req, res, req.supporter));

/* ---------- Öffentliche Routen (keine Session) ---------- */

app.get("/health", (req, res) => json(res, { ok: true, service: "dogfather-universe-interner-bereich" }));
app.post("/submit", wrap(submitApplication));
app.get("/live-status", wrap(handleLiveStatusGet));
app.post("/auth/login", wrap(login));

app.post("/supporter/register", wrap(registerSupporter));
app.post("/supporter/verify-email", wrap(verifySupporterEmail));
app.post("/supporter/login-request", wrap(requestSupporterLogin));
app.post("/supporter/verify-login", wrap(verifySupporterLogin));
app.post("/supporter/google-login", wrap(googleLoginSupporter));
app.get("/supporter/public-config", wrap(getPublicConfig));
// PayPal ruft diese Adresse direkt auf — eigene Signaturprüfung statt Session (siehe lib/paypal.js).
app.post("/supporter/paypal-webhook", wrap(handlePaypalWebhook));

/* ---------- Team-Bereich (interne Session, requireTeamSession) ---------- */

app.post("/auth/logout", requireTeamSession, wrap(logout));
app.post("/admin/owner/set-code", requireTeamSession, withSession(setOwnerCode));
app.post("/live-status/set", requireTeamSession, withSession(handleLiveStatusSet));

app.post("/admin/applications/list", requireTeamSession, withSession(listApplications));
app.post("/admin/applications/status", requireTeamSession, withSession(changeApplicationStatus));
app.post("/admin/applications/take-over", requireTeamSession, withSession(takeOverApplication));
app.post("/admin/applications/assign", requireTeamSession, withSession(assignApplication));
app.post("/admin/applications/archive", requireTeamSession, withSession(archiveApplication));
app.post("/admin/applications/restore", requireTeamSession, withSession(restoreApplication));
app.post("/admin/applications/delete", requireTeamSession, withSession(deleteApplication));
app.post("/admin/applications/mark-answered", requireTeamSession, withSession(markAnswered));

app.post("/admin/notes/list", requireTeamSession, withSession(listNotes));
app.post("/admin/notes/add", requireTeamSession, withSession(addNote));
app.post("/admin/notes/delete", requireTeamSession, withSession(deleteNote));

app.post("/admin/drafts/list", requireTeamSession, withSession(listDrafts));
app.post("/admin/drafts/save", requireTeamSession, withSession(saveDraft));

app.post("/admin/roles/list", requireTeamSession, withSession(listRoles));
app.post("/admin/roles/create", requireTeamSession, withSession(createRole));
app.post("/admin/roles/update", requireTeamSession, withSession(updateRole));
app.post("/admin/roles/delete", requireTeamSession, withSession(deleteRole));

app.post("/admin/users/list", requireTeamSession, withSession(listUsers));
app.post("/admin/users/prepare", requireTeamSession, withSession(prepareUser));
app.post("/admin/users/finalize", requireTeamSession, withSession(finalizeUser));
app.post("/admin/users/set-code", requireTeamSession, withSession(setUserCode));
app.post("/admin/users/reveal-code", requireTeamSession, withSession(revealUserCode));
app.post("/admin/users/lock", requireTeamSession, withSession(lockUser));
app.post("/admin/users/unlock", requireTeamSession, withSession(unlockUser));
app.post("/admin/users/delete", requireTeamSession, withSession(deleteUser));
app.post("/admin/users/set-role", requireTeamSession, withSession(setUserRole));
app.post("/admin/users/set-permissions", requireTeamSession, withSession(setUserPermissions));
app.post("/admin/users/end-sessions", requireTeamSession, withSession(endUserSessions));
app.post("/admin/users/update-meta", requireTeamSession, withSession(updateUserMeta));

app.post("/admin/audit/list", requireTeamSession, withSession(listAuditLog));

// Supporter-Abo — Administrationsbereich (interne Team-Session + eigene
// SUPPORTER_*-Rechte, siehe lib/permissions.js).
app.post("/admin/overview", requireTeamSession, withSession(getAdminOverview));
app.post("/admin/supporter/list", requireTeamSession, withSession(listSupporters));
app.post("/admin/supporter/detail", requireTeamSession, withSession(getSupporterDetail));
app.post("/admin/supporter/premium/update", requireTeamSession, withSession(updatePremium));
app.post("/admin/supporter/collections/list", requireTeamSession, withSession(listCollections));
app.post("/admin/supporter/collections/upsert", requireTeamSession, withSession(upsertCollection));

/* ---------- Öffentlicher Supporter-Bereich (eigene Session, requireSupporterAuth) ---------- */

app.post("/supporter/logout", requireSupporterAuth, wrap(logoutSupporter));
app.post("/supporter/me", requireSupporterAuth, withSupporter(getSupporterMe));
app.post("/supporter/update-profile", requireSupporterAuth, withSupporter(updateSupporterProfile));
app.post("/supporter/request-email-change", requireSupporterAuth, withSupporter(requestSupporterEmailChange));
app.post("/supporter/confirm-email-change", requireSupporterAuth, withSupporter(confirmSupporterEmailChange));
app.post("/supporter/redeem-premium", requireSupporterAuth, withSupporter(redeemPremium));
app.post("/supporter/delete-account", requireSupporterAuth, withSupporter(deleteSupporterAccount));
app.post("/supporter/paypal/start", requireSupporterAuth, withSupporter(startPaypalSubscription));
app.post("/supporter/paypal/confirm", requireSupporterAuth, withSupporter(confirmPaypalSubscription));
app.post("/supporter/paypal/cancel", requireSupporterAuth, withSupporter(cancelPaypalSubscription));

app.use((req, res) => json(res, { ok: false, error: "Not found." }, 404));

/* ---------- Fehler-Handler (muss NACH allen Routen stehen) ----------
   Fängt alles ab, was in einem Handler schiefgeht, und antwortet mit sauberem JSON statt einer
   HTML-Fehlerseite mit Stacktrace. Ersetzt gleichzeitig die try/catch-Behandlung für kaputtes
   JSON, die im Cloudflare-Original noch in jeder Route einzeln stand. */
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
  if (err?.type === "entity.parse.failed") {
    return json(res, { ok: false, error: "Ungültiges JSON." }, 400);
  }
  if (err?.type === "entity.too.large") {
    return json(res, { ok: false, error: "Die Anfrage ist zu groß." }, 413);
  }
  console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err);
  if (res.headersSent) return;
  return json(res, { ok: false, error: "Interner Fehler." }, 500);
});

/* Letztes Netz: Ein unbeaufsichtigter Fehler darf den Dienst nicht beenden. Ohne diese beiden
   Zeilen würde Node den Prozess bei einer unbehandelten Promise-Ablehnung sofort beenden und der
   gesamte interne Bereich (inkl. Zahlungsverarbeitung) wäre bis zum systemd-Neustart offline. */
process.on("unhandledRejection", (grund) => console.error("[unhandledRejection]", grund));
process.on("uncaughtException", (fehler) => console.error("[uncaughtException]", fehler?.stack || fehler));

/* ---------- Cron: läuft jede Minute, hält den Live-Punkt aktuell (ersetzt Cloudflares scheduled()) ---------- */
cron.schedule("* * * * *", () => {
  refreshLiveFromTikTok().catch((err) => console.error("[live-status] Fehler beim Aktualisieren:", err?.message || err));
});

app.listen(PORT, "127.0.0.1", () => {
  console.log(`DOGFATHER UNIVERSE — Interner Bereich läuft auf http://127.0.0.1:${PORT}`);
});
