import { Observable } from 'rxjs';
import { ProgressTracker } from './progress-tracker';
import { Hypermedia, HypermediaAction, HypermediaField } from '../interfaces';
import { SessionService } from '@core/services';
import { Injectable, Type } from '@angular/core';
import { HeaderProvider } from './header-provider';
// import { ReflectiveModelBinderFactory } from 'hypermedia/models/services/reflective-model-binder';
// import { ReflectiveQueryMatcherFactory } from 'hypermedia/models/services/reflective-query-matcher';
import { filter, map, catchError } from 'rxjs/operators';
import { HttpClient,  HttpErrorResponse } from '@angular/common/http';
import { ReflectiveModelBinderFactory } from '../../models/services/reflective-model-binder';
import { ReflectiveQueryMatcherFactory } from '../../models/services/reflective-query-matcher';

@Injectable()
export class ActionExecutor {
    private readonly fakeResponse = {
        500: {
            class: ['error'],
            properties: {
                msg: 'error.message.internalServerError'
            }
        }
    };

    constructor(private http: HttpClient,
        private modelBinderFactory: ReflectiveModelBinderFactory,
        private progressTracker: ProgressTracker,
        private headerProvider: HeaderProvider,
        private queryMatcherFactory: ReflectiveQueryMatcherFactory,
        private sessionService: SessionService) { }

    execute(action: HypermediaAction, fieldValues?: any, authorize: boolean = true, tokenOverride?: string): Observable<Hypermedia> {
        const fields = this.computeActionFields(action.fields, fieldValues);

        return this.http.request(action.method, action.href, {
            body: {
                name: action.name,
                fields
            },
            headers: this.headerProvider.getHeaders(authorize, tokenOverride),
            reportProgress: true
        }).pipe(
            map(response => this.computeHypermediaResponse(response)),
            catchError(error => this.handleActionErrors(error))
        );
    }

    executeAs<T>(action: HypermediaAction, type: Type<T>, fieldValues?: any, authorize: boolean = true): Observable<T> {
        const modelBinder = this.modelBinderFactory.make(type);
        const queryMatcher = this.queryMatcherFactory.make(type);

        return this.execute(action, fieldValues, authorize)
            .pipe(
                filter(response => queryMatcher.matches(response)),
                map(response => modelBinder.bind(response))
            )
    }

    computeHypermediaResponse(response: Response | any): Hypermedia {
        try {
            return response as Hypermedia;
        } catch (err) {
            return { class: ['no-content'] } as Hypermedia;
        }
    }

    computeActionFields(fields: Array<HypermediaField> = [], overrides: any = {}): any {
        const fieldValues = fields.reduce((acc, { name, value }) => ({ ...acc, [name]: value }), {});

        return { ...fieldValues, ...overrides };
    }

    private handleActionErrors(responseError: HttpErrorResponse): Array<Hypermedia> {
        if (!(responseError instanceof HttpErrorResponse)) {
            return [];
        }

        const { status } = responseError;
        const responseData = responseError.error;
        const { class: classes } = responseData; 

        if (status === 401) {
            this.intercept401(classes);
        }

        if (classes instanceof Array) {
            // if it's a valid hypermedia, emit on error
            return [responseData]; 
        }

        return [this.fakeResponse[status]];
    }

    private intercept401(classes: Array<string>): void {
        const requiredClasses = ['external', 'redirect', 'unauthorized'];

        if (requiredClasses.every(requiredClass => classes.includes(requiredClass))) {
            this.sessionService.renew();
        }
    }
}
