import { RxapError, JoinPath, coerceArray, IsRecord, assertsObject, isPromiseLike } from '@rxap/utilities';
import * as i0 from '@angular/core';
import { InjectionToken, isDevMode, Injectable, Optional, Inject } from '@angular/core';
import { HttpErrorResponse, HttpParams, HttpHeaders } from '@angular/common/http';
import { ConfigService } from '@rxap/config';
import Ajv from 'ajv';

class RxapOpenApiError extends RxapError {
    constructor(message, code, scope) {
        super('@rxap/open-api', message, code ?? '', scope);
    }
}

const RXAP_OPEN_API_CONFIG = new InjectionToken('rxap/open-api/config');
const RXAP_OPEN_API_STRICT_VALIDATOR = new InjectionToken('rxap/open-api/strict-validator');
const DEFAULT_OPEN_API_REMOTE_METHOD_META_DATA = new InjectionToken('rxap/open-api/default-remote-method-meta-data');
const DEFAULT_OPEN_API_DATA_SOURCE_META_DATA = new InjectionToken('rxap/open-api/default-data-source-meta-data');
const DISABLE_SCHEMA_VALIDATION = new InjectionToken('rxap/open-api/disable-schema-validation', {
    providedIn: 'root',
    factory: () => true,
});
const DISABLE_VALIDATION = new InjectionToken('rxap/open-api/disable-validation', {
    providedIn: 'root',
    factory: () => true,
});

function IsReferenceObject(obj) {
    return !!obj && obj.hasOwnProperty('$ref');
}
function NotContainsReferenceObjects(array) {
    return !!array && array.every(item => !IsReferenceObject(item) && !!item);
}
function IsOpenApiV3(obj) {
    return !!obj && obj.hasOwnProperty('openapi') && obj.openapi.match(/^3\./);
}
function AssertOpenApiV3(obj) {
    if (!IsOpenApiV3(obj)) {
        throw new RxapOpenApiError('Only openapi version 3.x.x is supported');
    }
}

