import { IssuesUpdaterService } from '@app/event-emitter/issues-updater/issues-updater.service';
import { IssuesService } from '@app/event-emitter/issues/issues.service';
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
import { Queue } from '@app/event-emitter/queue/queue';
import { FlatIssuesStore } from '@app/event-emitter/utils/flat-issues-store';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

export type UpdateRule = {
  issueIds: number[];
  add: string[];
  remove: string[];
};

type UpdateRuleSingleIssue = {
  issueId: number;
  issueData: RedmineTypes.Issue | null;
  userApiKey: string;
  add: string[];
  remove: string[];
};

type ModifyResult = {
  value: string;
  modified: boolean;
};

type ModifyCustomField = {
  field?: RedmineTypes.CustomField;
  modified?: boolean;
  success: boolean;
};

type UpdateResult = {
  issue?: RedmineTypes.Issue | null;
  delta?: Record<string, any>;
  modified?: boolean;
  success: boolean;
};

export function CreateTagManagerServiceProvider(providerName: string): any {
  return {
    provide: providerName,
    useFactory: (
      configService: ConfigService,
      issuesUpdaterService: IssuesUpdaterService,
      issuesService: IssuesService,
    ) => {
      const updateInverval = configService.get<number>(
        'tagManager.updateInverval',
      );
      const updateItemsLimit = configService.get<number>(
        'tagManager.updateItemsLimit',
      );
      const tagsCustomFieldName = configService.get<string>(
        'tagManager.tagsCustomFieldName',
      );
      return new TagsManagerService(
        issuesUpdaterService,
        updateInverval,
        updateItemsLimit,
        tagsCustomFieldName,
        issuesService,
      );
    },
    inject: [ConfigService, 'ISSUES_UPDATER_SERVICE', IssuesService],
  };
}

@Injectable()
export class TagsManagerService {
  private logger = new Logger(TagsManagerService.name);
  private queue: Queue<UpdateRuleSingleIssue, UpdateResult>;

  constructor(
    private issuesUpdaterService: IssuesUpdaterService,
    private updateInterval: number,
    private updateItemsLimit: number,
    private tagsCustomFieldName: string,
    private issuesService: IssuesService,
  ) {}

  async updateTags(
    userApiKey: string,
    updateRules: UpdateRule[],
  ): Promise<void> {
    this.logger.debug(`Params for tags updates - ${updateRules}`);
    const rules = await this.createUpdateRulesSingleIssue(
      userApiKey,
      updateRules,
    );
    const queue = this.getQueue();
    queue.add(rules);
  }

  private getQueue(): Queue<UpdateRuleSingleIssue, UpdateResult> {
    if (!this.queue) {
      this.queue = new Queue<UpdateRuleSingleIssue, UpdateResult>(
        this.updateInterval,
        this.updateItemsLimit,
        async (rules: UpdateRuleSingleIssue[]) => {
          const results = [] as UpdateResult[];
          for (let i = 0; i < rules.length; i++) {
            const rule = rules[i];
            let result: UpdateResult;
            try {
              result = await this.updateIssue(rule);
            } catch (ex) {
              this.logger.error(
                `Error at execution update task for issueId = ${rule.issueId}, ` +
                  `add = ${JSON.stringify(rule.add)}, ` +
                  `remove = ${JSON.stringify(rule.remove)}, ` +
                  `error = ${ex.message}`,
              );
              result = { success: false };
            }
            results.push(result);
          }
          return results;
        },
      );
      this.queue.start();
    }
    return this.queue;
  }

  private async updateIssue(
    rule: UpdateRuleSingleIssue,
  ): Promise<UpdateResult> {
    if (!rule.issueData) {
      return {
        success: false,
      };
    }
    if (rule.add.length == 0 && rule.remove.length == 0) {
      return {
        success: true,
        delta: {},
        issue: rule.issueData,
        modified: false,
      };
    }
    const result = this.modifyTagsForIssue(rule);
    if (result.modified) {
      const delta = { custom_fields: [result.field] };
      const updateResult = await this.issuesUpdaterService.updateIssue(
        rule.issueId,
        delta,
        rule.userApiKey,
      );
      return {
        success: updateResult,
        delta: delta,
        issue: rule.issueData,
        modified: result.modified,
      };
    } else {
      return {
        success: true,
        delta: {},
        issue: rule.issueData,
        modified: result.modified,
      };
    }
  }

  private async createIssuesStore(
    updateRules: UpdateRule[],
  ): Promise<FlatIssuesStore> {
    const issuesStore = new FlatIssuesStore();
    for (let i = 0; i < updateRules.length; i++) {
      const updateRule = updateRules[i];
      for (let j = 0; j < updateRule.issueIds.length; j++) {
        const issueId = updateRule.issueIds[j];
        issuesStore.push(issueId);
      }
    }

    const loader = this.issuesService.createDynamicIssuesLoader();
    await issuesStore.fillData(loader);

    return issuesStore;
  }

  private getTagsCustomField(
    issue: RedmineTypes.Issue,
  ): RedmineTypes.CustomField | null {
    if (!issue.custom_fields) return null;
    const customFields = issue.custom_fields;
    return (
      customFields.find((cf) => cf.name == this.tagsCustomFieldName) || null
    );
  }

  private getTags(tags: string): string[] {
    return tags
      .split(/[ ,;]/)
      .map((s) => s.trim())
      .filter((s) => !!s);
  }

  private modifyTags(
    tags: string,
    updateRule: UpdateRuleSingleIssue,
  ): ModifyResult {
    const t = this.getTags(tags);
    let modified = false;
    for (let i = 0; i < updateRule.remove.length; i++) {
      const tagForRemoving = updateRule.remove[i];
      const tagIndex = t.indexOf(tagForRemoving);
      if (tagIndex >= 0) {
        t.splice(tagIndex, 1);
        modified = true;
      }
    }
    for (let i = 0; i < updateRule.add.length; i++) {
      const tagForAdding = updateRule.add[i];
      const tagIndex = t.indexOf(tagForAdding);
      if (tagIndex < 0) {
        t.push(tagForAdding);
        modified = true;
      }
    }
    const result = modified ? t.join(' ') : tags;
    return { value: result, modified: modified };
  }

  private modifyTagsForIssue(
    updateRule: UpdateRuleSingleIssue,
  ): ModifyCustomField {
    const cf = this.getTagsCustomField(updateRule.issueData);
    if (!cf) return { success: false };
    const modifyResult = this.modifyTags(cf.value, updateRule);
    if (modifyResult.modified) cf.value = modifyResult.value;
    return { success: true, modified: modifyResult.modified, field: cf };
  }

  private async createUpdateRulesSingleIssue(
    userApiKey: string,
    items: UpdateRule[],
  ): Promise<UpdateRuleSingleIssue[]> {
    const res = [] as UpdateRuleSingleIssue[];
    const store = await this.createIssuesStore(items);
    for (let i = 0; i < items.length; i++) {
      const item = items[i];
      for (let j = 0; j < item.issueIds.length; j++) {
        const issueId = item.issueIds[j];
        const issue = this.getIssue(store, issueId);
        res.push({
          issueId: issueId,
          issueData: issue,
          userApiKey: userApiKey,
          add: item.add,
          remove: item.remove,
        });
      }
    }
    return res;
  }

  private getIssue(
    store: FlatIssuesStore,
    id: number,
  ): RedmineTypes.Issue | null {
    const result = store.getIssue(id);
    return result.data ? result.data : null;
  }
}
