import dayjs from 'dayjs';
import clone from 'lodash/clone';
import pAll from 'p-all';

import { notificationCronConcurrency } from '../libs/env';
import logger from '../libs/logger';
import { NotificationQueueJobType, notificationQueue } from '../queues/notification';
import type { Subscription } from '../store/models';
import type { Diff, Task } from './interface/base';

export interface BaseSubscriptionScheduleNotificationSubscription extends Subscription {
  diffs: Diff[];
}

export type BaseSubscriptionScheduleNotificationEventType = NotificationQueueJobType;

export abstract class BaseSubscriptionScheduleNotification<Options extends any> {
  readonly start: number;
  readonly end: number;
  abstract eventType: BaseSubscriptionScheduleNotificationEventType;

  constructor() {
    this.start = Date.now();
    this.end = dayjs(this.start).add(1, 'M').add(1, 'd').toDate().getTime();
  }

  abstract getSubscriptions(): Promise<Subscription[]>;

  async run() {
    const { name } = this.constructor;

    logger.info(`${name}.run start`, { start: dayjs(this.start).toISOString(), end: dayjs(this.end).toISOString() });

    const subscriptions = await this.getSubscriptions();
    logger.info(`${name}.run.${subscriptions.length}`, subscriptions.length);

    const scheduleSubscriptions = this.getScheduleSubscriptions(subscriptions);

    await this.addTaskToQueue(scheduleSubscriptions);

    logger.info(`${name}.run end`);
  }

  async reScheduleSubscriptionTasks(subscriptions: Subscription[]): Promise<void> {
    await this.deleteScheduleSubscriptionJobs(subscriptions);
    const scheduleSubscriptions = this.getScheduleSubscriptions(subscriptions);
    await this.addTaskToQueue(scheduleSubscriptions);
  }

  static readonly DIFFS: Diff[] = [
    {
      value: 1,
      unit: 'M',
    },
    {
      value: 7,
      unit: 'd',
    },
    {
      value: 3,
      unit: 'd',
    },
    {
      value: 1,
      unit: 'd',
    },
    {
      value: 6,
      unit: 'h',
    },
    {
      value: 30,
      unit: 'm',
    },
  ];

