import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ChangesService } from 'src/changes/changes.service';
import { EccmConfig } from 'src/models/eccm-config.model';
import { CurrentIssuesEccmReportService } from './current-issues-eccm.report.service';
import nano from 'nano';
import { UNLIMITED } from '@app/event-emitter/consts/consts';
import { TimestampConverter } from '@app/event-emitter/utils/timestamp-converter';
import { IssuesService } from '@app/event-emitter/issues/issues.service';
import { UsersService } from '@app/event-emitter/users/users.service';
import { GetParentsHint } from '@app/event-emitter/utils/get-parents-hint';
import { DailyEccmReportsDatasource } from 'src/couchdb-datasources/daily-eccm-reports.datasource';
import { Timestamped } from '@app/event-emitter/models/timestamped';
import { TimestampNowFill } from '@app/event-emitter/utils/timestamp-now-fill';
import { DateTime } from 'luxon';
import { ISO_DATE_FORMAT } from 'src/consts/date-time.consts';

// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace DailyEccmReport {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  export namespace Models {
    export type User = {
      id: number;
      firstname: string;
      lastname: string;
    };

    export type Status = {
      id: number;
      name: string;
    };

    export type IssuesAndStatus = {
      status: Status;
      issues: IssueWithParents[];
    };

    export type UserReport = {
      user: User;
      dailyMessage?: string;
      teamleadMessage?: string;
      issuesGroupedByStatus: IssuesAndStatus[];
      activities: IssueAndChanges[];
    };

    export type NewIssues = {
      newIssues: IssueWithParents[];
      newIssuesCount: number;
    };

    export type IssuesByStatuses = {
      status: string;
      issues: IssueWithParents[];
      issuesCount: number;
    };

    export type MetricsByVersion = {
      version: string;
      newIssues: NewIssues;
      issuesByStatuses: IssuesByStatuses[];
    };

    export type MetricsByParents = {
      parentIssueId: number;
      parentIssue: RedmineTypes.Issue;
      issuesByStatuses: IssuesByStatuses[];
    };

    /**
     * Метрики сгруппированные по версиям
     */
    export type Metrics = {
      byVersions: MetricsByVersion[];
      byParents: MetricsByParents[];
    };

    /**
     * Все данные для отчёта
     */
    export type Report = {
      byUsers: UserReport[];
      metrics?: Metrics;
      /** Переданные параметры для создания отчёта */
      params: Params;
    };

    /**
     * Данные по задаче + для hint-а подсказка о родительских задачах
     *
     * Полезно будет для восстановления контекста
     */
    export type IssueWithParents = {
      issue: RedmineTypes.Issue;
      parents: string;
    };

    /**
     * Параметры отчёта
     */
    export type Params = {
      from: string;
      to: string;
      name: string;
      project: string;
      versions: string[];
    };

    export type IssueAndChanges = {
      issue: RedmineTypes.Issue;
      parents: string;
      changes: Change[];
    };

    export type Change = {
      initiator?: RedmineTypes.PublicUser;
      dev?: RedmineTypes.PublicUser;
      cr?: RedmineTypes.PublicUser;
      qa?: RedmineTypes.PublicUser;
      current_user?: RedmineTypes.PublicUser;
      author?: RedmineTypes.PublicUser;
      issue_id: number;
      issue_url: string;
      issue_tracker: string;
      issue_subject: string;
      journal_note?: string;
      old_status?: {
        id?: number;
        name?: string;
      } | null;
      new_status?: {
        id?: number;
        name?: string;
      };
      created_on: string;
      created_on_timestamp: number | null;
      change_message: string;
      recipient: RedmineTypes.PublicUser;
    };
  }

  export class Report {
    private report: Models.Report;
    private logger = new Logger(Report.name);

    constructor(
      private eccmUsers: RedmineTypes.User[],
      private issuesService: IssuesService,
    ) {
      const users: Models.UserReport[] = this.eccmUsers
        .map((user) => {
          if (!user) {
            this.logger.error(`Not found user data`);
            return;
          }
          const u: Models.User = {
            id: user.id,
            lastname: user.lastname,
            firstname: user.firstname,
          };
          return {
            user: u,
            activities: [],
            issuesGroupedByStatus: [],
          } as Models.UserReport;
        })
        .filter((user) => Boolean(user));
      this.report = {
        byUsers: users,
        params: {
          from: '',
          to: '',
          name: '',
          project: '',
          versions: [],
        },
      };
    }

    get(): Models.Report {
      return this.report;
    }

    setParams(params: Models.Params): void {
      this.report.params.from = params.from;
      this.report.params.to = params.to;
      this.report.params.name = params.name;
      this.report.params.versions = [...params.versions];
      this.report.params.project = params.project;
    }

    async setCurrentIssues(issues: RedmineTypes.Issue[]): Promise<void> {
      for (let i = 0; i < issues.length; i++) {
        const issue = issues[i];
        await this.appendCurrentIssue(issue);
      }
      this.logger.debug(`Issues for report loaded successed`);
    }

    async setActivities(activities: Models.Change[]): Promise<void> {
      for (let i = 0; i < activities.length; i++) {
        const activity = activities[i];
        await this.appendActivity(activity);
      }
      this.logger.debug(`Activities for report loaded successed`);
    }

    async appendCurrentIssue(
      issue: RedmineTypes.Issue & Record<string, any>,
    ): Promise<boolean> {
      const currentUser: RedmineTypes.PublicUser = issue.current_user;
      if (!currentUser) {
        return false;
      }
      const byUser = this.report.byUsers.find(
        (item) => item.user.id == currentUser.id,
      );
      if (!byUser) {
        return false;
      }

      let byStatus: Models.IssuesAndStatus = byUser.issuesGroupedByStatus.find(
        (item) => item.status.name == issue.status.name,
      );
      if (!byStatus) {
        byStatus = {
          status: { ...issue.status },
          issues: [],
        };
        byUser.issuesGroupedByStatus.push(byStatus);
      }

      const existsIssue = byStatus.issues.find(
        (item) => item.issue.id == issue.id,
      );
      if (existsIssue) {
        return false;
      }

      const parents = await this.issuesService.getParents(issue.id);
      // parents.pop();
      const parentsHint = GetParentsHint(parents);
      byStatus.issues.push({
        issue: issue,
        parents: parentsHint,
      });

      return true;
    }

    async appendActivity(activity: Models.Change): Promise<boolean> {
      const recipient = activity.recipient;
      if (!recipient) {
        this.logger.debug(
          `Recipient is empty for activity ` +
            `created_on ${activity.created_on} ` +
            `for issue #${activity.issue_id} (${activity.issue_subject})`,
        );
        return false;
      }
      const recipientId = recipient.id;

      const byUser = this.report.byUsers.find(
        (byUser) => byUser.user.id == recipientId,
      );
      if (!byUser) {
        this.logger.log(`Not interested user ${recipient.name}`);
        return false;
      } else {
        this.logger.debug(
          `Found user section in report data ` +
            `user=${JSON.stringify(byUser.user)}`,
        );
      }

      let issueAndChanges: Models.IssueAndChanges = byUser.activities.find(
        (issueAndChanges) => {
          return issueAndChanges.issue.id == activity.issue_id;
        },
      );
      if (!issueAndChanges) {
        const issue = await this.issuesService.getIssue(activity.issue_id);
        if (!issue) {
          this.logger.error(`Not found issue data for #${activity.issue_id}`);
          return false;
        }
        const parents = await this.issuesService.getParents(activity.issue_id);
        const parentsHint = GetParentsHint(parents);
        issueAndChanges = {
          issue: issue,
          parents: parentsHint,
          changes: [],
        };
        this.logger.debug(
          `Created issue section in report data ` +
            `user=${JSON.stringify(byUser.user)} ` +
            `issue=${JSON.stringify({
              id: issueAndChanges.issue.id,
              subject: issueAndChanges.issue.subject,
            })}`,
        );
        byUser.activities.push(issueAndChanges);
      } else {
        this.logger.debug(
          `Found issue section in report data ` +
            `user=${JSON.stringify(byUser.user)} ` +
            `issue=${JSON.stringify({
              id: issueAndChanges.issue.id,
              subject: issueAndChanges.issue.subject,
            })}`,
        );
      }

      issueAndChanges.changes.push(activity);
      this.logger.debug(
        `Activity pushed into report ` +
          `user=${JSON.stringify(byUser.user)} ` +
          `issue=${JSON.stringify({
            id: issueAndChanges.issue.id,
            subject: issueAndChanges.issue.subject,
          })} ` +
          `activity=${JSON.stringify({
            created_on: activity.created_on,
            change_message: activity.change_message,
          })}`,
      );

      return true;
    }

    sortUsers(): void {
      this.report.byUsers.sort((a, b) => {
        const nameA = `${a.user.firstname} ${a.user.lastname}`;
        const nameB = `${b.user.firstname} ${b.user.lastname}`;
        if (nameA > nameB) {
          return 1;
        }
        if (nameA < nameB) {
          return -1;
        }
        return 0;
      });
    }

    sortActivities(): void {
      for (let i = 0; i < this.report.byUsers.length; i++) {
        const byUser = this.report.byUsers[i];
        for (let j = 0; j < byUser.activities.length; j++) {
          const activity = byUser.activities[j];
          activity.changes.sort((a, b) => {
            return a.created_on_timestamp - b.created_on_timestamp;
          });
        }
      }
    }
  }
}

