// Per-user "my account" endpoints for multi-tenant Recap. Distinct
// from admin-routes.js — these are scoped to the currently signed-in
// user (req.user), not to whichever tenant id you pass in the URL.
// Used by the lite settings panel to render "Active sessions" and
// "Sign out everywhere" actions for a tenant managing their own
// account.
//
// Endpoints:
//   GET    /api/account/sessions             — my active sessions
//   DELETE /api/account/sessions/:sessionId  — revoke a specific session of mine
//   POST   /api/account/sessions/revoke-others — revoke everything BUT the current session
//
// Multi-mode only. Single mode never mounts these — the synthetic
// "owner" user has no session table to manage.

import { getDb } from "./db.js";
import { requireUser } from "./tenant-auth.js";
import { hashPassword, validatePasswordPolicy } from "./auth-routes.js";
import fs from "fs/promises";
import path from "path";
import { getHistoryDir } from "./history.js";

export function setupAccountRoutes(app) {
  // ── My license key (for the "Take Recap home" flow) ────────────────
  // The Pro/Max license is a bearer credential — anyone with the LIC1-
  // string can present it to the relay. We only return the CALLING
  // user's own key (gated by req.user.id), never anyone else's. The key
  // is stored on users.keysat_license; the parsed entitlement state
  // already comes via /api/license-status. This endpoint is the one
  // place the raw string is exposed — used by the lite settings panel
  // to show a copy-to-clipboard "Take Recap home" block for paid
  // tenants who want to also run Recap on their own StartOS server.
  app.get("/api/account/license-key", requireUser, (req, res) => {
    if (!req.user || !req.user.id) {
      return res.status(403).json({ error: "no_user" });
    }
    try {
      const row = getDb()
        .prepare("SELECT keysat_license FROM users WHERE id = ?")
        .get(req.user.id);
      const key = (row?.keysat_license || "").trim();
      if (!key) {
        return res.status(404).json({ error: "no_license" });
      }
      res.json({ license_key: key });
    } catch (err) {
      console.error("[account] license-key lookup failed:", err);
      res.status(500).json({ error: "internal_error" });
    }
  });


  // List MY active sessions (the device list).
  app.get("/api/account/sessions", requireUser, (req, res) => {
    // Trial users (req.userId starts with "anon:") don't have a row in
    // the sessions table — they're tracked via anon_trials. Bail with
    // an empty list so the UI doesn't error.
    if (!req.user || !req.user.id) {
      return res.json({ sessions: [], current_session_id: null });
    }
    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(req.user.id, Date.now());
      res.json({
        sessions: rows,
        // Tell the UI which row is the CURRENT session so it can
        // disable the "Revoke" button for that one (sign out instead).
        current_session_id: req.session?.id || null,
      });
    } catch (err) {
      console.error("[account] sessions list failed:", err);
      res.status(500).json({ error: "internal_error" });
    }
  });

  // Revoke a single session of mine. The session id MUST belong to me
  // (we WHERE clause both id AND user_id). Otherwise a tenant could
  // delete someone else's session by guessing the id.
  app.delete(
    "/api/account/sessions/:sessionId",
    requireUser,
    (req, res) => {
      if (!req.user || !req.user.id) {
        return res.status(400).json({ error: "trial_has_no_sessions" });
      }
      const sessionId = req.params.sessionId;
      try {
        const result = getDb()
          .prepare("DELETE FROM sessions WHERE id = ? AND user_id = ?")
          .run(sessionId, req.user.id);
        if (result.changes === 0) {
          return res.status(404).json({ error: "session_not_found" });
        }
        res.json({ ok: true });
      } catch (err) {
        console.error("[account] revoke session failed:", err);
        res.status(500).json({ error: "internal_error" });
      }
    },
  );

  // ── Set / clear my password ────────────────────────────────────────
  // Magic-link is the primary auth. Setting a password is optional —
  // makes returning sign-in faster. Set and "change" use the same
  // endpoint (overwrite); the reset flow is "sign in via magic link,
  // then call this endpoint with the new password."
  //
  // POST /api/account/password   { password }   — set or overwrite
  // DELETE /api/account/password                — clear / revert to
  //                                              magic-link-only
  //
  // Lives in /api/account/* (not /auth/*) because both require an
  // active session and need the tenant-auth middleware to attach
  // req.user — /auth/* is the public-path namespace bypassed by the
  // middleware so unauthenticated visitors can request links / verify.
  app.post("/api/account/password", requireUser, (req, res) => {
    if (!req.user || !req.user.id) {
      return res.status(401).json({ error: "auth_required" });
    }
    const password = req.body?.password;
    const policyErr = validatePasswordPolicy(password);
    if (policyErr) {
      return res.status(400).json({
        error: policyErr,
        message:
          policyErr === "password_too_short"
            ? "Use at least 8 characters."
            : policyErr === "password_too_long"
            ? "256 characters max."
            : "Pick a password.",
      });
    }
    try {
      const hash = hashPassword(password);
      getDb()
        .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
        .run(hash, req.user.id);
      console.log(`[account] password set for user ${req.user.id}`);
      res.json({ ok: true });
    } catch (err) {
      console.error("[account] set-password failed:", err);
      res.status(500).json({ error: "internal_error" });
    }
  });

  app.delete("/api/account/password", requireUser, (req, res) => {
    if (!req.user || !req.user.id) {
      return res.status(401).json({ error: "auth_required" });
    }
    try {
      getDb()
        .prepare("UPDATE users SET password_hash = NULL WHERE id = ?")
        .run(req.user.id);
      console.log(`[account] password cleared for user ${req.user.id}`);
      res.json({ ok: true });
    } catch (err) {
      console.error("[account] clear-password failed:", err);
      res.status(500).json({ error: "internal_error" });
    }
  });

  // ── Delete my account ──────────────────────────────────────────────
  // GDPR-style hard delete of the calling user. Confirms via a body
  // sentinel ({confirm: "DELETE"}) so a stray DELETE request can't
  // wipe an account by accident. After deletion the session cookie is
  // cleared; the user lands back on the anonymous landing page.
  //
  // Cascades (via the schema's ON DELETE CASCADE) handle:
  //   - sessions (drop everywhere)
  //   - tenant_credits (drop balance row)
  //   - library_meta (drop the index entries)
  //   - subscriptions (drop billing history)
  //   - magic_link_tokens stay (they have no FK to users — they're
  //     keyed by email, harmless to leave)
  //   - anon_trials.converted_to_user_id SET NULL (analytics row stays)
  //
  // The filesystem-side history folder /data/history/<user_id>/ is
  // removed separately — SQLite cascades don't reach the FS. We do
  // this AFTER the DB delete so partial failures don't leave dangling
  // metadata.
  //
  // Admin users CANNOT delete themselves via this endpoint — that
  // would leave the multi-tenant Recap orphaned with no admin. They
  // can downgrade themselves first via SQL if they really mean it.
  app.delete("/api/account", requireUser, async (req, res) => {
    if (!req.user || !req.user.id) {
      return res.status(403).json({ error: "no_user" });
    }
    if (req.user.is_admin) {
      return res.status(400).json({
        error: "cannot_self_delete_admin",
        message:
          "Operator account can't be self-deleted (no admin would be left). Demote yourself first.",
      });
    }
    if (req.body?.confirm !== "DELETE") {
      return res.status(400).json({
        error: "confirmation_required",
        message: "Send {confirm: \"DELETE\"} in the body to confirm.",
      });
    }
    const userId = req.user.id;
    try {
      // FK cascade handles sessions, tenant_credits, library_meta,
      // subscriptions. Run inside a transaction so a crash mid-delete
      // doesn't leave the user partially intact.
      const db = getDb();
      const result = db
        .transaction(() => {
          return db
            .prepare("DELETE FROM users WHERE id = ?")
            .run(userId);
        })();
      if (result.changes === 0) {
        return res.status(404).json({ error: "user_not_found" });
      }
    } catch (err) {
      console.error("[account] delete user DB op failed:", err);
      return res.status(500).json({ error: "internal_error" });
    }
    // FS cleanup. Best-effort — failures here don't roll the user
    // back into existence. Worst case is an orphan folder that the
    // operator can sweep manually.
    try {
      const userDir = path.join(getHistoryDir(), userId);
      await fs.rm(userDir, { recursive: true, force: true });
    } catch (err) {
      console.warn(
        `[account] history dir cleanup failed for ${userId} (DB delete succeeded):`,
        err,
      );
    }
    // Clear the session cookie so the next request from this browser
    // is anonymous, not "stale-session-401-prompted-to-sign-in".
    res.setHeader(
      "Set-Cookie",
      "recap_session=; Max-Age=0; Path=/; HttpOnly; SameSite=Lax; Secure",
    );
    console.log(`[account] user ${userId} deleted their account`);
    res.json({ ok: true });
  });

  // "Sign out everywhere except this device" — useful after a
  // suspicious-login email or just for hygiene. Deletes every session
  // for this user EXCEPT the one carrying the request.
  app.post(
    "/api/account/sessions/revoke-others",
    requireUser,
    (req, res) => {
      if (!req.user || !req.user.id) {
        return res.status(400).json({ error: "trial_has_no_sessions" });
      }
      const currentSessionId = req.session?.id;
      try {
        let result;
        if (currentSessionId) {
          result = getDb()
            .prepare(
              "DELETE FROM sessions WHERE user_id = ? AND id != ?",
            )
            .run(req.user.id, currentSessionId);
        } else {
          // No current session detected (shouldn't happen if requireUser
          // passed, but defensively): treat as "revoke all of mine."
          result = getDb()
            .prepare("DELETE FROM sessions WHERE user_id = ?")
            .run(req.user.id);
        }
        res.json({ ok: true, revoked: result.changes });
      } catch (err) {
        console.error("[account] revoke-others failed:", err);
        res.status(500).json({ error: "internal_error" });
      }
    },
  );
}
