// app/services/csrf-token.ts
// Copyright (C) 2026 DTP Technologies, LLC
// All Rights Reserved

import { Request, Response, NextFunction, RequestHandler } from "express";
import { v4 as uuidv4 } from "uuid";
import dayjs from "dayjs";

import CsrfToken, { ICsrfToken } from "../models/csrf-token.js";

import { DtpService } from "../lib/service.js";

export interface CsrfTokenOptions {
  name: string;
  expiresMinutes: number;
  allowReuse: boolean;
}

export class CsrfTokenService extends DtpService {
  get name(): string {
    return "CsrfTokenService";
  }
  get slug(): string {
    return "csrfToken";
  }

  constructor() {
    super();
  }

  async start(): Promise<void> {
    this.log.info("service started");
  }

  async stop(): Promise<void> {
    this.log.info("service stopped");
  }

  middleware(options: CsrfTokenOptions): RequestHandler {
    return async (
      req: Request,
      _res: Response,
      next: NextFunction,
    ): Promise<void> => {
      const requestToken = req.body[`csrf-token-${options.name}`];
      if (!requestToken) {
        this.log.error("missing CSRF token", { name: options.name });
        const error = new Error("Must include valid CSRF token");
        error.statusCode = 401;
        return next(error);
      }

      const token = await CsrfToken.findOne({ token: requestToken });
      if (!token) {
        const error = new Error("CSRF request token is invalid");
        error.statusCode = 401;
        return next(error);
      }
      if (token.ip !== req.ip) {
        const error = new Error("CSRF request token client mismatch");
        error.statusCode = 401;
        return next(error);
      }
      if (token.claimedAt && !options.allowReuse) {
        const error = new Error(
          "Your request can't be accepted. Please refresh the page and try again.",
        );
        error.statusCode = 401;
        return next(error);
      }

      if (token.user) {
        if (!req.user) {
          const error = new Error("Must be logged in");
          error.statusCode = 401;
          return next(error);
        }
        if (!token.user._id.equals(req.user._id)) {
          const error = new Error("CSRF request token user mismatch");
          error.statusCode = 401;
          return next(error);
        }
      }

      await CsrfToken.updateOne(
        { _id: token._id },
        { $set: { claimed: new Date() } },
      );

      return next();
    };
  }

  async create(req: Request, options: CsrfTokenOptions): Promise<ICsrfToken> {
    const NOW = new Date();

    options = Object.assign(
      {
        expiresMinutes: 30,
      },
      options,
    );

    if (options.expiresMinutes > 120) {
      const error = new Error("CSRF tokens have a max lifespan of 120 minutes");
      error.statusCode = 400;
      throw error;
    }

    const token = new CsrfToken();
    token.name = `csrf-token-${options.name}`;
    token.createdAt = NOW;
    token.expiresAt = dayjs(NOW).add(options.expiresMinutes, "minute").toDate();
    if (req.user) {
      token.user = req.user._id;
    }
    if (req.ip) {
      token.ip = req.ip;
    }
    token.token = uuidv4();
    await token.save();

    return token.toObject();
  }
}

export default new CsrfTokenService();
