import { HttpErrorResponse } from '@angular/common/http';
import {
  afterNextRender,
  ChangeDetectionStrategy,
  Component,
  computed,
  DestroyRef,
  effect,
  inject,
  input,
  output,
  signal,
  untracked,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Subscription } from 'rxjs';

import { CartItem } from '../../../core/services/cart/cart.interface';
import { CartService } from '../../../core/services/cart/cart.service';
import {
  CatalogFeaturedItemVariant,
  CatalogVariantOptionsResponse,
  CatalogVariantSelector,
  CatalogVariantValue,
} from '../../../core/services/catalog/catalog.interface';
import { CatalogService } from '../../../core/services/catalog/catalog.service';
import { ModalService } from '../../../core/services/modal.service';
import { ToastService } from '../../../core/services/toast.service';
import { ButtonComponent } from '../button/button.component';
import { IconButtonComponent } from '../icon-button/icon-button.component';
import { TooltipComponent } from '../tooltip/tooltip.component';

type TicketSelectionStatus =
  | 'selecting'
  | 'checking'
  | 'reserving'
  | 'reserved'
  | 'removing'
  | 'error';

interface VariantAttribute {
  key: string;
  label: string;
}

interface TicketSelectionRow {
  id: number;
  variantId: number | null;
  reservedVariantId: number | null;
  cartItemId: number | null;
  selectedValues: Record<string, string | string[]>;
  selectors: CatalogVariantSelector[];
  reservedSelectedValues: Record<string, string | string[]>;
  status: TicketSelectionStatus;
  error: string | null;
}

