/**
 * Feishu OAuth + session routes for org admin web login.
 */
import { randomBytes } from "node:crypto";
import type { PrismaClient } from "@prisma/client";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { resolveActiveFeishuApplication } from "../../connections/feishuApplicationConnections.js";
import { upsertScopedFeishuIdentityInTransaction } from "../../feishu/identityNamespace.js";
import type { LocalSecretEnvelope } from "../../security/secretEnvelope.js";
import {
  buildAuthorizeUrl,
  DEFAULT_OAUTH_SCOPE,
  exchangeCodeForUser,
  FeishuOAuthError,
  type FeishuOAuthConfig,
} from "../auth/feishuOAuth.js";
import {
  HttpError,
  requireSession,
  type GuardDeps,
} from "../auth/guards.js";
import {
  OAUTH_STATE_COOKIE_NAME,
  SESSION_COOKIE_NAME,
  signOAuthState,
  signSession,
  verifyOAuthState,
  type SessionIdentity,
} from "../auth/session.js";
import { handleRouteError } from "../errors.js";

export interface AuthRouteConfig {
  readonly prisma: PrismaClient;
  readonly sessionSecret: string;
  readonly publicBaseUrl: string;
  readonly feishuAppId: string;
  readonly feishuAppSecret: string;
  readonly secretEnvelope: LocalSecretEnvelope;
  readonly oauthScope?: string;
  readonly cookieSecure: boolean;
  readonly fetchImpl?: typeof fetch;
  readonly allowLegacyFeishuOAuth?: boolean;
}

