import { ChangeDetectionStrategy, Component, Injector, computed, effect, forwardRef, inject, input, signal } from '@angular/core';

import { AbstractControl, ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';

import { FormField } from '../form-field/form-field';
import { FormValidationMessage } from '../form-validation-message/form-validation-message';
import { ValidationMessageMap } from '../form-validation-message/form-validation-message';

export type FormInputType = | 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search';

export type FormInputMode = | 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';

export type FormInputIconPosition = 'left' | 'right';
export type FormInputVariant = 'default' | 'floating';

@Component({
  selector: 'app-form-input',
  standalone: true,
  imports: [FormField, FormValidationMessage],
  templateUrl: './form-input.html',
  styleUrl: './form-input.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => FormInput),
      multi: true
    }
  ]
})
export class FormInput implements ControlValueAccessor {
  private readonly injector = inject(Injector);


  readonly inputId = input.required<string>();
  readonly label = input.required<string>();

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

  readonly type = input<FormInputType>('text');

  readonly placeholder = input('');
  readonly showPlaceholder = input(true);
  readonly useLabelAsPlaceholder = input(true);

  readonly required = input(false);
  readonly readonly = input(false);
  readonly disabled = input(false);

  readonly autocomplete = input<string | null>(null);
  readonly inputMode = input<FormInputMode | null>(null);

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

  readonly minLength = input<number | null>(null);
  readonly maxLength = input<number | null>(null);

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


  readonly min = input<number | null>(null);
  readonly max = input<number | null>(null);
  readonly step = input<number | string | null>(null);


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

  readonly hideValidation = input(false);
  readonly showValidationWhenDirty = input(false);
  readonly submitAttempted = input(false);

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

  readonly prefixIcon = input<string | null>(null);
  readonly suffixIcon = input<string | null>(null);

  readonly prefixIconClass = input('');
  readonly suffixIconClass = input('');

  readonly showPasswordToggle = input(true);
  readonly loading = input(false);

  readonly floating = input(false);
  readonly variant = input<FormInputVariant>('default');

  readonly wrapperClass = input('');
  readonly fieldContentClass = input('');
  readonly labelClass = input('');
  readonly inputClass = input('');


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


  readonly value = signal<string | number | null>(null);
  readonly formDisabled = signal(false);
  readonly passwordVisible = signal(false);
  readonly focused = signal(false);
  private readonly controlStateVersion = signal(0);

  private onChange: (value: string | number | null) => void = () => { };

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

  constructor() {
    effect(() => {
      if (this.type() !== 'password') {
        this.passwordVisible.set(false);
      }
    });

    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();
      });
    });
  }

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

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

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

  readonly hasValue = computed(() => {
    const currentValue = this.value();
    return currentValue !== '' && currentValue !== null && currentValue !== undefined;
  });

  readonly showError = computed(() => {
    this.controlStateVersion();
    const control = this.control();
    return !!control?.invalid && (this.submitAttempted() || (this.showValidationWhenDirty() && control.dirty));
  });

  readonly resolvedPlaceholder = computed(() => {
    if (!this.showPlaceholder()) {
      return '';
    }

    const configuredPlaceholder = this.placeholder().trim();

    if (configuredPlaceholder && !this.isFloating()) {
      return configuredPlaceholder;
    }

    if (this.isFloating()) {
      return ' ';
    }

    return this.useLabelAsPlaceholder()
      ? this.label()
      : null;
  });

  readonly resolvedType = computed<FormInputType>(() => {
    if (
      this.type() === 'password' &&
      this.passwordVisible()
    ) {
      return 'text';
    }

    return this.type();
  });

  readonly hasPrefixIcon = computed(() => {
    return !!this.prefixIcon();
  });

  readonly hasSuffixContent = computed(() => {
    return (
      !!this.suffixIcon() ||
      this.loading() ||
      (
        this.type() === 'password' &&
        this.showPasswordToggle()
      )
    );
  });

  readonly resolvedInputClass = computed(() => {
    this.controlStateVersion();

    const control = this.control();
    const showInvalidState = !!(
      control?.invalid &&
      (
        this.submitAttempted() ||
        (this.showValidationWhenDirty() && control.dirty)
      )
    );

    return [
      'form-control',
      this.hasPrefixIcon() ? '!ps-10' : '',
      this.hasSuffixContent() ? '!pe-10' : '',
      showInvalidState
        ? 'is-invalid'
        : '',
      this.inputClass()
    ]
      .filter(Boolean)
      .join(' ');
  });

  readonly describedBy = computed(() => {
    this.controlStateVersion();

    const ids: string[] = [];
    const control = this.control();

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

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

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

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

  writeValue(value: string | number | null): void {
    this.value.set(value ?? null);
  }

  registerOnChange(
    fn: (value: string | number | null) => void
  ): void {
    this.onChange = fn;
  }

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

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

  onInput(event: Event): void {
    const element = event.target as HTMLInputElement;

    const nextValue = this.resolveValue(element.value);

    this.value.set(nextValue);
    this.onChange(nextValue);
  }

  onBlur(): void {
    this.focused.set(false);
    this.onTouched();
  }

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

  togglePasswordVisibility(): void {
    if (
      this.type() !== 'password' ||
      this.isDisabled()
    ) {
      return;
    }

    this.passwordVisible.update(value => !value);
  }

  private resolveValue(
    rawValue: string
  ): string | number | null {
    if (this.type() !== 'number') {
      return rawValue;
    }

    if (rawValue === '') {
      return null;
    }

    const numericValue = Number(rawValue);

    return Number.isNaN(numericValue)
      ? null
      : numericValue;
  }
}
