// Libs
// -----------------------------------------------------------------------------
import {
  makeObservable,
  runInAction,
  observable,
  action,
  computed,
  override,
} from 'mobx';

// App
// -----------------------------------------------------------------------------
import { RestApi } from 'services/RestApi';
import { TypeDashboardPaginationState } from 'components/Dashboard/types';
import { Annotation } from 'mobx/dist/api/annotation';

/**
 * Коллекция элементов загружаемых из БД через REST API.
 */
export abstract class GenericDataStore<T, TB = T> {
  readonly api: RestApi<T, TB>;

  constructor(apiUrl: string) {
    this.api = new RestApi<T, TB>(apiUrl);
    makeObservable(this, {
      list: observable,
      isLoading: observable,
      fetchAll: action,
      create: action,
      delete: action,
      update: action,
      reset: action,
      total: observable,
      currentPage: observable,
      pageSize: observable,
      ordering: observable,
      fetchForCurrentPage: action,
      setOrdering: action,
      orderingString: computed,
      dashboardState: computed,
      setDashboardState: action,
      setCurrentPage: action,
      element: observable,
      isSingleLoading: observable,
      fetchOne: action,
    });
  }

  override = (extraOptions?: Record<string, Annotation>): void => {
    makeObservable(this, {
      list: override,
      isLoading: override,
      fetchAll: override,
      create: override,
      delete: override,
      update: override,
      reset: override,
      total: override,
      currentPage: override,
      pageSize: override,
      ordering: override,
      fetchForCurrentPage: override,
      setOrdering: override,
      orderingString: override,
      dashboardState: override,
      setDashboardState: override,
      setCurrentPage: override,
      element: override,
      isSingleLoading: override,
      fetchOne: override,
      ...extraOptions,
    });
  };

  // Список элементов
  list: Array<T> = [];

  // Одиночный элемент
  element: T | null = null;

  // Общее число элементов в таблице, актуально если загрузить не все сразу
  total = 0;

  // Текущий номер страницы
  currentPage = 1;

  // Число элементов на странице
  pageSize = 10;

  // Поле для сортировки таблицы
  ordering = '';

  // Активна ли загрузка
  isLoading = false;

  // Активна ли загрузка одного элемента
  isSingleLoading = false;

  // Загрузить один элемент
  fetchOne = async (entityId: number | string): Promise<void> => {
    this.isSingleLoading = true;
    try {
      const element = await this.api.read(entityId);
      runInAction(() => {
        this.element = element;
      });
    } finally {
      runInAction(() => {
        this.isSingleLoading = false;
      });
    }
  };

  // Загрузить все элементы
  fetchAll = async (): Promise<void> => {
    this.isLoading = true;
    try {
      const list = await this.api.readList({
        pagination: { page: 1, pageSize: 1000 },
      });
      runInAction(() => {
        this.list = list.results;
        this.total = list.count;
      });
    } finally {
      runInAction(() => {
        this.isLoading = false;
      });
    }
  };

  // Загрузить элементы постранично
  fetchForCurrentPage = async (): Promise<void> => {
    this.isLoading = true;
    try {
      const list = await this.api.readList({
        pagination: {
          page: this.currentPage,
          pageSize: this.pageSize,
          ordering: this.orderingString,
        },
      });
      runInAction(() => {
        this.list = list.results;
        this.total = list.count;
      });
    } finally {
      runInAction(() => {
        this.isLoading = false;
      });
    }
  };

  // Подготовить данные для отправки на бэкенд
  prepareDataForBackend: ((entity: T) => TB) | null = null;

  // Получить id элемента
  abstract getId: (entity: T) => number | string;

  // Получить имя элемента
  abstract getName: (entity: T) => string;

  // Создать элемент
  create = async (entity: T): Promise<void> => {
    this.isLoading = true;
    try {
      const preparedData = this.prepareDataForBackend
        ? this.prepareDataForBackend(entity)
        : entity;
      const newEntity = await this.api.create(preparedData);
      runInAction(() => {
        this.list = [...this.list, newEntity];
      });
    } finally {
      runInAction(() => {
        this.isLoading = false;
      });
    }
  };

  // Удалить элемент
  delete = async (entityId: number | string): Promise<void> => {
    this.isLoading = true;
    try {
      await this.api.delete(entityId);
      runInAction(() => {
        this.list = this.list.filter((currentEntity) => {
          const currentEntityId = this.getId(currentEntity);
          return currentEntityId.toString() !== entityId.toString();
        });
      });
    } finally {
      runInAction(() => {
        this.isLoading = false;
      });
    }
  };

  // Удалить несколько элементов
  deleteMany = async (ids: Array<number | string>): Promise<void> => {
    const makeDeletionPromise = (
      id: number | string,
    ): Promise<number | string> => {
      return new Promise((resolve, reject) => {
        return this.api
          .delete(id)
          .then(() => resolve(id))
          .catch(() => reject(id));
      });
    };

    this.isLoading = true;

    try {
      const deletionPromises: Array<Promise<number | string>> = ids.map(
        (item) => {
          return makeDeletionPromise(item);
        },
      );

      await Promise.allSettled(deletionPromises);

      runInAction(() => {
        this.list = this.list.filter((currentEntity) => {
          const currentEntityId = this.getId(currentEntity);
          for (const id of ids) {
            if (currentEntityId.toString() === id.toString()) {
              return true;
            }
          }
          return false;
        });
      });
    } finally {
      runInAction(() => {
        this.isLoading = false;
      });
    }
  };

  // Обновить элемент
  update = async (entityId: number | string, entity: T): Promise<void> => {
    this.isLoading = true;
    try {
      const preparedData = this.prepareDataForBackend
        ? this.prepareDataForBackend(entity)
        : entity;

      const updatedEntity = await this.api.update(entityId, preparedData);

      runInAction(() => {
        this.list = this.list.map((currentEntity) => {
          const currentEntityId = this.getId(currentEntity);
          return currentEntityId.toString() === entityId.toString()
            ? updatedEntity
            : currentEntity;
        });
      });
    } finally {
      runInAction(() => {
        this.isLoading = false;
      });
    }
  };

  // Полностью сбросить состояние
  reset = (): void => {
    this.list = [];
    this.total = 0;
    this.currentPage = 1;
    this.pageSize = 10;
    this.ordering = 'id';
    this.isLoading = false;
    this.isSingleLoading = false;
    this.element = null;
  };

  // Задать параметры сортировки
  setOrdering = (newOrdering: string): void => {
    this.ordering = newOrdering;
  };

  // Строка сортировки без имени параметра
  get orderingString(): string {
    return this.ordering
      ? this.ordering.replace('&ordering=', '').replace(',', '__')
      : '';
  }

  // Состояние пагинации таблицы
  get dashboardState(): TypeDashboardPaginationState {
    return {
      current: this.currentPage,
      pageSize: this.pageSize,
      total: this.total,
    };
  }

  // Задать состояние пагинации таблицы
  setDashboardState = (currentPage: number, pageSize: number): void => {
    this.currentPage = currentPage;
    this.pageSize = pageSize;
  };

  // Задать текущую страницу таблицы
  setCurrentPage = (newCurrentPage: number): void => {
    this.currentPage = newCurrentPage;
  };
}
