import session, { type SessionData } from 'express-session';
import type { Prisma, PrismaClient } from '../generated/prisma/client.js';

type Callback = (error?: unknown) => void;
const SESSION_TTL = 12 * 60 * 60 * 1000;

// Express serializes Cookie.expires as an ISO string and restores it on load.
export class PrismaSessionStore extends session.Store {
  private readonly timer: ReturnType<typeof setInterval>;
  private pruning: Promise<void> | undefined;
  private closed = false;

  constructor(private readonly db: PrismaClient) {
    super();
    this.timer = setInterval(() => {
      void this.prune().catch(() => console.error('Session cleanup failed.'));
    }, 60_000);
    this.timer.unref();
  }

  private expiry(value: SessionData) {
    return value.cookie.expires ? new Date(value.cookie.expires) : new Date(Date.now() + SESSION_TTL);
  }

  get(sid: string, callback: (error: unknown, value?: SessionData | null) => void) {
    void this.db.authSession.findUnique({ where: { sid } }).then(row => {
      if (!row || row.expire.getTime() <= Date.now()) return callback(null, null);
      callback(null, JSON.parse(JSON.stringify(row.sess)) as SessionData);
    }).catch(callback);
  }

  set(sid: string, value: SessionData, callback: Callback = () => {}) {
    const data = { sess: JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue, expire: this.expiry(value) };
    void this.db.authSession.upsert({ where: { sid }, create: { sid, ...data }, update: data })
      .then(() => callback()).catch(callback);
  }

  touch(sid: string, value: SessionData, callback: Callback = () => {}) {
    void this.db.authSession.updateMany({ where: { sid, expire: { gt: new Date() } }, data: { expire: this.expiry(value) } })
      .then(() => callback()).catch(callback);
  }

  destroy(sid: string, callback: Callback = () => {}) {
    void this.db.authSession.deleteMany({ where: { sid } }).then(() => callback()).catch(callback);
  }

  prune(): Promise<void> {
    if (this.closed) return Promise.resolve();
    if (this.pruning) return this.pruning;
    this.pruning = this.db.authSession.deleteMany({ where: { expire: { lte: new Date() } } })
      .then(() => {}).finally(() => { this.pruning = undefined; });
    return this.pruning;
  }

  async close() {
    this.closed = true;
    clearInterval(this.timer);
    await this.pruning;
  }
}