export async function registerAuthRoutes(app: FastifyInstance, config: AuthRouteConfig): Promise<void> {
  const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret };
  const redirectUri = `${trimTrailingSlash(config.publicBaseUrl)}/auth/feishu/callback`;
  const legacyOauthConfig: FeishuOAuthConfig = {
    appId: config.feishuAppId,
    appSecret: config.feishuAppSecret,
    redirectUri,
    scope: config.oauthScope ?? DEFAULT_OAUTH_SCOPE,
    ...(config.fetchImpl !== undefined ? { fetchImpl: config.fetchImpl } : {}),
  };

  if (config.allowLegacyFeishuOAuth === true) {
    app.get("/auth/feishu", async (request, reply) => {
      try {
        const returnTo = sanitizeReturnTo(
          typeof request.query === "object" && request.query !== null && "returnTo" in request.query
            ? String((request.query as { returnTo?: string }).returnTo ?? "")
            : "",
        );
        const nonce = randomBytes(16).toString("hex");
        const stateToken = signOAuthState({ nonce, returnTo }, config.sessionSecret);
        setOAuthNonceCookie(reply, nonce, config.cookieSecure);
        const url = buildAuthorizeUrl(legacyOauthConfig, stateToken);
        return reply.redirect(url);
      } catch (err) {
        return handleRouteError(reply, err);
      }
    });
  }

  app.get("/auth/feishu/:orgSlug", async (request, reply) => {
    try {
      const { orgSlug } = request.params as { orgSlug: string };
      const organization = await config.prisma.organization.findUnique({
        where: { slug: orgSlug },
        select: { id: true, status: true },
      });
      if (organization === null) throw new HttpError(404, "org_not_found", `organization not found: ${orgSlug}`);
      if (organization.status !== "ACTIVE") {
        throw new HttpError(403, "org_not_active", `organization is ${organization.status}`);
      }
      const credential = await resolveActiveFeishuApplication(
        config.prisma,
        config.secretEnvelope,
        { organizationId: organization.id },
      );
      const returnTo = sanitizeReturnTo(
        typeof request.query === "object" && request.query !== null && "returnTo" in request.query
          ? String((request.query as { returnTo?: string }).returnTo ?? "")
          : "",
      );
      const nonce = randomBytes(16).toString("hex");
      const stateToken = signOAuthState({
        nonce,
        returnTo,
        organizationId: organization.id,
        connectionId: credential.connectionId,
      }, config.sessionSecret);
      setOAuthNonceCookie(reply, nonce, config.cookieSecure);
      return reply.redirect(buildAuthorizeUrl({
        appId: credential.appId,
        appSecret: credential.appSecret,
        redirectUri,
        scope: config.oauthScope ?? DEFAULT_OAUTH_SCOPE,
        ...(config.fetchImpl !== undefined ? { fetchImpl: config.fetchImpl } : {}),
      }, stateToken));
    } catch (err) {
      return handleRouteError(reply, err);
    }
  });

  app.get("/auth/feishu/callback", async (request, reply) => {
    try {
      const query = request.query as {
        code?: string;
        state?: string;
        error?: string;
      };
      if (typeof query.error === "string" && query.error !== "") {
        return reply.redirect(`/admin/login?error=${encodeURIComponent(query.error)}`);
      }
      const code = query.code;
      const state = query.state;
      if (typeof code !== "string" || code === "" || typeof state !== "string" || state === "") {
        throw new HttpError(400, "bad_request", "missing OAuth code or state");
      }

      const statePayload = verifyOAuthState(state, config.sessionSecret);
      if (statePayload === null) {
        throw new HttpError(400, "bad_request", "invalid or expired OAuth state");
      }
      const cookieNonce = request.cookies[OAUTH_STATE_COOKIE_NAME];
      if (cookieNonce === undefined || cookieNonce !== statePayload.nonce) {
        throw new HttpError(400, "bad_request", "OAuth state mismatch");
      }
      reply.clearCookie(OAUTH_STATE_COOKIE_NAME, { path: "/" });

      let oauthConfig = legacyOauthConfig;
      if (statePayload.connectionId === undefined && config.allowLegacyFeishuOAuth !== true) {
        throw new HttpError(400, "bad_request", "unscoped Feishu OAuth is disabled");
      }
      if (statePayload.connectionId !== undefined && statePayload.organizationId !== undefined) {
        const credential = await resolveActiveFeishuApplication(
          config.prisma,
          config.secretEnvelope,
          { connectionId: statePayload.connectionId },
        );
        if (credential.organizationId !== statePayload.organizationId) {
          throw new HttpError(400, "bad_request", "OAuth state Organization scope mismatch");
        }
        oauthConfig = {
          appId: credential.appId,
          appSecret: credential.appSecret,
          redirectUri,
          scope: config.oauthScope ?? DEFAULT_OAUTH_SCOPE,
          ...(config.fetchImpl !== undefined ? { fetchImpl: config.fetchImpl } : {}),
        };
      }

      const feishuUser = await exchangeCodeForUser(oauthConfig, code);
      let userId: string;
      if (statePayload.connectionId !== undefined && statePayload.organizationId !== undefined) {
        const connectionId = statePayload.connectionId;
        const organizationId = statePayload.organizationId;
        const identity = await config.prisma.$transaction(async (tx) => {
          const resolved = await upsertScopedFeishuIdentityInTransaction(tx, {
            connectionId,
            expectedOrganizationId: organizationId,
            openId: feishuUser.openId,
            ...(feishuUser.unionId !== undefined ? { unionId: feishuUser.unionId } : {}),
            displayName: feishuUser.displayName,
            ...(feishuUser.avatarUrl !== null ? { avatarUrl: feishuUser.avatarUrl } : {}),
          });
          const activeMembership = await tx.organizationMembership.findFirst({
            where: {
              organizationId,
              userId: resolved.userId,
              revokedAt: null,
            },
            select: { id: true },
          });
          if (activeMembership === null) {
            const revokedMembership = await tx.organizationMembership.findFirst({
              where: {
                organizationId,
                userId: resolved.userId,
                revokedAt: { not: null },
              },
              select: { id: true },
            });
            if (revokedMembership === null) {
              await tx.organizationMembership.create({
                data: {
                  organizationId,
                  userId: resolved.userId,
                  role: "MEMBER",
                },
              });
              await tx.auditEntry.create({
                data: {
                  organizationId,
                  actorUserId: resolved.userId,
                  action: "organization_member.oauth_auto_joined",
                  metadata: { connectionId: resolved.connectionId, role: "MEMBER" },
                },
              });
            }
          }
          return resolved;
        });
        userId = identity.userId;
        setSessionCookie(reply, config, {
          userId,
          feishuIdentityId: identity.identityId,
          feishuConnectionId: identity.connectionId,
          feishuOrganizationId: identity.organizationId,
        });
      } else {
        const user = await config.prisma.user.upsert({
          where: { feishuOpenId: feishuUser.openId },
          create: {
            feishuOpenId: feishuUser.openId,
            displayName: feishuUser.displayName,
            avatarUrl: feishuUser.avatarUrl,
          },
          update: {
            displayName: feishuUser.displayName,
            avatarUrl: feishuUser.avatarUrl,
          },
        });
        userId = user.id;
        setSessionCookie(reply, config, {
          userId,
          feishuOpenId: user.feishuOpenId,
        });
      }

      const destination = await resolvePostLoginRedirect(
        config.prisma,
        userId,
        statePayload.returnTo,
        statePayload.organizationId !== undefined
          ? { intendedOrganizationId: statePayload.organizationId }
          : {},
      );
      return reply.redirect(destination);
    } catch (err) {
      request.log.error({
        requestId: request.id,
        operation: "feishu_oauth.callback",
        ...(err instanceof FeishuOAuthError
          ? {
              errorCode: err.code,
              failureCategory: err.category,
              ...(err.upstreamStatus !== undefined ? { upstreamStatus: err.upstreamStatus } : {}),
            }
          : {
              errorCode: "feishu_oauth_callback_failed",
              errorType: err instanceof Error ? err.name : typeof err,
              err,
            }),
      }, "feishu oauth callback failed");
      return reply.redirect(`/admin/login?error=${encodeURIComponent("oauth_failed")}`);
    }
  });

  app.post("/auth/logout", async (_request, reply) => {
    reply.clearCookie(SESSION_COOKIE_NAME, { path: "/" });
    return reply.status(204).send();
  });

  app.get("/auth/feishu/complete", async (request, reply) => {
    const query = request.query as { org?: string };
    const organizationName = typeof query.org === "string" && query.org.trim() !== ""
      ? query.org.trim()
      : "当前组织";
    return reply.type("text/html").send(`<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
  <title>Educraft 登录成功</title>
  <style>
    body{font-family:system-ui,sans-serif;display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0;background:#f6f7f9;color:#1a1a1a}
    .card{background:#fff;padding:2rem 2.5rem;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.08);max-width:25rem;text-align:center}
    .ok{font-size:3rem;margin:0 0 .5rem}.hint{color:#646a73;line-height:1.6}
  </style>
</head>
<body>
  <main class="card">
    <p class="ok">✅</p>
    <h1>登录并加入组织成功</h1>
    <p>你已加入 ${escapeHtml(organizationName)}。</p>
    <p class="hint">现在可以关闭本页面，返回飞书群再次 @机器人继续使用。</p>
  </main>
</body>
</html>`);
  });

  app.get("/api/me", async (request, reply) => {
    try {
      const auth = await requireSession(request, reply, guardDeps);
      if (auth === null) return;

      const memberships = await config.prisma.organizationMembership.findMany({
        where: {
          userId: auth.user.id,
          revokedAt: null,
          ...(auth.authenticationOrganizationId !== undefined
            ? { organizationId: auth.authenticationOrganizationId }
            : {}),
        },
        select: {
          role: true,
          organization: {
            select: { id: true, slug: true, name: true, status: true },
          },
        },
        orderBy: { createdAt: "asc" },
      });

      return {
        user: {
          id: auth.user.id,
          feishuOpenId: auth.feishuOpenId,
          displayName: auth.user.displayName,
          avatarUrl: auth.user.avatarUrl,
        },
        organizations: memberships.map((m) => ({
          id: m.organization.id,
          slug: m.organization.slug,
          name: m.organization.name,
          status: m.organization.status,
          role: m.role,
        })),
      };
    } catch (err) {
      return handleRouteError(reply, err);
    }
  });

  // Minimal login page until admin SPA lands (PR7).
  app.get("/admin/login", async (request: FastifyRequest, reply: FastifyReply) => {
    const q = request.query as { error?: string; returnTo?: string; orgSlug?: string };
    const returnTo = sanitizeReturnTo(q.returnTo ?? "");
    const orgSlug = typeof q.orgSlug === "string" && /^[a-z0-9][a-z0-9-]*$/.test(q.orgSlug)
      ? q.orgSlug
      : null;
    const loginHref = orgSlug === null
      ? null
      : returnTo === "/admin"
        ? `/auth/feishu/${encodeURIComponent(orgSlug)}`
        : `/auth/feishu/${encodeURIComponent(orgSlug)}?returnTo=${encodeURIComponent(returnTo)}`;
    const errorHtml =
      typeof q.error === "string" && q.error !== ""
        ? `<p class="err">Login failed: ${escapeHtml(q.error)}</p>`
        : "";
    const html = `<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
  <title>CPH Org Admin — Login</title>
  <style>
    body{font-family:system-ui,sans-serif;display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0;background:#f6f7f9;color:#1a1a1a}
    .card{background:#fff;padding:2rem 2.5rem;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.08);max-width:22rem;text-align:center}
    a.btn{display:inline-block;margin-top:1rem;padding:.7rem 1.2rem;background:#3370ff;color:#fff;border-radius:8px;text-decoration:none;font-weight:600}
    a.btn:hover{background:#245bdb}
    .err{color:#c45656;font-size:.9rem}
  </style>
</head>
<body>
  <div class="card">
    <h1>Org Admin</h1>
    <p>Sign in with Feishu to manage your organization.</p>
    ${errorHtml}
    ${loginHref === null
      ? "<p>请从组织专属登录链接进入。</p>"
      : `<a class="btn" href="${escapeHtml(loginHref)}">Login with Feishu</a>`}
  </div>
</body>
</html>`;
    return reply.type("text/html").send(html);
  });
}

