import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedPosition } from '@angular/cdk/overlay';
import {
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  Injector,
  ViewChild,
  computed,
  effect,
  forwardRef,
  inject,
  input,
  output,
  signal
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AbstractControl, ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
import {
  Subject,
  catchError,
  debounce,
  distinctUntilChanged,
  map,
  of,
  switchMap,
  timer
} from 'rxjs';

import { FormField, FormLabelPosition } from '../form-field/form-field';
import { ValidationMessageMap } from '../form-validation-message/form-validation-message';
import {
  AutocompleteDisplayFn,
  AutocompleteResolveValueFn,
  AutocompleteSearchFn,
  AutocompleteTrackFn,
  AutocompleteValueFn
} from './autocomplete.types';

interface SearchResult<TItem> {
  readonly term: string;
  readonly options: readonly TItem[];
  readonly failed: boolean;
  readonly preview: boolean;
}

interface SearchRequest {
  readonly term: string;
  readonly preview: boolean;
}

@Component({
  selector: 'app-autocomplete',
  standalone: true,
  imports: [CdkConnectedOverlay, CdkOverlayOrigin, FormField],
  templateUrl: './autocomplete.html',
  styleUrl: './autocomplete.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => Autocomplete),
    multi: true
  }]
})
export class Autocomplete<TItem, TValue> implements ControlValueAccessor {
  private static nextId = 0;

  private readonly injector = inject(Injector);
  private readonly generatedId = `autocomplete-${Autocomplete.nextId++}`;
  private readonly searchRequests$ = new Subject<SearchRequest>();
  private readonly valuesToResolve$ = new Subject<TValue>();
  private formValue: TValue | null = null;
  private readonly controlStateVersion = signal(0);
  private labelEdited = false;
  private onChange: (value: TValue | null) => void = () => {};
  private onTouched: () => void = () => {};

  @ViewChild('textInput') private textInput?: ElementRef<HTMLInputElement>;
  @ViewChild(CdkOverlayOrigin, { read: ElementRef }) private origin?: ElementRef<HTMLElement>;

  readonly searchFn = input.required<AutocompleteSearchFn<TItem>>();
  readonly displayWith = input.required<AutocompleteDisplayFn<TItem>>();
  readonly valueWith = input.required<AutocompleteValueFn<TItem, TValue>>();
  readonly trackBy = input<AutocompleteTrackFn<TItem> | null>(null);
  readonly selectedItem = input<TItem | null>(null);
  readonly resolveValueFn = input<AutocompleteResolveValueFn<TItem, TValue> | null>(null);
  readonly label = input('');
  readonly inputId = input<string | null>(null);
  readonly placeholder = input('Search...');
  readonly minSearchLength = input(1);
  readonly debounceTime = input(300);
  readonly limit = input(10);
  readonly previewLimit = input(5);
  readonly previewText = input('Type to search more...');
  readonly disabled = input(false);
  readonly readonly = input(false);
  readonly clearable = input(true);
  readonly required = input(false);
  readonly loadingText = input('Loading...');
  readonly emptyText = input('No results found');
  readonly typeToSearchText = input('Type to search');
  readonly errorText = input('Unable to load results');
  readonly showDropdownOnFocus = input(true);
  readonly closeOnSelect = input(true);
  readonly autocomplete = input('off');
  readonly ariaLabel = input<string | null>(null);
  readonly panelClass = input('');
  readonly inputClass = input('');
  readonly wrapperClass = input('');
  readonly hideLabel = input(false);
  readonly hideValidation = input(false);
  readonly submitAttempted = input(false);
  readonly validationMessages = input<ValidationMessageMap>({});
  readonly description = input<string | null>(null);
  readonly hint = input<string | null>(null);
   readonly help = input<string | null>(null);
  readonly labelPosition = input<FormLabelPosition>('top');
  readonly variant = input<'default' | 'floating'>('default');
  readonly size = input<'default' | 'sm'>('default');

