import { cookies } from "next/headers";
import { and, eq, gt, lt } from "drizzle-orm";
import { db } from "@/lib/db";
import { sessions, users, type User } from "@/lib/schema/db";
import { bearerTokenFrom, hashIp, hashToken, issueToken } from "./tokens";
import {
  SESSION_COOKIE,
  SESSION_TTL_DEFAULT_MS,
  SESSION_TTL_REMEMBER_MS,
  cookieSecurityOptions,
} from "./config";

export interface SessionContext {
  userAgent?: string | null;
  ip?: string | null;
  remember?: boolean;
}

/** Minimal shape both `Request` and `NextRequest` satisfy. */
export interface RequestLike {
  headers: { get(name: string): string | null };
}

/** Cookie attributes for the session token, shared by every issuing path. */
export function sessionCookieOptions(expiresAt: Date) {
  return cookieSecurityOptions({ expires: expiresAt });
}

/**
 * Inserts the session row and hands back the raw token. Only the digest is
 * persisted, so the returned token is the single copy that can authenticate.
 *
 * Callers that build their own `NextResponse` (the OAuth redirect) set the
 * cookie themselves; `createSession` below is the convenience path.
 */
export async function createSessionRecord(userId: string, context: SessionContext = {}) {
  const { token, hash } = issueToken();
  const ttl = context.remember ? SESSION_TTL_REMEMBER_MS : SESSION_TTL_DEFAULT_MS;
  const expiresAt = new Date(Date.now() + ttl);

  await db.insert(sessions).values({
    id: hash,
    userId,
    expiresAt,
    userAgent: context.userAgent?.slice(0, 255) ?? null,
    ipHash: hashIp(context.ip),
  });

  return { token, expiresAt };
}

/**
 * Issues a session. By default also writes the cookie via the request-scoped
 * cookie store (the web path). Native clients have no cookie jar and
 * authenticate with the raw token instead — pass `issueCookie: false` to skip
 * the cookie write; the caller is then responsible for returning `token` to
 * the client itself (see the login route's mobile branch).
 */
export async function createSession(
  userId: string,
  context: SessionContext = {},
  options: { issueCookie?: boolean } = {}
) {
  const { token, expiresAt } = await createSessionRecord(userId, context);

  if (options.issueCookie !== false) {
    const cookieStore = await cookies();
    cookieStore.set(SESSION_COOKIE, token, sessionCookieOptions(expiresAt));
  }

  return { token, expiresAt };
}

/**
 * Resolves the caller's account, or null when signed out / expired.
 *
 * Pass the incoming `Request`/`NextRequest` to also accept a native client's
 * `Authorization: Bearer <token>` header — checked first, falling back to the
 * session cookie so existing cookie-only callers (no argument) are unaffected.
 */
export async function getCurrentUser(request?: RequestLike): Promise<User | null> {
  const bearerToken = request ? bearerTokenFrom(request.headers) : null;
  const token = bearerToken ?? (await cookies()).get(SESSION_COOKIE)?.value;
  if (!token) return null;

  const rows = await db
    .select({ user: users })
    .from(sessions)
    .innerJoin(users, eq(sessions.userId, users.id))
    .where(and(eq(sessions.id, hashToken(token)), gt(sessions.expiresAt, new Date())))
    .limit(1);

  return rows[0]?.user ?? null;
}

/**
 * Drops the current session server-side and clears the cookie. Also accepts a
 * request to tear down a Bearer-token session (native sign-out) — both the
 * matching DB row and, if present, the cookie are removed either way.
 */
export async function destroySession(request?: RequestLike): Promise<void> {
  const cookieStore = await cookies();
  const bearerToken = request ? bearerTokenFrom(request.headers) : null;
  const token = bearerToken ?? cookieStore.get(SESSION_COOKIE)?.value;

  if (token) {
    await db.delete(sessions).where(eq(sessions.id, hashToken(token)));
  }
  // Passing the options (including the optional Domain) keeps the deletion on
  // the same scope the cookie was written with, so a shared subdomain session
  // is actually cleared rather than shadowed by a leftover host-only cookie.
  cookieStore.delete({ name: SESSION_COOKIE, ...cookieSecurityOptions() });
}

/** Invalidates every session of a user — used after a password-sensitive change. */
export async function destroyAllSessionsFor(userId: string): Promise<void> {
  await db.delete(sessions).where(eq(sessions.userId, userId));
}

/**
 * Opportunistic cleanup of expired rows. Called from the auth routes rather
 * than a cron job: the tables are small and the write cost is negligible.
 */
export async function pruneExpiredSessions(): Promise<void> {
  await db.delete(sessions).where(lt(sessions.expiresAt, new Date()));
}
