import { createHash } from 'node:crypto';
import { SignJWT, jwtVerify, errors as joseErrors } from 'jose';
import {
  GuestClaimsSchema,
  HostClaimsSchema,
  TvClaimsSchema,
  JWT_AUDIENCE,
  JWT_ISSUER,
  TOKEN_TTL,
  type GuestClaims,
  type HostClaims,
  type TvClaims,
} from '@pintrush/protocol';

/**
 * HS256 tokens shared with the web app (same AUTH_SECRET):
 *  - guest: minted here on join/reconnect, rotated on every reconnect
 *  - tv:    minted by web for a room's TV link; display-only
 *  - host:  minted by web for a signed-in venue user
 */
export class TokenService {
  private readonly key: Uint8Array;

  constructor(secret: string) {
    this.key = new TextEncoder().encode(secret);
  }

  async signGuest(claims: Omit<GuestClaims, 'scope'>, nowMs: number): Promise<{ token: string; expiresAt: number }> {
    const iat = Math.floor(nowMs / 1000);
    const exp = iat + TOKEN_TTL.guestSeconds;
    const token = await new SignJWT({ ...claims, scope: 'guest' })
      .setProtectedHeader({ alg: 'HS256' })
      .setIssuer(JWT_ISSUER)
      .setAudience(JWT_AUDIENCE)
      .setIssuedAt(iat)
      .setExpirationTime(exp)
      .setSubject(claims.sub)
      .sign(this.key);
    return { token, expiresAt: exp * 1000 };
  }

  /** Used by tests and the demo room; web mints TV/host tokens in production. */
  async signTv(claims: Omit<TvClaims, 'scope'>): Promise<string> {
    return new SignJWT({ ...claims, scope: 'tv' })
      .setProtectedHeader({ alg: 'HS256' })
      .setIssuer(JWT_ISSUER)
      .setAudience(JWT_AUDIENCE)
      .setIssuedAt()
      .sign(this.key);
  }

  async signHost(claims: Omit<HostClaims, 'scope'>, nowMs: number): Promise<string> {
    const iat = Math.floor(nowMs / 1000);
    return new SignJWT({ ...claims, scope: 'host' })
      .setProtectedHeader({ alg: 'HS256' })
      .setIssuer(JWT_ISSUER)
      .setAudience(JWT_AUDIENCE)
      .setIssuedAt(iat)
      .setExpirationTime(iat + TOKEN_TTL.hostSeconds)
      .setSubject(claims.sub)
      .sign(this.key);
  }

  async verify(token: string, nowMs: number): Promise<VerifiedToken | null> {
    try {
      const { payload } = await jwtVerify(token, this.key, {
        issuer: JWT_ISSUER,
        audience: JWT_AUDIENCE,
        algorithms: ['HS256'],
        currentDate: new Date(nowMs),
      });
      switch (payload.scope) {
        case 'guest': {
          const r = GuestClaimsSchema.safeParse(payload);
          return r.success ? { kind: 'guest', claims: r.data } : null;
        }
        case 'tv': {
          const r = TvClaimsSchema.safeParse(payload);
          return r.success ? { kind: 'tv', claims: r.data } : null;
        }
        case 'host': {
          const r = HostClaimsSchema.safeParse(payload);
          return r.success ? { kind: 'host', claims: r.data } : null;
        }
        default:
          return null;
      }
    } catch (err) {
      if (err instanceof joseErrors.JOSEError) return null;
      throw err;
    }
  }
}

export type VerifiedToken =
  | { kind: 'guest'; claims: GuestClaims }
  | { kind: 'tv'; claims: TvClaims }
  | { kind: 'host'; claims: HostClaims };

/** Device ids are random per-browser UUIDs; we only ever store a salted hash. */
export function hashDeviceId(deviceId: string, salt: string): string {
  return createHash('sha256').update(`${salt}:${deviceId}`).digest('hex');
}
