import {
  ChangeDetectionStrategy,
  Component,
  Injector,
  computed,
  forwardRef,
  inject,
  input,
  output,
  signal
} from '@angular/core';
import { AbstractControl, ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
import {
  NgFooterTemplateDirective,
  NgHeaderTemplateDirective,
  NgLabelTemplateDirective,
  NgMultiLabelTemplateDirective,
  NgOptionTemplateDirective,
  NgSelectComponent
} from '@ng-select/ng-select';

import { FormField, FormLabelPosition } from '../form-field/form-field';
import { ValidationMessageMap } from '../form-validation-message/form-validation-message';
import {
  FormSelectDropdownPosition,
  FormSelectMode,
  FormSelectOption,
  FormSelectPrimitive,
  FormSelectScrollEvent,
  FormSelectSearchEvent,
  FormSelectSearchMode,
  FormSelectValue
} from '../models/form-select.models';

@Component({
  selector: 'app-form-select',
  standalone: true,
  imports: [
    FormsModule,
    FormField,
    NgFooterTemplateDirective,
    NgHeaderTemplateDirective,
    NgLabelTemplateDirective,
    NgMultiLabelTemplateDirective,
    NgOptionTemplateDirective,
    NgSelectComponent
  ],
  templateUrl: './form-select.html',
  styleUrl: './form-select.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => FormSelect),
      multi: true
    }
  ]
})
export class FormSelect<TValue extends FormSelectPrimitive = string> implements ControlValueAccessor {
  private static nextId = 0;

  private readonly injector = inject(Injector);

  private readonly generatedInputId = `form-select-${FormSelect.nextId++}`;

  readonly inputId = input<string | null>(null);

  readonly label = input('');
  readonly variant = input<'default' | 'floating'>('default');

  readonly name = input<string | null>(null);

  readonly options = input<readonly FormSelectOption<TValue>[]>([]);

  readonly mode = input<FormSelectMode>('single');

  readonly searchable = input(true);

  readonly searchMode = input<FormSelectSearchMode>('client');

  readonly editableSearchTerm = input<boolean | null>(null);

  readonly clearable = input(true);

  readonly hideSelected = input(false);

  readonly closeOnSelect = input<boolean | null>(null);

  readonly maxSelectedItems = input<number | null>(null);

  readonly placeholder = input('');

  readonly required = input(false);

  readonly disabled = input(false);

  readonly readonly = input(false);

  readonly loading = input(false);

  readonly loadingText = input('Loading...');

  readonly notFoundText = input('No options found');

  readonly typeToSearchText = input('Type to search');

  readonly clearAllText = input('Clear all');

  readonly appendTo = input('');

  readonly dropdownPosition = input<FormSelectDropdownPosition>('auto');

  readonly virtualScroll = input(false);

  readonly bufferAmount = input(4);

  readonly groupBy = input('');

  readonly selectableGroup = input(false);

  readonly selectableGroupAsModel = input(false);

  readonly selectOnTab = input(true);

  readonly clearOnBackspace = input(true);

  readonly showCheckboxes = input(true);

  readonly showSelectAll = input(true);

  readonly showMultiSelectFooter = input(false);

  readonly confirmLabel = input('OK');

  readonly cancelLabel = input('Cancel');

  readonly selectAllLabel = input('Select All');

  readonly hideValidation = input(false);

  readonly showValidationWhenDirty = input(false);

  readonly submitAttempted = input(false);

  readonly validationMessages = input<ValidationMessageMap>({});

  readonly description = input<string | null>(null);

  readonly hint = input<string | null>(null);

  readonly labelPosition = input<FormLabelPosition>('top');

  readonly hideLabel = input(false);

  readonly wrapperClass = input('');

  readonly labelClass = input('');

  readonly fieldContentClass = input('');

  readonly selectClass = input('');

  readonly ariaLabel = input<string | null>(null);

  readonly ariaDescription = input<string | null>(null);

  readonly selectionChanged = output<FormSelectValue<TValue>>();

  readonly searchChanged = output<FormSelectSearchEvent>();

  readonly opened = output<void>();

  readonly closed = output<void>();

  readonly cleared = output<void>();

  readonly focused = output<void>();

  readonly blurred = output<void>();

  readonly scrolled = output<FormSelectScrollEvent>();

  readonly scrolledToEnd = output<void>();

  readonly value = signal<FormSelectValue<TValue>>(null);

  readonly pendingValue = signal<readonly TValue[]>([]);

