import { DashboardsService } from '@app/event-emitter/dashboards/dashboards.service';
import { IssuesService } from '@app/event-emitter/issues/issues.service';
import { Dashboard, Widget } from '@app/event-emitter/models/dashboard';
import { Injectable, Logger } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';
import { DailyEccmReportsV2Datasource } from 'src/couchdb-datasources/daily-eccm-reports-v2.datasource';
import { randomUUID } from 'crypto';
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
import { DateTime } from 'luxon';
import { Params } from './daily-eccm-v2-report-task-handler';

export type Job = {
  id: string;
  params: Params;
};

export type JobHandlerParams = {
  job: Job;
  dashboard: Dashboard;
  widget: Widget;
};

export const WIDGET_TYPE = 'daily_eccm_v2';

export const JOB_PREFIX = 'daily_eccm_v2';

export const UPDATE_RATE = 60 * 1000;

@Injectable()
export class DailyEccmV2ReportTaskRunnerService {
  private logger = new Logger(DailyEccmV2ReportTaskRunnerService.name);

  private dashboardsService: DashboardsService;

  private previousAutoScanTime = 0;

  private cronJobs: Record<string, CronJob> = {};

  constructor(
    private schedulerRegistry: SchedulerRegistry,
    private issuesService: IssuesService,
  ) {}

  /**
   * Auto scan jobs every UPDATE_RATE seconds.
   * First call to autoScanJobs is done immediately.
   * Each subsequent call is done after the timeout.
   * The timeout is reset after each call to autoScanJobs.
   */
  initAutoScanJobs() {
    const tick = () => {
      setTimeout(tick, UPDATE_RATE);
      this.autoScanJobs();
    };
    tick();
  }

  async autoScanJobs() {
    this.logger.debug('Auto scan jobs started');
    const dbs = this.getDashboardsService();
    if (!dbs) {
      this.logger.warn('Dashboards service not initialized');
      this.logger.debug('Auto scan jobs finished');
      return;
    }
    const nowTime = new Date().getTime();
    const dashboards = await dbs.findDashboardsByWidgetType(
      WIDGET_TYPE,
      this.previousAutoScanTime,
    );
    this.previousAutoScanTime = nowTime;
    for (let i = 0; i < dashboards.length; i++) {
      const dashboard: Dashboard = dashboards[i];
      for (let j = 0; j < dashboard.data.widgets.length; j++) {
        const widget = dashboard.data.widgets[j];
        if (widget.type === WIDGET_TYPE) {
          const jobId = `${JOB_PREFIX}_${dashboard.id}_${widget.id}`;
          this.updateCronJob(jobId, widget, dashboard);
        }
      }
    }
    this.logger.debug('Auto scan jobs finished');
  }

  private updateCronJob(
    jobId: string,
    widget: Widget,
    dashboard: Dashboard,
  ): void {
    if (this.cronJobs[jobId]) {
      this.cronJobs[jobId].stop();
      this.schedulerRegistry.deleteCronJob(jobId);
    }
    const job = new CronJob(
      widget.dataLoaderParams?.schedule || '0 0 * * *',
      this.createJobHandler(jobId, dashboard, widget),
    );
    this.cronJobs[jobId] = job;
    this.schedulerRegistry.addCronJob(jobId, job);
    job.start();
  }

  getDashboardsService(): DashboardsService | null {
    if (!this.dashboardsService) {
      this.logger.warn('Dashboards service not initialized');
      return null;
    }
    return this.dashboardsService;
  }

  setDashboardsService(dashboardsService: DashboardsService) {
    this.dashboardsService = dashboardsService;
  }

  private createJobHandler(
    jobId: string,
    dashboard: Dashboard,
    widget: Widget,
  ): () => Promise<void> {
    this.logger.debug(
      `Create job handler for cron job ${jobId}, ` +
        `dashboard ${dashboard.id}, ` +
        `widget ${widget.id}`,
    );
    return async (): Promise<void> => {
      this.logger.debug(
        `Cron job ${jobId} started - dashboard ${dashboard.id}, widget ${widget.id}`,
      );

      const query = widget.dataLoaderParams?.query;
      if (!query) {
        this.logger.log(
          `Cron job ${jobId} finished - dashboard ${dashboard.id}, widget ${widget.id}`,
        );
        return;
      }

      let issues: RedmineTypes.Issue[] = [];
      try {
        issues = await this.issuesService.mergedTreesAndFind(query);
        await this.saveReport(dashboard, widget, issues);
      } catch (error) {
        this.logger.error(error);
      }

      this.logger.log(
        `Cron job ${jobId} finished - ` +
          `dashboard ${dashboard.id}, ` +
          `widget ${widget.id}, ` +
          `issues count ${issues.length}`,
      );
    };
  }

