/**
 * `/database/*` route aggregator.
 *
 * Owns the `/database` HTTP surface (single place new sub-routes get wired in).
 * Handlers use ABSOLUTE paths (no Fastify prefix) so every route greps as the
 * literal string it serves.
 *
 *   /database/admin      — Feishu-only login page (+ dev one-click button)
 *   /database/dashboard  — sidebar + content admin shell, session-gated
 *   /database/dev-login  — DEV ONLY bypass, registered only when the flag is on
 *
 * The dev bypass (button + /database/dev-login) is self-contained here and
 * gated by allowDevLoginBypass (computed in ./plugin.ts from HUB_DEV_LOGIN_BYPASS
 * + NODE_ENV). Production requires real Feishu OAuth.
 */
import type { FastifyInstance } from "fastify";
import type { PrismaClient } from "@prisma/client";
import path from "node:path";
import { SESSION_COOKIE_NAME, signSession, verifySession } from "../../admin/auth/session.js";
import { registerFileLibRoutes } from "./filelibRoutes.js";
import { registerFileRoutes } from "./fileRoutes.js";
import { registerTeacherApp } from "./teacherApp.js";
import { renderLibraryBrowser } from "./libraryBrowser.js";
import { renderGroupsPanel, renderUsersPanel } from "./adminPanels.js";
import { createInMemoryVersionStore } from "../filelib/versionStore.js";
import { createTeamGroupResolver } from "../filelib/groupResolver.js";
import { createHttpGroupResolver } from "../filelib/groupResolverHttp.js";
import { createManifestStubAdapter } from "../filelib/exportService.js";
import { FILE_LIB_AUDIT_ACTIONS } from "../filelib/audit.js";
import { UI_HEAD_FONTS, UI_THEME_CSS } from "./uiTheme.js";
import type { FileLibRouteDeps } from "../filelib/routeShared.js";

export interface DatabaseRouteConfig {
  readonly prisma: PrismaClient;
  /** HMAC secret for the signed session cookie — reused from the admin plane. */
  readonly sessionSecret: string;
  /** Silo Organization slug — builds the org-scoped Feishu login link. */
  readonly siloOrganizationSlug: string;
  /** DEV ONLY. Enables the one-click button and the /database/dev-login route. */
  readonly allowDevLoginBypass: boolean;
}