class OpenApiConfigService {
    static { this.Config = null; }
    static { this.DefaultServerIndex = 0; }
    static { this._operations = null; }
    static { this.serverConfigMap = new Map(); }
    get serverConfigMap() {
        return OpenApiConfigService.serverConfigMap;
    }
    get serverIds() {
        return Object.keys(this.serverConfigMap);
    }
    constructor(config = null) {
        this.defaultServerIndex = OpenApiConfigService.DefaultServerIndex;
        this._operations = null;
        this._config = null;
        config = config ?? OpenApiConfigService.Config;
        if (config) {
            this._config = config;
            this._operations = OpenApiConfigService.LoadOperations(config);
        }
        OpenApiConfigService.Config ??= config;
    }
    get config() {
        if (!this._config) {
            throw new Error('Could not load open api config');
        }
        return this._config;
    }
    /**
     * Used to load the app open api config from a remote resource.
     *
     * Promise.all([ OpenApiDataSourceLoader.Load() ])
     * .then(() => platformBrowserDynamic().bootstrapModule(AppModule))
     * .catch(err => console.error(err))
     *
     */
    static async Load(openApiUrl = 'openapi.json') {
        if (!openApiUrl) {
            throw new Error('The open api url is not defined!');
        }
        let config = null;
        try {
            const response = await fetch(openApiUrl);
            config = await response.json();
        }
        catch (error) {
            console.debug(`Could not load the open api config from '${openApiUrl}'!`, error);
            console.error(`Could not load the open api config from '${openApiUrl}'!`, error.message);
        }
        if (config) {
            const expandedConfig = await this.ExpandOpenApi(config);
            AssertOpenApiV3(expandedConfig);
            this.Config = expandedConfig;
        }
        else {
            this.Config = null;
        }
    }
    static GetOperation(operationId) {
        if (!this.Config) {
            throw new Error('Could not load open api config');
        }
        if (!this._operations) {
            this._operations = this.LoadOperations(this.Config);
        }
        if (!this._operations.has(operationId)) {
            throw new Error(`The operation '${operationId}' is not defined in the openapi-json`);
        }
        return this._operations.get(operationId);
    }
    static LoadOperations(config) {
        const operations = new Map();
        Object.entries(config.paths)
            .filter((item) => item[0] !== undefined)
            .filter(([_, methods]) => methods &&
            Object.keys(methods).some((method) => ['get', 'put', 'post', 'delete', 'patch'].includes(method)))
            .forEach(([path, methods]) => Object.entries(methods)
            .filter((item) => item[0] !== undefined)
            .forEach(([method, operation]) => {
            if (!operation.operationId) {
                throw new Error('The OpenApu OperationId is not defined');
            }
            if (operations.has(operation.operationId)) {
                throw new Error(`The OpenApi Operation '${operation.operationId}' is already defined.`);
            }
            operations.set(operation.operationId, {
                ...operation,
                path: path,
                method: method.toUpperCase(),
            });
        }));
        return operations;
    }
    static GetBaseUrl(config, serverIndex = this.DefaultServerIndex) {
        const server = config.servers ? config.servers[serverIndex] : null;
        if (!server) {
            console.debug(`Could not extract the server config with the index '${serverIndex}' from the open api config`, config);
            throw new Error(`Could not extract the server config with the index '${serverIndex}' from the open api config`);
        }
        return server.url;
    }
    /**
     * Replaces all $ref with the concrete definition
     *
     * @param config
     * @private
     */
    static ExpandOpenApi(config) {
        return Promise.resolve(config);
    }
    getOperation(operationId) {
        if (!this._operations) {
            if (!this._config) {
                throw new Error('The operations map is not loaded and the config object is not loaded');
            }
            this._operations = OpenApiConfigService.LoadOperations(this._config);
        }
        if (!this._operations.has(operationId)) {
            console.error(`The operation '${operationId}' is not defined in the openapi-json`, isDevMode() ? this.config : undefined);
            throw new Error(`The operation '${operationId}' is not defined in the openapi-json`);
        }
        return this._operations.get(operationId);
    }
    getBaseUrl(serverIndex = this.defaultServerIndex, serverId) {
        let serverConfig;
        const servers = this._config?.servers ?? [];
        if (!serverId && servers) {
            serverConfig = servers[serverIndex];
        }
        if (this.serverConfigMap.has(serverId)) {
            serverConfig = this.serverConfigMap.get(serverId)[serverIndex];
        }
        if (!serverConfig) {
            console.error(`Could not determine the base url with the index '${serverIndex}' and the serverId '${serverId}'`, isDevMode() ? servers : undefined);
            throw new Error(`Could not determine the base url with the index '${serverIndex}' and the serverId '${serverId}'`);
        }
        return serverConfig.url;
    }
    static InsertServer(serverConfig, index, serverId, config = this.Config) {
        if (!serverId && config) {
            if (!config.servers) {
                config.servers = [];
            }
            config.servers.splice(index, 1, serverConfig);
        }
        if (!this.serverConfigMap.has(serverId)) {
            this.serverConfigMap.set(serverId, []);
        }
        const list = this.serverConfigMap.get(serverId);
        list[index] = serverConfig;
        this.serverConfigMap.set(serverId, list);
    }
    insertServer(serverConfig, index = this.defaultServerIndex, serverId) {
        OpenApiConfigService.InsertServer(serverConfig, index, serverId, this.config);
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: OpenApiConfigService, deps: [{ token: RXAP_OPEN_API_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: OpenApiConfigService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: OpenApiConfigService, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }], ctorParameters: () => [{ type: undefined, decorators: [{
                    type: Optional
                }, {
                    type: Inject,
                    args: [RXAP_OPEN_API_CONFIG]
                }] }] });

class OpenApiHttpResponseError extends HttpErrorResponse {
    constructor(httpErrorResponse, metadata) {
        super({
            error: httpErrorResponse.error,
            headers: httpErrorResponse.headers,
            status: httpErrorResponse.status,
            statusText: httpErrorResponse.statusText,
            url: httpErrorResponse.url ?? undefined,
        });
        this.httpErrorResponse = httpErrorResponse;
        this.metadata = metadata;
        if (Error['captureStackTrace']) {
            Error['captureStackTrace'](this, OpenApiHttpResponseError);
        }
        Reflect.set(this, 'name', 'OpenApiHttpResponseError');
    }
    get operationId() {
        return this.metadata.id;
    }
    get serverId() {
        return this.metadata.serverId;
    }
}

