import type { AstroCookies } from 'astro';
import { sql } from 'drizzle-orm';
import { getDb, type DbEnv } from './db.js';

export type Session = {
  userId: string;
  role: string;
  email: string;
};

type SessionBlob = Session & { createdAt?: string };

export type SessionEnv = {
  SESSION: KVNamespace;
};

const SESSION_COOKIE = 'sf_sid';
const SESSION_TTL_SECS = 60 * 60 * 24 * 7;
const USER_SESSIONS_PREFIX = 'user_sessions:';

function normalizeExecuteRows(result: unknown): unknown[] {
  if (Array.isArray(result)) return result;
  const rows = (result as { rows?: unknown[] } | null)?.rows;
  return rows ?? [];
}

function parseRoles(raw: unknown): string[] {
  if (typeof raw !== 'string') return [];
  try {
    const parsed: unknown = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed.filter((r): r is string => typeof r === 'string') : [];
  } catch {
    return [];
  }
}

function deriveSessionRole(raw: unknown): string {
  const roles = parseRoles(raw);
  return roles.includes('owner') ? 'owner' : 'user';
}

function parseTimestamptz(raw: unknown): Date | null {
  if (raw instanceof Date) return raw;
  if (typeof raw === 'string') {
    const d = new Date(raw);
    return Number.isNaN(d.getTime()) ? null : d;
  }
  return null;
}

function isSessionStaleAfterPasswordReset(blob: SessionBlob, passwordChangedAt: unknown): boolean {
  const changedAt = parseTimestamptz(passwordChangedAt);
  if (!changedAt) return false;

  const createdAt = blob.createdAt ? parseTimestamptz(blob.createdAt) : null;
  if (createdAt) {
    return createdAt.getTime() < changedAt.getTime();
  }

  const windowMs = SESSION_TTL_SECS * 1000;
  return Date.now() - changedAt.getTime() < windowMs;
}

async function invalidateSession(
  env: SessionEnv,
  cookies: AstroCookies,
  sessionId: string,
  userId: string,
): Promise<void> {
  await deleteSessionById(env, sessionId);
  await removeUserSessionId(env, userId, sessionId);
  cookies.delete(SESSION_COOKIE, { path: '/' });
}

async function readUserSessionIds(env: SessionEnv, userId: string): Promise<string[]> {
  const raw = await env.SESSION.get(`${USER_SESSIONS_PREFIX}${userId}`);
  if (!raw) return [];
  try {
    const ids: unknown = JSON.parse(raw);
    return Array.isArray(ids) ? ids.filter((id): id is string => typeof id === 'string') : [];
  } catch {
    return [];
  }
}

async function writeUserSessionIds(env: SessionEnv, userId: string, ids: string[]): Promise<void> {
  if (ids.length === 0) {
    await env.SESSION.delete(`${USER_SESSIONS_PREFIX}${userId}`);
    return;
  }
  await env.SESSION.put(`${USER_SESSIONS_PREFIX}${userId}`, JSON.stringify(ids), {
    expirationTtl: SESSION_TTL_SECS,
  });
}

async function appendUserSessionId(env: SessionEnv, userId: string, sessionId: string): Promise<void> {
  const ids = await readUserSessionIds(env, userId);
  if (!ids.includes(sessionId)) {
    ids.push(sessionId);
  }
  await writeUserSessionIds(env, userId, ids);
}

async function removeUserSessionId(env: SessionEnv, userId: string, sessionId: string): Promise<void> {
  const ids = await readUserSessionIds(env, userId);
  await writeUserSessionIds(
    env,
    userId,
    ids.filter((id) => id !== sessionId),
  );
}

async function deleteSessionById(env: SessionEnv, sessionId: string): Promise<void> {
  await env.SESSION.delete(`session:${sessionId}`);
}

export async function getSession(
  env: SessionEnv & Partial<DbEnv>,
  cookies: AstroCookies,
): Promise<Session | null> {
  const sessionId = cookies.get(SESSION_COOKIE)?.value;
  if (!sessionId) return null;

  const raw = await env.SESSION.get(`session:${sessionId}`);
  if (!raw) return null;

  let session: SessionBlob;
  try {
    session = JSON.parse(raw) as SessionBlob;
  } catch {
    return null;
  }

  if (!session.userId) return session;

  if (!env.DB && !env.DATABASE_URL) return session;

  try {
    const { db } = getDb(env as DbEnv);
    const res = await db.execute(
      sql`SELECT status, roles, password_changed_at FROM auth_users WHERE id = ${session.userId} LIMIT 1`,
    );
    const rows = normalizeExecuteRows(res);
    const row = rows[0] as
      | { status?: unknown; roles?: unknown; password_changed_at?: unknown }
      | undefined;
    const status = typeof row?.status === 'string' ? row.status : null;

    if (!row || status !== 'active') {
      await invalidateSession(env, cookies, sessionId, session.userId);
      return null;
    }

    if (isSessionStaleAfterPasswordReset(session, row.password_changed_at)) {
      await invalidateSession(env, cookies, sessionId, session.userId);
      return null;
    }

    session = { ...session, role: deriveSessionRole(row.roles) };
  } catch {
    // Fail-open for regular users on transient DB errors to avoid a mass logout during an outage.
    // Fail-closed for privileged roles: a revoked owner must not retain access for the session TTL.
    if (session.role === 'owner') {
      return null;
    }
  }

  return session;
}

export async function setSession(
  env: SessionEnv,
  cookies: AstroCookies,
  session: Session,
): Promise<void> {
  const sessionId = crypto.randomUUID();
  const blob: SessionBlob = { ...session, createdAt: new Date().toISOString() };
  await env.SESSION.put(`session:${sessionId}`, JSON.stringify(blob), {
    expirationTtl: SESSION_TTL_SECS,
  });
  await appendUserSessionId(env, session.userId, sessionId);
  cookies.set(SESSION_COOKIE, sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/',
    maxAge: SESSION_TTL_SECS,
  });
}

export async function clearSession(env: SessionEnv, cookies: AstroCookies): Promise<void> {
  const sessionId = cookies.get(SESSION_COOKIE)?.value;
  if (sessionId) {
    const raw = await env.SESSION.get(`session:${sessionId}`);
    await deleteSessionById(env, sessionId);
    if (raw) {
      try {
        const session = JSON.parse(raw) as Session;
        if (session.userId) {
          await removeUserSessionId(env, session.userId, sessionId);
        }
      } catch {
        // ignore malformed session blob
      }
    }
  }
  cookies.delete(SESSION_COOKIE, { path: '/' });
}

/** Delete every KV session for a user (e.g. after password reset). */
export async function invalidateAllSessionsForUser(env: SessionEnv, userId: string): Promise<void> {
  const ids = await readUserSessionIds(env, userId);
  await Promise.all(ids.map((id) => deleteSessionById(env, id)));
  await env.SESSION.delete(`${USER_SESSIONS_PREFIX}${userId}`);
}
