import { IssueEnhancerInterface } from '@app/event-emitter/issue-enhancers/issue-enhancer-interface';
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
import { Injectable, Logger } from '@nestjs/common';
import * as Luxon from 'luxon';
import { CalendarEvent } from '../models/calendar-event';
import * as DatesParser from '../utils/string-with-dates-parser';

export type DescriptionParserParams = {
  title: string;
  lineRegexp: string;
};

export type CustomFieldParserParams = {
  dateFormat: string;
  customFieldName: string;
  /** Время в минутах */
  interval?: number;
  fullDay?: boolean;
  alias?: string;
};

export const UNKNOWN_CALENDAR_EVENT = 'Unknown calendar event';

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

  name = 'calendar';

  constructor(
    public useForProjects: string[],
    public customFields: CustomFieldParserParams[],
    public descriptionCalendarParams: DescriptionParserParams,
    public calendarEventsKey: string,
  ) {
    const initParams = {
      useForProjects,
      customFields,
      descriptionCalendarParams,
      calendarEventsKey,
    };
    this.logger.debug(
      `Calendar enhancer init with ${JSON.stringify(initParams)}`,
    );
  }

  async enhance(
    issue: RedmineTypes.ExtendedIssue,
  ): Promise<RedmineTypes.ExtendedIssue> {
    const res: RedmineTypes.ExtendedIssue = { ...issue };

    if (!this.checkProject(res)) {
      return res;
    }

    try {
      res[this.calendarEventsKey] = this.getCalendarEvents(res);
    } catch (ex) {
      this.logger.error(
        `Error at parsing calendar events, message - ${ex}: ${
          (ex as Error)?.stack
        }`,
      );
      return res;
    }
    this.logger.debug(
      `Calendar events for #${issue.id}: issue.${
        this.calendarEventsKey
      } = ${JSON.stringify(res[this.calendarEventsKey])}`,
    );

    return res;
  }

  private checkProject(issue: RedmineTypes.ExtendedIssue): boolean {
    if (this.useForProjects.indexOf('*') >= 0) {
      return true;
    }
    if (
      this.useForProjects.length > 0 &&
      this.useForProjects.indexOf(issue.project.name) >= 0
    ) {
      return true;
    }
    return false;
  }

  private getCalendarEvents(
    issue: RedmineTypes.ExtendedIssue,
  ): CalendarEvent[] {
    return [
      ...this.getCalendarEventsFromCustomFields(issue),
      ...this.getCalendarEventsFromSubject(issue),
      ...this.getCalendarEventsFromDescription(issue),
    ];
  }

  private getCalendarEventsFromCustomFields(
    issue: RedmineTypes.ExtendedIssue,
  ): CalendarEvent[] {
    const res: CalendarEvent[] = [];
    for (let i = 0; i < this.customFields.length; i++) {
      const cfParam = this.customFields[i];
      const event = this.getCalendarEventFromCustomField(issue, cfParam);
      if (event) res.push(event);
    }
    return res;
  }

  private getCalendarEventFromCustomField(
    issue: RedmineTypes.ExtendedIssue,
    params: CustomFieldParserParams,
  ): CalendarEvent | null {
    if (typeof params.interval !== 'number' && params.fullDay !== true)
      return null;

    const cf = issue.custom_fields.find((issueCf) => {
      return issueCf.name === params.customFieldName;
    });
    if (!cf) return null;

    const from = Luxon.DateTime.fromFormat(cf.value, params.dateFormat);
    if (!from.isValid) return null;

    let to: Luxon.DateTime;
    if (typeof params.interval === 'number') {
      const interval = Luxon.Duration.fromObject({ minutes: params.interval });
      to = from.plus(interval);
    } else if (params.fullDay) {
      from.set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
      to = from.plus(Luxon.Duration.fromObject({ day: 1 }));
    } else {
      return null;
    }
    if (!to.isValid) return null;

    return {
      from: from.toISO(),
      fromTimestamp: from.toMillis(),
      to: to.toISO(),
      toTimestamp: to.toMillis(),
      fullDay: Boolean(params.fullDay),
      description: params.alias || cf.name || UNKNOWN_CALENDAR_EVENT,
    };
  }

  private getCalendarEventsFromSubject(
    issue: RedmineTypes.ExtendedIssue,
  ): CalendarEvent[] {
    const matches = issue.subject.matchAll(/(?<=\()[^()]*(?=\))/g);
    const items = [...matches].map((i) => i[0]).filter((i) => !!i);

    return items
      .map((item) => DatesParser.parseToCalendarEvent(item))
      .filter((i) => !!i);
  }

  private getCalendarEventsFromDescription(
    issue: RedmineTypes.ExtendedIssue,
  ): CalendarEvent[] {
    const text = issue.description;

    const lines = text.split('\n').map((line) => line.trim());

    const calendarStartIndex = lines.indexOf(
      this.descriptionCalendarParams.title,
    );
    if (calendarStartIndex < 0) return [];

    let index = calendarStartIndex + 1;
    let line = this.extractFromList(lines[index]);
    if (!line) {
      index++;
      line = this.extractFromList(lines[index]);
    }
    if (!line) return [];

    const res: string[] = [];

    do {
      res.push(line);
      index++;
      line = this.extractFromList(lines[index]);
    } while (line);

    return res.map((line) => DatesParser.parseToCalendarEvent(line));
  }

  private extractFromList(line: string): string | null {
    if (!line || line.length <= 0) return null;
    const regexp = new RegExp(this.descriptionCalendarParams.lineRegexp);
    const match = line.match(regexp);
    return match && match[0] ? match[0] : null;
  }
}
