/**
 * OPS_STATE=PROD(구 REAL) 일 때 이메일(@ncue.net) 매직 링크 인증
 */
const crypto = require("crypto");
const { isOpsProdMode } = require("./lib/ops-state");
const path = require("path");
const fsSync = require("fs");
const { v4: uuidv4 } = require("uuid");

let nodemailer = null;
try {
  nodemailer = require("nodemailer");
} catch {
  /* optional */
}

const OPS_AUTH_COOKIE = "ops_user_session";
const ALLOWED_EMAIL_SUFFIX = "@ncue.net";
/** 메일 안 인증 링크 유효 시간(토큰 만료) */
const MAGIC_LINK_MAX_AGE_MS = 15 * 60 * 1000;

/**
 * 만기 없음 모드일 때 Set-Cookie max-age 상한(브라우저·HTTP 한도; 서명 payload의 exp는 Number.MAX_SAFE_INTEGER)
 * 10년마다 쿠키가 사라지면 다시 이메일 인증(매직 링크)이 필요할 수 있음
 */
const OPS_SESSION_BROWSER_MAX_AGE_MS = 10 * 365 * 24 * 60 * 60 * 1000;

/** OPS_SESSION_TZ 기준 달력 날짜 키 (YYYY-MM-DD) */
function calendarDateKeyInTz(tsMs, tz) {
  const parts = new Intl.DateTimeFormat("en-CA", {
    timeZone: tz,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  }).formatToParts(new Date(tsMs));
  const y = parts.find((p) => p.type === "year").value;
  const mo = parts.find((p) => p.type === "month").value;
  const da = parts.find((p) => p.type === "day").value;
  return `${y}-${mo}-${da}`;
}

/** 그레고리력 YYYY-MM-DD 문자열에 일수 더하기(타임존 무관, 달력 날짜만) */
function addCalendarDaysToKey(key, days) {
  const n = Math.floor(Number(days) || 0);
  const [y, m, d] = key.split("-").map(Number);
  const base = new Date(Date.UTC(y, m - 1, d));
  base.setUTCDate(base.getUTCDate() + n);
  const yy = base.getUTCFullYear();
  const mm = String(base.getUTCMonth() + 1).padStart(2, "0");
  const dd = String(base.getUTCDate()).padStart(2, "0");
  return `${yy}-${mm}-${dd}`;
}

/** dateKey 달력 날짜에 속하는 임의 시각(ms) 찾기 */
function findMsOnCalendarDay(dateKey, tz) {
  const [y, m, d] = dateKey.split("-").map(Number);
  const start = Date.UTC(y, m - 1, d, 0, 0, 0, 0);
  for (let k = -24 * 4; k < 96 * 4; k += 1) {
    const ms = start + k * 15 * 60 * 1000;
    if (calendarDateKeyInTz(ms, tz) === dateKey) return ms;
  }
  throw new Error(`findMsOnCalendarDay: ${dateKey} (${tz})`);
}

/**
 * OPS_SESSION_TZ에서 dateKey 해당 날의 마지막 순간(그날 23:59:59.999에 해당하는 epoch ms)
 */
function getLastMsOfCalendarDayInTz(dateKey, tz) {
  const anchor = findMsOnCalendarDay(dateKey, tz);
  let lo = anchor;
  let hi = anchor + 48 * 60 * 60 * 1000;
  let guard = 0;
  while (calendarDateKeyInTz(hi, tz) === dateKey && guard < 400) {
    hi += 24 * 60 * 60 * 1000;
    guard += 1;
  }
  if (calendarDateKeyInTz(hi, tz) === dateKey) {
    hi = anchor + 400 * 24 * 60 * 60 * 1000;
  }
  while (hi - lo > 1) {
    const mid = Math.floor((lo + hi) / 2);
    if (calendarDateKeyInTz(mid, tz) === dateKey) lo = mid;
    else hi = mid;
  }
  return hi - 1;
}

/**
 * 로그인 세션 만료 시각(ms)
 * - OPS_SESSION_TTL_DAYS: 미설정·0·never·none → 만기 없음(서명 exp = Number.MAX_SAFE_INTEGER)
 * - 양의 정수 N → 로그인일(OPS_SESSION_TZ 달력) + N일의 마지막 순간
 */
