/**
 * Signed session cookies for the venue host dashboard (`pr_host`) and the platform admin
 * (`pr_admin`): HS256 JWTs via jose, keyed with the shared AUTH_SECRET.
 *
 * Each kind has its own audience, so a host cookie can never verify as an admin session (or the
 * other way round), and neither verifies as a game-server token (audience `pintrush-game`).
 * Pure functions (secret and clock are parameters) so they are unit-testable without Next.js.
 */
import { SignJWT, jwtVerify } from 'jose';
import { z } from 'zod';
import { VenueRoleSchema } from '@pintrush/protocol';

/** Both sessions last 12 hours (same as the host socket token minted from them). */
export const SESSION_TTL_SECONDS = 12 * 60 * 60;

export type SessionKind = 'host' | 'admin';

const ISSUER = 'pintrush-web';
const AUDIENCE: Record<SessionKind, string> = {
  host: 'pintrush-host-session',
  admin: 'pintrush-admin-session',
};

const Uuid = z.uuid();

export const HostSessionSchema = z.object({
  sub: Uuid,
  venueId: Uuid,
  role: VenueRoleSchema,
});
export const AdminSessionSchema = z.object({ sub: Uuid });

export type HostSession = z.output<typeof HostSessionSchema>;
export type AdminSession = z.output<typeof AdminSessionSchema>;

type ClaimsFor<K extends SessionKind> = K extends 'host' ? HostSession : AdminSession;

const SCHEMAS: { [K in SessionKind]: z.ZodType<ClaimsFor<K>> } = {
  host: HostSessionSchema,
  admin: AdminSessionSchema,
};

function keyFrom(secret: string): Uint8Array {
  if (secret.length < 32) throw new Error('Session secret must be at least 32 characters');
  return new TextEncoder().encode(secret);
}

/** Sign a session token. `nowMs` is injectable for tests. */
export async function signSession<K extends SessionKind>(
  kind: K,
  claims: ClaimsFor<K>,
  secret: string,
  nowMs: number = Date.now(),
): Promise<string> {
  const parsed = SCHEMAS[kind].parse(claims) as ClaimsFor<K>;
  const iat = Math.floor(nowMs / 1000);
  const { sub, ...rest } = parsed as ClaimsFor<K> & { sub: string };
  return new SignJWT({ ...rest, kind })
    .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
    .setIssuer(ISSUER)
    .setAudience(AUDIENCE[kind])
    .setSubject(sub)
    .setIssuedAt(iat)
    .setExpirationTime(iat + SESSION_TTL_SECONDS)
    .sign(keyFrom(secret));
}

/**
 * Verify a session token. Returns null for anything that is missing, malformed, tampered with,
 * expired, signed with another key, or meant for the other kind — callers never need to care which.
 */
export async function verifySession<K extends SessionKind>(
  kind: K,
  token: string | undefined | null,
  secret: string,
  nowMs: number = Date.now(),
): Promise<ClaimsFor<K> | null> {
  if (!token || token.length > 4096) return null;
  try {
    const { payload } = await jwtVerify(token, keyFrom(secret), {
      algorithms: ['HS256'],
      issuer: ISSUER,
      audience: AUDIENCE[kind],
      currentDate: new Date(nowMs),
      requiredClaims: ['sub', 'exp', 'iat'],
    });
    if (payload.kind !== kind) return null;
    const result = SCHEMAS[kind].safeParse(payload);
    return result.success ? (result.data as ClaimsFor<K>) : null;
  } catch {
    return null;
  }
}
