/* eslint-disable @typescript-eslint/no-namespace */
import { RedmineTypes } from '../models/redmine-types';
import { CacheTTL, Injectable, Logger } from '@nestjs/common';
import { Issues } from '../couchdb-datasources/issues';
import { RedmineEventsGateway } from '../events/redmine-events.gateway';
import { RedmineIssuesCacheWriterService } from '../issue-cache-writer/redmine-issues-cache-writer.service';
import { RedmineDataLoader } from '../redmine-data-loader/redmine-data-loader';
import { MemoryCache } from '../utils/memory-cache';
import nano from 'nano';
import { UNLIMITED } from '../consts/consts';
import { GetParentsHint } from '../utils/get-parents-hint';
import { TreeIssuesStore } from '../utils/tree-issues-store';
import { FlatIssuesStore } from '../utils/flat-issues-store';

export const ISSUE_MEMORY_CACHE_LIFETIME = 30 * 1000;
const ISSUE_MEMORY_CACHE_AUTOCLEAN_INTERVAL = 1000 * 60 * 5;

export namespace IssuesServiceNs {
  export type IssuesLoader = (
    ids: number[],
  ) => Promise<Record<number, RedmineTypes.Issue | null>>;

  export type TreesAndQuery = {
    rootIds?: number[];
    rootIssues?: RedmineTypes.Issue[];
    query?: nano.MangoQuery;
  };
}

@Injectable()
export class IssuesService {
  private logger = new Logger(IssuesService.name);

  private memoryCache = new MemoryCache<number, RedmineTypes.Issue>(
    ISSUE_MEMORY_CACHE_LIFETIME,
    ISSUE_MEMORY_CACHE_AUTOCLEAN_INTERVAL,
  );

  constructor(
    private redmineDataLoader: RedmineDataLoader,
    private issues: Issues,
    private redmineIssuesCacheWriterService: RedmineIssuesCacheWriterService,
    private redmineEventsGateway: RedmineEventsGateway,
  ) {}

  async find(query: nano.MangoQuery): Promise<RedmineTypes.Issue[]> {
    const issueDb = await this.issues.getDatasource();
    const res = await issueDb.find(query);
    return res.docs;
  }

  @CacheTTL(60)
  async getIssues(ids: number[]): Promise<RedmineTypes.Issue[]> {
    const issueDb = await this.issues.getDatasource();
    try {
      const docs = await issueDb.find({
        selector: {
          id: {
            $in: ids,
          },
        },
        limit: UNLIMITED,
      });
      return docs.docs;
    } catch (ex) {
      return [];
    }
  }

  @CacheTTL(120)
  async getParents(
    issueId: number,
    count?: number,
  ): Promise<RedmineTypes.Issue[]> {
    let index = 0;
    const res: RedmineTypes.Issue[] = [];
    let currentIssue = await this.getIssue(issueId);
    res.unshift(currentIssue);
    let parentIssueId = currentIssue.parent?.id || null;
    while (
      parentIssueId !== null &&
      currentIssue.id >= 0 &&
      (typeof count === 'undefined' || count === null || index < count)
    ) {
      currentIssue = await this.getIssue(parentIssueId);
      res.unshift(currentIssue);
      parentIssueId = currentIssue.parent?.id || null;
      index++;
    }
    const parentsHint = GetParentsHint(res);
    this.logger.debug(`Parents for issue #${issueId} - ${parentsHint}`);
    return res;
  }

