import {
  patchState,
  signalStore,
  type,
  withHooks,
  withMethods,
} from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import {
  addEntity,
  entityConfig,
  setAllEntities,
  updateEntity,
  withEntities,
} from '@ngrx/signals/entities';
import { pipe, switchMap, tap } from 'rxjs';
import { ReturnSearchService } from '../services';
import { tapResponse } from '@ngrx/operators';
import { effect, inject } from '@angular/core';
import { QueryTokenSchema } from '../schemas';
import {
  Callback,
  ListResponseArgs,
  takeUntilKeydownEscape,
} from '@isa/common/data-access';
import { ReceiptListItem } from '../models';
import { Query } from '@isa/shared/filter';
import { SessionStorageProvider, withStorage } from '@isa/core/storage';
import { TabService } from '@isa/core/tabs';

/**
 * Enum representing the status of a return search process.
 */
export enum ReturnSearchStatus {
  Idle = 'idle',
  Pending = 'pending',
  Success = 'success',
  Error = 'error',
}

/**
 * Type definition for a return search entity.
 *
 * @property {number} processId - Unique identifier for the search process
 * @property {ReturnSearchStatus} status - Current status of the search process
 * @property {ReceiptListItem[]} [items] - List of receipt items returned by the search
 * @property {number} [hits] - Total number of results
 * @property {string | unknown} [error] - Error details, if any
 */
export type ReturnSearchEntity = {
  processId: number;
  status: ReturnSearchStatus;
  items?: ReceiptListItem[];
  hits?: number;
  error?: string | unknown;
};

const config = entityConfig({
  entity: type<ReturnSearchEntity>(),
  selectId: (entity) => entity.processId,
});

/**
 * Signal store for managing return search state and operations.
 */
export const ReturnSearchStore = signalStore(
  { providedIn: 'root' },
  withStorage('oms-data-access.return-search-store', SessionStorageProvider),
  withEntities<ReturnSearchEntity>(config),
  withMethods((store) => ({
    /**
     * Retrieves a return search entity by its process ID.
     *
     * @param {number} processId - The unique identifier of the search process.
     * @returns {ReturnSearchEntity | undefined} The corresponding entity or undefined if not found.
     */
    getEntity(processId: number): ReturnSearchEntity | undefined {
      return store.entities().find((e) => e.processId === processId);
    },
    /**
     * Removes all entities associated with a specific process ID.
     *
     * @param {number} processId - The unique identifier of the process whose entities should be removed.
     * @returns {void}
     */
    removeAllEntitiesByProcessId(processId: number): void {
      const entities = store
        .entities()
        .filter((entity) => entity.processId !== processId);
      patchState(store, setAllEntities(entities, config));
    },
  })),
  withMethods((store) => ({
    /**
     * Prepares the store state before initiating a search operation.
     *
     * @param {number} processId - The unique identifier of the search process.
     * @param {boolean} [clear=true] - Flag indicating whether to clear existing items.
     */
    beforeSearch(processId: number, clear = true) {
      const entity = store.getEntity(processId);
      if (entity) {
        let items = entity.items ?? [];

        if (clear) {
          items = [];
        }

        patchState(
          store,
          updateEntity(
            {
              id: processId,
              changes: {
                status: ReturnSearchStatus.Pending,
                items,
                hits: 0,
              },
            },
            config,
          ),
        );
      } else {
        const entity: ReturnSearchEntity = {
          processId,
          status: ReturnSearchStatus.Pending,
        };
        patchState(store, addEntity(entity, config));
      }
    },

    /**
     * Handles the success response of a search operation.
     *
     * @param {Object} options - Options for handling the success response.
     * @param {number} options.processId - The unique identifier of the search process.
     * @param {ListResponseArgs<ReceiptListItem>} options.response - The search response.
     */
    handleSearchSuccess({
      processId,
      response,
    }: {
      processId: number;
      response: ListResponseArgs<ReceiptListItem>;
    }) {
      const entityItems = store.getEntity(processId)?.items;
      patchState(
        store,
        updateEntity(
          {
            id: processId,
            changes: {
              status: ReturnSearchStatus.Success,
              hits: response.hits,
              items: entityItems
                ? [...entityItems, ...response.result]
                : response.result,
            },
          },
          config,
        ),
      );

      store.saveToStorage();
    },

    /**
     * Handles errors encountered during a search operation.
     *
     * @param {Object} options - Options for handling the error.
     * @param {number} options.processId - The unique identifier of the search process.
     * @param {unknown} options.error - The error encountered.
     */
    handleSearchError({
      processId,
      error,
    }: {
      processId: number;
      error: unknown;
    }) {
      console.error(error);
      patchState(
        store,
        updateEntity(
          {
            id: processId,
            changes: {
              items: [],
              hits: 0,
              status: ReturnSearchStatus.Error,
              error,
            },
          },
          config,
        ),
      );
    },
    handleSearchCompleted(processId: number) {
      const entity = store.getEntity(processId);

      if (entity?.status !== ReturnSearchStatus.Pending) {
        return;
      }

      patchState(
        store,
        updateEntity(
          {
            id: processId, // Assuming we want to update the first entity
            changes: {
              status: ReturnSearchStatus.Idle,
            },
          },
          config,
        ),
      );
    },
  })),
  withMethods((store, returnSearchService = inject(ReturnSearchService)) => ({
    /**
     * Initiates a search operation.
     *
     * @param {Object} options - Options for the search operation.
     * @param {number} options.processId - The unique identifier of the search process.
     * @param {Query} options.query - The search query parameters.
     * @param {Callback<ListResponseArgs<ReceiptListItem>>} [options.cb] - Optional callback for handling the response.
     * @param {Record<string, string>} options.params - Search parameters.
     */
    search: rxMethod<{
      processId: number;
      query: Query;
      clear: boolean;
      cb?: Callback<ListResponseArgs<ReceiptListItem>>;
    }>(
      pipe(
        tap(({ processId, clear }) => store.beforeSearch(processId, clear)),
        switchMap(({ processId, query, cb }) =>
          returnSearchService
            .search(
              QueryTokenSchema.parse({
                ...query,
                skip: store.getEntity(processId)?.items?.length ?? 0,
              }),
            )
            .pipe(
              takeUntilKeydownEscape(),
              tapResponse(
                (response) => {
                  store.handleSearchSuccess({ processId, response });
                  cb?.({ data: response });
                },
                (error) => {
                  store.handleSearchError({ processId, error });
                  cb?.({ error });
                },
                () => {
                  store.handleSearchCompleted(processId);
                },
              ),
            ),
        ),
      ),
    ),
  })),
  withHooks((store, tabService = inject(TabService)) => ({
    onInit() {
      effect(() => {
        const tabIds = tabService.ids();
        const orphanedEntity = store
          .entities()
          .find((entity) => !tabIds.includes(entity.processId));
        if (orphanedEntity) {
          store.removeAllEntitiesByProcessId(orphanedEntity.processId);
        }
      });
    },
  })),
);
