import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
import { SaveResponse } from '@app/event-emitter/models/save-response';
import { UsersService } from '@app/event-emitter/users/users.service';
import { TimestampConverter } from '@app/event-emitter/utils/timestamp-converter';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Change } from 'src/models/change.model';
import { StatusChangesConfig } from 'src/models/status-changes-config.model';
import { StatusesConfig } from 'src/models/statuses-config.model';
import Handlebars from 'handlebars';
import { ChangeMessage } from 'src/models/change-message.model';
import { Subject } from 'rxjs';
import { RedminePublicUrlConverter } from '@app/event-emitter/converters/redmine-public-url.converter';

@Injectable()
export class StatusChangeNotificationsService {
  private logger = new Logger(StatusChangeNotificationsService.name);
  private statuses: StatusesConfig.Config;
  private statusChanges: StatusChangesConfig.Config;

  $changes = new Subject<Change>();
  $batchChanges = new Subject<Change[]>();

  constructor(
    private usersService: UsersService,
    private config: ConfigService,
    private redminePublicUrlConverter: RedminePublicUrlConverter,
  ) {
    this.statuses = this.config.get<StatusesConfig.Config>('redmineStatuses');
    this.statusChanges = this.config.get<StatusChangesConfig.Config>(
      'redmineStatusChanges',
    );
    this.logger.debug(
      `StatusChangeNotificationsService created, ` +
        `statuses = ${JSON.stringify(this.statuses.map((s) => s.name))}, ` +
        `statusChanges.length = ${this.statusChanges.length}, ` +
        `statusChanges = ` +
        // eslint-disable-next-line prettier/prettier
        `${JSON.stringify(this.statusChanges.map((c) => `${c.from} -> ${c.to}`))}`,
    );
  }

  async getChanges(saveResponse: SaveResponse): Promise<Change[]> {
    this.logger.debug(
      `Analize change statuses for issue #${saveResponse.current.id} ` +
        `(${saveResponse.current.subject}) for journalsDiff.length = ${saveResponse.journalsDiff.length} start`,
    );

    const changes: Change[] = [];

    const issue = saveResponse.current;

    if (!saveResponse.prev) {
      const newChange = await this.getMessagesForNewIssue(issue);
      changes.push(newChange);
    }

    for (let i = 0; i < saveResponse.journalsDiff.length; i++) {
      const journal = saveResponse.journalsDiff[i];
      const change = await this.getMessagesForChangeStatus(issue, journal);
      if (change) {
        changes.push(change);
      }
    }

    this.logger.debug(
      `Analize change statuses for issue #${saveResponse.current.id} ` +
        `(${saveResponse.current.subject}) for journalsDiff.length = ${saveResponse.journalsDiff.length} finished`,
    );

    changes.forEach((c) => this.$changes.next(c));
    this.$batchChanges.next(changes);

    return changes;
  }

  private getStatusChangeDetails(
    journal: RedmineTypes.Journal,
  ): RedmineTypes.JournalDetail | null {
    if (!journal?.details || journal?.details.length <= 0) return null;
    const details: RedmineTypes.JournalDetail[] = journal?.details;
    return (
      details.find((d) => {
        return d.name === 'status_id';
      }) || null
    );
  }

  private async getMessagesForChangeStatus(
    issue: RedmineTypes.Issue & Record<string, any>,
    journal: RedmineTypes.Journal,
  ): Promise<Change | null> {
    const statusChangeDetails = this.getStatusChangeDetails(journal);
    if (!statusChangeDetails) return null;
    const change: Change = {
      initiator: await this.usersService.getUser(journal?.user?.id),
      dev: await this.usersService.getUser(issue?.dev?.id),
      qa: await this.usersService.getUser(issue?.qa?.id),
      cr: await this.usersService.getUser(issue?.cr?.id),
      current_user: await this.usersService.getUser(issue?.current_user?.id),
      author: await this.usersService.getUser(issue?.author?.id),
      old_status: this.findStatusById(statusChangeDetails.old_value),
      new_status: this.findStatusById(statusChangeDetails.new_value),
      issue_id: issue.id,
      issue_url: this.redminePublicUrlConverter.convert(issue.id),
      issue_tracker: issue.tracker?.name || '',
      issue_subject: issue.subject || '',
      created_on: journal.created_on,
      created_on_timestamp: TimestampConverter.toTimestamp(journal.created_on),
      journal_note: journal.notes || '',
      messages: [],
    };
    const messages = await this.generateMessages(statusChangeDetails, change);
    if (messages) {
      change.messages = messages;
    }
    return change;
  }