export function setSessionCookie(
  reply: FastifyReply,
  config: Pick<AuthRouteConfig, "sessionSecret" | "cookieSecure">,
  identity: SessionIdentity,
): void {
  const token = signSession(identity, config.sessionSecret);
  reply.setCookie(SESSION_COOKIE_NAME, token, {
    path: "/",
    httpOnly: true,
    sameSite: "lax",
    secure: config.cookieSecure,
    maxAge: 7 * 24 * 60 * 60,
  });
}

/** Exported for tests that need a pre-authenticated cookie value. */
export function mintSessionToken(
  identity: SessionIdentity,
  sessionSecret: string,
): string {
  return signSession(identity, sessionSecret);
}

export function sessionCookieHeader(token: string): string {
  return `${SESSION_COOKIE_NAME}=${token}`;
}

async function resolvePostLoginRedirect(
  prisma: PrismaClient,
  userId: string,
  returnTo: string,
  options: { readonly intendedOrganizationId?: string | undefined } = {},
): Promise<string> {
  if (options.intendedOrganizationId !== undefined) {
    const intended = await prisma.organizationMembership.findFirst({
      where: {
        userId,
        organizationId: options.intendedOrganizationId,
        revokedAt: null,
        organization: { status: "ACTIVE" },
      },
      select: { organization: { select: { slug: true, name: true } } },
    });
    if (intended === null) return "/admin?error=not_an_active_org_member";
    const orgRoot = "/admin";
    // Default / missing returnTo sanitizes to "/admin". Always land in the org
    // admin SPA (not the legacy static "close this tab" complete page).
    if (returnTo === "/admin") {
      return orgRoot;
    }
    return normalizeAdminReturnTo(returnTo) ?? orgRoot;
  }
  if (returnTo !== "/admin" && returnTo.startsWith("/admin")) {
    return normalizeAdminReturnTo(returnTo) ?? "/admin";
  }
  const membership = await prisma.organizationMembership.findFirst({
    where: {
      userId,
      revokedAt: null,
      role: { in: ["OWNER", "ADMIN"] },
      organization: { status: "ACTIVE" },
    },
    select: { organization: { select: { slug: true } } },
    orderBy: { createdAt: "asc" },
  });
  if (membership !== null) {
    return "/admin";
  }
  // Member-only or no org: still land on a shell page (SPA will explain).
  const any = await prisma.organizationMembership.findFirst({
    where: { userId, revokedAt: null, organization: { status: "ACTIVE" } },
    select: { organization: { select: { slug: true } } },
    orderBy: { createdAt: "asc" },
  });
  if (any !== null) {
    return "/admin/projects";
  }
  return "/admin/login?error=no_organization";
}