  readonly formDisabled = signal(false);

  readonly dropdownOpen = signal(false);

  readonly searchTerm = signal('');
  readonly hasFocus = signal(false);

  private onChange: (value: FormSelectValue<TValue>) => void = () => {};

  private onTouched: () => void = () => {};

  readonly control = computed<AbstractControl | null>(() => {
    return this.injector.get(NgControl, null, {
      self: true,
      optional: true
    })?.control ?? null;
  });

  readonly resolvedInputId = computed(() =>
    this.inputId()?.trim() || this.generatedInputId
  );

  readonly resolvedName = computed(() =>
    this.name()?.trim() || this.resolvedInputId()
  );

  readonly isMultiple = computed(() => this.mode() === 'multiple');

  readonly isDisabled = computed(() =>
    this.disabled() ||
    this.formDisabled() ||
    this.loading()
  );

  readonly isFloating = computed(() => this.variant() === 'floating');

  readonly hasValue = computed(() => {
    const currentValue = this.value();
    return Array.isArray(currentValue) ? currentValue.length > 0 : currentValue !== null && currentValue !== '';
  });

  readonly shouldFloat = computed(() => this.hasFocus() || this.dropdownOpen() || this.hasValue());

  readonly resolvedReadonly = computed(() =>
    this.readonly() ||
    this.isDisabled()
  );

  readonly resolvedCloseOnSelect = computed(() =>
    this.closeOnSelect() ?? !this.isMultiple()
  );

  readonly resolvedEditableSearchTerm = computed(() =>
    this.editableSearchTerm() ?? false
  );

  readonly resolvedOptions = computed<readonly FormSelectOption<TValue>[]>(() => {
    const options = this.options();
    const term = this.searchTerm().trim().toLocaleLowerCase();

    if (
      !this.searchable() ||
      this.searchMode() === 'server' ||
      !term
    ) {
      return options;
    }

    return options.filter(option =>
      this.optionMatchesSearchTerm(option, term)
    );
  });

  readonly selectItems = computed<readonly FormSelectOption<TValue>[]>(() => {
    const selected = new Set(this.activeSelectedValues());
    const selectedOptions = this.options()
      .filter(option => selected.has(option.value));
    const optionValues = new Set(selectedOptions.map(option => option.value));
    const visibleOptions = this.resolvedOptions()
      .filter(option => !optionValues.has(option.value));

    return [
      ...selectedOptions,
      ...visibleOptions
    ];
  });

  readonly showDropdownHeader = computed(() =>
    this.searchable() ||
    (
      this.isMultiple() &&
      this.showSelectAll()
    )
  );

  readonly selectedValues = computed<readonly TValue[]>(() => {
    const currentValue = this.value();

    return Array.isArray(currentValue)
      ? currentValue
      : [];
  });

  readonly modelValue = computed<FormSelectValue<TValue>>(() => {
    if (
      this.isMultiple() &&
      this.showMultiSelectFooter() &&
      this.dropdownOpen()
    ) {
      return this.pendingValue();
    }

    return this.value();
  });

  readonly allEnabledValues = computed<readonly TValue[]>(() =>
    this.options()
      .filter(option => !option.disabled)
      .map(option => option.value)
  );

  readonly allSelected = computed(() => {
    const selected = new Set(
      this.showMultiSelectFooter()
        ? this.pendingValue()
        : this.selectedValues()
    );

    const enabledValues = this.allEnabledValues();

    return (
      enabledValues.length > 0 &&
      enabledValues.every(value => selected.has(value))
    );
  });

  readonly partiallySelected = computed(() => {
    const selected = new Set(
      this.showMultiSelectFooter()
        ? this.pendingValue()
        : this.selectedValues()
    );

    const selectedCount = this.allEnabledValues()
      .filter(value => selected.has(value))
      .length;

    return (
      selectedCount > 0 &&
      selectedCount < this.allEnabledValues().length
    );
  });

  readonly selectedCount = computed(() =>
    this.selectedValues().length
  );

  readonly resolvedPlaceholder = computed(() =>
    this.placeholder().trim() ||
    (
      this.label().trim()
        ? `Select ${this.label().trim()}`
        : 'Select'
    )
  );

  readonly resolvedSearchPlaceholder = computed(() =>
    this.placeholder().trim() ||
    (
      this.label().trim()
        ? `Search ${this.label().trim()}`
        : 'Search'
    )
  );

  