  abstract getEndTime(subscription: Subscription): number;
  /**
   * @see https://github.com/blocklet/payment-kit/issues/236#issuecomment-1824129965
   * @description
   * @param {Subscription[]} subscriptions
   * @return {*}  {SubscriptionForWillRenew[]}
   * @memberof SubscriptionWillRenewSchedule
   */
  getScheduleSubscriptions(subscriptions: Subscription[]): BaseSubscriptionScheduleNotificationSubscription[] {
    return subscriptions.map((subscription: Subscription): BaseSubscriptionScheduleNotificationSubscription => {
      const s: BaseSubscriptionScheduleNotificationSubscription = clone(
        subscription
      ) as BaseSubscriptionScheduleNotificationSubscription;
      const currentPeriodStart: number = s.current_period_start * 1000;
      const currentPeriodEnd: number = this.getEndTime(s);

      if (
        dayjs(currentPeriodEnd).diff(this.start, 'M') >= 1 &&
        dayjs(currentPeriodEnd).diff(currentPeriodStart, 'M') > 1
      ) {
        s.diffs = BaseSubscriptionScheduleNotification.DIFFS.slice(0);
      } else if (
        dayjs(currentPeriodEnd).diff(this.start, 'd') >= 7 &&
        dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 7
      ) {
        s.diffs = BaseSubscriptionScheduleNotification.DIFFS.slice(1);
      } else if (
        dayjs(currentPeriodEnd).diff(this.start, 'd') >= 3 &&
        dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 3
      ) {
        s.diffs = BaseSubscriptionScheduleNotification.DIFFS.slice(2);
      } else if (
        dayjs(currentPeriodEnd).diff(this.start, 'd') >= 1 &&
        dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 1
      ) {
        s.diffs = BaseSubscriptionScheduleNotification.DIFFS.slice(3);
      } else if (
        dayjs(currentPeriodEnd).diff(this.start, 'h') >= 6 &&
        dayjs(currentPeriodEnd).diff(currentPeriodStart, 'h') > 6
      ) {
        s.diffs = BaseSubscriptionScheduleNotification.DIFFS.slice(4);
      } else if (
        dayjs(currentPeriodEnd).diff(this.start, 'm') >= 30 &&
        dayjs(currentPeriodEnd).diff(currentPeriodStart, 'm') > 30
      ) {
        s.diffs = BaseSubscriptionScheduleNotification.DIFFS.slice(5);
      } else {
        s.diffs = [];
      }

      // 每一个周期都至少收到一封邮件
      if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'M') > 1) {
        s.diffs.forEach((x) => {
          if (x.value === 7 && x.unit === 'd') {
            x.required = true;
          }
        });
      } else if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'd') > 7) {
        s.diffs.forEach((x) => {
          if (x.value === 3 && x.unit === 'd') {
            x.required = true;
          }
        });
      } else if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'h') >= 6) {
        s.diffs.forEach((x) => {
          if (x.value === 6 && x.unit === 'h') {
            x.required = true;
          }
        });
      } else if (dayjs(currentPeriodEnd).diff(currentPeriodStart, 'm') >= 30) {
        s.diffs.forEach((x) => {
          if (x.value === 30 && x.unit === 'm') {
            x.required = true;
          }
        });
      }

      return s;
    });
  }

  abstract getTask({ subscription, diff }: { subscription: Subscription; diff: Diff }): Task<Options>;

  async addTaskToQueue(subscriptions: BaseSubscriptionScheduleNotificationSubscription[]): Promise<void> {
    const tasks: Task<Options>[] = [];
    const deleteTasks = [];
    for (const subscription of subscriptions) {
      for (const diff of subscription.diffs) {
        try {
          const task: Task<Options> = this.getTask({ subscription, diff });
          tasks.push(task);
        } catch (error) {
          logger.error('getTask error', { error, subscriptionId: subscription.id, diff });
          deleteTasks.push(() => this.deleteScheduleSubscriptionJobs([subscription]));
        }
      }
    }

    await pAll(
      deleteTasks.map((task) => () => task()),
      {
        concurrency: notificationCronConcurrency,
        stopOnError: false,
      }
    );
    await pAll(
      tasks.map((x) => {
        return async () => {
          const job = await notificationQueue.get(x.id);
          if (job) {
            return;
          }

          logger.info(`BaseSubscriptionScheduleNotification.addTaskToQueue.${this.eventType}`, x);
          notificationQueue.push(x);
        };
      }),
      {
        concurrency: notificationCronConcurrency,
        stopOnError: false,
      }
    );
  }

  async deleteScheduleSubscriptionJobs(subscriptions: Subscription[]): Promise<void> {
    const jobsToDelete = await Promise.all(
      subscriptions.flatMap((subscription) =>
        BaseSubscriptionScheduleNotification.DIFFS.map(async (diff) => {
          const jobId = `${subscription.id}.${this.eventType}.${diff.value}.${diff.unit}`;
          const job = await notificationQueue.get(jobId);
          return job ? { jobId, subscriptionId: subscription.id } : null;
        })
      )
    );

    const existingJobs = jobsToDelete.filter((x): x is { jobId: string; subscriptionId: string } => x !== null);

    const deletePromises = existingJobs.map(
      ({ jobId, subscriptionId }: { jobId: string; subscriptionId: string }) =>
        async () => {
          try {
            const deleted = await notificationQueue.delete(jobId);
            if (deleted) {
              logger.info('Deleted subscription job', { jobId, subscriptionId, eventType: this.eventType });
            }
          } catch (error) {
            logger.error('Failed to delete subscription job', {
              jobId,
              subscriptionId,
              eventType: this.eventType,
              error: error instanceof Error ? error.message : String(error),
            });
          }
        }
    );

    await pAll(deletePromises, {
      concurrency: notificationCronConcurrency,
      stopOnError: false,
    });
  }
}