function setOAuthNonceCookie(reply: FastifyReply, nonce: string, secure: boolean): void {
  // Signed state carries scope/returnTo; the cookie mirrors nonce for double-submit CSRF.
  reply.setCookie(OAUTH_STATE_COOKIE_NAME, nonce, {
    path: "/",
    httpOnly: true,
    sameSite: "lax",
    secure,
    maxAge: 600,
  });
}

/**
 * Only allow relative same-origin paths under /admin to avoid open redirects.
 */
export function sanitizeReturnTo(raw: string): string {
  if (raw === "" || !raw.startsWith("/") || raw.startsWith("//") || raw.includes("://")) {
    return "/admin";
  }
  if (!raw.startsWith("/admin")) {
    return "/admin";
  }
  return normalizeAdminReturnTo(raw) ?? "/admin";
}

/** Map legacy `/admin/org/:slug[...]` bookmarks onto slugless `/admin[...]` paths. */
export function normalizeAdminReturnTo(path: string): string | null {
  if (!path.startsWith("/admin")) return null;
  const legacy = path.match(/^\/admin\/org\/[^/]+(\/.*)?$/);
  if (legacy) {
    const rest = legacy[1] ?? "";
    return rest === "" ? "/admin" : `/admin${rest}`;
  }
  return path;
}

function trimTrailingSlash(url: string): string {
  return url.endsWith("/") ? url.slice(0, -1) : url;
}

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