function OpenApiInit(environment, options = {}) {
    const api = ConfigService.Get('api', {}, ConfigService.Config);
    options.origin ??= environment.origin ?? location.origin;
    options.load ??= environment.openApi?.load ?? false;
    if (api.baseUrl) {
        OpenApiConfigService.InsertServer({
            url: api.baseUrl.match(/^https?:\/\//) ? api.baseUrl : JoinPath(options.origin, api.baseUrl),
        }, OpenApiConfigService.DefaultServerIndex);
    }
    for (const [serverId, config] of Object.entries(api)) {
        if (config && typeof config === 'object') {
            const baseUrl = config.baseUrl;
            if (baseUrl) {
                const url = baseUrl.match(/^https?:\/\//) ? baseUrl : JoinPath(options.origin, baseUrl);
                console.debug(`Add server '${serverId}' with url '${url}'`);
                OpenApiConfigService.InsertServer({ url }, OpenApiConfigService.DefaultServerIndex, serverId);
            }
        }
    }
    if (options.load) {
        return OpenApiConfigService.Load(typeof options.load === 'object' ? options.load.openApiUrl : undefined);
    }
    return undefined;
}

class SchemaValidationMixin {
    static { this.STRICT = false; }
    /**
     * Validates the parameters against the schema specified in the operation object
     *
     * @param operation
     * @param parameters
     * @param strict
     * @protected
     */
    validateParameters(operation, parameters, strict = false) {
        const operationParameters = coerceArray(operation.parameters);
        if (!NotContainsReferenceObjects(operationParameters)) {
            throw new RxapOpenApiError('The operation parameters contains ReferenceObject!');
        }
        if (parameters === undefined) {
            // TODO : find concept to definition witch parameter should not be checked if required
            // header parameters are never required if changes the semantic release manager breaks
            const requiredParameters = operationParameters.filter(parameter => parameter.required &&
                parameter.in !==
                    'header');
            if (requiredParameters.length) {
                if (isDevMode()) {
                    console.debug('Some operation parameters are required!', requiredParameters.map(p => p.name));
                }
                this.validationError('Some operation parameters are required!', strict);
            }
        }
        else if (IsRecord(parameters)) {
            for (const parameter of operationParameters) {
                if (parameter.required) {
                    // header parameters are never required if changes the semantic release manager breaks
                    if (parameter.in !== 'header') {
                        if (!parameters.hasOwnProperty(parameter.name)) {
                            this.validationError(`The operation parameter '${parameter.name}' is required!`, strict);
                        }
                    }
                }
                if (parameter.schema) {
                    if (parameters.hasOwnProperty(parameter.name)) {
                        const value = parameters[parameter.name];
                        if (!this.validate(parameter.schema, value)) {
                            this.validationError(`The parameter '${parameter.name}' is not valid against the schema!`, strict, parameter.schema, value);
                        }
                    }
                }
            }
        }
        else {
            if (operationParameters.length === 0 || operationParameters.every(op => !op.required)) {
                if (isDevMode()) {
                    console.warn(`The operation ${operation.operationId} does not expect any parameters. But a parameter is provided.`, parameters);
                }
            }
            else {
                throw new Error('The parameters object is not a record');
            }
        }
    }
    /**
     * Validates the http response against the schema specified in the operation object
     *
     * @param operation
     * @param response
     * @param strict
     * @protected
     */
    validateResponse(operation, response, strict = false) {
        // region only validate the response if the content type is undefined or application/json
        const contentType = response.headers.get('Content-Type');
        if (contentType && contentType !== 'application/json') {
            if (isDevMode()) {
                console.warn('Response validation is only supported for content type application/json');
            }
            return;
        }
        // endregion
        if (operation.responses) {
            const status = response.status;
            // region extract the response object based on the response status
            let responseObject;
            if (operation.responses[status]) {
                responseObject = operation.responses[status];
            }
            else {
                // use the default response object if no matching was found.
                responseObject = operation.responses['default'];
            }
            // endregion
            if (responseObject) {
                if (IsReferenceObject(responseObject)) {
                    throw new RxapOpenApiError('Found a reference object. The operation config is not expand!');
                }
                // region validate the response against the schema if defined
                // TODO : create schema that validates all parameters at once
                if (responseObject.content &&
                    responseObject.content['application/json'] &&
                    responseObject.content['application/json'].schema) {
                    const schema = responseObject.content['application/json'].schema;
                    const data = response.body ?? response.data;
                    if (!this.validate(schema, data)) {
                        this.validationError('The response is not valid ageist the operation schema!', strict, schema, data);
                    }
                }
                // endregion
            }
        }
    }
    validateRequestBody(operation, body, strict = false) {
        // only validate body if type of object
        if (typeof body !== 'object') {
            return;
        }
        if (operation.requestBody) {
            if (IsReferenceObject(operation.requestBody)) {
                throw new RxapOpenApiError('Found a reference object. The operation config is not expand!');
            }
            if (body === undefined && operation.requestBody.required) {
                this.validationError('The request body is required!', strict);
            }
            if (operation.requestBody.content && operation.requestBody.content['application/json']) {
                const schema = operation.requestBody.content['application/json'].schema;
                if (schema) {
                    if (!this.validate(schema, body)) {
                        this.validationError('The request body is not valid!', strict, schema, body);
                    }
                }
            }
        }
    }
    validationError(message, strict, schema, value) {
        if (isDevMode() && schema !== undefined) {
            console.debug(message, {
                schema,
                value,
            });
        }
        if (strict || SchemaValidationMixin.STRICT) {
            throw new RxapOpenApiError(message, '', 'SchemaValidationMixin');
        }
        else {
            console.warn(message);
        }
    }
    buildHttpParams(operationParameters, parameters, ignoreUndefined = true) {
        let params = new HttpParams();
        if (IsRecord(parameters)) {
            for (const parameter of operationParameters.filter(p => p.in === 'query')) {
                const parameterName = parameter.name;
                if (parameters.hasOwnProperty(parameter.name)) {
                    const value = parameters[parameter.name];
                    if (value === undefined && ignoreUndefined) {
                        continue;
                    }
                    if (Array.isArray(value)) {
                        if (value.length) {
                            const first = value.shift();
                            params = params.set(parameterName, typeof first === 'object' ? JSON.stringify(first) : first);
                            for (const item of value) {
                                params = params.append(parameterName, typeof item === 'object' ? JSON.stringify(item) : item);
                            }
                        }
                    }
                    else {
                        params = params.set(parameter.name, typeof value === 'object' ? JSON.stringify(value) : value);
                    }
                }
            }
        }
        return params;
    }
    buildHttpHeaders(operationParameters, parameters) {
        let headers = new HttpHeaders();
        if (IsRecord(parameters)) {
            for (const parameter of operationParameters.filter(p => p.in === 'header')) {
                if (parameters.hasOwnProperty(parameter.name)) {
                    const value = parameters[parameter.name];
                    if (Array.isArray(value)) {
                        if (value.length) {
                            const first = value.shift();
                            headers = headers.set(parameters['name'], typeof first === 'object' ? JSON.stringify(first) : first);
                            for (const item of value) {
                                headers = headers.append(parameters['name'], typeof item === 'object' ? JSON.stringify(item) : item);
                            }
                        }
                    }
                    else {
                        headers = headers.set(parameter.name, typeof value === 'object' ? JSON.stringify(value) : value);
                    }
                }
            }
        }
        return headers;
    }
    buildHttpPathParams(operationParameters, parameters) {
        const pathParams = {};
        if (IsRecord(parameters)) {
            for (const parameter of operationParameters.filter(p => p.in === 'path')) {
                if (parameters.hasOwnProperty(parameter.name)) {
                    pathParams[parameter.name] = encodeURIComponent(typeof parameters[parameter.name] === 'object' ?
                        JSON.stringify(parameters[parameter.name]) :
                        parameters[parameter.name]);
                }
            }
        }
        return pathParams;
    }
    /**
     * Converts open api parameters into the corresponding http options to
     * create a http request. The transformation is guided by the openapi definition
     *
     * @param operation
     * @param parameters
     * @param requestBody
     * @param ignoreUndefined
     */
    buildHttpOptions(operation, parameters, requestBody, ignoreUndefined = true) {
        const options = {};
        const operationParameters = coerceArray(operation.parameters);
        if (!NotContainsReferenceObjects(operationParameters)) {
            throw new RxapOpenApiError('The operation parameters contains ReferenceObject!');
        }
        const params = this.buildHttpParams(operationParameters, parameters, ignoreUndefined);
        const headers = this.buildHttpHeaders(operationParameters, parameters);
        const pathParams = this.buildHttpPathParams(operationParameters, parameters);
        if (params.keys().length) {
            options.params = params;
        }
        if (headers.keys().length) {
            options.headers = headers;
        }
        if (Object.keys(pathParams).length) {
            options.pathParams = pathParams;
        }
        if (requestBody !== undefined) {
            const [body, contentType] = this.buildBody(operation, requestBody);
            if (body !== undefined) {
                options.body = body;
                if (contentType !== undefined) {
                    options.headers ??= new HttpHeaders();
                    options.headers.set('Content-Type', contentType);
                }
            }
        }
        return options;
    }
    buildBody(operation, requestBody) {
        const accept = [];
        if (operation.requestBody && !IsReferenceObject(operation.requestBody)) {
            if (operation.requestBody.content) {
                for (const contentType of Object.keys(operation.requestBody.content)) {
                    accept.push(contentType);
                }
            }
        }
        if (!accept.length) {
            console.warn('No content type found for the request body! Omitting the body!');
            return [undefined, undefined];
        }
        if (accept.length > 1) {
            console.warn('Multiple content types found for the request body! Using the first one!');
        }
        const contentType = accept[0];
        switch (contentType) {
            case 'application/json':
                assertsObject(requestBody);
                return [requestBody, contentType];
            case 'application/x-www-form-urlencoded':
                assertsObject(requestBody);
                // eslint-disable-next-line no-case-declarations
                let params = new HttpParams();
                for (const [key, value] of Object.entries(requestBody)) {
                    params = params.set(key, value);
                }
                return [params.toString(), contentType];
            case 'multipart/form-data':
                assertsObject(requestBody);
                // eslint-disable-next-line no-case-declarations
                const formData = new FormData();
                // Iterate through the JSON object and append each field to FormData
                for (const [key, value] of Object.entries(requestBody)) {
                    formData.append(key, value);
                }
                return [formData, contentType];
            default:
                return [requestBody, contentType];
        }
    }
    validate(schema, value) {
        if (this.disableSchemaValidation) {
            return true;
        }
        if (typeof schema !== 'object') {
            throw new Error('The schema must be an object!');
        }
        if (!schema) {
            throw new Error('The schema must not be null or undefined!');
        }
        let result;
        const ajv = new Ajv();
        const validate = ajv.compile(schema);
        try {
            result = validate(value);
        }
        catch (e) {
            console.error(e.message);
            return false;
        }
        if (isPromiseLike(result)) {
            throw new Error('Async schema validation is not yet supported. Ensure the all refs in the openapi schema are internal!');
        }
        return result;
    }
}

// region 
// endregion

/**
 * Generated bundle index. Do not edit.
 */

export { AssertOpenApiV3, DEFAULT_OPEN_API_DATA_SOURCE_META_DATA, DEFAULT_OPEN_API_REMOTE_METHOD_META_DATA, DISABLE_SCHEMA_VALIDATION, DISABLE_VALIDATION, IsOpenApiV3, IsReferenceObject, NotContainsReferenceObjects, OpenApiConfigService, OpenApiHttpResponseError, OpenApiInit, RXAP_OPEN_API_CONFIG, RXAP_OPEN_API_STRICT_VALIDATOR, RxapOpenApiError, SchemaValidationMixin };
//# sourceMappingURL=rxap-open-api.mjs.map
