import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import { DomainError } from "../../shared/errors.ts";

export interface SessionRecord {
  readonly id: string;
  readonly userId: string;
  readonly tokenHash: string;
  readonly csrfSecret: string;
  readonly expiresAt: Date;
  readonly createdAt: Date;
  readonly rotatedFrom?: string;
  revokedAt?: Date;
}

export interface SessionRepository {
  insert(record: SessionRecord): Promise<void>;
  findByTokenHash(tokenHash: string): Promise<SessionRecord | null>;
  revoke(id: string, now: Date): Promise<void>;
}

function sha256(value: string): string {
  return createHash("sha256").update(value, "utf8").digest("base64url");
}

function opaqueToken(): string { return randomBytes(32).toString("base64url"); }

export class SessionService {
  private readonly repository: SessionRepository;
  private readonly ttlMs: number;

  constructor(repository: SessionRepository, ttlMs = 1000 * 60 * 60 * 24 * 7) {
    this.repository = repository;
    this.ttlMs = ttlMs;
  }

  async issue(userId: string, now = new Date(), rotatedFrom?: string): Promise<{ token: string; session: SessionRecord }> {
    const token = opaqueToken();
    const session: SessionRecord = {
      id: crypto.randomUUID(), userId, tokenHash: sha256(token), csrfSecret: opaqueToken(),
      expiresAt: new Date(now.getTime() + this.ttlMs), createdAt: now,
      ...(rotatedFrom ? { rotatedFrom } : {}),
    };
    await this.repository.insert(session);
    return { token, session };
  }

  async authenticate(token: string, now = new Date()): Promise<SessionRecord> {
    if (!token || token.length > 256) throw new DomainError("UNAUTHENTICATED", "Authentication required.");
    const session = await this.repository.findByTokenHash(sha256(token));
    if (!session || session.revokedAt || session.expiresAt <= now) throw new DomainError("UNAUTHENTICATED", "Authentication required.");
    return session;
  }

  async rotate(token: string, now = new Date()): Promise<{ token: string; session: SessionRecord }> {
    const current = await this.authenticate(token, now);
    await this.repository.revoke(current.id, now);
    return this.issue(current.userId, now, current.id);
  }
}

export function verifyCsrfToken(submitted: string, expected: string): void {
  const left = Buffer.from(submitted || "", "utf8");
  const right = Buffer.from(expected || "", "utf8");
  if (left.length === 0 || left.length !== right.length || !timingSafeEqual(left, right)) {
    throw new DomainError("CSRF_INVALID", "Request verification failed.");
  }
}