@Injectable()
export class DailyEccmReportService {
  private logger = new Logger(DailyEccmReportService.name);
  private eccmConfig: EccmConfig.Config;
  private report: DailyEccmReport.Report;

  constructor(
    private configService: ConfigService,
    private currentIssuesEccmReportService: CurrentIssuesEccmReportService,
    private changesService: ChangesService,
    private issuesService: IssuesService,
    private usersService: UsersService,
    private dailyEccmReportsDatasource: DailyEccmReportsDatasource,
  ) {
    this.eccmConfig = this.configService.get<EccmConfig.Config>('redmineEccm');
  }

  async getReport(): Promise<DailyEccmReport.Report> {
    if (!this.report) {
      await this.createEmptyReport();
    }
    return this.report;
  }

  async generateReport(
    params: DailyEccmReport.Models.Params,
  ): Promise<DailyEccmReport.Models.Report> {
    await this.createEmptyReport();
    const report = this.report;
    report.setParams(params);

    const currentIssues = await this.getCurrentIssues();
    this.logger.log(`Current issues count - ${currentIssues.length}`);
    await report.setCurrentIssues(currentIssues);

    const activities = await this.getActivities(params);
    this.logger.log(`Activities count - ${activities.length}`);
    await report.setActivities(activities);

    report.sortUsers();
    report.sortActivities();

    this.logger.log(`Daily eccm report created successed`);

    return report.get();
  }