export async function registerDatabaseRoutes(
  app: FastifyInstance,
  config: DatabaseRouteConfig,
): Promise<void> {
  // 文件库依赖在下方装配;概览页统计在请求时经此引用读取(请求一定晚于装配完成)。
  let filelibDepsForStats: FileLibRouteDeps | null = null;

  app.get("/database/admin", async (request, reply) => {
    // Already signed in → straight to the dashboard.
    if ((await resolveUser(request.cookies[SESSION_COOKIE_NAME], config)) !== null) {
      return reply.redirect("/database/dashboard");
    }
    return reply.type("text/html").send(renderLoginPage(config));
  });

  app.get("/database/dashboard", async (request, reply) => {
    const user = await resolveUser(request.cookies[SESSION_COOKIE_NAME], config);
    if (user === null) return reply.redirect("/database/admin");
    const stats = await loadDashboardStats(config.prisma, filelibDepsForStats);
    return reply.type("text/html").send(renderDashboard(user.displayName, stats, config.siloOrganizationSlug));
  });

  // DEV ONLY bypass — self-contained here, registered only when the flag is on
  // (see ./plugin.ts). Mints a session for an existing OWNER/ADMIN, reusing the
  // scoped SessionIdentity shape the Feishu OAuth callback produces so the
  // session guard behaves identically. Never registered in production.
  if (config.allowDevLoginBypass) {
    app.get("/database/dev-login", async (_request, reply) => {
      const membership = await config.prisma.organizationMembership.findFirst({
        where: {
          revokedAt: null,
          role: { in: ["OWNER", "ADMIN"] },
          organization: { status: "ACTIVE" },
        },
        select: { userId: true, organizationId: true },
        orderBy: { createdAt: "asc" },
      });
      if (membership === null) {
        return reply.status(404).send({ error: { code: "no_admin", message: "no active OWNER/ADMIN to impersonate" } });
      }
      const identity = await config.prisma.feishuUserIdentity.findFirst({
        where: {
          userId: membership.userId,
          connection: { organizationId: membership.organizationId, status: "ACTIVE" },
        },
        select: { id: true, connectionId: true, connection: { select: { organizationId: true } } },
      });
      if (identity === null) {
        return reply.status(404).send({ error: { code: "no_identity", message: "admin has no active scoped Feishu identity" } });
      }
      const token = signSession(
        {
          userId: membership.userId,
          feishuIdentityId: identity.id,
          feishuConnectionId: identity.connectionId,
          feishuOrganizationId: identity.connection.organizationId,
        },
        config.sessionSecret,
      );
      // Local dev is http://127.0.0.1, so secure:false. This route only ever
      // runs outside production (double-gated in ./plugin.ts).
      reply.setCookie(SESSION_COOKIE_NAME, token, {
        path: "/",
        httpOnly: true,
        sameSite: "lax",
        secure: false,
        maxAge: 7 * 24 * 60 * 60,
      });
      reply.log.warn({ userId: membership.userId, orgId: membership.organizationId }, "DEV database login bypass used");
      return reply.redirect("/database/dashboard");
    });
  }

  // 文件库(独立模块,《文件库-接口契约.md》):API + 浏览页 + 老师端 /app。
  // 依赖装配:VersionStore 当前为内存+快照实现(版本团队 npm 包到位后替换);
  // GroupResolver 默认读 hub Team,HUB_GROUP_SERVICE_URL 配置后切 HTTP(C2);
  // 导出适配器当前为 manifest stub(OPEN-6,真导出工具到位后替换)。
  const siloOrg = await config.prisma.organization.findUnique({
    where: { slug: config.siloOrganizationSlug },
    select: { id: true },
  });
  if (siloOrg === null) {
    app.log.warn({ slug: config.siloOrganizationSlug }, "filelib: silo organization not found, routes not registered");
    return;
  }
  const storageRoot = process.env["HUB_FILELIB_STORAGE_ROOT"] ?? path.resolve(".filelib-repos");
  const versionStore = createInMemoryVersionStore(path.join(storageRoot, ".version-store.json"));
  const groupServiceUrl = process.env["HUB_GROUP_SERVICE_URL"];
  const filelibDeps: FileLibRouteDeps = {
    prisma: config.prisma,
    sessionSecret: config.sessionSecret,
    organizationId: siloOrg.id,
    storageRoot,
    groupResolver: groupServiceUrl === undefined || groupServiceUrl.trim() === ""
      ? createTeamGroupResolver(config.prisma, siloOrg.id)
      : createHttpGroupResolver({ baseUrl: groupServiceUrl }),
    versionStore,
    exportAdapters: [createManifestStubAdapter(versionStore)],
  };
  await registerFileLibRoutes(app, filelibDeps);
  await registerFileRoutes(app, filelibDeps);
  await registerTeacherApp(app, {
    prisma: config.prisma,
    sessionSecret: config.sessionSecret,
    siloOrganizationSlug: config.siloOrganizationSlug,
    allowDevLoginBypass: config.allowDevLoginBypass,
  });

  // 独立文件库页已并入后台「文件库」tab(/database/dashboard#library),旧地址跳转保留兼容。
  app.get("/database/library", async (_request, reply) =>
    reply.redirect("/database/dashboard#library"),
  );

  filelibDepsForStats = filelibDeps;
}

/* ------------------------------------------------------------ 概览页统计 */

interface DashboardStats {
  readonly folders: number;
  readonly projects: number;
  readonly files: number;
  readonly grants: number;
  readonly recent: ReadonlyArray<{
    readonly action: string;
    readonly actor: string;
    readonly label: string;
    readonly when: Date;
  }>;
}

