
import {catchError, switchMap, filter, tap, map} from 'rxjs/operators';
import { AfterViewInit, Directive, EventEmitter, Host, Input, OnDestroy, Output } from '@angular/core';
import { FormControl, FormGroup, NgForm } from '@angular/forms';
import { Subscription } from 'rxjs';
import { ActionExecutor, Hypermedia, HypermediaAction } from 'first-npm-package-nicule/core';

export function actionNameFactory(ngForm: NgForm): string {
    return ngForm.name;
}

@Directive({
    selector: '[hmForm]'
})
export class FormDirective implements OnDestroy {
    private submitSubscription: Subscription;
    private action: HypermediaAction;

    @Output() submitSuccess = new EventEmitter();
    @Output() submitError = new EventEmitter();
    @Output() submitComplete = new EventEmitter();

    @Input()
    set hmForm(action: HypermediaAction | any) {
        if (action) {
            this.action = action;
            this.ngForm.name = action.name;
            this.subscribeToFormSubmit();
        }
    }

    @Input() tokenOverride;

    ngOnDestroy(): void {
        if (this.submitSubscription) {
            this.submitSubscription.unsubscribe();
        }
    }

    constructor(@Host() private ngForm: NgForm, private actionExecutor: ActionExecutor) { }

    private subscribeToFormSubmit(): void {
        if (this.submitSubscription) {
            this.submitSubscription.unsubscribe();
        }

        this.submitSubscription =
            this.ngForm
                .ngSubmit.pipe(
                map(event => event as Event),
                tap(event => event.preventDefault()),
                filter(_ => {
                    if (!this.isFormValid()) {
                        this.submitComplete.emit();
                    }

                    return this.isFormValid();
                }),
                switchMap(event => {
                    return this.tokenOverride ? this.actionExecutor.execute(this.action, this.ngForm.value, true, this.tokenOverride) : this.actionExecutor.execute(this.action, this.ngForm.value);
                }),
                catchError((err, caught) => { 
                    this.submitError.emit(err);
                    this.submitComplete.emit();
                    return []; 
                }),)
                .subscribe({ next: this.onResponse.bind(this) });
    }

    private isFormValid(): boolean {
        this.validateForm(this.ngForm.control);

        return this.ngForm.valid || Object.keys(this.ngForm.controls).length === 0;
    }

    validateForm(formGroup: FormGroup): void {
        Object.keys(formGroup.controls)
            .forEach(field => {
                const control = formGroup.get(field);
                if (control instanceof FormControl) {
                    control.markAsTouched({ onlySelf: true });
                    control.markAsDirty({ onlySelf: true });
                } else if (control instanceof FormGroup) {
                    this.validateForm(control);
                }
            });
    }

    computeClassifiedErrors(messages: Array<string>): any {
        return messages.reduce((acc, message) => ({ ...acc, [message.slice(message.lastIndexOf('.') + 1)]: message }), {});
    }

    private onResponse(hypermedia: Hypermedia): void {
        if (!hypermedia) {
            this.submitError.emit();
            this.submitComplete.emit();
            return;
        }
        const { class: classes, properties } = hypermedia;       

        if (classes && classes.includes('error')) {
            if (properties.validationErrors) {
                const validationErrors = properties.validationErrors;

                validationErrors.forEach(
                    validationError =>
                        this.ngForm
                            .form
                            .controls[validationError.name]
                            .setErrors(
                                this.computeClassifiedErrors(validationError.msg)));
                this.submitError.emit(validationErrors);
            }
            if (properties.msg) {
                this.submitError.emit(properties.msg);
            }
            if (classes.includes('service-unavailable')) {
                this.submitError.emit({ errorType: 'service-unavailable' });
            }
        } else {
            this.submitSuccess.emit(hypermedia);
        }

        this.submitComplete.emit();
    }
}