  generateParams(
    from: string,
    to: string,
    name?: string,
  ): DailyEccmReport.Models.Params {
    const fromDate = DateTime.fromISO(from);
    if (!fromDate.isValid) throw new Error('from is invalid date');
    const toDate = DateTime.fromISO(to);
    if (!toDate.isValid) throw new Error('to is invalid date');
    let nameValue: string | null = name || null;
    if (!nameValue) {
      nameValue = DateTime.now().toFormat(ISO_DATE_FORMAT);
    }
    return {
      from: fromDate.toISO(),
      to: toDate.toISO(),
      name: nameValue,
      project: this.eccmConfig.projectName,
      versions: [...this.eccmConfig.currentVersions],
    };
  }

  async saveReport(
    report: DailyEccmReport.Models.Report,
    overwrite?: boolean,
  ): Promise<boolean> {
    const name = report.params.name;
    const existsReport = await this.loadReport(name);
    if (existsReport && !overwrite) {
      return false;
    }

    type ReportType = nano.DocumentGetResponse &
      DailyEccmReport.Models.Report &
      Timestamped;

    const newReport: ReportType = TimestampNowFill({
      ...report,
      _id: name,
      _rev: existsReport?._rev,
    });

    const datasource = await this.dailyEccmReportsDatasource.getDatasource();
    await datasource.insert(newReport);

    return true;
  }

  async loadReport(
    name: string,
  ): Promise<
    | (DailyEccmReport.Models.Report & nano.DocumentGetResponse & Timestamped)
    | null
  > {
    const datasource = await this.dailyEccmReportsDatasource.getDatasource();
    let resp: any = null;
    try {
      resp = await datasource.get(name);
    } catch (ex) {
      this.logger.warn(
        `Cannot load report ${name} with error message ${ex.message}`,
      );
    }
    return resp;
  }

