import {
  patchState,
  signalStore,
  withComputed,
  withHooks,
  withMethods,
  withProps,
} from '@ngrx/signals';
import {
  withEntities,
  setAllEntities,
  updateEntity,
} from '@ngrx/signals/entities';
import { IDBStorageProvider, withStorage } from '@isa/core/storage';
import { computed, effect, inject } from '@angular/core';
import { TabService } from '@isa/core/tabs';
import { Receipt, ReceiptItem, ReturnProcess } from '../models';
import {
  CreateReturnProcessError,
  CreateReturnProcessErrorReason,
} from '../errors/return-process';
import { logger } from '@isa/core/logging';
import { canReturnReceiptItem } from '../helpers/return-process';
import { ProductCategory } from '../questions';

/**
 * Interface representing the parameters required to start a return process.
 */
export type StartProcess = {
  processId: number;
  returns: {
    receipt: Receipt;
    items: {
      receiptItem: ReceiptItem;
      quantity: number;
      category: ProductCategory;
    }[];
  }[];
};

/**
 * Store for managing return process entities.
 *
 * This store is responsible for handling the state and behavior of return process entities used in the application.
 * It leverages persistence with the IDBStorageProvider, supports entity management operations, and includes computed
 * properties and hooks to synchronize state based on external dependencies.
 *
 * Key Features:
 * - Entity Management: Maintains return process entities using methods to add, update, and remove them.
 * - Persistence: Automatically stores state changes via the configured IDBStorageProvider.
 * - Computed Properties: Includes a computed "nextId" for generating new unique entity identifiers.
 * - Process Actions: Provides methods to handle various actions on return process entities, such as:
 *   - removeAllEntitiesByProcessId: Removes entities not matching specified process IDs.
 *   - setAnswer and removeAnswer: Manage answers associated with specific questions for entities.
 *   - setProductCategory: Assigns a product category to an entity.
 *   - startProcess: Initializes a new return process by filtering receipt items, validating them, and creating a new set of entities.
 *
 * Hooks:
 * - onInit: Ensures that any entities with process IDs not recognized by the ProcessService are automatically cleaned up.
 *
 * Exceptions:
 * - Throws a NoReturnableItemsError if no returnable items are identified.
 * - Throws a MismatchReturnableItemsError if the number of returnable items does not match the expected count.
 */
export const ReturnProcessStore = signalStore(
  { providedIn: 'root' },
  withStorage('return-process', IDBStorageProvider),
  withEntities<ReturnProcess>(),
  withProps(() => ({
    _logger: logger(() => ({
      store: 'ReturnProcessStore',
    })),
  })),
  withComputed((store) => ({
    nextId: computed(() => Math.max(0, ...store.ids().map(Number)) + 1),
  })),
  withMethods((store) => ({
    /**
     * Removes all entities associated with the specified process IDs from the store.
     * @param processIds - The process IDs to filter entities by.
     * @returns void
     */
    removeAllEntitiesByProcessId: (...processIds: number[]) => {
      const entitiesToRemove = store
        .entities()
        .filter(
          (entity) =>
            !processIds.includes(entity.processId) && !entity.returnReceipt,
        );
      patchState(store, setAllEntities(entitiesToRemove));
      store.saveToStorage();
    },

    /**
     * Sets an answer for a specific question associated with an entity.
     * @param id - The ID of the entity to update.
     * @param question - The question associated with the answer.
     * @param answer - The answer to set for the specified question.
     * @returns void
     */
    setAnswer: <T>(id: number, question: string, answer: T) => {
      const entity = store.entityMap()[id];
      if (entity && !entity.returnReceipt) {
        const answers = { ...entity.answers, [question]: answer };
        patchState(
          store,
          updateEntity({ id: entity.id, changes: { answers } }),
        );
        store.saveToStorage();
      }
    },

    /**
     * Removes an answer for a specific question associated with an entity.
     * @param id - The ID of the entity to update.
     * @param question - The question associated with the answer to remove.
     * @returns void
     */
    removeAnswer: (id: number, question: string) => {
      const entity = store.entityMap()[id];
      if (entity && !entity.returnReceipt) {
        const answers = { ...entity.answers };
        delete answers[question];
        patchState(
          store,
          updateEntity({ id: entity.id, changes: { answers } }),
        );
        store.saveToStorage();
      }
    },
  })),
  withMethods((store) => ({
    /**
     * Initializes a new return process by removing previous entities for the given process id,
     * then filtering and validating the receipt items, and finally creating new return process entities.
     *
     * @param params - The configuration for starting a new return process.
     * @param params.processId - The unique identifier for the return process.
     * @param params.receipt - The associated receipt.
     * @param params.items - An array of receipt items to be processed.
     *
     * @throws {CreateReturnProcessError} Throws an error if no returnable items are found.
     * @throws {CreateReturnProcessError} Throws an error if the number of returnable items does not match the total items.
     */
    startProcess: (params: StartProcess) => {
      // Remove existing entities related to the process to start fresh.
      store.removeAllEntitiesByProcessId(params.processId);
      const entities: ReturnProcess[] = [];
      const nextId = store.nextId();

      const returnableItems = params.returns
        .flatMap((r) => r.items)
        .map((item) => item.receiptItem)
        .filter(canReturnReceiptItem);

      if (returnableItems.length === 0) {
        const err = new CreateReturnProcessError(
          CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS,
          params,
        );
        store._logger.error(err.message, err);
        throw err;
      }

      if (
        returnableItems.length !== params.returns.flatMap((r) => r.items).length
      ) {
        const err = new CreateReturnProcessError(
          CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS,
          params,
        );
        store._logger.error(err.message, err);
        throw err;
      }

      for (const { receipt, items } of params.returns) {
        for (const item of items) {
          entities.push({
            id: nextId + entities.length,
            processId: params.processId,
            receiptId: receipt.id,
            productCategory: item.category,
            quantity: item.quantity,
            receiptDate: receipt.printedDate,
            receiptItem: item.receiptItem,
            answers: {},
          });
        }
      }

      patchState(store, setAllEntities(entities));
      store.saveToStorage();
    },
    finishProcess: (returnReceipts: { [id: number]: Receipt }) => {
      const entities = store.entities().map((entity, i) => {
        const receipt = returnReceipts[i];
        if (receipt) {
          return { ...entity, returnReceipt: receipt };
        }
        return entity;
      });
      patchState(store, setAllEntities(entities));
      store.saveToStorage();
    },
  })),

  withHooks((store, tabService = inject(TabService)) => ({
    /**
     * Lifecycle hook that runs when the store is initialized.
     * Sets up an effect to clean up orphaned entities that are no longer associated with active processes.
     */
    onInit() {
      effect(() => {
        const tabIds = tabService.ids();
        const orphanedEntity = store
          .entities()
          .find((entity) => !tabIds.includes(entity.processId));
        if (orphanedEntity) {
          store.removeAllEntitiesByProcessId(orphanedEntity.processId);
        }
      });
    },
  })),
);