  readonly itemSelected = output<TItem>();
  readonly cleared = output<void>();
  readonly searchChanged = output<string>();
  readonly opened = output<void>();
  readonly closed = output<void>();
  readonly loadError = output<void>();

  readonly isOpen = signal(false);
  readonly loading = signal(false);
  readonly options = signal<readonly TItem[]>([]);
  readonly activeIndex = signal(-1);
  readonly activeItem = signal<TItem | null>(null);
  readonly searchText = signal('');
  readonly error = signal<string | null>(null);
  readonly showingPreview = signal(false);
  readonly formDisabled = signal(false);
  readonly panelWidth = signal(0);
  readonly focused = signal(false);

  readonly resolvedInputId = computed(() => this.inputId()?.trim() || this.generatedId);
  readonly panelId = computed(() => `${this.resolvedInputId()}-listbox`);
  readonly statusId = computed(() => `${this.resolvedInputId()}-status`);
  readonly describedBy = computed(() =>
    this.showError()
      ? `${this.statusId()} ${this.resolvedInputId()}-validation`
      : this.statusId()
  );
  readonly control = computed<AbstractControl | null>(() =>
    this.injector.get(NgControl, null, { self: true, optional: true })?.control ?? null
  );
  readonly isDisabled = computed(() => this.disabled() || this.formDisabled());
  readonly isFloating = computed(() => this.variant() === 'floating');
  readonly shouldFloat = computed(() =>
    this.focused() || this.isOpen() || this.hasSelectedItem() || this.searchText().length > 0
  );
  readonly showError = computed(() => {
    this.controlStateVersion();
    return !!this.control()?.invalid && this.submitAttempted();
  });
  readonly hasSelectedItem = computed(() => this.activeItem() !== null);
  readonly activeDescendant = computed(() => {
    const index = this.activeIndex();
    return this.isOpen() && index >= 0 ? `${this.resolvedInputId()}-option-${index}` : null;
  });
  readonly message = computed(() => {
    if (this.loading()) return this.loadingText();
    if (this.error()) return this.errorText();
    if (this.showingPreview()) return '';
    if (this.searchText().trim().length < this.minSearchLength()) {
      return `${this.typeToSearchText()} (at least ${this.minSearchLength()} ${this.minSearchLength() === 1 ? 'character' : 'characters'})`;
    }
    return this.options().length ? '' : this.emptyText();
  });
  readonly resolvedInputClass = computed(() => {
    this.controlStateVersion();
    const control = this.control();
    const invalid = !!(control?.invalid && this.submitAttempted());
    return [
      this.isFloating()
        ? 'autocomplete-input autocomplete-input--floating'
        : 'ti-form-select w-full rounded-sm border border-defaultborder bg-white text-defaulttextcolor',
      this.isFloating() ? '' : 'dark:border-defaultborder/10 dark:bg-bodybg dark:text-white/70',
      'placeholder:text-textmuted placeholder:opacity-100 dark:placeholder:text-white/50',
      'focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none',
      'pe-10 transition-colors',
      invalid ? 'is-invalid border-danger' : '',
      this.isDisabled() ? 'cursor-not-allowed bg-light opacity-60 dark:bg-black/20' : '',
      this.inputClass()
    ].filter(Boolean).join(' ');
  });