  private async analyzeIssueMetrics(
    dashboard: Dashboard,
    widget: Widget,
    issues: RedmineTypes.Issue[],
    previousIssues?: RedmineTypes.Issue[],
  ): Promise<Record<string, any>> {
    this.logger.log(
      `Analyzing issues metrics for dashboard ${dashboard.id}, widget ${widget.id}`,
    );

    // ШАГ 1. Подсчет количества задач по статусам
    this.logger.debug(`Step 1. Calculating issues by status`);
    const issuesByStatusCount = issues.reduce((acc, issue) => {
      const status = issue.status?.name;
      if (status) {
        if (!acc[status]) {
          acc[status] = {
            count: 0,
            issueIds: [],
            byVersion: {} as Record<
              string,
              { count: number; issueIds: number[] }
            >,
          };
        }
        acc[status].count = acc[status].count + 1;
        acc[status].issueIds.push(issue.id);
        const version = issue.fixed_version?.name;
        if (version) {
          if (!acc[status].byVersion[version]) {
            acc[status].byVersion[version] = { count: 0, issueIds: [] };
          }
          acc[status].byVersion[version].count++;
          acc[status].byVersion[version].issueIds.push(issue.id);
        }
      }
      return acc;
    }, {});
    this.logger.debug(
      'Step 1. Calculating issues by status - done',
      JSON.stringify(issuesByStatusCount),
    );

    // ШАГ 2. Подсчет количества задач по версиям
    this.logger.debug('Step 2. Calculating issues by versions');
    const issuesByVersionsCount = issues.reduce((acc, issue) => {
      const version = issue.fixed_version?.name;
      if (version) {
        if (!acc[version]) {
          acc[version] = {
            count: 0,
            issueIds: [],
            byStatus: {} as Record<
              string,
              { count: number; issueIds: number[] }
            >,
          };
        }
        acc[version].count = acc[version].count + 1;
        acc[version].issueIds.push(issue.id);
        const status = issue.status?.name;
        if (status) {
          if (!acc[version].byStatus[status]) {
            acc[version].byStatus[status] = { count: 0, issueIds: [] };
          }
          acc[version].byStatus[status].count += 1;
          acc[version].byStatus[status].issueIds.push(issue.id);
        }
      }
      return acc;
    }, {});
    this.logger.debug(
      'Step 2. Calculating issues by versions - done',
      JSON.stringify(issuesByVersionsCount),
    );

    // ШАГ 3. Подсчёт количества задач по работникам
    this.logger.debug('Step 3. Calculating issues by user names');
    const issuesByUsername = {};
    issues.forEach((issue: any) => {
      const currentUser = issue.current_user?.name;
      if (currentUser) {
        if (!issuesByUsername[currentUser]) {
          issuesByUsername[currentUser] = { count: 0, issueIds: [] };
        }
        issuesByUsername[currentUser].count += 1;
        issuesByUsername[currentUser].issueIds.push(issue.id);
      }
    });
    this.logger.debug(
      'Step 3. Calculating issues by user names - done',
      JSON.stringify(issuesByUsername),
    );

    // ШАГ 4. Подсчет количества внутренних изменений
    this.logger.debug('Step 4. Calculating internal changes');
    const changesInterval =
      (widget?.dataLoaderParams['changesInterval'] as number) ??
      24 * 60 * 60 * 1000;
    const changesCount = {
      totalChanges: 0,
      totalComments: 0,
      byIssue: {} as Record<number, { changes: number; comments: number }>,
      byStatus: {} as Record<string, { changes: number; comments: number }>,
      byVersion: {} as Record<string, { changes: number; comments: number }>,
      byUserName: {} as Record<string, { changes: number; comments: number }>,
    };
    const now = DateTime.now().toMillis();
    if (
      typeof changesInterval === 'number' &&
      changesInterval > 0 &&
      issues?.length > 0
    ) {
      issues.forEach((issue) => {
        const status = issue.status?.name ?? 'no_status';
        const version = issue.fixed_version?.name ?? 'no_version';
        const changes = issue.journals?.reduce(
          (acc, journal) => {
            const createdOnTimestamp = DateTime.fromISO(
              journal.created_on,
            ).toMillis();
            const currentUser = journal?.user?.name ?? 'unknown';
            if (
              createdOnTimestamp > now - changesInterval &&
              createdOnTimestamp <= now
            ) {
              if (!changesCount.byStatus[status]) {
                changesCount.byStatus[status] = { changes: 0, comments: 0 };
              }
              if (!changesCount.byVersion[version]) {
                changesCount.byVersion[version] = { changes: 0, comments: 0 };
              }
              if (!changesCount.byUserName[currentUser]) {
                changesCount.byUserName[currentUser] = {
                  changes: 0,
                  comments: 0,
                };
              }
              acc.changes += 1;
              changesCount.totalChanges += 1;
              changesCount.byStatus[status].changes += 1;
              changesCount.byVersion[version].changes += 1;
              changesCount.byUserName[currentUser].changes += 1;
              if (
                typeof journal.notes === 'string' &&
                journal.notes.length > 0
              ) {
                acc.comments += 1;
                changesCount.totalComments += 1;
                changesCount.byStatus[status].comments += 1;
                changesCount.byVersion[version].comments += 1;
                changesCount.byUserName[currentUser].comments += 1;
              }
            }
            return acc;
          },
          {
            changes: 0,
            comments: 0,
          },
        );
        if (changes && (changes.changes > 0 || changes.comments > 0)) {
          changesCount.byIssue[issue.id] = changes;
        }
      });
    }
    this.logger.debug(
      'Step 4. Calculating internal changes - done',
      JSON.stringify(changesCount),
    );

    // ШАГ 5: Количество задач с оценками трудозатрат
    this.logger.debug('Step 5. Counting issues with estimates and spent hours');
    const issuesWithEstimatesAndSpenthoursCount = {
      withEstimates: { count: 0, issueIds: [] },
      withoutEstimates: { count: 0, issueIds: [] },
      withSpentHoursOverEstimates: { count: 0, issueIds: [] },
      byVersion: {} as Record<
        string,
        {
          withEstimates: { count: number; issueIds: number[] };
          withoutEstimates: { count: number; issueIds: number[] };
          withSpentHoursOverEstimates: { count: number; issueIds: number[] };
        }
      >,
      byStatus: {} as Record<
        string,
        {
          withEstimates: { count: number; issueIds: number[] };
          withoutEstimates: { count: number; issueIds: number[] };
          withSpentHoursOverEstimates: { count: number; issueIds: number[] };
        }
      >,
    };
    issues.forEach((issue) => {
      const version = issue.fixed_version?.name;
      if (
        version &&
        !issuesWithEstimatesAndSpenthoursCount.byVersion[version]
      ) {
        issuesWithEstimatesAndSpenthoursCount.byVersion[version] = {
          withEstimates: { count: 0, issueIds: [] },
          withoutEstimates: { count: 0, issueIds: [] },
          withSpentHoursOverEstimates: { count: 0, issueIds: [] },
        };
      }

      const status = issue.status?.name;
      if (status && !issuesWithEstimatesAndSpenthoursCount.byStatus[status]) {
        issuesWithEstimatesAndSpenthoursCount.byStatus[status] = {
          withEstimates: { count: 0, issueIds: [] },
          withoutEstimates: { count: 0, issueIds: [] },
          withSpentHoursOverEstimates: { count: 0, issueIds: [] },
        };
      }

      if (
        typeof issue.estimated_hours === 'number' &&
        issue.estimated_hours > 0
      ) {
        issuesWithEstimatesAndSpenthoursCount.withEstimates.count += 1;
        issuesWithEstimatesAndSpenthoursCount.withEstimates.issueIds.push(
          issue.id,
        );

        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byStatus[status].withEstimates.count += 1;
        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byStatus[status].withEstimates.issueIds.push(issue.id);
        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byVersion[version].withEstimates.count += 1;
        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byVersion[version].withEstimates.issueIds.push(issue.id);
      } else {
        issuesWithEstimatesAndSpenthoursCount.withoutEstimates.count += 1;
        issuesWithEstimatesAndSpenthoursCount.withoutEstimates.issueIds.push(
          issue.id,
        );

        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byStatus[status].withoutEstimates.count += 1;
        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byStatus[status].withoutEstimates.issueIds.push(issue.id);
        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byVersion[version].withoutEstimates.count += 1;
        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byVersion[version].withoutEstimates.issueIds.push(issue.id);
      }
      if (
        typeof issue.spent_hours === 'number' &&
        typeof issue.estimated_hours === 'number' &&
        issue.spent_hours > issue.estimated_hours
      ) {
        issuesWithEstimatesAndSpenthoursCount.withSpentHoursOverEstimates.count += 1;
        issuesWithEstimatesAndSpenthoursCount.withSpentHoursOverEstimates.issueIds.push(
          issue.id,
        );

        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byStatus[status].withSpentHoursOverEstimates.count += 1;
        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byStatus[status].withSpentHoursOverEstimates.issueIds.push(issue.id);
        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byVersion[version].withSpentHoursOverEstimates.count += 1;
        // eslint-disable-next-line prettier/prettier
        issuesWithEstimatesAndSpenthoursCount.byVersion[version].withSpentHoursOverEstimates.issueIds.push(issue.id);
      }
    });
    this.logger.debug(
      'Step 5. Counting issues with estimates and spent hours - done',
      JSON.stringify(issuesWithEstimatesAndSpenthoursCount),
    );

    // ШАГ 6: Счётчики сравнения с предыдущим отчётом
    this.logger.debug('Step 6: Calculating differences with previous report');
    const differencesCount = {
      newIssues: { count: 0, issueIds: [] },
      lostIssues: { count: 0, issueIds: [] },
      reopenedIssues: { count: 0, issueIds: [] },
      closedIssues: { count: 0, issueIds: [] },
    };
    issues.forEach((issue) => {
      const issueIntoPreviousReport = previousIssues.find(
        (prevIssue) => prevIssue.id === issue.id,
      );
      if (!issueIntoPreviousReport) {
        differencesCount.newIssues.count += 1;
        differencesCount.newIssues.issueIds.push(issue.id);
      }
    });
    previousIssues.forEach((prevIssue) => {
      const issueIntoCurrentReport = issues.find(
        (currIssue) => currIssue.id === prevIssue.id,
      );
      if (!issueIntoCurrentReport) {
        differencesCount.lostIssues.count += 1;
        differencesCount.lostIssues.issueIds.push(prevIssue.id);
      }
    });
    issues.forEach((issue) => {
      const issueStatus: any = issue.status;
      if (issueStatus.is_closed) {
        const prevReportClosedIssue = previousIssues.find((prevIssue) => {
          return (
            prevIssue.id === issue.id &&
            prevIssue.status &&
            (prevIssue.status as any).is_closed
          );
        });
        if (!prevReportClosedIssue) {
          differencesCount.closedIssues.count += 1;
          differencesCount.closedIssues.issueIds.push(issue.id);
        }
      } else {
        const prevReportOpenIssue = previousIssues.find((prevIssue) => {
          return (
            prevIssue.id === issue.id &&
            prevIssue.status &&
            !(prevIssue.status as any)?.is_closed
          );
        });
        if (!prevReportOpenIssue) {
          differencesCount.reopenedIssues.count += 1;
          differencesCount.reopenedIssues.issueIds.push(issue.id);
        }
      }
    });
    this.logger.debug(
      'Step 6: Calculating differences with previous report - done',
      JSON.stringify(differencesCount),
    );

    const metrics = {
      issuesByStatusCount: issuesByStatusCount,
      issuesByVersionsCount: issuesByVersionsCount,
      issuesByUsername: issuesByUsername,
      changesCount: changesCount,
      issuesWithEstimatesAndSpenthoursCount:
        issuesWithEstimatesAndSpenthoursCount,
      differencesCount: differencesCount,
    };
    this.logger.log(
      `Metrics generated for dashboard ${dashboard.id}, widget ${widget.id}`,
    );
    return metrics;
  }

