/**
 * This file is part of the NocoBase (R) project.
 * Copyright (c) 2020-2024 NocoBase Co., Ltd.
 * Authors: NocoBase Team.
 *
 * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
 * For more information, please refer to: https://www.nocobase.com/agreement.
 */

import { ITokenBlacklistService } from '@nocobase/auth';
import { Repository } from '@nocobase/database';
import { CronJob } from 'cron';
import AuthPlugin from './plugin';
import { BloomFilter } from '@nocobase/cache';

export class TokenBlacklistService implements ITokenBlacklistService {
  repo: Repository;
  cronJob: CronJob;
  bloomFilter: BloomFilter;
  cacheKey = 'token-black-list';

  constructor(protected plugin: AuthPlugin) {
    this.repo = plugin.db.getRepository('tokenBlacklist');

    // Try to create a bloom filter and cache blocked tokens in it
    plugin.app.on('beforeStart', async () => {
      try {
        this.bloomFilter = await plugin.app.cacheManager.createBloomFilter({
          shouldCloneBeforeSet: false,
        });
        // https://redis.io/docs/data-types/probabilistic/bloom-filter/#reserving-bloom-filters
        // 0.1% error rate requires 14.4 bits per item
        // 14.4*1000000/8/1024/1024 = 1.72MB
        await this.bloomFilter.reserve(this.cacheKey, 0.001, 1000000);
        const data = await this.repo.find({
          fields: ['token'],
          filter: {
            expiration: {
              $dateAfter: new Date(),
            },
          },
          raw: true,
        });
        const tokens = data.map((item: any) => item.token);
        if (!tokens.length) {
          return;
        }
        await this.bloomFilter.mAdd(this.cacheKey, tokens);
      } catch (error) {
        plugin.app.logger.warn('token-blacklist: create bloom filter failed', error);
        this.bloomFilter = null;
      }
    });
  }

  get app() {
    return this.plugin.app;
  }

  async has(token: string) {
    if (this.bloomFilter) {
      const exists = await this.bloomFilter.exists(this.cacheKey, token);
      if (!exists) {
        return false;
      }
    }
    return !!(await this.repo.findOne({
      filter: {
        token,
      },
    }));
  }

  async add(values) {
    await this.deleteExpiredTokens();
    const { token } = values;
    if (this.bloomFilter) {
      await this.bloomFilter.add(this.cacheKey, token);
    }
    return this.repo.model.findOrCreate({
      defaults: values,
      where: {
        token,
      },
    });
  }

  async deleteExpiredTokens() {
    return this.repo.destroy({
      filter: {
        expiration: {
          $dateNotAfter: new Date(),
        },
      },
    });
  }
}