  readonly describedBy = computed(() => {
    const ids: string[] = [];
    const control = this.control();

    if (this.description()) {
      ids.push(`${this.resolvedInputId()}-description`);
    }

    if (this.hint()) {
      ids.push(`${this.resolvedInputId()}-hint`);
    }

    if (
      control?.invalid &&
      (
        this.submitAttempted() ||
        (this.showValidationWhenDirty() && control.dirty)
      )
    ) {
      ids.push(`${this.resolvedInputId()}-validation`);
    }

    return ids.length ? ids.join(' ') : null;
  });

  readonly inputAttrs = computed<Record<string, string>>(() => {
    const attrs: Record<string, string> = {
      name: this.resolvedName(),
      'data-form-control': ''
    };

    const describedBy = this.describedBy();
    const ariaDescription = this.ariaDescription();

    if (describedBy) {
      attrs['aria-describedby'] = describedBy;
    }

    if (ariaDescription) {
      attrs['aria-description'] = ariaDescription;
    }

    if (this.required()) {
      attrs['aria-required'] = 'true';
    }

    if (this.control()?.invalid && this.submitAttempted()) {
      attrs['aria-invalid'] = 'true';
    }

    return attrs;
  });

  readonly resolvedSelectClass = computed(() => {
    const control = this.control();
    const showInvalidState = !!(
      control?.invalid &&
      (
        this.submitAttempted() ||
        (this.showValidationWhenDirty() && control.dirty)
      )
    );

    return [
      'ti-form-select',
      'rounded-sm',
      'border-defaultborder',
      'text-defaulttextcolor',
      'dark:border-defaultborder/10',
      'dark:text-white/70',
      'w-full',
      'app-form-select-control',
      showInvalidState ? 'is-invalid' : '',
      this.isDisabled() ? 'opacity-60 pointer-events-none' : '',
      this.selectClass()
    ]
      .filter(Boolean)
      .join(' ');
  });

  writeValue(value: FormSelectValue<TValue>): void {
    this.value.set(this.normalizeValue(value));
  }

  registerOnChange(fn: (value: FormSelectValue<TValue>) => void): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  setDisabledState(disabled: boolean): void {
    this.formDisabled.set(disabled);
  }

  onValueChange(incomingValue: FormSelectValue<TValue>): void {
    const normalizedValue = this.normalizeValue(incomingValue);

    if (
      this.isMultiple() &&
      this.showMultiSelectFooter() &&
      this.dropdownOpen()
    ) {
      this.pendingValue.set(
        Array.isArray(normalizedValue)
          ? normalizedValue
          : []
      );

      return;
    }

    this.commitValue(normalizedValue);
  }

  onSearchInput(event: Event): void {
    const inputElement = event.target instanceof HTMLInputElement
      ? event.target
      : null;
    const term = inputElement?.value ?? '';

    this.searchTerm.set(term);
    this.searchChanged.emit({
      term: term.trim()
    });
  }

  toggleSelectAll(): void {
    if (!this.isMultiple() || this.isDisabled()) {
      return;
    }

    const nextValue = this.allSelected()
      ? []
      : [...this.allEnabledValues()];

    if (
      this.showMultiSelectFooter() &&
      this.dropdownOpen()
    ) {
      this.pendingValue.set(nextValue);
      return;
    }

    this.commitValue(nextValue);
  }

  onOpen(): void {
    this.dropdownOpen.set(true);
    this.searchTerm.set('');

    if (
      this.isMultiple() &&
      this.showMultiSelectFooter()
    ) {
      this.pendingValue.set([
        ...this.selectedValues()
      ]);
    }

    this.opened.emit();
  }

  onClose(): void {
    this.dropdownOpen.set(false);
    this.searchTerm.set('');
    this.onTouched();
    this.closed.emit();
  }

  onFocus(): void {
    this.hasFocus.set(true);
    this.focused.emit();
  }

  onBlur(): void {
    this.hasFocus.set(false);
    this.onTouched();
    this.blurred.emit();
  }

  onClear(): void {
    this.searchTerm.set('');
    this.cleared.emit();
  }

  onScroll(event: FormSelectScrollEvent): void {
    this.scrolled.emit(event);
  }

  onScrollToEnd(): void {
    this.scrolledToEnd.emit();
  }

  confirmSelection(select: NgSelectComponent): void {
    if (!this.isMultiple()) {
      return;
    }

    this.commitValue([
      ...this.pendingValue()
    ]);

    select.close();
  }

  cancelSelection(select: NgSelectComponent): void {
    this.pendingValue.set([
      ...this.selectedValues()
    ]);

    select.close();
  }

