import { BehaviorSubject } from 'rxjs';
import { EventsListener } from '../events/events-listener';
import { Injectable, Logger } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';
import { randomUUID } from 'crypto';
import { ConfigService } from '@nestjs/config';
import { IssuesService } from '../issues/issues.service';
import { RedmineDataLoader } from '../redmine-data-loader/redmine-data-loader';
import { DateTime } from 'luxon';
import { RedmineTypes } from '../models/redmine-types';

export type CsvIssue = Record<string, any>;

export type Task = {
  schedule: string;
  updatedAtFieldName: string;
  dateTimeFormat: string;
  csvLinks: string[];
};

export type CsvListenerParams = {
  tasks: Task[];
};

export type CsvIssuesStore = Record<number, CsvIssue>;

@Injectable()
export class CsvListenerFactory {
  private csvListener: CsvListener;

  constructor(
    private configService: ConfigService,
    private schedulerRegistry: SchedulerRegistry,
    private issuesService: IssuesService,
    private redmineDataLoader: RedmineDataLoader,
  ) {}

  getCsvListener(): CsvListener {
    if (!this.csvListener) {
      const params = this.configService.get('csvListener');
      this.csvListener = new CsvListener(
        params,
        this.schedulerRegistry,
        'default_csv_listener',
        this.issuesService,
        this.redmineDataLoader,
      );
    }
    return this.csvListener;
  }
}

@Injectable()
export class CsvListener implements EventsListener {
  private logger = new Logger(CsvListener.name);

  issues: BehaviorSubject<number[]> = new BehaviorSubject([]);

  constructor(
    private params: CsvListenerParams,
    private schedulerRegistry: SchedulerRegistry,
    private jobPrefix: string,
    private issuesService: IssuesService,
    private redmineDataLoader: RedmineDataLoader,
  ) {
    this.logger.log(
      `Csv listener created with params - ${JSON.stringify(params)}`,
    );
  }

  start(): void {
    const tasks = this?.params?.tasks || [];
    this.logger.debug(`Scheduling ${tasks.length} tasks`);
    for (let i = 0; i < tasks.length; i++) {
      const task = this.params.tasks[i];
      const cronJobName = this.createCronJobName();
      const cronJob = new CronJob(
        task.schedule,
        this.createLoader(task, cronJobName),
      );
      this.schedulerRegistry.addCronJob(cronJobName, cronJob);
      cronJob.start();
    }
  }

  stop(): void {
    const jobs = this.schedulerRegistry.getCronJobs();
    jobs.forEach((job, jobName) => {
      if (this.isListenerCronJon(jobName)) {
        job.stop();
        this.schedulerRegistry.deleteCronJob(jobName);
      }
    });
  }

  private createCronJobName(): string {
    return `${this.jobPrefix}_${randomUUID()}`;
  }

  private isListenerCronJon(name: string): boolean {
    return name.startsWith(`${this.jobPrefix}_`);
  }

  private createLoader(task: Task, cronJobName: string): () => Promise<void> {
    return async () => {
      this.logger.log(
        `Execute task ${cronJobName} ` +
          `by schedule ${task.schedule} ` +
          `with ${task.csvLinks.length} queries`,
      );
      this.logger.debug(`Queries - ${JSON.stringify(task.csvLinks)}`);
      const csvIssuesStore = await this.loadCsv(task);
      this.logger.debug(
        `Loaded from csv issues count - ${Object.keys(csvIssuesStore).length}`,
      );
      const existsIssuesStore = await this.getCachedIssues(csvIssuesStore);
      const ids = this.filterIssueIdsForUpdate(
        csvIssuesStore,
        existsIssuesStore,
      );
      this.logger.debug(
        `Issues for update ${ids.length} - ${JSON.stringify(ids)}`,
      );
      if (ids && ids.length > 0) {
        this.issues.next(ids);
      }
    };
  }

  private async loadCsv(task: Task): Promise<CsvIssuesStore> {
    const res: CsvIssuesStore = {};
    for (let i = 0; i < task.csvLinks.length; i++) {
      const csvLink = task.csvLinks[i];
      const csvData = await this.redmineDataLoader.loadCsv(csvLink);
      if (!csvData || csvData.length <= 0) continue;
      for (let j = 0; j < csvData.length; j++) {
        const issue = csvData[j];
        const issueId = Object.values(issue)[0];
        issue.id = Number(issueId);
        issue.updatedAt = DateTime.fromFormat(
          issue[task.updatedAtFieldName],
          task.dateTimeFormat,
        );
        res[issueId] = issue;
      }
    }
    return res;
  }

  private async getCachedIssues(
    store: CsvIssuesStore,
  ): Promise<Record<number, RedmineTypes.ExtendedIssue>> {
    const ids = Object.keys(store).map((k) => Number(k));
    const issues = await this.issuesService.getIssues(ids);
    return issues.reduce((acc, issue) => {
      acc[issue.id] = issue;
      return acc;
    }, {});
  }

  private filterIssueIdsForUpdate(
    csvIssues: CsvIssuesStore,
    existsIssues: Record<number, RedmineTypes.ExtendedIssue>,
  ): number[] {
    const res: number[] = [];
    for (const [issueId, issue] of Object.entries(csvIssues)) {
      const id = Number(issueId);
      if (!existsIssues[id]) {
        res.push(id);
        continue;
      }
      const existingIssue = existsIssues[id];
      const csvIssueTimestamp = issue.updatedAt?.isValid
        ? issue.updatedAt.toMillis()
        : null;
      const existingIssueTimestamp =
        this.getTimestampFromCachedIssue(existingIssue);
      if (!existingIssueTimestamp) {
        res.push(id);
        continue;
      }
      if (existingIssueTimestamp < csvIssueTimestamp) {
        res.push(id);
        continue;
      }
    }
    return res;
  }

  private getTimestampFromCachedIssue(
    issue: RedmineTypes.ExtendedIssue,
  ): number | null {
    if (
      typeof issue.updated_on_timestamp === 'number' &&
      issue.updated_on_timestamp > 0
    ) {
      return issue.updated_on_timestamp;
    }

    let dt = DateTime.fromISO(issue.updated_on);
    if (dt.isValid) {
      return dt.toMillis();
    }

    if (
      typeof issue.created_on_timestamp === 'number' &&
      issue.created_on_timestamp > 0
    ) {
      return issue.created_on_timestamp;
    }

    dt = DateTime.fromISO(issue.created_on);
    if (dt.isValid) {
      return dt.toMillis();
    }

    return null;
  }
}
