/* eslint-disable @typescript-eslint/no-namespace */
import { IssueEnhancerInterface } from '../issue-enhancers/issue-enhancer-interface';
import { IssuesServiceNs } from '../issues/issues.service';
import { RedmineTypes } from '../models/redmine-types';

export namespace FlatIssuesStoreNs {
  export type IssuesLoader = IssuesServiceNs.IssuesLoader;

  export namespace Models {
    export type ByStatus = {
      status: string;
      count: number;
      issues: RedmineTypes.Issue[];
    };

    export type ByStatuses = ByStatus[];

    export enum FindErrors {
      NOT_FOUND = 'NOT_FOUND',
      NOT_LOADED = 'NOT_LOADED',
    }

    export type FindResult = {
      data?: RedmineTypes.Issue;
      error?: FindErrors;
    };
  }
}

export class FlatIssuesStore {
  private issues: Record<number, RedmineTypes.Issue | null> = {};

  push(issue: number | string | RedmineTypes.Issue): void {
    let id: any;
    let data: any;

    if (
      typeof issue === 'number' ||
      (typeof issue === 'string' && Number.isFinite(Number(issue)))
    ) {
      id = Number(issue);
      data = null;
    } else if (typeof issue === 'object' && typeof issue['id'] === 'number') {
      id = issue['id'];
      data = issue;
    }

    if (
      typeof id === 'number' &&
      !Object.prototype.hasOwnProperty.call(this.issues, id)
    ) {
      this.issues[id] = data;
    }
  }

  async fillData(loader: FlatIssuesStoreNs.IssuesLoader): Promise<void> {
    const ids = [] as number[];
    for (const id in this.issues) {
      if (Object.prototype.hasOwnProperty.call(this.issues, id)) {
        const issue = this.issues[id];
        if (!issue) {
          ids.push(Number(id));
        }
      }
    }
    const data = await loader(ids);
    for (const id in data) {
      if (Object.prototype.hasOwnProperty.call(data, id)) {
        const issue = data[id];
        if (issue) {
          this.issues[id] = issue;
        }
      }
    }
    return;
  }

  async enhanceIssues(
    enhancers: (IssueEnhancerInterface | null)[],
  ): Promise<void> {
    for (const issueId in this.issues) {
      if (Object.prototype.hasOwnProperty.call(this.issues, issueId)) {
        let issue = this.issues[issueId];
        for (let i = 0; i < enhancers.length; i++) {
          const enhancer = enhancers[i];
          if (!enhancer) continue;
          issue = await enhancer.enhance(issue);
          this.issues[issueId] = issue;
        }
      }
    }
  }

  getIds(): number[] {
    return Object.keys(this.issues).map((i) => Number(i));
  }

  getIssues(): RedmineTypes.Issue[] {
    return Object.values(this.issues).filter((i) => !!i);
  }

  hasIssue(id: number): boolean {
    return Object.prototype.hasOwnProperty.call(this.issues, id);
  }

  loadedIssue(id: number): boolean {
    return this.hasIssue(id) && !!this.issues[id];
  }

  getIssue(id: number): FlatIssuesStoreNs.Models.FindResult {
    if (!this.hasIssue(id)) {
      return { error: FlatIssuesStoreNs.Models.FindErrors.NOT_FOUND };
    } else if (!this.issues[id]) {
      return { error: FlatIssuesStoreNs.Models.FindErrors.NOT_LOADED };
    } else {
      return { data: this.issues[id] };
    }
  }

  isFullLoaded(): boolean {
    return Object.values(this.issues).indexOf(null) < 0;
  }

  groupBy(
    iteratee: (issue: RedmineTypes.Issue) => string | number,
  ): Record<string | number, RedmineTypes.Issue[]> {
    const res = {} as Record<string | number, RedmineTypes.Issue[]>;
    const items = this.getIssues();
    for (let i = 0; i < items.length; i++) {
      const issue = items[i];
      const key = iteratee(issue);
      if (!Object.prototype.hasOwnProperty.call(res, key)) {
        res[key] = [];
      }
      res[key].push(issue);
    }
    return res;
  }

  groupByToStories(
    iteratee: (issue: RedmineTypes.Issue) => string | number,
  ): Record<string | number, FlatIssuesStore> {
    const res = {} as Record<string | number, FlatIssuesStore>;
    const rawData = this.groupBy(iteratee);
    for (const key in rawData) {
      if (Object.prototype.hasOwnProperty.call(rawData, key)) {
        const issues = rawData[key];
        res[key] = new FlatIssuesStore();
        for (let i = 0; i < issues.length; i++) {
          const issue = issues[i];
          res[key].push(issue);
        }
      }
    }
    return res;
  }

  groupByStatus(statuses: string[]): FlatIssuesStoreNs.Models.ByStatuses {
    const res = [] as FlatIssuesStoreNs.Models.ByStatuses;
    for (let i = 0; i < statuses.length; i++) {
      const status = statuses[i];
      res.push({ status: status, count: 0, issues: [] });
    }
    const groupedIssues = this.groupBy((issue) => issue.status.name);
    for (const status in groupedIssues) {
      if (Object.prototype.hasOwnProperty.call(groupedIssues, status)) {
        const issues = groupedIssues[status];
        const foundItem = res.find((i) => i.status === status);
        if (!foundItem) continue;
        foundItem.issues.push(...issues);
      }
    }
    for (let i = 0; i < res.length; i++) {
      const item = res[i];
      item.count = item.issues.length;
    }
    return res;
  }

  groupByStatusWithExtra(
    iteratee: (issue: RedmineTypes.Issue) => string | number,
    statuses: string[],
  ): Record<string | number, FlatIssuesStoreNs.Models.ByStatuses> {
    const res = {} as Record<
      string | number,
      FlatIssuesStoreNs.Models.ByStatuses
    >;
    const groupedIssues = this.groupByToStories(iteratee);
    for (const key in groupedIssues) {
      if (Object.prototype.hasOwnProperty.call(groupedIssues, key)) {
        const store = groupedIssues[key];
        res[key] = store.groupByStatus(statuses);
      }
    }
    return res;
  }

  groupByToMultipleStories(
    iteratee: (issue: RedmineTypes.Issue) => (string | number)[],
  ): Record<string | number, FlatIssuesStore> {
    const res = {} as Record<string | number, FlatIssuesStore>;
    const items = this.getIssues();
    for (let i = 0; i < items.length; i++) {
      const issue = items[i];
      const keys = iteratee(issue);
      for (let j = 0; j < keys.length; j++) {
        const key = keys[j];
        if (!Object.prototype.hasOwnProperty.call(res, key)) {
          res[key] = new FlatIssuesStore();
        }
        res[key].push(issue);
      }
    }
    return res;
  }

  groupByStatusWithExtraToMultipleStories(
    iteratee: (issue: RedmineTypes.Issue) => (string | number)[],
    statuses: string[],
  ): Record<string | number, FlatIssuesStoreNs.Models.ByStatuses> {
    const res = {} as Record<
      string | number,
      FlatIssuesStoreNs.Models.ByStatuses
    >;
    const groupedIssues = this.groupByToMultipleStories(iteratee);
    for (const key in groupedIssues) {
      if (Object.prototype.hasOwnProperty.call(groupedIssues, key)) {
        const store = groupedIssues[key];
        res[key] = store.groupByStatus(statuses);
      }
    }
    return res;
  }
}