  async getIssue(
    issueId: number,
    force = false,
  ): Promise<RedmineTypes.Issue | null> {
    const issueFromMemoryCache = this.getIssueFromMemoryCache(issueId);
    if (issueFromMemoryCache) {
      return issueFromMemoryCache;
    }
    const issueFromCache = await this.getIssueFromCache(issueId);
    if (issueFromCache) {
      this.memoryCache.set(issueId, issueFromCache);
      return issueFromCache;
    }
    if (force) {
      // force = true - прямо из redmine
      const issueFromRedmine = await this.redmineDataLoader.loadIssue(issueId);
      if (issueFromRedmine) {
        await this.redmineIssuesCacheWriterService.saveIssue(issueFromRedmine);
        this.memoryCache.set(issueId, issueFromRedmine);
        return issueFromRedmine;
      } else {
        return null;
      }
    } else {
      // force = false - через очередь
      this.redmineEventsGateway.addIssues([issueId]);
      const unknownIssue = { ...RedmineTypes.Unknown.issue };
      this.memoryCache.set(issueId, unknownIssue);
      return unknownIssue;
    }
  }

  getIssueFromMemoryCache(issueId: number): RedmineTypes.Issue | null {
    const issue = this.memoryCache.get(issueId);
    if (issue) {
      this.logger.debug(
        `Get issue from memory cache: id = ${issueId}, subject = ${issue.subject}`,
      );
    }
    return issue;
  }

  async getIssueFromRedmine(
    issueId: number,
  ): Promise<RedmineTypes.Issue | null> {
    const issue = await this.redmineDataLoader.loadIssue(issueId);
    if (issue) {
      this.logger.debug(
        `Get issue from redmine: id = ${issueId}, subject = ${issue.subject}`,
      );
    }
    return issue;
  }

  async getIssueFromCache(issueId: number): Promise<RedmineTypes.Issue | null> {
    const issueDb = await this.issues.getDatasource();
    try {
      const issue = (await issueDb.get(String(issueId))) as any;
      this.logger.debug(
        `Get issue from couchdb: id = ${issueId}, subject = ${issue.subject}`,
      );
      return issue;
    } catch (ex) {
      return null;
    }
  }

  createDynamicIssuesLoader(): IssuesServiceNs.IssuesLoader {
    const fn = async (
      ids: number[],
    ): Promise<Record<number, RedmineTypes.Issue>> => {
      const issues = await this.getIssues(ids);
      const res = {} as Record<number, RedmineTypes.Issue | null>;
      for (let i = 0; i < issues.length; i++) {
        const issue = issues[i];
        res[issue.id] = issue;
      }
      return res;
    };
    return fn;
  }

  async getIssuesWithChildren(
    rootIssue: RedmineTypes.Issue,
  ): Promise<RedmineTypes.Issue[]> {
    const treeIssuesStore = new TreeIssuesStore();
    treeIssuesStore.setRootIssue(rootIssue);
    const loader = this.createDynamicIssuesLoader();
    await treeIssuesStore.fillData(loader);
    return treeIssuesStore.getIssuesWithChildren();
  }

  async getIssuesFromRoot(
    rootIssue: RedmineTypes.Issue,
  ): Promise<RedmineTypes.Issue[]> {
    const treeStore = new TreeIssuesStore();
    treeStore.setRootIssue(rootIssue);
    const loader = this.createDynamicIssuesLoader();
    await treeStore.fillData(loader);
    return treeStore.getFlatStore().getIssues();
  }

  async mergedTreesAndFind(
    query: IssuesServiceNs.TreesAndQuery,
  ): Promise<RedmineTypes.Issue[]> {
    const flatStore = new FlatIssuesStore();
    const loader = this.createDynamicIssuesLoader();
    if (query.query && query.query.selector) {
      const issues = await this.find(query.query);
      issues.forEach((issue) => flatStore.push(issue));
    }

    const rootIssues = [];
    if (query.rootIds) {
      const issues = await this.getIssues(query.rootIds);
      rootIssues.push(...issues);
    }
    if (query.rootIssues) {
      rootIssues.push(...query.rootIssues);
    }
    for (let i = 0; i < rootIssues.length; i++) {
      const rootIssue = rootIssues[i];
      const issues = await this.getIssuesFromRoot(rootIssue);
      issues.forEach((issue) => flatStore.push(issue));
    }

    await flatStore.fillData(loader);

    return flatStore.getIssues();
  }
}