  readonly overlayPositions: ConnectedPosition[] = [
    { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
    { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 }
  ];

  constructor() {
    effect((onCleanup) => {
      const control = this.control();
      if (!control) return;

      const statusSubscription = control.statusChanges.subscribe(() => {
        this.controlStateVersion.update(value => value + 1);
      });
      const valueSubscription = control.valueChanges.subscribe(() => {
        this.controlStateVersion.update(value => value + 1);
      });
      const eventsSubscription = control.events?.subscribe(() => {
        this.controlStateVersion.update(value => value + 1);
      });

      onCleanup(() => {
        statusSubscription.unsubscribe();
        valueSubscription.unsubscribe();
        eventsSubscription?.unsubscribe();
      });
    });

    this.searchRequests$.pipe(
      map(request => ({ ...request, term: request.term.trim() })),
      debounce(request => timer(request.preview ? 0 : Math.max(0, this.debounceTime()))),
      distinctUntilChanged((previous, current) =>
        previous.term === current.term && previous.preview === current.preview
      ),
      switchMap(request => {
        if (!request.preview && request.term.length < this.minSearchLength()) {
          return of<SearchResult<TItem>>({ ...request, options: [], failed: false });
        }
        this.loading.set(true);
        this.error.set(null);
        const limit = request.preview ? this.previewLimit() : this.limit();
        return this.searchFn()(request.term, limit).pipe(
          map(options => ({ ...request, options, failed: false })),
          catchError(() => of<SearchResult<TItem>>({ ...request, options: [], failed: true }))
        );
      }),
      takeUntilDestroyed()
    ).subscribe(result => {
      this.loading.set(false);
      this.showingPreview.set(result.preview && !result.failed);
      this.options.set(result.options);
      this.activeIndex.set(-1);
      this.error.set(result.failed ? this.errorText() : null);
      if (result.failed) this.loadError.emit();
    });

    this.valuesToResolve$.pipe(
      switchMap(value => this.resolveValueFn()?.(value) ?? of(null)),
      takeUntilDestroyed()
    ).subscribe(item => this.applyResolvedItem(item));

    effect(() => {
      const item = this.selectedItem();
      if (item === null) {
        if (this.formValue === null) this.applyResolvedItem(null);
        return;
      }
      if (this.valuesEqual(this.valueWith()(item), this.formValue)) this.applyResolvedItem(item);
    });
  }

  writeValue(value: TValue | null): void {
    this.formValue = value ?? null;
    this.labelEdited = false;
    if (!this.hasResolvableValue(this.formValue)) {
      this.applyResolvedItem(null);
      return;
    }
    const supplied = this.selectedItem();
    if (supplied !== null && this.valuesEqual(this.valueWith()(supplied), this.formValue)) {
      this.applyResolvedItem(supplied);
    } else {
      this.applyResolvedItem(null);
      if (this.resolveValueFn()) this.valuesToResolve$.next(this.formValue);
    }
  }

  registerOnChange(fn: (value: TValue | null) => void): void { this.onChange = fn; }
  registerOnTouched(fn: () => void): void { this.onTouched = fn; }
  setDisabledState(disabled: boolean): void {
    this.formDisabled.set(disabled);
    if (disabled) this.close();
  }

  onInput(event: Event): void {
    if (!(event.target instanceof HTMLInputElement) || this.isDisabled() || this.readonly()) return;
    const text = event.target.value;
    const previousText = this.searchText();
    this.searchText.set(text);
    this.activeItem.set(null);
    this.showingPreview.set(false);
    this.options.set([]);
    this.labelEdited = this.formValue !== null && text !== previousText;
    this.searchChanged.emit(text.trim());
    this.open();
    this.searchRequests$.next({ term: text, preview: false });
  }

  onFocus(): void {
    this.focused.set(true);
    if (this.showDropdownOnFocus()) this.openPreview();
  }

  onClick(): void {
    if (this.showDropdownOnFocus()) this.openPreview();
  }

  onControlClick(event: MouseEvent): void {
    if (event.target instanceof HTMLButtonElement || this.isDisabled() || this.readonly()) return;
    this.textInput?.nativeElement.focus();
    this.onClick();
  }

  onBlur(): void {
    this.focused.set(false);
    queueMicrotask(() => {
      this.onTouched();
      if (this.labelEdited) this.clearValue(false, false);
    });
  }

  onKeydown(event: KeyboardEvent): void {
    if (this.isDisabled() || this.readonly()) return;
    const options = this.options();
    switch (event.key) {
      case 'ArrowDown':
        event.preventDefault(); this.open(); this.setActive(Math.min(this.activeIndex() + 1, options.length - 1)); break;
      case 'ArrowUp':
        event.preventDefault(); this.open(); this.setActive(Math.max(this.activeIndex() - 1, 0)); break;
      case 'Home':
        if (this.isOpen() && options.length) { event.preventDefault(); this.setActive(0); } break;
      case 'End':
        if (this.isOpen() && options.length) { event.preventDefault(); this.setActive(options.length - 1); } break;
      case 'Enter': {
        const item = options[this.activeIndex()];
        if (this.isOpen() && item !== undefined) { event.preventDefault(); this.select(item); }
        break;
      }
      case 'Escape':
        if (this.isOpen()) { event.preventDefault(); this.close(); } break;
    }
  }

  select(item: TItem): void {
    if (this.isDisabled() || this.readonly()) return;
    this.activeItem.set(item);
    this.formValue = this.valueWith()(item);
    this.searchText.set(this.displayWith()(item));
    this.labelEdited = false;
    this.onChange(this.formValue);
    this.onTouched();
    this.itemSelected.emit(item);
    if (this.closeOnSelect()) this.close();
  }

  clear(event?: MouseEvent): void {
    event?.preventDefault();
    event?.stopPropagation();
    if (this.isDisabled() || this.readonly()) return;
    this.clearValue(true, true);
  }

  open(): void {
    if (this.isDisabled() || this.readonly() || this.isOpen()) return;
    this.panelWidth.set(this.origin?.nativeElement.getBoundingClientRect().width ?? 0);
    this.isOpen.set(true);
    this.opened.emit();
  }

  close(): void {
    if (!this.isOpen()) return;
    this.isOpen.set(false);
    this.activeIndex.set(-1);
    this.closed.emit();
  }

  optionId(index: number): string { return `${this.resolvedInputId()}-option-${index}`; }
  optionKey(item: TItem, index: number): string | number { return this.trackBy()?.(item) ?? index; }
  isOptionSelected(item: TItem): boolean {
    return this.formValue !== null && this.valuesEqual(this.valueWith()(item), this.formValue);
  }
  optionClass(item: TItem, index: number): string {
    const highlighted = this.activeIndex() === index;
    const selected = this.isOptionSelected(item);
    return [
      'block w-full px-3 py-2 text-start text-[0.8125rem] transition-colors',
      highlighted || selected
        ? 'bg-primary text-white'
        : 'bg-white text-defaulttextcolor hover:bg-primary hover:text-white dark:bg-bodybg dark:text-white/70 dark:hover:bg-primary dark:hover:text-white'
    ].join(' ');
  }

  private setActive(index: number): void {
    if (index < 0 || index >= this.options().length) return;
    this.activeIndex.set(index);
    queueMicrotask(() => document.getElementById(this.optionId(index))?.scrollIntoView({ block: 'nearest' }));
  }

  private clearValue(emitCleared: boolean, restoreFocus: boolean): void {
    this.formValue = null;
    this.activeItem.set(null);
    this.searchText.set('');
    this.options.set([]);
    this.error.set(null);
    this.loading.set(false);
    this.showingPreview.set(false);
    this.labelEdited = false;
    this.onChange(null);
    this.onTouched();
    if (emitCleared) this.cleared.emit();
    this.close();
    if (restoreFocus) queueMicrotask(() => this.textInput?.nativeElement.focus());
  }

  private applyResolvedItem(item: TItem | null, clearText = true): void {
    this.activeItem.set(item);
    if (item !== null) this.searchText.set(this.displayWith()(item));
    else if (clearText) this.searchText.set('');
    this.labelEdited = false;
  }

  private valuesEqual(left: TValue, right: TValue | null): boolean { return Object.is(left, right); }

  private hasResolvableValue(value: TValue | null): value is TValue {
    return value !== null && (typeof value !== 'string' || value.trim().length > 0);
  }

  private openPreview(): void {
    if (this.isDisabled() || this.readonly() || this.isOpen()) return;
    this.open();
    if (!this.searchText().trim()) {
      this.searchRequests$.next({ term: '', preview: true });
    }
  }
}