function getOpsSessionExpiresAtMs(nowMs = Date.now()) {
  const tz = (process.env.OPS_SESSION_TZ || "Asia/Seoul").trim() || "Asia/Seoul";
  const raw = String(process.env.OPS_SESSION_TTL_DAYS ?? "0").trim() || "0";
  const lower = raw.toLowerCase();
  if (lower === "never" || lower === "none" || raw === "0") {
    return Number.MAX_SAFE_INTEGER;
  }
  const parsed = parseInt(raw, 10);
  if (Number.isFinite(parsed) && parsed > 0) {
    const loginDayKey = calendarDateKeyInTz(nowMs, tz);
    const targetKey = addCalendarDaysToKey(loginDayKey, parsed);
    return getLastMsOfCalendarDayInTz(targetKey, tz);
  }
  return Number.MAX_SAFE_INTEGER;
}

/** res.cookie maxAge(ms) — 만기 없음일 때 astronomically large 값 대신 브라우저에 맞는 상한 */
function getOpsSessionCookieMaxAgeMs(sessionExpMs) {
  const remain = Math.max(0, sessionExpMs - Date.now());
  if (sessionExpMs === Number.MAX_SAFE_INTEGER) {
    return Math.min(remain, OPS_SESSION_BROWSER_MAX_AGE_MS);
  }
  return remain;
}

function isOpsProd() {
  return isOpsProdMode();
}

function getAuthSecret() {
  return (process.env.AUTH_SECRET || process.env.ADMIN_TOKEN || "ncue-admin").trim();
}

function getBaseUrl() {
  const b = (process.env.BASE_URL || "").trim().replace(/\/$/, "");
  if (b) return b;
  const port = process.env.PORT || 8030;
  return `http://localhost:${port}`;
}

/**
 * 매직 링크 절대 URL — BASE_URL + `/auth/verify/:token` 만 조합 (본문 문장·조사와 문자열 결합 금지)
 */
function buildMagicVerifyUrl(baseUrl, token) {
  const base = String(baseUrl || "").trim().replace(/\/$/, "");
  const t = String(token || "").trim();
  if (!base) throw new Error("BASE_URL이 비어 있습니다.");
  if (!t) throw new Error("인증 토큰이 비어 있습니다.");
  const path = `/auth/verify/${encodeURIComponent(t)}`;
  return new URL(path, `${base}/`).href;
}

const RETURN_TO_DEFAULT = "/learning";

/**
 * 인증 후·로그인 returnTo — 상대 경로만 허용(오픈 리다이렉트·한글 조사 등 오염 차단)
 * @param {unknown} v
 * @returns {string}
 */
