import * as _ from 'lodash';
import { catchError, map } from 'rxjs/operators';
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpRequest } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Injectable } from '@angular/core';

@Injectable()
export class Api {
    private _endPoint = process.env.API;
    private _token: string = '';

    private _options: Record<string, unknown> = {
        headers: null,
        observe: 'response',
        withCredentials: true,
    };

    constructor(private _http: HttpClient) {}

    public get endPoint(): string {
        return this._endPoint;
    }

    public post(url: string, data: Record<string, unknown>, options?: Record<string, unknown>): Observable<unknown> {
        // Our wrapper that injects the options into HTTP service
        let httpOptions = _.clone(this._options);
        if (options) {
            httpOptions = this._mergeObjects(httpOptions, options);
        }
        httpOptions.headers = this._prepareHeaders(<HttpHeaders>httpOptions.headers);
        return this._http.post(this._endPoint + url, data, httpOptions).pipe(
            map((res: { body: unknown }) => res['body']),
            catchError(this._handleError()),
        );
    }

    public download(url: string, data: Record<string, unknown>, options?: Record<string, unknown>): Observable<unknown> {
        // Fetchs a file from the server
        let httpOptions = _.clone(this._options);
        httpOptions.responseType = 'blob';
        httpOptions.method = 'post';
        if (options) {
            httpOptions = this._mergeObjects(httpOptions, options);
        }
        httpOptions.headers = this._prepareHeaders(<HttpHeaders>httpOptions.headers);
        return this._http.post(this._endPoint + url, data, httpOptions).pipe(
            map((res: { body: unknown }) => res['body']),
            catchError(this._handleError()),
        );
    }

    public upload(url: string, data: File | File[], options?: Record<string, unknown>): Observable<unknown> {
        // Prepares a new HttpRequest with the reportProgress indicator
        let httpOptions = _.clone(this._options);
        httpOptions.reportProgress = true;
        if (options) {
            httpOptions = this._mergeObjects(httpOptions, options);
        }
        httpOptions.headers = this._prepareHeaders(<HttpHeaders>httpOptions.headers);
        const formData: FormData = new FormData();
        if (_.isArray(data)) {
            for (let i = 0; i < data.length; i++) {
                const item = data[i];
                formData.append('upload[]', item, item.name);
            }
        } else {
            formData.append('upload', data, data.name);
        }
        const req = new HttpRequest('POST', this._endPoint + url, formData, httpOptions);
        return this._http.request(req).pipe(
            map((res: unknown) => <{ body?: unknown }>res['body']),
            catchError(this._handleError()),
        );
    }

    public get(url: string, options?: Record<string, unknown>): Observable<unknown> {
        // Our wrapper that injects the options into HTTP service
        let httpOptions = _.clone(this._options);
        if (options) {
            httpOptions = this._mergeObjects(httpOptions, options);
        }
        httpOptions.headers = this._prepareHeaders(<HttpHeaders>httpOptions.headers);
        return this._http.get(this._endPoint + url, httpOptions).pipe(
            map((res: { body: unknown }) => res['body']),
            catchError(this._handleError()),
        );
    }

    public delete(url: string, options?: Record<string, unknown>): Observable<unknown> {
        // Our wrapper the injects the options into the HTTP service
        let httpOptions = _.clone(this._options);
        if (options) {
            httpOptions = this._mergeObjects(httpOptions, options);
        }
        httpOptions.headers = this._prepareHeaders(<HttpHeaders>httpOptions.headers);
        return this._http.delete(this._endPoint + url, httpOptions).pipe(
            map((res: { body: unknown }) => res['body']),
            catchError(this._handleError()),
        );
    }

    private _handleError(): (error: HttpErrorResponse | unknown) => Observable<{
        data: unknown;
        error: boolean;
    }> {
        return (error: HttpErrorResponse | unknown) => of({ data: error, error: true });
    }

    private _mergeObjects(obj1: Record<string, unknown>, obj2: Record<string, unknown>): Record<string, unknown> {
        for (const k in obj2) {
            if (obj1[k]) {
                _.extend(obj1[k], obj2[k]);
            } else {
                obj1[k] = obj2[k];
            }
        }
        return obj1;
    }

    private _prepareHeaders(headers: HttpHeaders): HttpHeaders {
        return (headers ? headers : new HttpHeaders()).set('X-XSRF-TOKEN', this._token).set('X-ANGULAR', '1');
    }
}
