import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import { and, eq, gt, lte } from "drizzle-orm";
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
import { Hono } from "hono";
import { createMiddleware } from "hono/factory";
import { deleteCookie, getCookie, getSignedCookie, setCookie, setSignedCookie } from "hono/cookie";
import * as schema from "../db/schema";
import type { PublicUser } from "../shared/user";
import { isAuthConfigured, type AuthConfig } from "./config";
import { fetchDiscordProfile, type FetchDiscord } from "./discord";

const { users, sessions } = schema;
const SESSION_SECONDS = 30 * 24 * 60 * 60;
const STATE_SECONDS = 10 * 60;
export type AuthEnv = { Variables: { user: PublicUser; requestId: string } };
export type AppDatabase = BunSQLiteDatabase<typeof schema>;
export const hashToken = (token: string) => createHash("sha256").update(token).digest("hex");
const randomToken = () => randomBytes(32).toString("base64url");

export function normalizeTimezone(value: unknown): string {
  if (typeof value !== "string" || !value || value.length > 100 || /^[+-]/.test(value)) return "UTC";
  try {
    return new Intl.DateTimeFormat("en-US", { timeZone: value }).resolvedOptions().timeZone;
  } catch {
    return "UTC";
  }
}

function publicUser(user: typeof users.$inferSelect): PublicUser {
  return {
    id: user.id,
    discordId: user.discordId,
    username: user.username,
    displayName: user.globalName ?? user.username,
    avatarUrl: user.avatarHash
      ? `https://cdn.discordapp.com/avatars/${user.discordId}/${user.avatarHash}.${user.avatarHash.startsWith("a_") ? "gif" : "png"}`
      : null,
    timezone: user.timezone,
  };
}

