import { DetailsComponentBase } from './details-component-base';
import { IBaseFleioObjectModel } from '@fleio-api/base-model/base-fleio-object.model';
import { AfterViewChecked, AfterViewInit, Directive, ViewChild } from '@angular/core';
import { FleioApiService } from '@fleio-api/fleio-api.service';
import { catchError } from 'rxjs/operators';
import { EMPTY, Observable } from 'rxjs';
import { AbstractControl, UntypedFormArray, UntypedFormGroup, ValidatorFn } from '@angular/forms';
import { HttpEvent } from '@angular/common/http';
import { FormErrorsHelper } from '@shared/ui-api/helpers/form-errors-helper';
import { UiHelper } from '@shared/ui-api/helpers/ui-helper';
import { FormErrorsComponent } from '@shared/error-handling/form-errors/form-errors.component';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { ConfigService } from '@shared/config/config.service';

@Directive()
export class DetailsFormBase<ObjectType extends IBaseFleioObjectModel>
  extends DetailsComponentBase<ObjectType> implements AfterViewChecked, AfterViewInit {
  @ViewChild('formErrors') protected formErrors: FormErrorsComponent;
  public backendErrors: any = {};
  public validationErrors: any = {};
  public fieldErrors: any = {};
  protected formGroup: UntypedFormGroup;
  protected focusFirstInput = true;
  protected skipFirstFocusableInput = false;

  private validated = false;

  private static scrollToTop() {
    window.scroll({
      top: 0,
      left: 0,
      behavior: 'smooth'
    });
  }

  ngAfterViewChecked() {
    if (!this.formGroup) {
      if (!this.formErrors) {
        this.validated = false;
        return;
      } else {
        this.formGroup = this.formErrors.formGroup;
        if (this.formGroup) {
          this.formGroup.valueChanges.subscribe(() => {
            this.formErrors.hide();
            this.validate();
          });
        }
      }
    }
  }

  ngAfterViewInit() {
    if (this.focusFirstInput) {
      UiHelper.focusAndSelectFirstInput(
        this.elementRef,
        this.changeDetector,
        false,
        this.skipFirstFocusableInput
      );
    }
  }

  private updateFieldErrors(formGroup: UntypedFormGroup, prefix: string) {
    for (const controlName of Object.keys(formGroup.controls)) {
      const control = formGroup.controls[controlName];
      const errorName = prefix ? `${prefix}.${controlName}` : controlName;
      if (control instanceof UntypedFormGroup) {
        this.updateFieldErrors(control, errorName);
      } else {
        if (this.backendErrors[controlName]) {
          this.fieldErrors[errorName] = this.backendErrors[controlName];
        }
      }
    }

    for (const fieldName of Object.keys(this.backendErrors)) {
      if (formGroup && formGroup.controls[fieldName]) {
        if (formGroup.controls[fieldName] instanceof UntypedFormArray && Array.isArray(this.backendErrors[fieldName])) {
          const formArray = formGroup.controls[fieldName] as UntypedFormArray;
          // for form arrays, show errors in order they came
          let index = 0;
          for (const controlError of this.backendErrors[fieldName]) {
            const formArrayControl = formArray.controls[index] as UntypedFormGroup;
            if (formArrayControl instanceof UntypedFormGroup) {
              for (const controlErrorFieldName of Object.keys(controlError)) {
                formArrayControl.controls[controlErrorFieldName].setErrors(controlError[controlErrorFieldName]);
              }
            }
            index++;
          }
        } else {
          this.fieldErrors[fieldName] = this.backendErrors[fieldName];
        }
      }
    }
  }

  private setValidationErrors(formGroup: UntypedFormGroup, prefix: string) {
    this.validationErrors = FormErrorsHelper.setValidationErrors(formGroup, prefix, this.validationErrors);
  }

  protected validate() {
    // clear errors before validation
    this.validationErrors = {};
    this.fieldErrors = {};

    if (this.formGroup) {
      if (this.formGroup.invalid) {
        this.setValidationErrors(this.formGroup, '');
        this.fieldErrors = {...this.validationErrors};
        // ensure backend errors are displayed if form is invalid
        this.updateFieldErrors(this.formGroup, '');
      } else {
        // clear backend errors if from is valid
        this.backendErrors = {};
      }
      this.validated = true;
    } else {
      console.error('Form cannot be validated, no <app-form-errors> component found.');
    }
  }

  protected setErrors(backendErrors: Record<string, any>, scrollToTop = true) {
    if (!this.validated) {
      console.error('Form cannot be validated, no <app-form-errors> component found.');
      console.error('setErrors called on a non validated form, aborting');
      return;
    }
    this.backendErrors = backendErrors;
    this.formErrors.setBackendErrors(backendErrors, this.formErrors.formGroup);
    this.updateFieldErrors(this.formGroup, '');
    if (scrollToTop) {
      DetailsFormBase.scrollToTop();
    }
  }

  private displayFormGroupErrors(formGroup: UntypedFormGroup | UntypedFormArray, scrollToTop = true) {
    Object.keys(formGroup.controls).map(name => {
      const control = formGroup.controls[name];
      if (control instanceof UntypedFormGroup) {
        this.displayFormGroupErrors(control, scrollToTop);
      } else if (control instanceof UntypedFormArray) {
        this.displayFormGroupErrors(control, scrollToTop);
      } else {
        if (control.invalid) {
          control.markAsTouched();
          if (scrollToTop) {
            DetailsFormBase.scrollToTop();
          }
        }
      }
    });
  }

  protected displayControlErrors(scrollToTop = true) {
    this.displayFormGroupErrors(this.formGroup, scrollToTop);
  }

  // TODO: rework this so it can be easily used in sub components
  protected createOrUpdate(
    api: FleioApiService<ObjectType>,
    value: ObjectType | FormData,
    raise: boolean = false,
    action?: string,
  ): Observable<ObjectType | HttpEvent<ObjectType>> {
    if (this.objectController.loading) {
      return EMPTY;
    }
    this.objectController.loading = true;
    this.validate();
    if (this.formGroup.invalid) {
      this.displayControlErrors();
      this.objectController.loading = false;
      return EMPTY;
    }

    let request;
    if (this.object.id) {
      if (value instanceof FormData) {
        throw new Error('Form data not supported on update');
      }
      value.id = this.object.id;
      request = api.update(value.id, value);
    } else {
      if (value instanceof FormData) {
        request = api.createWithUpload(value, action);
      } else {
        request = api.create(value);
      }
    }

    return request.pipe(catchError((error) => {
      this.objectController.loading = false;
      if (error.error) {
        this.setErrors(error.error);
        if (raise) {
          throw error;
        } else {
          return EMPTY;
        }
      } else {
        throw error;
      }
    }));
  }

  protected redirectAfterCreate(path: string, router: Router, config: ConfigService, queryParams = null) {
    const navigationExtras: NavigationExtras = {};
    if (!queryParams) {
      navigationExtras.queryParamsHandling = 'merge';
    } else {
      navigationExtras.queryParams = queryParams;
    }
    if (this.object?.id) {
      router.navigateByUrl(
        config.getPanelUrl(path + `/${this.object.id}`)
      ).catch(() => {
      });
    } else {
      router.navigate([config.getPanelUrl(path)], navigationExtras).catch(() => {
      });
    }
  }

  protected validObjectSelectedValidator(): ValidatorFn {
    return (control: AbstractControl): { [key: string]: any } | null => {
      const validObjectSelected = !control.value || control.value.id;
      return validObjectSelected ? null : {invalidObjectSelected: {value: control.value}};
    };
  }

  protected getRedirectQueryParamsAfterCreate(activatedRoute: ActivatedRoute): Record<string, string> {
    const redirectQueryParams = {} as Record<string, string>;
    if (activatedRoute.snapshot.queryParams?.filtering) {
      redirectQueryParams.filtering = activatedRoute.snapshot.queryParams.filtering;
    }
    if (activatedRoute.snapshot.queryParams?.search) {
      redirectQueryParams.search = activatedRoute.snapshot.queryParams.search;
    }
    return redirectQueryParams;
  }

  public ngSubmitForm() {
    if (this.objectController) {
      if (this.objectController?.dialogActionCallback) {
        this.objectController.dialogActionCallback();
      } else {
        this.objectController.actionCallback(this.primaryAction);
      }
    }
  }
}
