import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  input,
  model,
  output,
  signal,
  untracked,
} from '@angular/core';
import { FormsModule } from '@angular/forms';

export interface VariantAttributeOption {
  value: string;
  label: string;
}

export type VariantAttributeScalar = string | VariantAttributeOption;
export type VariantAttributeValue = VariantAttributeScalar | VariantAttributeScalar[];

export interface VariantSelectorVariant {
  id: unknown;
  values: Record<string, VariantAttributeValue>;
}

export interface VariantSelectorSelectionChange {
  values: Record<string, VariantAttributeValue>;
  selectedVariant: unknown;
}

interface VariantSelectorOption {
  key: string;
  label: string;
  value: VariantAttributeValue;
}

interface VariantSelectorGroup {
  key: string;
  label: string;
  options: VariantSelectorOption[];
}

@Component({
  selector: 'app-variant-selector',
  standalone: true,
  imports: [FormsModule],
  templateUrl: './variant-selector.component.html',
  styleUrl: './variant-selector.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VariantSelectorComponent {
  readonly variants = input<VariantSelectorVariant[]>([]);
  readonly selectedVariant = model<unknown>(null);
  readonly disabled = input(false);
  readonly compact = input(false);
  readonly autoSelectFirst = input(true);
  readonly resetToken = input(0);
  readonly selectionValuesChange = output<VariantSelectorSelectionChange>();

  protected readonly selectedValues = signal<Record<string, VariantAttributeValue>>({});
  protected readonly compareValues = (
    left: VariantAttributeValue | null,
    right: VariantAttributeValue | null,
  ): boolean => left !== null && right !== null && this.sameValue(left, right);
  protected readonly attributeKeys = computed(() =>
    Array.from(new Set(this.variants().flatMap((variant) => Object.keys(variant.values)))),
  );
  protected readonly selectors = computed<VariantSelectorGroup[]>(() => {
    const variants = this.variants();
    const keys = this.attributeKeys();
    const selectedValues = this.selectedValues();

    return keys.map((key, index) => {
      const previousKeys = keys.slice(0, index);
      const compatibleVariants = variants.filter((variant) =>
        previousKeys.every((previousKey) =>
          this.sameValue(variant.values[previousKey], selectedValues[previousKey]),
        ),
      );

      return {
        key,
        label: this.formatVariantLabel(key),
        options: this.optionsFor(compatibleVariants, key),
      };
    });
  });

  constructor() {
    let initializedResetToken = false;
    let previousResetToken = 0;

    effect(() => {
      const resetToken = this.resetToken();

      untracked(() => {
        if (!initializedResetToken) {
          initializedResetToken = true;
          previousResetToken = resetToken;
          return;
        }

        if (resetToken === previousResetToken) return;

        previousResetToken = resetToken;
        this.selectedValues.set({});
        this.selectedVariant.set(null);
      });
    });

    effect(() => {
      const variants = this.variants();
      const selectedVariant = this.selectedVariant();
      const autoSelectFirst = this.autoSelectFirst();

      untracked(() => {
        if (variants.length === 0) {
          this.selectedValues.set({});
          if (selectedVariant !== null) this.selectedVariant.set(null);
          return;
        }

        const selected = variants.find((variant) => Object.is(variant.id, selectedVariant));

        if (!selected && !autoSelectFirst) {
          this.selectedValues.set(this.reconcileManualSelection(this.selectedValues(), variants));
          if (selectedVariant !== null) this.selectedVariant.set(null);
          return;
        }

        const resolvedSelection = selected ?? variants[0];
        this.selectedValues.set({ ...resolvedSelection.values });
        if (!Object.is(resolvedSelection.id, selectedVariant)) {
          this.selectedVariant.set(resolvedSelection.id);
        }
      });
    });
  }

  protected onValueChange(key: string, value: VariantAttributeValue | null): void {
    const variants = this.variants();
    const keys = this.attributeKeys();
    const changedIndex = keys.indexOf(key);
    const values = { ...this.selectedValues() };

    if (value === null) delete values[key];
    else values[key] = value;

    for (let index = changedIndex + 1; index < keys.length; index++) {
      const currentKey = keys[index];
      const previousKeys = keys.slice(0, index);
      const compatibleVariants = variants.filter((variant) =>
        previousKeys.every((previousKey) =>
          this.sameValue(variant.values[previousKey], values[previousKey]),
        ),
      );
      const options = this.optionsFor(compatibleVariants, currentKey);

      if (!options.some((option) => this.sameValue(option.value, values[currentKey]))) {
        const firstOption = options[0];
        if (firstOption && this.autoSelectFirst()) values[currentKey] = firstOption.value;
        else delete values[currentKey];
      }
    }

    const matchingVariant = variants.find((variant) =>
      keys.every((attributeKey) =>
        this.sameValue(variant.values[attributeKey], values[attributeKey]),
      ),
    );

    this.selectedValues.set(values);
    this.selectedVariant.set(matchingVariant?.id ?? null);
    this.selectionValuesChange.emit({
      values: { ...values },
      selectedVariant: matchingVariant?.id ?? null,
    });
  }

  private optionsFor(variants: VariantSelectorVariant[], key: string): VariantSelectorOption[] {
    const options = new Map<string, VariantSelectorOption>();

    for (const variant of variants) {
      const value = variant.values[key];
      if (value === undefined || value === '') continue;

      const optionKey = this.valueKey(value);
      if (!options.has(optionKey)) {
        options.set(optionKey, {
          key: optionKey,
          label: this.valueLabel(value),
          value,
        });
      }
    }

    return Array.from(options.values());
  }

  private reconcileManualSelection(
    selectedValues: Record<string, VariantAttributeValue>,
    variants: VariantSelectorVariant[],
  ): Record<string, VariantAttributeValue> {
    const reconciled: Record<string, VariantAttributeValue> = {};

    for (const key of this.attributeKeys()) {
      const selectedValue = selectedValues[key];
      if (selectedValue === undefined) break;

      const compatibleVariants = variants.filter((variant) =>
        Object.entries(reconciled).every(([previousKey, previousValue]) =>
          this.sameValue(variant.values[previousKey], previousValue),
        ),
      );
      const selectionIsAvailable = this.optionsFor(compatibleVariants, key).some((option) =>
        this.sameValue(option.value, selectedValue),
      );

      if (!selectionIsAvailable) break;
      reconciled[key] = selectedValue;
    }

    return reconciled;
  }

  private sameValue(
    left: VariantAttributeValue | undefined,
    right: VariantAttributeValue | undefined,
  ): boolean {
    return (
      left !== undefined && right !== undefined && this.valueKey(left) === this.valueKey(right)
    );
  }

  private valueKey(value: VariantAttributeValue): string {
    const comparableValue = Array.isArray(value)
      ? value.map((item) => this.scalarValue(item))
      : this.scalarValue(value);

    return JSON.stringify(comparableValue);
  }

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

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

  private formatVariantLabel(key: string): string {
    const label = key.replace(/[_-]+/g, ' ');
    return label.charAt(0).toUpperCase() + label.slice(1);
  }
}
