// Operator (is_admin = 1) endpoints for multi-tenant Recap. Every
// route here is gated by requireOperator from tenant-auth.js — non-
// admin users get 403. Single-mode never mounts these.
//
// What's exposed:
//   GET    /api/admin/tenants                 — list all users + credits + license status
//   POST   /api/admin/tenants/:id/grant       — add credits to a user's local balance
//   POST   /api/admin/tenants/:id/tier        — set a user's subscription tier (relay-owned)
//   GET    /api/admin/tenants/:id/sessions    — list a user's active sessions
//   DELETE /api/admin/tenants/:id/sessions    — revoke ALL of a user's sessions
//   DELETE /api/admin/sessions/:sessionId     — revoke a specific session
//   GET    /api/admin/recent-signups          — signups grouped by IP/UA in the last N hours
//
// All responses are JSON. Tenant rows shape:
//   { id, email, display_name, is_admin, tier, has_license, balance,
//     lifetime_granted, lifetime_consumed, created_at, last_signin_at,
//     signup_ip, signup_user_agent, session_count }
//
// Volume notes: SQLite, ~100s of tenants tops for an alpha cohort.
// No pagination yet — caps at LIMIT 500 to be defensive.

import { getDb } from "./db.js";
import { requireOperator } from "./tenant-auth.js";
import fs from "fs/promises";
import path from "path";
import { getHistoryDir } from "./history.js";
import { getRelayOperatorKey } from "./relay-default.js";

const MAX_TENANT_ROWS = 500;
const RECENT_SIGNUPS_MAX_HOURS = 24 * 14; // 2 weeks