function sanitizeReturnTo(v) {
  const s = (v || "").toString().trim();
  if (!s.startsWith("/") || s.startsWith("//")) return RETURN_TO_DEFAULT;
  if (s.includes("\\") || s.includes("..")) return RETURN_TO_DEFAULT;

  const qIdx = s.indexOf("?");
  const hIdx = s.indexOf("#");
  let pathEnd = s.length;
  if (qIdx >= 0) pathEnd = Math.min(pathEnd, qIdx);
  if (hIdx >= 0) pathEnd = Math.min(pathEnd, hIdx);
  const pathPart = s.slice(0, pathEnd);
  const suffix = s.slice(pathEnd);

  if (pathPart !== "/" && !/^\/[a-zA-Z0-9][a-zA-Z0-9/_\-.]*$/.test(pathPart)) {
    if (s !== RETURN_TO_DEFAULT) {
      console.warn("[OPS] invalid returnTo path rejected:", s.slice(0, 160));
    }
    return RETURN_TO_DEFAULT;
  }
  if (suffix && !/^[?#][a-zA-Z0-9_\-=&.%+]*$/.test(suffix)) {
    if (s !== RETURN_TO_DEFAULT) {
      console.warn("[OPS] invalid returnTo query rejected:", s.slice(0, 160));
    }
    return RETURN_TO_DEFAULT;
  }
  return s;
}

function appendVerifiedParam(returnPath) {
  const s = sanitizeReturnTo(returnPath);
  return s.includes("?") ? `${s}&verified=1` : `${s}?verified=1`;
}

function isAllowedXavisEmail(email) {
  const s = String(email || "")
    .trim()
    .toLowerCase();
  if (!s.includes("@")) return false;
  return s.endsWith(ALLOWED_EMAIL_SUFFIX);
}

function signSessionCookie(email, expMs, iatMs = Date.now()) {
  const exp = expMs;
  const iat = iatMs;
  const payload = `${email}|${exp}|${iat}`;
  const sig = crypto.createHmac("sha256", getAuthSecret()).update(payload).digest("hex");
  return Buffer.from(JSON.stringify({ email, exp, iat, sig })).toString("base64url");
}

function parseSessionCookie(val) {
  if (!val || typeof val !== "string") return null;
  try {
    const j = JSON.parse(Buffer.from(val, "base64url").toString("utf8"));
    if (!j.email || typeof j.exp !== "number") return null;
    if (Date.now() > j.exp) return null;
    const email = String(j.email).toLowerCase();
    const exp = j.exp;
    const iat = typeof j.iat === "number" ? j.iat : 0;
    const payloadNew = `${email}|${exp}|${iat}`;
    const sigNew = crypto.createHmac("sha256", getAuthSecret()).update(payloadNew).digest("hex");
    if (sigNew === j.sig) {
      return { email, exp, iat };
    }
    const payloadLegacy = `${email}|${exp}`;
    const sigLegacy = crypto.createHmac("sha256", getAuthSecret()).update(payloadLegacy).digest("hex");
    if (sigLegacy === j.sig) {
      return { email, exp, iat: 0 };
    }
    return null;
  } catch {
    return null;
  }
}

function getOpsSessionEmail(req) {
  const session = parseSessionCookie(req.cookies?.[OPS_AUTH_COOKIE]);
  return session ? session.email : null;
}

async function resolveOpsSessionEmail(req, hooks) {
  const session = parseSessionCookie(req.cookies?.[OPS_AUTH_COOKIE]);
  if (!session) return null;
  if (typeof hooks.isSessionRevoked === "function") {
    const revoked = await hooks.isSessionRevoked({
      email: session.email,
      iatMs: session.iat,
    });
    if (revoked) return null;
  }
  return session.email;
}

function loadMagicLinks(MAGIC_LINK_PATH) {
  try {
    const raw = fsSync.readFileSync(MAGIC_LINK_PATH, "utf8");
    const arr = JSON.parse(raw);
    return Array.isArray(arr) ? arr : [];
  } catch {
    return [];
  }
}

function saveMagicLinks(MAGIC_LINK_PATH, list) {
  fsSync.mkdirSync(path.dirname(MAGIC_LINK_PATH), { recursive: true });
  fsSync.writeFileSync(MAGIC_LINK_PATH, JSON.stringify(list, null, 2), "utf8");
}

function pruneExpired(list) {
  const now = Date.now();
  return list.filter((x) => x.expiresAt > now);
}

/** 클라이언트에 내부 주소·스택이 노출되지 않도록 SMTP 오류 메시지 정리 */
function publicSmtpErrorMessage(err) {
  const raw = String(err?.message || err || "");
  const code = err?.code || err?.cause?.code;
  if (code === "ECONNREFUSED" || raw.includes("ECONNREFUSED")) {
    return "메일 서버에 연결할 수 없습니다. PROD 환경의 SMTP_HOST·포트가 앱 서버에서 접근 가능한지(방화벽·사내 전용 게이트웨이 여부)를 확인하세요.";
  }
  if (code === "ETIMEDOUT" || raw.includes("ETIMEDOUT") || /timeout/i.test(raw)) {
    return "메일 서버 연결 시간이 초과되었습니다.";
  }
  if (code === "ENOTFOUND" || raw.includes("ENOTFOUND")) {
    return "메일 서버 주소(SMTP_HOST)를 찾을 수 없습니다.";
  }
  if (code === "ECONNRESET" || raw.includes("ECONNRESET")) {
    return "메일 서버와의 연결이 끊겼습니다. TLS/포트(SMTP_SECURE, SMTP_PORT) 설정을 확인하세요.";
  }
  return "인증 메일을 보내지 못했습니다. 잠시 후 다시 시도하거나 관리자에게 문의하세요.";
}

function createSmtpTransport() {
  const host = (process.env.SMTP_HOST || "").trim();
  const port = Number(process.env.SMTP_PORT || 587);
  const secure = process.env.SMTP_SECURE === "1";
  const user = (process.env.SMTP_USER || "").trim();
  const pass = (process.env.SMTP_PASS || "").trim();
  const requireTlsEnv = process.env.SMTP_REQUIRE_TLS;
  const requireTLS =
    requireTlsEnv === "0"
      ? false
      : requireTlsEnv === "1" || (!secure && port === 587);
  return nodemailer.createTransport({
    host,
    port,
    secure,
    auth: user ? { user, pass } : undefined,
    requireTLS,
    connectionTimeout: 15000,
    greetingTimeout: 15000,
    socketTimeout: 20000,
    tls: { minVersion: "TLSv1.2" },
  });
}

function isOpsPublicPath(req) {
  const p = req.path || "";
  if (p === "/login") return true;
  if (p === "/logout") return true;
  /** 쿼리형·경로형 인증 링크 모두 허용 (메일 클라이언트는 ?token= 링크를 깨뜨리는 경우가 있음) */
  if (p === "/auth/verify" || p.startsWith("/auth/verify/")) return true;
  if (p === "/api/auth/request-link" && req.method === "POST") return true;
  return false;
}

/** HTML 이메일용 이스케이프 (속성·본문 공통) */
function escapeHtmlEmail(s) {
  return String(s)
    .replace(/&/g, "&amp;")
    .replace(/"/g, "&quot;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

/**
 * 인증 메일 본문 HTML — 인라인 스타일·테이블 기반(주요 클라이언트 호환)
 */
function buildMagicLinkEmailHtml(linkUrl, linkMinutes) {
  const href = String(linkUrl).replace(/&/g, "&amp;").replace(/"/g, "&quot;");
  const linkText = escapeHtmlEmail(linkUrl);
  return `<!DOCTYPE html>
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>인증 안내</title>
</head>
<body style="margin:0;padding:0;background-color:#f1f5f9;-webkit-text-size-adjust:100%;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color:#f1f5f9;padding:24px 12px;">
  <tr>
    <td align="center">
      <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="max-width:560px;background-color:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e2e8f0;">
        <tr>
          <td style="padding:28px 22px 8px 22px;font-family:'Apple SD Gothic Neo','Malgun Gothic',Helvetica,Arial,sans-serif;">
            <p style="margin:0 0 4px 0;font-size:13px;font-weight:700;letter-spacing:0.02em;color:#2563eb;">NCue</p>
            <p style="margin:0 0 20px 0;font-size:17px;font-weight:700;color:#0f172a;line-height:1.35;">AI Platform</p>
            <p style="margin:0 0 14px 0;font-size:15px;line-height:1.65;color:#334155;">계정 인증을 위해 아래의 인증 링크를 안내드립니다.</p>
            <p style="margin:0 0 24px 0;font-size:15px;line-height:1.65;color:#334155;">아래 버튼을 누르시면 인증이 완료되며, 이후 서비스를 정상적으로 이용하실 수 있습니다.</p>
            <table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" style="margin:0 auto 20px auto;">
              <tr>
                <td align="center" style="border-radius:8px;background-color:#2563eb;">
                  <a href="${href}" target="_blank" rel="noopener noreferrer" style="display:inline-block;padding:14px 32px;font-size:15px;font-weight:600;color:#ffffff;text-decoration:none;font-family:'Apple SD Gothic Neo','Malgun Gothic',Helvetica,Arial,sans-serif;">인증 완료하기</a>
                </td>
              </tr>
            </table>
            <p style="margin:0 0 8px 0;font-size:12px;line-height:1.5;color:#64748b;">버튼이 동작하지 않으면 아래 주소를 브라우저에 복사해 붙여 넣어 주세요.</p>
            <p style="margin:0 0 22px 0;font-size:12px;line-height:1.5;color:#2563eb;word-break:break-all;"><a href="${href}" style="color:#2563eb;text-decoration:underline;">${linkText}</a></p>
            <div style="border-top:1px solid #e2e8f0;padding-top:18px;margin-top:4px;">
              <p style="margin:0 0 8px 0;font-size:13px;line-height:1.6;color:#64748b;">※ 본 인증 링크는 보안을 위해 발송 시점으로부터 <strong style="color:#475569;">${linkMinutes}분</strong> 동안만 유효합니다.</p>
              <p style="margin:0 0 18px 0;font-size:13px;line-height:1.6;color:#64748b;">※ 인증 시간이 만료된 경우, 다시 인증 메일을 요청해 주시기 바랍니다.</p>
              <p style="margin:0 0 6px 0;font-size:13px;line-height:1.6;color:#64748b;">본 메일은 발신 전용이며, 문의 사항이 있으실 경우 <strong style="color:#475569;">AI혁신팀</strong>으로 문의 부탁합니다.</p>
              <p style="margin:0;font-size:14px;font-weight:600;color:#0f172a;">감사합니다.</p>
            </div>
          </td>
        </tr>
      </table>
      <p style="margin:16px 0 0 0;font-size:11px;line-height:1.4;color:#94a3b8;font-family:'Apple SD Gothic Neo','Malgun Gothic',Helvetica,Arial,sans-serif;">© NCue · 본 메일은 시스템에 의해 자동 발송되었습니다.</p>
    </td>
  </tr>
</table>
</body>
</html>`;
}

async function sendMagicLinkEmail(to, linkUrl) {
  const host = (process.env.SMTP_HOST || "").trim();
  if (!nodemailer || !host) {
    console.warn("[OPS] SMTP 미설정 — 아래 링크를 수동으로 전달하거나 SMTP_HOST 등을 설정하세요.");
    console.warn("[OPS] Magic link for", to, "→", linkUrl);
    return;
  }
  const from = (process.env.SMTP_FROM || (process.env.SMTP_USER || "").trim() || "noreply@ncue.net").trim();
  const transporter = createSmtpTransport();
  const linkMinutes = Math.max(1, Math.floor(MAGIC_LINK_MAX_AGE_MS / 60000));
  const textBody = [
    "계정 인증을 위해 아래의 인증 링크를 안내드립니다.",
    "아래 링크를 클릭하시면 인증이 완료되며, 이후 서비스를 정상적으로 이용하실 수 있습니다.",
    "",
    "인증 링크:",
    linkUrl,
    "",
    `※ 본 인증 링크는 보안을 위해 발송 시점으로부터 ${linkMinutes}분 동안만 유효합니다.`,
    "※ 인증 시간이 만료된 경우, 다시 인증 메일을 요청해 주시기 바랍니다.",
    "",
    "본 메일은 발신 전용이며, 문의 사항이 있으실 경우 AI혁신팀으로 문의 부탁합니다.",
    "",
    "감사합니다.",
  ].join("\n");
  const htmlBody = buildMagicLinkEmailHtml(linkUrl, linkMinutes);
  await transporter.sendMail({
    from,
    to,
    subject: "[인증] NCue AI Platform 인증 링크",
    text: textBody,
    html: htmlBody,
  });
}

module.exports = function createOpsAuth(DATA_DIR, hooks = {}) {
  const MAGIC_LINK_PATH = path.join(DATA_DIR, "ops-magic-links.json");
  const BASE_URL = getBaseUrl();

  function opsAccessMiddleware(req, res, next) {
    if (!isOpsProd()) {
      res.locals.opsAuthRequired = false;
      res.locals.opsUserEmail = null;
      return next();
    }
    res.locals.opsAuthRequired = true;
    if (isOpsPublicPath(req)) {
      return next();
    }
    resolveOpsSessionEmail(req, hooks)
      .then((email) => {
        if (email) {
          res.locals.opsUserEmail = email;
          return next();
        }
        if (req.path.startsWith("/api/")) {
          return res.status(401).json({ error: "로그인이 필요합니다." });
        }
        const returnTo = req.originalUrl || "/learning";
        return res.redirect("/login?returnTo=" + encodeURIComponent(returnTo));
      })
      .catch((err) => {
        console.error("[OPS] session resolve failed:", err?.message || err);
        if (req.path.startsWith("/api/")) {
          return res.status(500).json({ error: "인증 확인에 실패했습니다." });
        }
        return res.redirect("/login");
      });
  }

  function registerRoutes(app) {
    app.get("/login", async (req, res) => {
      if (!isOpsProd()) {
        return res.redirect("/learning");
      }
      const email = await resolveOpsSessionEmail(req, hooks);
      if (email) {
        return res.redirect(sanitizeReturnTo(req.query.returnTo));
      }
      return res.render("login", {
        returnTo: sanitizeReturnTo(req.query.returnTo),
        layoutMinimal: true,
      });
    });

    app.get("/logout", async (req, res) => {
      const sessionEmail = await resolveOpsSessionEmail(req, hooks);
      if (typeof hooks.onLogout === "function" && sessionEmail) {
        try {
          await hooks.onLogout({ email: sessionEmail, req });
        } catch (hookErr) {
          console.error("[OPS] onLogout hook:", hookErr?.message || hookErr);
        }
      }
      res.clearCookie(OPS_AUTH_COOKIE, { path: "/" });
      if (isOpsProd()) {
        return res.redirect("/login");
      }
      return res.redirect("/learning");
    });

    app.post("/api/auth/request-link", async (req, res) => {
      try {
        if (!isOpsProd()) {
          return res.status(400).json({ error: "PROD(운영) 모드가 아닙니다." });
        }
        const email = String(req.body?.email || "")
          .trim()
          .toLowerCase();
        if (!email) {
          return res.status(400).json({ error: "이메일을 입력해 주세요." });
        }
        if (!isAllowedXavisEmail(email)) {
          return res.status(403).json({ error: "허용된 임직원이 아닙니다." });
        }
        let list = pruneExpired(loadMagicLinks(MAGIC_LINK_PATH));
        const token = uuidv4();
        const expiresAt = Date.now() + MAGIC_LINK_MAX_AGE_MS;
        const ret = sanitizeReturnTo(req.body?.returnTo);
        list.push({
          token,
          email,
          expiresAt,
          used: false,
          createdAt: new Date().toISOString(),
          returnTo: ret,
        });
        saveMagicLinks(MAGIC_LINK_PATH, list);
        /** 경로에 토큰만 두기 (쿼리 `?token=`는 일부 웹메일/Outlook에서 href가 잘림) */
        const linkUrl = buildMagicVerifyUrl(BASE_URL, token);
        await sendMagicLinkEmail(email, linkUrl);
        if (typeof hooks.onMagicLinkRequested === "function") {
          try {
            await hooks.onMagicLinkRequested({ email, returnTo: ret, req });
          } catch (hookErr) {
            console.error("[OPS] onMagicLinkRequested hook:", hookErr?.message || hookErr);
          }
        }
        return res.json({
          ok: true,
          message: "회사 메일로 인증 링크를 보냈습니다. 메일함을 확인해 주세요.",
        });
      } catch (err) {
        console.error("[OPS] request-link:", err?.message || err);
        return res.status(500).json({ error: publicSmtpErrorMessage(err) });
      }
    });

    async function handleMagicLinkVerify(req, res) {
      if (!isOpsProd()) {
        return res.redirect("/learning");
      }
      const token = String(req.params.token || req.query.token || "").trim();
      if (!token) {
        return res.status(400).send("유효하지 않은 링크입니다.");
      }
      let list = pruneExpired(loadMagicLinks(MAGIC_LINK_PATH));
      const idx = list.findIndex((x) => x.token === token && !x.used);
      if (idx === -1) {
        return res.status(400).send("유효하지 않거나 만료된 링크입니다.");
      }
      const row = list[idx];
      if (Date.now() > row.expiresAt) {
        return res.status(400).send("만료된 링크입니다.");
      }
      row.used = true;
      saveMagicLinks(MAGIC_LINK_PATH, list);
      if (typeof hooks.onLoginSuccess === "function") {
        try {
          await hooks.onLoginSuccess({
            email: row.email,
            returnTo: row.returnTo,
            req,
          });
        } catch (hookErr) {
          console.error("[OPS] onLoginSuccess hook:", hookErr?.message || hookErr);
        }
      }
      const sessionExp = getOpsSessionExpiresAtMs();
      const issuedAt = Date.now();
      const cookieVal = signSessionCookie(row.email, sessionExp, issuedAt);
      const secure = process.env.NODE_ENV === "production";
      res.cookie(OPS_AUTH_COOKIE, cookieVal, {
        httpOnly: true,
        maxAge: getOpsSessionCookieMaxAgeMs(sessionExp),
        sameSite: "lax",
        path: "/",
        secure,
      });
      const dest = appendVerifiedParam(row.returnTo || RETURN_TO_DEFAULT);
      return res.redirect(dest);
    }

    /** 구 메일 호환: ?token= */
    app.get("/auth/verify", handleMagicLinkVerify);
    /** 권장: 쿼리 없이 경로만 (메일 HTML 파서 깨짐 방지) */
    app.get("/auth/verify/:token", handleMagicLinkVerify);
  }

  return {
    middleware: opsAccessMiddleware,
    registerRoutes,
    isOpsProd,
    /** @deprecated REAL→PROD 이전 호환용 */
    isOpsReal: isOpsProd,
  };
};
