import { HttpClient } from '@angular/common/http';
import { Injectable, NgZone } from '@angular/core';
import { RouterExtensions } from '@nativescript/angular';
import { parse, XElement } from 'nativescript-xmlobjects';
import { Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { testUrl } from '~/constants/api.constants';
import { transitionOptions } from '~/constants/shared.constants';
import { BaseInput } from '~/core/entities/input/baseInput';
import { BaseOutput } from '~/core/entities/output/baseOutput';
import { LoginDataService } from '~/core/services/login-data.service';
import { lowerCaseFirstLetter } from '~/core/utils/functions';

/**
 * Services that takes care of communication with the skautIs web services
 */
@Injectable()
export class WebApiService {
    constructor(
        private httpClient: HttpClient,
        private loginDataSrv: LoginDataService,
        private routerExtensions: RouterExtensions,
        private zone: NgZone,
    ) {
    }

    /**
     * Create call on specific web service by $webSrvName and the function is choose by entity name
     *
     * @param {BaseInput} entity For which entity will be call request
     * @param {() => BaseOutput} outputEntityCreator Function for creating outputEntity (BaseOutput) for returned result
     * @param {string} webSrvName Name of web services of api
     * @return Observable<BaseOutput[]|BaseOutput>
     */
    call(entity: BaseInput, outputEntityCreator: () => BaseOutput, webSrvName: string): Observable<any> {
        const entityName: string = entity.constructor.name;
        const options = {
            headers: {
                'Content-Type': 'text/xml; charset=utf-8',
                SOAPAction: `https://is.skaut.cz/${entityName}`
            },
            responseType: 'text' as 'text'
        };
        const ent = this.getEnvelope(entity, entityName)
        return this.httpClient.post(
            testUrl(webSrvName),
            ent,
            options
        ).pipe(
            map(response => this.parseResponse(response, entity, entityName, outputEntityCreator)),
            catchError(error => {
                if (typeof error.error === 'string' && error.error.indexOf('Uživatel byl odhlášen') !== -1) {
                    this.loginDataSrv.logout().then(() => {
                        this.zone.run(() =>
                            this.routerExtensions.navigate(
                                ['/home'], { transition: transitionOptions }))
                    })
                }

                throw error;
            })
        );
    }

    /**
     * Function parse xml response from webService to array of entity created by $outputEntityCreator
     * It retrieve element according of response type (XML only with EntityResult or with EntityOutput array).
     * Each element is converted to OutputEntity using eleToType function.
     *
     * @param {string} response Response from webService
     * @param {BaseInput} entity For which entity will be call request
     * @param {string} entityName
     * @param {() => BaseOutput} outputEntityCreator Function for creating outputEntity
     * @returns {BaseOutput[] | BaseOutput}
     */
    protected parseResponse(response: string, entity: BaseInput, entityName: string, outputEntityCreator: () => BaseOutput): BaseOutput[] | BaseOutput {
        if (response.includes('DocumentDetailData')) {
            // console.log(response);
        }

        const doc = parse(response);

        const elementRes: XElement = doc.root
            .element('soap:Body')
            .element(`${entityName}Response`);

        let elements: XElement[];

        if (!elementRes.element(`${entityName}Result`)) {
            return outputEntityCreator();
        }

        if ((['FunctionAllPerson', 'MembershipAllPerson'].indexOf(entityName) !== -1) && elementRes.element(`${entityName}Result`) && elementRes.element(`${entityName}Result`).element(`${entityName.replace('Person', '')}Output`)) {
            // this branch is due to BAD API - Because FunctionAllPerson service has in output only FunctionAllOutput
            elements = elementRes
                .element(`${entityName}Result`)
                .elements(`${entityName.replace('Person', '')}Output`);
            // also BAD API - EvaluationAnswerAllEvaluationPersonResult has in output 'EvaluationAnswerAllOutput'
        } else if ((['EvaluationAnswerAllEvaluationPerson'].indexOf(entityName) !== -1) && elementRes.element(`${entityName}Result`) && elementRes.element(`${entityName}Result`).element(`${entityName.replace('EvaluationPerson', '')}Output`)) {
            elements = elementRes
                .element(`${entityName}Result`)
                .elements(`${entityName.replace('EvaluationPerson', '')}Output`);
        } else if (elementRes.element(`${entityName}Result`) && elementRes.element(`${entityName}Result`).element(`${entityName}Output`)) {
            elements = elementRes
                .element(`${entityName}Result`)
                .elements(`${entityName}Output`);
        } else {
            elements = elementRes
                .elements(`${entityName}Result`);
        }

        // because of no attributes in DocumentDetailDataResult element
        if ('DocumentDetailData'.indexOf(entityName) !== -1 && elements.length === 1) {
            return elements[0].value;
        }

        return elements
            .filter((ele: XElement) => ele.nodes().length > 0)
            .map((ele: XElement) => this.eleToType(ele, outputEntityCreator()));
    }

    /**
     * Convert nodes of element to filled entity of specific type - outputEntity
     * It filter properties from element of response and leave only properties which are define in outputEntity.
     * To converting individual properties is used convertPropertyToType function
     *
     * @param {XElement} ele Element which should be converted to outputEntity
     * @param {BaseOutput} outputEntity
     * @returns {BaseOutput}
     */
    protected eleToType(ele: XElement, outputEntity: BaseOutput): BaseOutput {
        //TODO
        const nodes = ele.nodes();
        if (outputEntity instanceof Array) {
            if (outputEntity.length >= 1 && typeof outputEntity[0] !== 'object') {
                nodes.forEach((eleF: XElement) => {
                    outputEntity.push(this.convertPropertyToType(0, eleF.value.toString(), outputEntity))
                })
            }
        } else {
            nodes
                .filter((eleF: XElement) => outputEntity.hasOwnProperty(eleF.name.toString()))
                .forEach((eleF: XElement) => {
                    const children = eleF.elements()
                    let current: any = outputEntity[eleF.name.toString()]

                    if (children.length > 1) {
                        if (current instanceof BaseOutput) {
                            current = this.eleToType(eleF, current)
                        } else if (current instanceof Array) {
                            children.forEach(
                                (el) => this.eleToType(el, current))
                            current.shift();
                        }
                    } else if (eleF.value) {
                        current = this.convertPropertyToType(eleF.name.toString(), eleF.value.toString(), outputEntity);
                    }
                    outputEntity[eleF.name.toString()] = current;
                });

        }
        return outputEntity;
    }

    /**
     * Convert property to specific type by outputEntity types of properties
     * Supported types are number, boolean or default string
     *
     * @param {string} propertyName Name of property of outputEntity
     * @param {string} property Value of property of outputEntity
     * @param {string} outputEntity OutputEntity for retrieve type of property
     * @returns {any}
     */
    private convertPropertyToType(propertyName: string | number, property: string, outputEntity: BaseOutput) {
        const type = typeof outputEntity[propertyName];
        switch (type) {
            case 'number': {
                return +property;
            }

            case 'boolean': {
                return property.toLowerCase() === 'true';
            }

            default: {
                return property;
            }
        }
    }

    /**
     * Return envelope xml structure which is send in request like a body
     * If entity contains wrapper property, this property is not added to request and using value of wrapper we wrappering the xml
     * In envelope are added properties from InputEntity
     *
     * @param {BaseInput} entity InputEntity is used for creating request
     * @param {string} entityName Name of entity of web service
     * @returns {string}
     */
    private getEnvelope(entity: BaseInput, entityName: string) {
        const arrayOfEle = Object.getOwnPropertyNames(entity)
            .filter(ele => ele !== 'wrapper')
            .filter(ele => entity[ele] !== undefined)
            .map(ele => `<${ele}>${this.getElementXML(entity[ele])}</${ele}>`
            );

        let wrapperOrInputElement;
        let wrapperOrInputElementClose;
        if (entity.hasOwnProperty(('wrapper'))) {
            wrapperOrInputElement = `<${entity['wrapper']}>`;
            wrapperOrInputElementClose = `</${entity['wrapper']}>`;
        } else {
            wrapperOrInputElement = `<${lowerCaseFirstLetter(entityName)}Input>`;
            wrapperOrInputElementClose = `</${lowerCaseFirstLetter(entityName)}Input>`;
        }

        return `<?xml version="1.0" encoding="utf-8"?>
                <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
                  <soap12:Body>
                    <${entityName} xmlns="https://is.skaut.cz/">
                      ${wrapperOrInputElement}
                        ${arrayOfEle.join('')}
                      ${wrapperOrInputElementClose}
                    </${entityName}>
                  </soap12:Body>
                </soap12:Envelope>`;
    }

    /**
     * Create html element of specific type by attribute type
     *
     * @param attribute
     * @param closing boolean if true create closing element with slash
     */
    private createAttributeTypeElement(attribute, closing = false) {
        switch (typeof attribute) {
            case 'string':
                return /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{0,3}Z?/.test(attribute) ? `<${closing ? '/' : ''}dateTime>` : `<${closing ? '/' : ''}string>`;
            case 'boolean':
                return `<${closing ? '/' : ''}boolean>`;
            case 'number':
                return attribute - Math.floor(attribute) === 0 ? `<${closing ? '/' : ''}int>` : `<${closing ? '/' : ''}decimal>`;
            case 'object':
                return '';
            default:
                return `<${closing ? '/' : ''}string>`;
        }
    }

    /**
     * Recursively transforms element to xml form.
     *
     * @param element Element to be transformed to xml form. Type can be array, object or simple type
     * @returns xml form of element
     */
    private getElementXML(element): string {
        if (typeof element === 'object') {
            let finalString = '';
            if (Array.isArray(element)) {
                element.forEach(ele => finalString += `${this.createAttributeTypeElement(ele)}${this.getElementXML(ele)}${this.createAttributeTypeElement(ele, true)}`);
            } else {
                finalString = `<${element.constructor.name}>`;
                Object.getOwnPropertyNames(element)
                    .filter(ele => element[ele] !== undefined)
                    .forEach(subElement => finalString += `<${subElement}>${this.getElementXML(element[subElement])}</${subElement}>`);
                finalString += `</${element.constructor.name}>`;
            }

            return finalString;
        }

        return `${element}`;
    }
}