  async deleteReport(name: string): Promise<boolean> {
    const datasource = await this.dailyEccmReportsDatasource.getDatasource();
    const report = await this.loadReport(name);
    let res = false;
    try {
      await datasource.destroy(report._id, report._rev);
      res = true;
    } catch (ex) {}
    return res;
  }

  private async createEmptyReport(): Promise<DailyEccmReport.Report> {
    const usersList: string[] = this.eccmConfig.groups.reduce((acc, group) => {
      group.people.forEach((fullname) => {
        acc.push(fullname);
      });
      return acc;
    }, []);
    this.logger.debug(
      `Create new daily report for users - ${JSON.stringify(usersList)}`,
    );
    const usersPromises = usersList.map((fullname) => {
      return this.usersService.findUserByFullname(fullname);
    });
    const users = await Promise.all(usersPromises);
    this.report = new DailyEccmReport.Report(users, this.issuesService);
    return this.report;
  }

  private async getCurrentIssues(): Promise<RedmineTypes.Issue[]> {
    return await this.currentIssuesEccmReportService.getData({
      project: this.eccmConfig.projectName,
      statuses: this.eccmConfig.currentIssuesStatuses,
      versions: this.eccmConfig.currentVersions,
    });
  }

  private async getActivities(
    params: DailyEccmReport.Models.Params,
  ): Promise<DailyEccmReport.Models.Change[]> {
    const fromTimestamp = TimestampConverter.toTimestamp(params.from);
    const toTimestamp = TimestampConverter.toTimestamp(params.to);
    const query: nano.MangoQuery = {
      selector: {
        $and: [
          {
            created_on_timestamp: {
              $gte: fromTimestamp,
            },
          },
          {
            created_on_timestamp: {
              $lt: toTimestamp,
            },
          },
        ],
      },
      limit: UNLIMITED,
    };
    this.logger.debug(
      `Query for get activities data - ${JSON.stringify(query)}`,
    );
    const changes = await this.changesService.find(query);

    this.logger.log(
      `Loaded changes for daily report - count = ${changes.length}`,
    );

    const uniqIssueIds: number[] = [];
    changes.forEach((change) => {
      const issueId = change.issue_id;
      if (!uniqIssueIds.includes(issueId)) {
        uniqIssueIds.push(issueId);
      }
    });

    this.logger.debug(`Uniq issues - ${JSON.stringify(uniqIssueIds)}`);

    const issues = await this.issuesService.getIssues(uniqIssueIds);
    this.logger.debug(`Loaded data for uniq issues - count = ${issues.length}`);
    const issuesMap: Record<number, RedmineTypes.Issue> = {};
    issues.forEach((issue) => {
      if (!issue || !issue.fixed_version) {
        return;
      }
      const version = issue.fixed_version.name;
      if (this.eccmConfig.currentVersions.includes(version)) {
        issuesMap[issue.id] = issue;
      }
    });

    const reportChanges: DailyEccmReport.Models.Change[] = changes.reduce(
      (acc: DailyEccmReport.Models.Change[], change) => {
        change.messages.forEach((message) => {
          if (!message || !message.recipient || !message.change_message) {
            return;
          }
          const c: DailyEccmReport.Models.Change = {
            initiator: change.initiator,
            dev: change.dev,
            cr: change.cr,
            qa: change.qa,
            current_user: change.current_user,
            author: change.author,
            issue_id: change.issue_id,
            issue_url: change.issue_url,
            issue_tracker: change.issue_tracker,
            issue_subject: change.issue_subject,
            journal_note: change.journal_note,
            old_status: change.old_status && {
              id: change.old_status.id,
              name: change.old_status.name,
            },
            new_status: change.new_status && {
              id: change.new_status.id,
              name: change.new_status.name,
            },
            created_on: change.created_on,
            created_on_timestamp: change.created_on_timestamp,
            recipient: {
              firstname: message.recipient.firstname,
              lastname: message.recipient.lastname,
              id: message.recipient.id,
              login: message.recipient.login,
              name: message.recipient.name,
            },
            change_message: message.change_message,
          };
          acc.push(c);
        });
        return acc;
      },
      [] as DailyEccmReport.Models.Change[],
    );

    this.logger.log(
      `Prepared activities for report - count = ${reportChanges.length}`,
    );

    return reportChanges;
  }
}