/** 概览页统计:org 范围内的文件夹/项目/授权(DB)+ 文件(版本库)+ 最近活动(AuditEntry)。 */
async function loadDashboardStats(
  prisma: PrismaClient,
  deps: FileLibRouteDeps | null,
): Promise<DashboardStats | null> {
  if (deps === null) return null;
  const organizationId = deps.organizationId;
  const [folders, projects, grants] = await Promise.all([
    prisma.fileLibNode.count({ where: { organizationId, kind: "FOLDER", deletedAt: null } }),
    prisma.fileLibNode.count({ where: { organizationId, kind: "PROJECT", deletedAt: null } }),
    prisma.fileLibGrant.count({ where: { organizationId, revokedAt: null } }),
  ]);
  // 文件计数:遍历 READY 项目问版本库(demo 规模;真版本包到位后应换成存储侧统计)
  const readyProjects = await prisma.fileLibNode.findMany({
    where: {
      organizationId, kind: "PROJECT", deletedAt: null,
      provisionStatus: "READY", storageDir: { not: null },
    },
    select: { storageDir: true },
  });
  let files = 0;
  for (const project of readyProjects) {
    if (project.storageDir === null) continue;
    try {
      files += (await deps.versionStore.list(project.storageDir)).length;
    } catch { /* repo 缺失(如重启未恢复)不计 */ }
  }
  const entries = await prisma.auditEntry.findMany({
    where: { organizationId, action: { in: Object.values(FILE_LIB_AUDIT_ACTIONS) } },
    orderBy: { createdAt: "desc" },
    take: 8,
  });
  const actorIds = [...new Set(entries.map((e) => e.actorUserId).filter((x): x is string => x !== null))];
  const users = actorIds.length === 0
    ? []
    : await prisma.user.findMany({ where: { id: { in: actorIds } }, select: { id: true, displayName: true } });
  const nameById = new Map(users.map((u) => [u.id, u.displayName]));
  const recent = entries.map((entry) => {
    const meta = (entry.metadata ?? {}) as Record<string, unknown>;
    const label =
      (typeof meta["name"] === "string" ? meta["name"] : undefined) ??
      (typeof meta["to"] === "string" ? meta["to"] : undefined) ??
      (typeof meta["path"] === "string" ? meta["path"] : undefined) ??
      (typeof meta["objectId"] === "string" ? meta["objectId"].slice(0, 8) : "");
    return {
      action: entry.action,
      actor: nameById.get(entry.actorUserId ?? "") ?? entry.actorUserId ?? "unknown",
      label,
      when: entry.createdAt,
    };
  });
  return { folders, projects, files, grants, recent };
}

/** Verify the session cookie and load the user, or null if not signed in. */
async function resolveUser(
  rawCookie: string | undefined,
  config: DatabaseRouteConfig,
): Promise<{ displayName: string } | null> {
  if (rawCookie === undefined || rawCookie === "") return null;
  const session = verifySession(rawCookie, config.sessionSecret);
  if (session === null) return null;
  const user = await config.prisma.user.findUnique({
    where: { id: session.userId },
    select: { displayName: true },
  });
  return user;
}

/** 管理后台共享 head(全局 UI 主题,与老师端 /app 同源)。 */
function pageHead(title: string): string {
  return `<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
  <title>${title}</title>
  ${UI_HEAD_FONTS}
  <style>${UI_THEME_CSS}</style>
</head>`;
}

function renderLoginPage(config: DatabaseRouteConfig): string {
  const feishuHref = `/auth/feishu/${encodeURIComponent(config.siloOrganizationSlug)}`;
  const devButton = config.allowDevLoginBypass
    ? `<div style="display:flex;align-items:center;gap:10px;margin:22px 0;color:var(--text-3);font-size:11px">
         <span style="flex:1;border-top:1px solid var(--border-soft)"></span>开发模式
         <span style="flex:1;border-top:1px solid var(--border-soft)"></span>
       </div>
       <a href="/database/dev-login" class="btn" style="width:100%;justify-content:center">⚡ 一键登录管理员</a>
       <p style="margin:10px 0 0;text-align:center;font-size:11px;color:var(--text-3)">仅开发环境可见 · 跳过飞书 OAuth</p>`
    : "";

  return `<!doctype html>
<html lang="zh-CN">
${pageHead("Database Admin · 登录")}
<body>
  <div style="min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px">
    <div style="width:100%;max-width:380px;background:var(--panel);border:1px solid var(--border-soft);border-radius:16px;padding:40px 36px;box-shadow:var(--shadow-pop)">
      <div style="font-size:26px;font-weight:600;text-align:center">Database Admin</div>
      <p style="margin:10px 0 30px;text-align:center;font-size:13px;color:var(--text-3)">使用飞书登录以管理数据库</p>
      <a href="${feishuHref}" class="btn btn-primary" style="width:100%;justify-content:center;padding:11px 16px;font-size:14px">使用飞书登录</a>
      ${devButton}
    </div>
  </div>
</body>
</html>`;
}