export function createAuth(db: AppDatabase, config: AuthConfig, request: FetchDiscord = fetch, now = Date.now) {
  const secure = config.origin.startsWith("https:");
  const sessionName = secure ? "__Host-minabot_session" : "minabot_session";
  const stateName = secure ? "__Host-minabot_oauth" : "minabot_oauth";
  const cookieOptions = { path: "/", httpOnly: true, sameSite: "Lax" as const, secure };
  const readToken = (value: string | undefined) => value && /^[A-Za-z0-9_-]{43}$/.test(value) ? value : undefined;
  const routes = new Hono<AuthEnv>();

  // Apply to all future cookie-authenticated mutations, including JSON requests.
  const requireSameOrigin = createMiddleware<AuthEnv>(async (c, next) => {
    if (c.req.header("Origin") !== config.origin) return c.json({ error: "Forbidden origin" }, 403);
    await next();
  });
  const requireAuth = createMiddleware<AuthEnv>(async (c, next) => {
    c.header("Cache-Control", "no-store");
    const token = readToken(getCookie(c, sessionName));
    const session = token ? db.select({ user: users }).from(sessions)
      .innerJoin(users, eq(users.id, sessions.userId))
      .where(and(eq(sessions.tokenHash, hashToken(token)), gt(sessions.expiresAt, now()))).get() : undefined;
    if (!session) {
      if (token) db.delete(sessions).where(and(eq(sessions.tokenHash, hashToken(token)), lte(sessions.expiresAt, now()))).run();
      deleteCookie(c, sessionName, cookieOptions);
      return c.json({ error: "Unauthorized" }, 401);
    }
    c.set("user", publicUser(session.user));
    await next();
  });

  routes.use("*", async (c, next) => {
    c.header("Cache-Control", "no-store");
    c.header("Referrer-Policy", "no-referrer");
    await next();
  });
  routes.get("/discord", async c => {
    if (!isAuthConfigured(config)) return c.redirect("/?auth_error=not_configured");
    // Use one configured origin for redirects and cookie ownership.
    if (new URL(c.req.url).origin !== config.origin) return c.redirect(`${config.origin}/api/auth/discord?${new URLSearchParams({ timezone: normalizeTimezone(c.req.query("timezone")) })}`);
    const state = randomToken();
    await setSignedCookie(c, stateName, JSON.stringify({
      state,
      timezone: normalizeTimezone(c.req.query("timezone")),
      expiresAt: now() + STATE_SECONDS * 1000,
    }), config.cookieSecret, { ...cookieOptions, maxAge: STATE_SECONDS });
    const url = new URL("https://discord.com/oauth2/authorize");
    url.search = new URLSearchParams({
      client_id: config.clientId,
      response_type: "code",
      scope: "identify",
      redirect_uri: `${config.origin}/api/auth/discord/callback`,
      state,
    }).toString();
    return c.redirect(url.toString());
  });
  routes.get("/discord/callback", async c => {
    if (!isAuthConfigured(config)) return c.redirect("/?auth_error=not_configured");
    const signedState = await getSignedCookie(c, config.cookieSecret, stateName);
    deleteCookie(c, stateName, cookieOptions);
    let state: { state?: unknown; timezone?: unknown; expiresAt?: unknown } | null = null;
    try { state = signedState ? JSON.parse(signedState) : null; } catch { /* Invalid cookie. */ }
    const returnedState = c.req.query("state");
    if (!state || typeof state.state !== "string" || typeof returnedState !== "string" ||
        !/^[A-Za-z0-9_-]{43}$/.test(state.state) || !/^[A-Za-z0-9_-]{43}$/.test(returnedState) ||
        !timingSafeEqual(Buffer.from(state.state), Buffer.from(returnedState)) ||
        typeof state.expiresAt !== "number" || state.expiresAt <= now()) {
      return c.redirect("/?auth_error=invalid_state");
    }
    if (c.req.query("error")) return c.redirect("/?auth_error=denied");
    const code = c.req.query("code");
    if (!code || code.length > 2048) return c.redirect("/?auth_error=invalid_code");
    let profile;
    try {
      profile = await fetchDiscordProfile(code, config, request);
    } catch {
      // Never log authorization codes, provider responses, or credentials.
      return c.redirect("/?auth_error=discord_unavailable");
    }
    const timestamp = now();
    const token = randomToken();
    const previousToken = readToken(getCookie(c, sessionName));
    db.transaction(tx => {
      const user = tx.insert(users).values({
        id: crypto.randomUUID(), discordId: profile.id, username: profile.username,
        globalName: profile.global_name, avatarHash: profile.avatar,
        timezone: normalizeTimezone(state.timezone), createdAt: timestamp, updatedAt: timestamp,
      }).onConflictDoUpdate({ target: users.discordId, set: {
        username: profile.username, globalName: profile.global_name,
        avatarHash: profile.avatar, updatedAt: timestamp,
        // A browser or travel timezone must not overwrite an existing preference.
      } }).returning().get();
      if (previousToken) tx.delete(sessions).where(eq(sessions.tokenHash, hashToken(previousToken))).run();
      tx.delete(sessions).where(lte(sessions.expiresAt, timestamp)).run();
      tx.insert(sessions).values({ tokenHash: hashToken(token), userId: user.id,
        createdAt: timestamp, expiresAt: timestamp + SESSION_SECONDS * 1000 }).run();
    });
    setCookie(c, sessionName, token, { ...cookieOptions, maxAge: SESSION_SECONDS });
    return c.redirect("/");
  });
  routes.post("/logout", requireSameOrigin, c => {
    const token = readToken(getCookie(c, sessionName));
    if (token) db.delete(sessions).where(eq(sessions.tokenHash, hashToken(token))).run();
    deleteCookie(c, sessionName, cookieOptions);
    return c.body(null, 204);
  });
  return { routes, requireAuth, requireSameOrigin, clearSession: (c: Parameters<typeof requireAuth>[0]) => deleteCookie(c, sessionName, cookieOptions) };
}