  private async getMessagesForNewIssue(
    issue: RedmineTypes.Issue & Record<string, any>,
  ): Promise<Change> {
    const changeParams = this.statusChanges.find((p) => p.new_issue);
    const change: Change = {
      initiator: await this.usersService.getUser(issue.author.id),
      dev: await this.usersService.getUser(issue?.dev?.id),
      qa: await this.usersService.getUser(issue?.qa?.id),
      cr: await this.usersService.getUser(issue?.cr?.id),
      current_user: await this.usersService.getUser(issue?.current_user?.id),
      author: await this.usersService.getUser(issue?.author?.id),
      old_status: this.findStatusById(issue.status.id),
      new_status: this.findStatusById(issue.status.id),
      issue_id: issue.id,
      issue_url: this.redminePublicUrlConverter.convert(issue.id),
      issue_tracker: issue.tracker?.name || '',
      issue_subject: issue.subject || '',
      created_on: issue.created_on,
      created_on_timestamp: TimestampConverter.toTimestamp(issue.created_on),
      journal_note: '',
      messages: [],
    };
    const filledMessages = await Promise.all(
      changeParams.messages.map(async (messageParams: any) => {
        return await this.generateMessage(messageParams, change);
      }),
    );
    change.messages = filledMessages.filter((m) => Boolean(m));
    return change;
  }

  private findStatusById(id: number | string): StatusesConfig.Item | null {
    if (
      typeof id === 'string' &&
      !Number.isNaN(id) &&
      Number.isFinite(Number(id))
    ) {
      id = Number(id);
    }
    return this.statuses.find((s) => s.id === id) || null;
  }

  private async generateMessages(
    detail: RedmineTypes.JournalDetail,
    change: Change,
  ): Promise<ChangeMessage[]> {
    const oldStatus = this.findStatusById(detail.old_value);
    const newStatus = this.findStatusById(detail.new_value);
    if (!oldStatus || !newStatus) return null;
    const changeParams = this.findChangeParams(oldStatus.name, newStatus.name);
    if (!changeParams || !changeParams.messages) return null;
    const filledMessages = await Promise.all(
      changeParams.messages.map(async (messageParams: any) => {
        return await this.generateMessage(messageParams, change);
      }),
    );
    return filledMessages.filter((m) => Boolean(m));
  }

  private findChangeParams(
    oldStatus: string,
    newStatus: string,
  ): StatusChangesConfig.Item | null {
    let foundParam: StatusChangesConfig.Item | null = null;
    foundParam = this.statusChanges.find(
      (p) => p.from == oldStatus && p.to == newStatus,
    );
    if (!foundParam) {
      foundParam = this.statusChanges.find((p) => !p.from && p.to == newStatus);
    }
    if (!foundParam) {
      foundParam = this.statusChanges.find((p) => p.from == newStatus && !p.to);
    }
    if (!foundParam) {
      foundParam = this.statusChanges.find((p) => p.default);
    }
    return foundParam;
  }

  private async generateMessage(
    messageParams: StatusChangesConfig.Message,
    change: Change,
  ): Promise<ChangeMessage | null> {
    if (!messageParams) return null;
    const recipientUser = await this.usersService.getUser(
      change[messageParams.recipient]?.id,
    );
    if (!recipientUser) return null;

    let changeMessage = null;
    if (messageParams.changes_message) {
      const changeMessageTemplate = Handlebars.compile(
        messageParams.changes_message,
      );
      changeMessage = changeMessageTemplate(change);
    }

    let notificationMessage = null;
    if (messageParams.notification_message) {
      const notificationMessageTemplate = Handlebars.compile(
        messageParams.notification_message,
      );
      notificationMessage = notificationMessageTemplate(change);
    }

    return {
      recipient: recipientUser,
      change_message: changeMessage,
      notification_message: notificationMessage,
    };
  }
}