export function setupAdminRoutes(app) {
  // ── Run / test the subscription expiry-reminder scan ───────────────────
  // Operator-only. With { test_email } it sends a SAMPLE reminder to that
  // address — verifies the recaps.cc SMTP + the email rendering without
  // needing a real near-expiry subscription. Without it, forces an
  // immediate scan of real expiring subscriptions and returns the summary
  // ({ sent, skipped } or a { skipped: <reason> } if a precondition fails).
  app.post("/api/admin/reminders/run", requireOperator, async (req, res) => {
    try {
      const testEmail =
        typeof req.body?.test_email === "string" ? req.body.test_email.trim() : "";
      if (testEmail) {
        const { isSmtpReady, sendMail } = await import("./smtp.js");
        if (!isSmtpReady()) {
          return res.status(503).json({
            error: "smtp_not_ready",
            message: "Configure StartOS System SMTP first.",
          });
        }
        const { renderSubscriptionReminderEmail } = await import(
          "./email-template.js"
        );
        const { getConfigSnapshot } = await import("./config.js");
        const snap = await getConfigSnapshot();
        const publicUrl = (snap.recap_public_url || "")
          .trim()
          .replace(/\/$/, "");
        const msg = renderSubscriptionReminderEmail({
          brandName: "Recaps",
          tier: "pro",
          expiresAt: new Date(Date.now() + 7 * 86_400_000).toISOString(),
          daysLeft: 7,
          kind: "upcoming_7d",
          manageUrl: `${publicUrl}/?renew=1`,
        });
        await sendMail({
          to: testEmail,
          subject: msg.subject,
          text: msg.text,
          html: msg.html,
        });
        return res.json({ ok: true, test_email_sent_to: testEmail });
      }
      const { runReminderScan } = await import("./subscription-reminders.js");
      const result = await runReminderScan({ force: true });
      res.json({ ok: true, ...result });
    } catch (err) {
      console.error("[admin] reminder run failed:", err?.message || err);
      res.status(500).json({
        error: "reminder_run_failed",
        message: err?.message || String(err),
      });
    }
  });

  // ── List all tenants ───────────────────────────────────────────────────
  app.get("/api/admin/tenants", requireOperator, (req, res) => {
    try {
      const rows = getDb()
        .prepare(
          `SELECT
             u.id, u.email, u.display_name, u.is_admin, u.tier,
             u.created_at, u.last_signin_at,
             u.signup_ip, u.signup_user_agent,
             CASE WHEN u.keysat_license IS NOT NULL AND length(u.keysat_license) > 0 THEN 1 ELSE 0 END AS has_license,
             COALESCE(tc.purchased_balance, 0)  AS purchased_balance,
             COALESCE(tc.replenish_balance, 0)  AS replenish_balance,
             COALESCE(tc.purchased_balance, 0) + COALESCE(tc.replenish_balance, 0) AS balance,
             COALESCE(tc.lifetime_granted, 0)   AS lifetime_granted,
             COALESCE(tc.lifetime_consumed, 0)  AS lifetime_consumed,
             (SELECT COUNT(*) FROM sessions s
              WHERE s.user_id = u.id AND s.expires_at > ?) AS session_count
           FROM users u
           LEFT JOIN tenant_credits tc ON tc.user_id = u.id
           ORDER BY u.created_at DESC
           LIMIT ?`,
        )
        .all(Date.now(), MAX_TENANT_ROWS);
      // The relay-owned tier can only be set when this server holds the
      // matching operator key (otherwise the grant just 502s). The UI hides
      // the per-row "Tier" control when this is false, so a self-hosted
      // operator — who has no matching key for the canonical relay — doesn't
      // see a button that can't work.
      res.json({
        tenants: rows,
        relay_operator_key_configured: !!getRelayOperatorKey(),
      });
    } catch (err) {
      console.error("[admin] /tenants failed:", err);
      res.status(500).json({ error: "internal_error" });
    }
  });

  // ── Grant credits to a tenant ──────────────────────────────────────────
  // Body: { amount }. Positive integer.
  //
  // Upserts tenant_credits and increments balance + lifetime_granted.
  // Records the operator's id + a timestamp so we have an audit trail
  // when /api/admin/audit lands later (currently the grant is implicit
  // via lifetime_granted; the trail can be reconstructed by diffing).
  app.post(
    "/api/admin/tenants/:id/grant",
    requireOperator,
    async (req, res) => {
      const userId = req.params.id;
      const amount = parseInt(req.body?.amount, 10);
      if (!Number.isFinite(amount) || amount <= 0 || amount > 100000) {
        return res
          .status(400)
          .json({ error: "bad_amount", message: "amount must be 1..100000" });
      }
      try {
        const db = getDb();
        // Ensure the user exists before we upsert credits.
        const user = db
          .prepare("SELECT id, email FROM users WHERE id = ?")
          .get(userId);
        if (!user) {
          return res.status(404).json({ error: "user_not_found" });
        }
        // Admin grants go into the PURCHASED bucket — they're explicit
        // operator-initiated credits and shouldn't get wiped by the
        // next replenishment cycle. Helper handles upsert + lifetime_granted.
        const { addPurchased } = await import("./tenant-credits.js");
        const row = addPurchased(userId, amount);
        const total =
          (row?.purchased_balance || 0) + (row?.replenish_balance || 0);
        console.log(
          `[admin] granted ${amount} credits to ${user.email} (by ${req.user.email}) — total balance ${total}`,
        );
        res.json({
          ok: true,
          user_id: userId,
          balance: total,
          purchased_balance: row?.purchased_balance || 0,
          replenish_balance: row?.replenish_balance || 0,
          granted: amount,
        });
      } catch (err) {
        console.error("[admin] grant failed:", err);
        res.status(500).json({ error: "internal_error" });
      }
    },
  );

  // ── Set a tenant's subscription tier ───────────────────────────────────
  // Body: { tier }. One of "core" | "pro" | "max".
  //
  // Core-decoupling: the RELAY owns the subscription (keyed by Recaps
  // user-id), NOT a per-user Keysat license. This route writes the
  // relay-side tier FIRST (the authoritative owner), and only on success
  // caches it on the local users row. That ordering keeps the two from
  // drifting — we never show a user as Pro in the UI while the relay still
  // rejects their cloud calls. The cached users.tier is what the per-user
  // feature gates (tts-routes, license-status) and the providers'
  // cloud-identity selection read on each request.
  app.post(
    "/api/admin/tenants/:id/tier",
    requireOperator,
    async (req, res) => {
      const userId = req.params.id;
      const tier = String(req.body?.tier || "").trim().toLowerCase();
      if (!["core", "pro", "max"].includes(tier)) {
        return res.status(400).json({
          error: "bad_tier",
          message: 'tier must be "core", "pro", or "max"',
        });
      }
      try {
        const db = getDb();
        const user = db
          .prepare("SELECT id, email FROM users WHERE id = ?")
          .get(userId);
        if (!user) {
          return res.status(404).json({ error: "user_not_found" });
        }
        // 1) Authoritative write to the relay (the subscription owner).
        //    Throws if the operator key / relay base URL isn't configured,
        //    or if the relay rejects the call — in which case we DON'T
        //    touch the local cache.
        const { setRelayUserTier } = await import("./providers/relay.js");
        try {
          await setRelayUserTier({ userId, tier });
        } catch (err) {
          console.error(
            "[admin] relay tier push failed:",
            err?.message || err,
          );
          return res.status(502).json({
            error: "relay_tier_failed",
            message:
              "Couldn't set the tier on the relay (the subscription owner). " +
              "Check the relay operator key + base URL, then retry. " +
              (err?.message || ""),
          });
        }
        // 2) Cache locally so feature gates + the badge see it immediately.
        db.prepare("UPDATE users SET tier = ? WHERE id = ?").run(tier, userId);
        console.log(
          `[admin] set tier=${tier} for ${user.email} (by ${req.user.email})`,
        );
        res.json({ ok: true, user_id: userId, tier });
      } catch (err) {
        console.error("[admin] set tier failed:", err);
        res.status(500).json({ error: "internal_error" });
      }
    },
  );

  // ── List a tenant's active sessions ────────────────────────────────────
  app.get(
    "/api/admin/tenants/:id/sessions",
    requireOperator,
    (req, res) => {
      const userId = req.params.id;
      try {
        const rows = getDb()
          .prepare(
            `SELECT id, created_at, expires_at, last_used_at, user_agent, ip_address
             FROM sessions
             WHERE user_id = ? AND expires_at > ?
             ORDER BY last_used_at DESC, created_at DESC`,
          )
          .all(userId, Date.now());
        res.json({ sessions: rows });
      } catch (err) {
        console.error("[admin] sessions list failed:", err);
        res.status(500).json({ error: "internal_error" });
      }
    },
  );

  // ── Revoke ALL sessions for a tenant ───────────────────────────────────
  // Operator-side "sign this user out everywhere" button. Useful when
  // a tenant flags account theft, or for ban-hammer scenarios. The
  // user can sign back in via magic link unless the operator also
  // disables their email (future feature).
  app.delete(
    "/api/admin/tenants/:id/sessions",
    requireOperator,
    (req, res) => {
      const userId = req.params.id;
      try {
        const result = getDb()
          .prepare("DELETE FROM sessions WHERE user_id = ?")
          .run(userId);
        console.log(
          `[admin] revoked ${result.changes} session(s) for user ${userId} (by ${req.user.email})`,
        );
        res.json({ ok: true, revoked: result.changes });
      } catch (err) {
        console.error("[admin] revoke all sessions failed:", err);
        res.status(500).json({ error: "internal_error" });
      }
    },
  );

  // ── Revoke a single session by id ──────────────────────────────────────
  app.delete(
    "/api/admin/sessions/:sessionId",
    requireOperator,
    (req, res) => {
      const sessionId = req.params.sessionId;
      try {
        const result = getDb()
          .prepare("DELETE FROM sessions WHERE id = ?")
          .run(sessionId);
        res.json({ ok: true, revoked: result.changes });
      } catch (err) {
        console.error("[admin] revoke session failed:", err);
        res.status(500).json({ error: "internal_error" });
      }
    },
  );

  // ── Delete a tenant (admin ban-hammer) ─────────────────────────────────
  // Operator removes a tenant — abuse response, account cleanup, etc.
  // Mirrors the user-side /api/account DELETE but operator-scoped:
  //   - Refuses to delete another admin (admins protect each other)
  //   - Refuses to delete the calling operator (use the user-side
  //     endpoint or SQL if you really need that)
  //   - Cascades sessions, tenant_credits, library_meta via FK ON DELETE
  //   - FS cleanup of /data/history/<user_id>/ is best-effort
  app.delete(
    "/api/admin/tenants/:id",
    requireOperator,
    async (req, res) => {
      const userId = req.params.id;
      if (userId === req.user.id) {
        return res.status(400).json({
          error: "cannot_delete_self",
          message:
            "Use the user-side endpoint to delete your own account, or demote first.",
        });
      }
      try {
        const db = getDb();
        const target = db
          .prepare("SELECT id, email, is_admin FROM users WHERE id = ?")
          .get(userId);
        if (!target) {
          return res.status(404).json({ error: "user_not_found" });
        }
        if (target.is_admin) {
          return res.status(400).json({
            error: "cannot_delete_admin",
            message:
              "Demote this user from admin first if you really want to delete them.",
          });
        }
        db.prepare("DELETE FROM users WHERE id = ?").run(userId);
        console.log(
          `[admin] deleted tenant ${target.email} (id ${userId}) (by ${req.user.email})`,
        );
        // FS cleanup, best-effort.
        try {
          const userDir = path.join(getHistoryDir(), userId);
          await fs.rm(userDir, { recursive: true, force: true });
        } catch (err) {
          console.warn(
            `[admin] history-dir cleanup failed for ${userId}:`,
            err?.message || err,
          );
        }
        res.json({ ok: true });
      } catch (err) {
        console.error("[admin] delete tenant failed:", err);
        res.status(500).json({ error: "internal_error" });
      }
    },
  );

  // ── Recent signups (forensic / abuse detection) ────────────────────────
  // Query: ?hours=N (default 24, clamped to RECENT_SIGNUPS_MAX_HOURS).
  // Returns:
  //   {
  //     window_hours: 24,
  //     by_ip:        [{ ip, count, emails[], first_seen, last_seen }],
  //     by_ua:        [{ ua, count, ... }],
  //     by_hour:      [{ hour, count }],
  //     totals:       { signups, magic_link_requests }
  //   }
  app.get("/api/admin/recent-signups", requireOperator, (req, res) => {
    const hoursReq = parseInt(req.query?.hours, 10) || 24;
    const hours = Math.min(
      Math.max(1, hoursReq),
      RECENT_SIGNUPS_MAX_HOURS,
    );
    const cutoff = Date.now() - hours * 60 * 60 * 1000;
    try {
      const db = getDb();

      // Tracked sources:
      //   - users.signup_* — confirmed signups (someone clicked magic link)
      //   - magic_link_tokens.request_* — link requests, includes unconverted
      //
      // Top-line metrics show both so the operator can spot patterns
      // like "10000 link requests, 2 actual signups" → scripted abuse.

      // Signups by IP
      const byIp = db
        .prepare(
          `SELECT signup_ip AS ip, COUNT(*) AS count,
                  MIN(created_at) AS first_seen,
                  MAX(created_at) AS last_seen,
                  GROUP_CONCAT(email, '|') AS emails_joined
           FROM users
           WHERE created_at > ? AND signup_ip IS NOT NULL AND signup_ip != ''
           GROUP BY signup_ip
           ORDER BY count DESC, last_seen DESC
           LIMIT 50`,
        )
        .all(cutoff)
        .map((r) => ({
          ip: r.ip,
          count: r.count,
          first_seen: r.first_seen,
          last_seen: r.last_seen,
          emails: (r.emails_joined || "").split("|").filter(Boolean),
        }));

      // Signups by UA (truncated for readability — group on the first
      // 80 chars so "Mozilla/5.0 ... Chrome/120" rows collapse into
      // one even if patch versions differ slightly).
      const byUa = db
        .prepare(
          `SELECT substr(signup_user_agent, 1, 80) AS ua, COUNT(*) AS count,
                  MIN(created_at) AS first_seen,
                  MAX(created_at) AS last_seen
           FROM users
           WHERE created_at > ? AND signup_user_agent IS NOT NULL AND signup_user_agent != ''
           GROUP BY substr(signup_user_agent, 1, 80)
           ORDER BY count DESC, last_seen DESC
           LIMIT 50`,
        )
        .all(cutoff);

      // Signups per hour (for a sparkline; small array).
      const byHour = db
        .prepare(
          `SELECT (created_at / 3600000) * 3600000 AS hour, COUNT(*) AS count
           FROM users
           WHERE created_at > ?
           GROUP BY hour
           ORDER BY hour ASC`,
        )
        .all(cutoff);

      const signupCount = db
        .prepare("SELECT COUNT(*) AS n FROM users WHERE created_at > ?")
        .get(cutoff).n;
      const magicLinkCount = db
        .prepare(
          "SELECT COUNT(*) AS n FROM magic_link_tokens WHERE created_at > ?",
        )
        .get(cutoff).n;

      // Magic-link request distribution by IP (catches "lots of
      // requests, no signups" abuse — abusers who keep requesting
      // links but never click them, or who can't because the email
      // isn't real). Truncate emails-joined to first 5 to keep
      // payload reasonable.
      const linkByIp = db
        .prepare(
          `SELECT request_ip AS ip, COUNT(*) AS count,
                  MIN(created_at) AS first_seen,
                  MAX(created_at) AS last_seen,
                  COUNT(DISTINCT email) AS distinct_emails
           FROM magic_link_tokens
           WHERE created_at > ? AND request_ip IS NOT NULL AND request_ip != ''
           GROUP BY request_ip
           ORDER BY count DESC, last_seen DESC
           LIMIT 50`,
        )
        .all(cutoff);

      res.json({
        window_hours: hours,
        totals: {
          signups: signupCount,
          magic_link_requests: magicLinkCount,
        },
        signups_by_ip: byIp,
        signups_by_ua: byUa,
        signups_by_hour: byHour,
        magic_links_by_ip: linkByIp,
      });
    } catch (err) {
      console.error("[admin] recent-signups failed:", err);
      res.status(500).json({ error: "internal_error" });
    }
  });
}