/** Sidebar nav items. `active` marks the current page. `href` "#" = placeholder. */
const NAV_TABS: ReadonlyArray<{ id: string; label: string; icon: string }> = [
  { id: "overview", label: "概览", icon: "M4 13h6V4H4v9Zm0 7h6v-5H4v5Zm10 0h6V11h-6v9Zm0-16v5h6V4h-6Z" },
  { id: "library", label: "文件库", icon: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" },
  { id: "users", label: "用户管理", icon: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm13 10v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" },
  { id: "groups", label: "Group 管理", icon: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm14 10v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75M23 21v-2a4 4 0 0 0-3-3.87" },
  { id: "search", label: "查询", icon: "m21 21-4.3-4.3M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z" },
  { id: "settings", label: "设置", icon: "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm7-3 2 1-2 3-2-1a7 7 0 0 1-2 1l-1 2h-4l-1-2a7 7 0 0 1-2-1l-2 1-2-3 2-1a7 7 0 0 1 0-2l-2-1 2-3 2 1a7 7 0 0 1 2 1l1-2h4l1 2a7 7 0 0 1 0 2l2-1 2 3-2 1a7 7 0 0 1 0 2Z" },
];

function renderDashboard(displayName: string, stats: DashboardStats | null, orgSlug: string): string {
  const nav = NAV_TABS.map((t) => `
    <button class="admin-tab" data-tab="${t.id}" style="display:flex;align-items:center;gap:10px;border-radius:10px;padding:9px 14px;font-size:13px;color:var(--text-3);background:none;border:none;cursor:pointer;text-align:left;width:100%">
      <svg style="width:16px;height:16px;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="${t.icon}"/></svg>
      ${t.label}
    </button>`).join("\n");

  const cards = [
    { label: "文件夹", value: stats?.folders ?? "—" },
    { label: "项目", value: stats?.projects ?? "—" },
    { label: "文件", value: stats?.files ?? "—" },
    { label: "活跃授权", value: stats?.grants ?? "—" },
  ].map((s) => `
        <div class="panel" style="padding:18px 20px">
          <p style="font-size:12.5px;color:var(--text-3)">${s.label}</p>
          <p style="margin-top:6px;font-size:28px;font-weight:600;color:var(--text)">${s.value}</p>
        </div>`).join("");

  const recentRows = stats === null || stats.recent.length === 0
    ? `<div style="padding:26px 0;text-align:center;font-size:12.5px;color:var(--text-3)">暂无文件库活动 · 到「文件库」里创建第一个文件夹吧</div>`
    : stats.recent.map((r) => `
          <div style="display:flex;align-items:center;gap:12px;border-top:1px solid var(--border-soft);padding:9px 0;font-size:13px">
            <span class="tag">${escapeHtml(r.action)}</span>
            <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)">${escapeHtml(r.label)}</span>
            <span style="margin-left:auto;flex-shrink:0;font-size:11.5px;color:var(--text-3)">${escapeHtml(r.actor)} · ${escapeHtml(r.when.toLocaleString("zh-CN"))}</span>
          </div>`).join("");

  const initial = escapeHtml(displayName.slice(0, 1) || "U");

  return `<!doctype html>
<html lang="zh-CN">
${pageHead("Database Admin")}
<body>
  <div style="display:flex;height:100vh">
    <aside style="width:240px;flex-shrink:0;background:var(--sidebar);border-right:1px solid var(--border-soft);display:flex;flex-direction:column">
      <div style="padding:16px 16px 12px;border-bottom:1px solid var(--border-soft)">
        <span style="font-size:15px;font-weight:600">Database Admin</span>
      </div>
      <nav style="flex:1;display:flex;flex-direction:column;gap:2px;padding:10px">
        ${nav}
      </nav>
      <div style="margin:10px;padding:10px 12px;border-top:1px solid var(--border-soft);display:flex;align-items:center;gap:9px">
        <div style="width:26px;height:26px;border-radius:50%;background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;flex-shrink:0">${initial}</div>
        <div style="min-width:0;flex:1">
          <p style="font-size:10.5px;color:var(--text-3)">已登录</p>
          <p style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;color:var(--text)">${escapeHtml(displayName)}</p>
        </div>
        <button id="logout" class="btn" style="padding:3px 10px;font-size:11px">退出</button>
      </div>
    </aside>

    <div style="flex:1;display:flex;flex-direction:column;min-width:0">
      <section id="tab-overview" class="admin-tab-section" style="flex:1;overflow-y:auto;padding:28px">
        <h1 style="font-size:18px;font-weight:600;margin-bottom:4px">概览</h1>
        <p style="font-size:11.5px;color:var(--text-3);margin-bottom:20px">文件库实时数据</p>
        <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:16px">
          ${cards}
        </div>
        <div class="panel" style="margin-top:18px">
          <h2 style="font-size:13.5px;font-weight:600;margin-bottom:8px">最近活动</h2>
          ${recentRows}
        </div>
      </section>

      <section id="tab-library" class="admin-tab-section" style="display:none;flex:1;min-height:0">
        ${renderLibraryBrowser()}
      </section>

      <section id="tab-users" class="admin-tab-section" style="display:none;flex:1;overflow-y:auto;padding:28px">
        <h1 style="font-size:18px;font-weight:600;margin-bottom:16px">用户管理</h1>
        ${renderUsersPanel(orgSlug)}
      </section>

      <section id="tab-groups" class="admin-tab-section" style="display:none;flex:1;overflow-y:auto;padding:28px">
        <h1 style="font-size:18px;font-weight:600;margin-bottom:16px">Group 管理</h1>
        ${renderGroupsPanel(orgSlug)}
      </section>

      <section id="tab-search" class="admin-tab-section" style="display:none;flex:1;overflow-y:auto;padding:28px">
        <h1 style="font-size:18px;font-weight:600;margin-bottom:4px">查询</h1>
        <p style="font-size:12.5px;color:var(--text-3)">查询功能建设中</p>
      </section>

      <section id="tab-settings" class="admin-tab-section" style="display:none;flex:1;overflow-y:auto;padding:28px">
        <h1 style="font-size:18px;font-weight:600;margin-bottom:4px">设置</h1>
        <p style="font-size:12.5px;color:var(--text-3)">设置功能建设中</p>
      </section>
    </div>
  </div>

  <script>
    (function () {
      const tabs = [...document.querySelectorAll(".admin-tab")];
      const sections = Object.fromEntries(
        [...document.querySelectorAll(".admin-tab-section")].map((s) => [s.id.replace("tab-", ""), s]),
      );
      function activate(id) {
        tabs.forEach((t) => {
          const on = t.dataset.tab === id;
          t.style.background = on ? "var(--selected)" : "none";
          t.style.color = on ? "var(--text)" : "var(--text-3)";
          t.style.fontWeight = on ? "600" : "400";
        });
        Object.entries(sections).forEach(([key, s]) => {
          s.style.display = key === id ? (key === "library" ? "block" : "block") : "none";
        });
        if (location.hash !== "#" + id) history.replaceState(null, "", "#" + id);
      }
      tabs.forEach((t) => t.addEventListener("click", () => activate(t.dataset.tab)));
      document.getElementById("logout").addEventListener("click", async () => {
        try { await fetch("/auth/logout", { method: "POST", credentials: "same-origin" }); } catch (e) {}
        location.href = "/database/admin";
      });
      const fromHash = location.hash.replace(/^#/, "");
      activate(tabs.some((t) => t.dataset.tab === fromHash) ? fromHash : "overview");
    })();
  </script>
</body>
</html>`;
}

function escapeHtml(value: string): string {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}