  isOptionSelected(option: FormSelectOption<TValue>): boolean {
    const value = option.value;
    const source = this.activeSelectedValues();

    return source.includes(value);
  }

  getMultiLabelText(items: readonly FormSelectOption<TValue>[]): string {
    const firstLabel = items[0]?.label ?? '';
    const remainingCount = items.length - 1;

    return remainingCount > 0
      ? `${firstLabel} +${remainingCount}`
      : firstLabel;
  }

  trackOption(option: FormSelectOption<TValue>): TValue {
    return option.value;
  }

  getOptionContainerClass(option: FormSelectOption<TValue>): string {
    return [
      'flex min-w-0 w-full items-start gap-2 text-inherit',
      option.disabled ? 'text-textmuted opacity-60' : '',
      this.isOptionSelected(option) && !option.disabled ? 'text-white' : ''
    ]
      .filter(Boolean)
      .join(' ');
  }

  getOptionLabelClass(option: FormSelectOption<TValue>): string {
    return [
      'block truncate text-inherit',
      option.disabled ? 'text-textmuted' : ''
    ]
      .filter(Boolean)
      .join(' ');
  }

  getOptionDisplayLabel(
    option: FormSelectOption<TValue>,
    resolvedLabel: string | null | undefined
  ): string {
    return this.cleanText(option.label) || this.cleanText(resolvedLabel);
  }

  getSelectionPrefixText(item: FormSelectOption<TValue> | TValue): string {
    return this.isOption(item)
      ? item.prefixText?.trim() ?? ''
      : '';
  }

  getSelectionDisplayLabel(
    item: FormSelectOption<TValue> | TValue,
    resolvedLabel: string | null | undefined
  ): string {
    if (this.isOption(item)) {
      return this.cleanText(item.label) || this.cleanText(resolvedLabel);
    }

    return this.cleanText(resolvedLabel) || this.findOptionLabel(item);
  }

  getOptionPrefixClass(option: FormSelectOption<TValue>): string {
    return [
      'shrink-0 rounded-sm px-1.5 py-0.5 text-[0.6875rem] font-medium',
      option.disabled
        ? 'bg-light text-textmuted dark:bg-black/20'
        : 'bg-light text-primary dark:bg-black/20'
    ]
      .filter(Boolean)
      .join(' ');
  }

  private commitValue(value: FormSelectValue<TValue>): void {
    this.value.set(value);
    this.onChange(value);
    this.selectionChanged.emit(value);
  }

  private activeSelectedValues(): readonly TValue[] {
    if (
      this.isMultiple() &&
      this.showMultiSelectFooter() &&
      this.dropdownOpen()
    ) {
      return this.pendingValue();
    }

    const currentValue = this.value();

    if (this.isValueArray(currentValue)) {
      return currentValue;
    }

    return currentValue === null
      ? []
      : [currentValue];
  }

  private normalizeValue(value: FormSelectValue<TValue>): FormSelectValue<TValue> {
    if (this.isMultiple()) {
      return this.isValueArray(value)
        ? value.filter(item => !this.isEmptyStringValue(item))
        : [];
    }

    return this.isValueArray(value)
      ? this.normalizeSingleValue(value[0] ?? null)
      : this.normalizeSingleValue(value);
  }

  private normalizeSingleValue(value: TValue | null): TValue | null {
    return this.isEmptyStringValue(value)
      ? null
      : value ?? null;
  }

  private isValueArray(value: FormSelectValue<TValue>): value is readonly TValue[] {
    return Array.isArray(value);
  }

  private isEmptyStringValue(value: TValue | null): boolean {
    return typeof value === 'string' && value.trim() === '';
  }

  private isOption(value: FormSelectOption<TValue> | TValue): value is FormSelectOption<TValue> {
    return (
      typeof value === 'object' &&
      value !== null &&
      'value' in value &&
      'label' in value
    );
  }

  private findOptionLabel(value: TValue): string {
    return this.options()
      .find(option => option.value === value)
      ?.label
      ?.trim() ?? '';
  }

  private optionMatchesSearchTerm(
    option: FormSelectOption<TValue>,
    term: string
  ): boolean {
    return [
      option.label,
      option.prefixText ?? '',
      option.description ?? ''
    ].some(value =>
      this.cleanText(value).toLocaleLowerCase().includes(term)
    );
  }

  private cleanText(value: string | null | undefined): string {
    return value?.trim() ?? '';
  }
}