@Component({
  selector: 'app-product-ticket-selector',
  imports: [ButtonComponent, IconButtonComponent, TooltipComponent],
  templateUrl: './product-ticket-selector.component.html',
  styleUrl: './product-ticket-selector.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductTicketSelectorComponent {
  private readonly catalogService = inject(CatalogService);
  private readonly cartService = inject(CartService);
  private readonly modalService = inject(ModalService);
  private readonly toastService = inject(ToastService);
  private readonly destroyRef = inject(DestroyRef);
  private readonly reservationRequests = new Map<number, Subscription>();
  private readonly viewReady = signal(false);
  private readonly mapReady = signal(false);
  private readonly variants = signal<CatalogFeaturedItemVariant[]>([]);
  protected readonly attributes = signal<VariantAttribute[]>([]);
  private mapRequest: Subscription | null = null;

  readonly productId = input.required<number>();
  readonly title = input.required<string>();
  readonly description = input<string>('');
  readonly price = input<number>(0);
  readonly imageUrl = input<string | null>(null);
  readonly disabled = input(false);
  readonly unavailableMessage = input<string | null>(null);

  readonly buy = output<number[]>();

  protected readonly rows = signal<TicketSelectionRow[]>([this.createRow(1)]);
  private readonly remotePriceRange = signal<{ minimum: number; maximum: number } | null>(null);
  private nextRowId = 2;

  protected readonly availableVariantCount = computed(() => {
    const reservedIds = new Set(
      this.rows().flatMap(({ reservedVariantId }) =>
        reservedVariantId === null ? [] : [reservedVariantId],
      ),
    );
    return this.variants().filter(({ id }) => !reservedIds.has(id)).length;
  });
  protected readonly hasSelection = computed(
    () =>
      this.rows().length > 0 &&
      this.rows().every(
        (row) =>
          row.status === 'reserved' &&
          row.variantId !== null &&
          row.variantId === row.reservedVariantId,
      ),
  );
  protected readonly canAddRow = computed(
    () =>
      this.availableVariantCount() > 0 &&
      this.rows().every((row) => row.status === 'reserved') &&
      !this.rows().some((row) => this.isBusy(row)),
  );
  protected readonly priceRange = computed(() => {
    const range = this.remotePriceRange();
    const minimum = range?.minimum ?? this.price();
    const maximum = range?.maximum ?? this.price();
    return {
      minimum: this.formatCurrency(minimum),
      maximum: this.formatCurrency(maximum),
      hasRange: minimum !== maximum,
    };
  });

  constructor() {
    effect(() => {
      const viewReady = this.viewReady();
      const mapReady = this.mapReady();
      const cart = this.cartService.cart?.() ?? null;
      if (!viewReady) return;

      untracked(() => {
        if (!mapReady) {
          this.loadVariantMap();
          return;
        }
        this.synchronizeWithCart(cart?.items ?? []);
      });
    });

    afterNextRender(() => this.viewReady.set(true));
    this.destroyRef.onDestroy(() => {
      this.mapRequest?.unsubscribe();
      this.reservationRequests.forEach((request) => request.unsubscribe());
    });
  }

  protected onBuy(): void {
    const variantIds = this.rows().flatMap((row) =>
      row.reservedVariantId === null ? [] : [row.reservedVariantId],
    );
    if (this.hasSelection() && variantIds.length > 0) this.buy.emit(variantIds);
  }

  protected addRow(): void {
    if (!this.canAddRow()) return;
    const row = this.createRow(this.nextRowId++);
    row.status = 'selecting';
    row.selectors = this.buildSelectors(row.selectedValues, row);
    this.rows.update((rows) => [...rows, row]);
  }

  protected removeRow(rowId: number): void {
    const row = this.findRow(rowId);
    if (!row || this.isBusy(row)) return;
    this.modalService
      .openConfirmDelete({
        title: 'Eliminar entrada',
        content: `Se eliminará esta entrada de “${this.title()}”. Si ya estaba reservada, se liberará del carrito.`,
        confirmLabel: 'Sí, eliminar',
        cancelLabel: 'Cancelar',
        size: 'md',
      })
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((confirmed) => {
        if (confirmed) this.confirmRemoveRow(row);
      });
  }

  private confirmRemoveRow(row: TicketSelectionRow): void {
    if (row.cartItemId === null) {
      this.deleteRow(row.id);
      return;
    }
    this.patchRow(row.id, { status: 'removing', error: null });
    const request = this.cartService
      .withoutLoading()
      .removeItem(row.cartItemId)
      .subscribe({
        next: () => this.deleteRow(row.id),
        error: (error: HttpErrorResponse) => {
          this.patchRow(row.id, {
            status: 'reserved',
            error: this.errorMessage(error, 'No se pudo liberar la entrada.'),
          });
        },
      });
    this.reservationRequests.set(row.id, request);
  }

  protected onSelectionChange(
    rowId: number,
    selectorKey: string,
    value: CatalogVariantValue | null,
  ): void {
    const row = this.findRow(rowId);
    if (!row || this.isBusy(row)) return;
    const selectorIndex = this.attributes().findIndex(({ key }) => key === selectorKey);
    if (selectorIndex < 0) return;

    const selectedValues = { ...row.selectedValues };
    if (value === null) delete selectedValues[selectorKey];
    else selectedValues[selectorKey] = this.normalizeValue(value);
    this.attributes()
      .slice(selectorIndex + 1)
      .forEach(({ key }) => delete selectedValues[key]);

    const variant = this.resolveVariant(row, selectedValues);
    this.patchRow(rowId, {
      variantId: variant?.id ?? null,
      selectedValues,
      selectors: this.buildSelectors(selectedValues, row),
      status: 'selecting',
      error: null,
    });
    if (variant !== null) this.reserveRow(rowId, variant.id, selectedValues);
  }

  protected optionLabel(value: CatalogVariantValue): string {
    const values = Array.isArray(value) ? value : [value];
    return values.map((item) => (typeof item === 'string' ? item : item.label)).join(', ');
  }

  protected selectedOption(row: TicketSelectionRow, key: string): string | string[] | null {
    return row.selectedValues[key] ?? null;
  }

  protected optionKey(value: CatalogVariantValue): string {
    return this.valueKey(value);
  }

  protected selectedOptionKey(row: TicketSelectionRow, key: string): string {
    const selectedOption = this.selectedOption(row, key);
    return selectedOption === null ? '' : this.valueKey(selectedOption);
  }

  protected onSelectionKeyChange(
    rowId: number,
    selector: CatalogVariantSelector,
    selectedKey: string,
  ): void {
    const selectedOption =
      selector.options.find((option) => this.valueKey(option) === selectedKey) ?? null;
    this.onSelectionChange(rowId, selector.key, selectedOption);
  }

  protected rowDisabled(row: TicketSelectionRow): boolean {
    return this.disabled() || this.isBusy(row);
  }

  private loadVariantMap(
    onLoaded?: () => void,
    onError?: (error: HttpErrorResponse) => void,
  ): void {
    if (this.mapRequest !== null && !onLoaded) return;
    this.mapRequest?.unsubscribe();
    const request = this.catalogService
      .withoutLoading()
      .getVariantOptions(this.productId(), { selected_values: {} })
      .subscribe({
        next: (response) => {
          this.applyVariantMap(response);
          this.mapReady.set(true);
          this.mapRequest = null;
          onLoaded?.();
        },
        error: (error: HttpErrorResponse) => {
          this.mapRequest = null;
          if (onError) {
            onError(error);
            return;
          }
          const message = this.errorMessage(error, 'No se pudo consultar la disponibilidad.');
          this.rows.update((rows) =>
            rows.map((row) => ({ ...row, status: 'error', error: message })),
          );
          this.toastService.danger(message);
        },
      });
    this.mapRequest = request;
  }

  private applyVariantMap(response: CatalogVariantOptionsResponse): void {
    this.variants.set(response.variants);
    this.attributes.set(response.selectors.map(({ key, label }) => ({ key, label })));
    this.remotePriceRange.set({
      minimum: Number(response.price_range.minimum),
      maximum: Number(response.price_range.maximum),
    });
  }

  private buildSelectors(
    selectedValues: Record<string, string | string[]>,
    row: TicketSelectionRow,
    variants = this.availableVariantsFor(row),
  ): CatalogVariantSelector[] {
    return this.attributes().map(({ key, label }, index, attributes) => {
      const previousKeys = attributes.slice(0, index).map((attribute) => attribute.key);
      const compatibleVariants = variants.filter((variant) =>
        previousKeys.every(
          (previousKey) =>
            previousKey in selectedValues &&
            this.valueKey(variant.values[previousKey]) ===
              this.valueKey(selectedValues[previousKey]),
        ),
      );
      return {
        key,
        label,
        options: this.uniqueValues(compatibleVariants.map((variant) => variant.values[key])),
        enabled: index === 0 || previousKeys.every((previousKey) => previousKey in selectedValues),
      };
    });
  }

  private resolveVariant(
    row: TicketSelectionRow,
    selectedValues: Record<string, string | string[]>,
  ): CatalogFeaturedItemVariant | null {
    const attributes = this.attributes();
    if (attributes.length === 0 || attributes.some(({ key }) => !(key in selectedValues)))
      return null;
    const matches = this.availableVariantsFor(row).filter((variant) =>
      attributes.every(
        ({ key }) => this.valueKey(variant.values[key]) === this.valueKey(selectedValues[key]),
      ),
    );
    return matches.length === 1 ? matches[0] : null;
  }

  private availableVariantsFor(row: TicketSelectionRow): CatalogFeaturedItemVariant[] {
    const reservedByOtherRows = new Set(
      this.rows().flatMap((candidate) =>
        candidate.id !== row.id && candidate.reservedVariantId !== null
          ? [candidate.reservedVariantId]
          : [],
      ),
    );
    const variants = [...this.variants()];
    const reservedVariant = this.reservedVariantData(row);
    if (reservedVariant !== null && !variants.some(({ id }) => id === reservedVariant.id)) {
      variants.push(reservedVariant);
    }
    return variants.filter(({ id }) => !reservedByOtherRows.has(id));
  }

  private reservedVariantData(row: TicketSelectionRow): CatalogFeaturedItemVariant | null {
    if (row.cartItemId === null || row.reservedVariantId === null) return null;
    const item = this.cartService.cart?.()?.items.find(({ id }) => id === row.cartItemId);
    if (!item?.variant) return null;
    return {
      id: item.variant.id,
      precio: item.variant.precio,
      maximum_addable_quantity: item.variant.stock_tecnico,
      values: item.variant.values,
    };
  }

  private uniqueValues(values: Array<CatalogVariantValue | undefined>): CatalogVariantValue[] {
    const unique = new Map<string, CatalogVariantValue>();
    values.forEach((value) => {
      if (value !== undefined) unique.set(this.valueKey(value), value);
    });
    return [...unique.values()];
  }

  private reserveRow(
    rowId: number,
    variantId: number,
    selectedValues: Record<string, string | string[]>,
  ): void {
    const row = this.findRow(rowId);
    if (!row || (row.status === 'reserved' && row.reservedVariantId === variantId)) return;
    this.patchRow(rowId, { status: 'reserving', error: null });
    const cartService = this.cartService.withoutLoading();
    const operation =
      row.cartItemId === null
        ? cartService.addItem(this.productId(), variantId, 1)
        : cartService.updateItemVariant(row.cartItemId, 1, variantId);
    const request = operation.subscribe({
      next: (response) => {
        const cartItem =
          row.cartItemId === null
            ? response.data.items.find(
                (item) =>
                  item.catalog_item_id === this.productId() && item.variant_id === variantId,
              )
            : response.data.items.find((item) => item.id === row.cartItemId);
        if (!cartItem) {
          this.patchRow(rowId, {
            variantId: row.reservedVariantId,
            status: row.reservedVariantId === null ? 'error' : 'reserved',
            error: 'No se pudo identificar la entrada reservada.',
          });
          return;
        }
        const currentRow = this.findRow(rowId) ?? row;
        this.patchRow(rowId, {
          variantId,
          reservedVariantId: variantId,
          selectedValues,
          selectors: this.buildSelectors(selectedValues, currentRow),
          reservedSelectedValues: selectedValues,
          cartItemId: cartItem.id,
          status: 'reserved',
          error: null,
        });
      },
      error: (error: HttpErrorResponse) => {
        const message = this.errorMessage(error, 'La entrada ya no está disponible.');
        if (error.status === 422) {
          this.recoverFromUnavailableVariant(rowId, variantId, message);
          return;
        }
        this.restoreReservedRowAfterError(rowId, row, message);
      },
    });
    this.reservationRequests.set(rowId, request);
  }

  private recoverFromUnavailableVariant(
    rowId: number,
    failedVariantId: number,
    message: string,
  ): void {
    this.patchRow(rowId, { status: 'checking', error: null });
    this.loadVariantMap(
      () => this.backtrackSelection(rowId, failedVariantId, message),
      () => {
        const row = this.findRow(rowId);
        if (row) this.restoreReservedRowAfterError(rowId, row, message);
      },
    );
  }

  private backtrackSelection(rowId: number, failedVariantId: number, message: string): void {
    const row = this.findRow(rowId);
    if (!row) return;
    const variants = this.availableVariantsFor(row).filter(({ id }) => id !== failedVariantId);
    const selectedValues = { ...row.selectedValues };
    const attributes = this.attributes();

    for (let index = attributes.length - 1; index >= 0; index--) {
      delete selectedValues[attributes[index].key];
      const prefixKeys = attributes.slice(0, index).map(({ key }) => key);
      const hasCompatibleVariant = variants.some((variant) =>
        prefixKeys.every(
          (key) =>
            key in selectedValues &&
            this.valueKey(variant.values[key]) === this.valueKey(selectedValues[key]),
        ),
      );
      if (!hasCompatibleVariant) continue;
      this.patchRow(rowId, {
        variantId: null,
        selectedValues: { ...selectedValues },
        selectors: this.buildSelectors(selectedValues, row, variants),
        status: 'selecting',
        error: message,
      });
      this.toastService.danger(message);
      return;
    }

    this.patchRow(rowId, {
      variantId: null,
      selectedValues: {},
      selectors: this.buildSelectors({}, row, variants),
      status: 'error',
      error: message,
    });
    this.toastService.danger(message);
  }

  private restoreReservedRowAfterError(
    rowId: number,
    row: TicketSelectionRow,
    message: string,
  ): void {
    if (row.reservedVariantId === null) {
      this.patchRow(rowId, { variantId: null, status: 'error', error: message });
    } else {
      const selectedValues = row.reservedSelectedValues;
      this.patchRow(rowId, {
        variantId: row.reservedVariantId,
        selectedValues,
        selectors: this.buildSelectors(selectedValues, row),
        status: 'reserved',
        error: null,
      });
    }
    this.toastService.danger(message);
  }

  private createRow(id: number): TicketSelectionRow {
    return {
      id,
      variantId: null,
      reservedVariantId: null,
      cartItemId: null,
      selectedValues: {},
      selectors: [],
      reservedSelectedValues: {},
      status: 'checking',
      error: null,
    };
  }

  private createCartRow(item: CartItem, id = this.nextRowId++): TicketSelectionRow | null {
    if (item.variant_id === null || item.variant === null) return null;
    const selectedValues = Object.fromEntries(
      Object.entries(item.variant.values).map(([key, value]) => [key, this.normalizeValue(value)]),
    );
    return {
      id,
      variantId: item.variant_id,
      reservedVariantId: item.variant_id,
      cartItemId: item.id,
      selectedValues,
      selectors: [],
      reservedSelectedValues: selectedValues,
      status: 'reserved',
      error: null,
    };
  }

  private synchronizeWithCart(items: CartItem[]): void {
    const cartItems = items.filter(
      (item) => item.catalog_item_id === this.productId() && item.variant_id !== null,
    );
    const currentRows = this.rows();
    if (cartItems.length === 0) {
      const localRows = currentRows.filter((row) => row.cartItemId === null);
      const rows = localRows.length > 0 ? localRows : [this.createRow(this.nextRowId++)];
      this.replaceRows(this.withRefreshedSelectors(rows));
      return;
    }

    const synchronizedRows = cartItems.flatMap((item) => {
      const existingRow = currentRows.find((row) => row.cartItemId === item.id);
      if (existingRow?.reservedVariantId === item.variant_id) return [existingRow];
      const cartRow = this.createCartRow(item, existingRow?.id);
      return cartRow === null ? [] : [cartRow];
    });
    const localRows = currentRows.filter(
      (row) => row.cartItemId === null && Object.keys(row.selectedValues).length > 0,
    );
    this.replaceRows(this.withRefreshedSelectors([...synchronizedRows, ...localRows]));
  }

  private withRefreshedSelectors(rows: TicketSelectionRow[]): TicketSelectionRow[] {
    const previousRows = this.rows();
    this.rows.set(rows);
    const refreshed = rows.map((row) => ({
      ...row,
      status: row.status === 'checking' ? ('selecting' as const) : row.status,
      selectors: this.buildSelectors(row.selectedValues, row),
    }));
    this.rows.set(previousRows);
    return refreshed;
  }

  private replaceRows(rows: TicketSelectionRow[]): void {
    const retainedIds = new Set(rows.map(({ id }) => id));
    this.rows().forEach((row) => {
      if (retainedIds.has(row.id)) return;
      this.reservationRequests.get(row.id)?.unsubscribe();
      this.reservationRequests.delete(row.id);
    });
    this.rows.set(rows);
  }

  private findRow(rowId: number): TicketSelectionRow | undefined {
    return this.rows().find((row) => row.id === rowId);
  }

  private patchRow(rowId: number, patch: Partial<TicketSelectionRow>): void {
    this.rows.update((rows) => rows.map((row) => (row.id === rowId ? { ...row, ...patch } : row)));
  }

  private deleteRow(rowId: number): void {
    this.rows.update((rows) => rows.filter((row) => row.id !== rowId));
    this.reservationRequests.delete(rowId);
  }

  private isBusy(row: TicketSelectionRow): boolean {
    return ['checking', 'reserving', 'removing'].includes(row.status);
  }

  private normalizeValue(value: CatalogVariantValue): string | string[] {
    return Array.isArray(value)
      ? value.map((item) => this.scalarValue(item))
      : this.scalarValue(value);
  }

  private scalarValue(value: string | { value: string }): string {
    return typeof value === 'string' ? value : value.value;
  }

  private valueKey(value: CatalogVariantValue): string {
    return JSON.stringify(this.normalizeValue(value));
  }

  private errorMessage(error: HttpErrorResponse, fallback: string): string {
    return typeof error.error?.message === 'string' ? error.error.message : fallback;
  }

  private formatCurrency(value: number): string {
    return new Intl.NumberFormat('es-AR', {
      style: 'currency',
      currency: 'ARS',
      maximumFractionDigits: 0,
    }).format(value);
  }
}