  private async saveReport(
    dashboard: Dashboard,
    widget: Widget,
    issues: RedmineTypes.Issue[],
  ): Promise<void> {
    const datasource = await DailyEccmReportsV2Datasource.getDatasource();
    const id = randomUUID();
    const dashboardId = dashboard.id;
    const widgetId = widget.id;
    const now = DateTime.now();
    const datetime = now.toMillis();
    const datetimeFormatted = now.toISO();
    const reportIssues = issues.map((issue: any) => {
      return {
        id: issue.id,
        subject: issue.subject,
        created_on: issue.created_on,
        updated_on: issue.updated_on,
        closed_on: issue.closed_on,
        status: issue.status,
        fixed_version: issue.fixed_version,
        priority: issue.priority,
        author: issue.author,
        assigned_to: issue.assigned_to,
        dev: issue.dev,
        qa: issue.qa,
        cr: issue.cr,
        current_user: issue.current_user,
        tracker: issue.tracker,
        start_date: issue.start_date,
        due_date: issue.due_date,
        done_ratio: issue.done_ratio,
        estimated_hours: issue.estimated_hours,
        total_estimated_hours: issue.total_estimated_hours,
        spent_hours: issue.spent_hours,
        total_spent_hours: issue.total_spent_hours,
      };
    });
    const prevDataResponse = await datasource.find({
      selector: {
        dashboardId: dashboardId,
        widgetId: widget.id,
        latest: true,
      },
      limit: 1,
    });
    const prevData = prevDataResponse.docs[0];
    const prevIssues = prevData ? prevData.reportIssues : [];
    if (prevData) {
      prevData.latest = false;
      await datasource.insert(prevData);
    }
    const issuesMetrics = await this.analyzeIssueMetrics(
      dashboard,
      widget,
      issues,
      prevIssues,
    );
    const item: any = {
      _id: id,
      id: id,
      dashboardId,
      widgetId,
      datetime,
      datetimeFormatted,
      reportIssues,
      issuesMetrics,
      latest: true,
    };
    await datasource.insert(item);
    this.logger.debug(
      `Report saved to db - id ${id}, ` +
        `issues count ${reportIssues.length}, ` +
        `datetime ${datetimeFormatted}`,
    );
  }
}
