import * as i0 from '@angular/core';
import { Injectable, Inject, InjectionToken, Optional } from '@angular/core';
import { firstValueFrom, Observable, catchError, EMPTY, race, ReplaySubject } from 'rxjs';
import * as i1 from '@angular/common/http';
import { HttpClient } from '@angular/common/http';
import { finalize, share } from 'rxjs/operators';
import { SetToObject, getFromObject, JoinPath, deepMerge, coerceArray, CoercePrefix, SetObjectValue } from '@rxap/utilities';
import { log } from '@rxap/rxjs';

class ConfigLoaderService {
    constructor(http) {
        this.http = http;
        this.configs = new Map();
        this.configLoading = new Map();
    }
    async load$(url) {
        if (this.configs.has(url)) {
            return this.configs.get(url);
        }
        if (this.configLoading.has(url)) {
            return this.configLoading.get(url).toPromise();
        }
        const loading$ = this.http.get(url).pipe(finalize(() => this.configLoading.delete(url)), share());
        this.configLoading.set(url, loading$);
        return firstValueFrom(loading$);
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: ConfigLoaderService, deps: [{ token: HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: ConfigLoaderService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: ConfigLoaderService, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }], ctorParameters: () => [{ type: i1.HttpClient, decorators: [{
                    type: Inject,
                    args: [HttpClient]
                }] }] });

class ConfigTestingService {
    constructor() {
        this.config = {};
    }
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    clearLocalConfig() {
    }
    set(propertyPath, value) {
        SetToObject(this.config, propertyPath, value);
    }
    get(propertyPath, defaultValue) {
        return getFromObject(this.config, propertyPath, defaultValue);
    }
    getOrThrow(propertyPath) {
        const value = this.get(propertyPath);
        if (value === undefined) {
            throw new Error(`Could not find config in path '${propertyPath}'`);
        }
        return value;
    }
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    setLocalConfig(config) {
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: ConfigTestingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: ConfigTestingService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: ConfigTestingService, decorators: [{
            type: Injectable
        }] });

const RXAP_CONFIG = new InjectionToken('rxap/config');

// TODO : move to separate package
/**
 * Similar to Promise.race() but only resolves with the first successful promise.
 * It will only reject if all promises reject.
 */
async function raceSuccess(promises) {
    return new Promise((resolve, reject) => {
        let rejectionCount = 0;
        promises.forEach((promise) => {
            promise.then(
            // On success, resolve the entire raceSuccess promise
            (value) => resolve(value), 
            // On rejection, count it and check if all promises rejected
            () => {
                rejectionCount++;
                if (rejectionCount === promises.length) {
                    reject(new Error('All promises were rejected'));
                }
                // Otherwise continue waiting for other promises
            });
        });
    });
}
async function dnsResolver(endpoint, name, type) {
    const response = await fetch(`${endpoint}?name=${name}&type=${type}`, {
        method: 'GET',
        headers: { 'Accept': 'application/dns-json' },
    });
    if (!response.ok) {
        throw new Error(`Failed to resolve DNS via ${endpoint} (${response.status}): ${response.statusText}`);
    }
    const result = await response.json();
    // Basic validation of the response structure
    if (!result || !Array.isArray(result.Answer) || result.Answer.length === 0 || !result.Answer[0].data) {
        throw new Error(`Invalid DNS response structure from ${endpoint} for ${name} ${type}`);
    }
    const data = result.Answer[0].data;
    // Remove surrounding quotes often found in TXT records
    return data.replace(/^"(.*)"$/, '$1');
}
async function dnsLookup(name, type, dnsServers = [
    'https://dns.google/resolve',
    'https://cloudflare-dns.com/dns-query'
]) {
    type = type.toUpperCase();
    console.log(`Performing DNS lookup for ${type} record of ${name} using servers: ${dnsServers.join(', ')}`);
    try {
        // Use Promise.race to get the first successful response
        return await raceSuccess(dnsServers.map(server => dnsResolver(server, name, type)));
    }
    catch (error) {
        console.error(`Failed to resolve DNS TXT record for ${name} using any server: ${error.message}`);
        throw new Error(`DNS lookup failed for ${name} (${type})`); // Re-throw a more specific error
    }
}
function fetchContentViaHttp(url) {
    return new Observable((subscriber) => {
        let controller = new AbortController(); // For cancellation
        fetch(url)
            .then(response => {
            if (!response.ok) {
                // Don't complete here, let catchError handle it
                throw new Error(`HTTP error ${response.status} for ${url}: ${response.statusText}`);
            }
            return response.blob();
        })
            .then(blob => {
            if (!subscriber.closed) {
                subscriber.next(blob);
                subscriber.complete();
            }
        })
            .catch(error => {
            if (error.name !== 'AbortError' && !subscriber.closed) {
                console.warn(`Fetch error for ${url}:`, error);
            }
        });
        // Cleanup function for when the observable is unsubscribed
        return () => {
            controller?.abort();
            controller = null; // Release reference
        };
    }).pipe(catchError(error => {
        // Log the error but return null to signal failure without stopping the race
        console.error(`Fetch failed for ${url}: ${error.message}`);
        return EMPTY; // Signal failure with null
    }));
}
function fetchCidContentViaHttp(cid, path) {
    return race(
    // Attempt 1: w3s.link
    fetchContentViaHttp(JoinPath(`https://${cid}.ipfs.w3s.link`, path)).pipe(log(`w3s fetch attempt for ${cid}`)), 
    // Attempt 2: Local gateway
    fetchContentViaHttp(JoinPath(`${location.origin}/ipfs/${cid}`, path)).pipe(log(`local fetch attempt for ${cid}`))
    // Add more sources here if needed
    ).pipe(log(`Race winner for ${cid}`));
}
async function fetchCidContent(cid, path) {
    console.log(`Fetching content for CID: ${cid}`);
    // Example: Fetch from an IPFS gateway or other source
    // const gatewayUrl = `https://ipfs.io/ipfs/${cid}`;
    try {
        return await firstValueFrom(race(fetchCidContentViaHttp(cid, path)), { defaultValue: null } // Return null if the stream completes empty
        );
    }
    catch (error) {
        console.error(`Error fetching or parsing content for CID ${cid}:`, error);
        return null; // Return null on error
    }
}
async function fetchCidContentAsJson(cid, path) {
    console.log(`Fetching JSON content for CID: ${cid}`);
    const blob = await fetchCidContent(cid, path);
    if (blob) {
        try {
            const text = await blob.text();
            return JSON.parse(text);
        }
        catch (error) {
            console.error(`Error parsing JSON content for CID ${cid}:`, error);
            return null; // Return null on error
        }
    }
    console.warn(`No blob available for CID ${cid}, cannot parse as JSON.`);
    return null; // Return null if blob is not available
}

class ConfigService {
    static { this.onError = new ReplaySubject(1); }
    static { this.onRequestError = new ReplaySubject(1); }
    static { this.onErrorFnc = []; }
    static { this.onRequestErrorFnc = []; }
    static { this.Config = null; }
    /**
     * Static default values for the config object.
     * Will be overwritten by an dynamic config file specified in
     * the Urls array.
     */
    static { this.Defaults = {}; }
    /**
     * Any value definition in the Overwrites object will overwrite any
     * value form the Defaults values or dynamic config files
     */
    static { this.Overwrites = {}; }
    static { this.LocalStorageKey = 'rxap/config/local-config'; }
    /**
     * @deprecated instead use the url property of the ConfigLoadOptions
     */
    static { this.Urls = []; }
    constructor(config = null) {
        this.config = ConfigService.Config;
        if (config) {
            this.config = deepMerge(this.config ?? {}, config);
        }
        if (!this.config) {
            throw new Error('config not available');
        }
    }
    /**
     * Used to load the app config from a remote resource.
     *
     * Promise.all([ ConfigService.Load() ])
     * .then(() => platformBrowserDynamic().bootstrapModule(AppModule))
     * .catch(err => console.error(err))
     *
     */
    static async Load(options, environment) {
        options ??= {};
        let config = deepMerge(this.Defaults, {});
        if (environment?.config) {
            if (typeof environment.config === 'string') {
                options.url = environment.config;
            }
            else {
                options.static = environment.config.static;
                options.url = environment.config.url;
                options.fromUrlParam = environment.config.fromUrlParam;
                options.fromLocalStorage = environment.config.fromLocalStorage;
                options.schema = environment.config.schema;
            }
        }
        config = deepMerge(config, options?.static ?? {});
        const urls = (options?.url ? coerceArray(options.url) : ConfigService.Urls).map(url => {
            if (typeof url === 'function') {
                if (!environment) {
                    throw new Error('environment is required when url is a function');
                }
                return coerceArray(url(environment));
            }
            return coerceArray(url);
        }).flat();
        for (const url of urls) {
            config = deepMerge(config, await this.loadConfig(url, true, options?.schema));
        }
        config = deepMerge(config, this.Overwrites);
        if (options?.fromLocalStorage !== false) {
            const localConfig = localStorage.getItem(ConfigService.LocalStorageKey);
            if (localConfig) {
                try {
                    config = deepMerge(config, JSON.parse(localConfig));
                }
                catch (e) {
                    console.error('local config could not be parsed');
                }
            }
        }
        if (options?.fromUrlParam) {
            const param = typeof options.fromUrlParam === 'string' ? options.fromUrlParam : 'config';
            config = deepMerge(config, this.LoadConfigDefaultFromUrlParam(param));
        }
        if (options?.fromDns) {
            await this.loadConfigFromDns(options);
        }
        if (options?.fromCid) {
            config = deepMerge(config, await this.loadConfigFromCid(options));
        }
        console.debug('app config', config);
        this.Config = config;
    }
    static async loadConfigFromCid(options) {
        console.debug('Loading config from CID: ', options.fromCid);
        try {
            const cidContent = await fetchCidContentAsJson(options.fromCid);
            if (cidContent && typeof cidContent === 'object') {
                console.log(`Merging configuration from CID ${options.fromCid}.`, cidContent);
                // Merge CID content into the existing config object
                console.log('Configuration merged successfully.');
                return cidContent;
            }
            else if (cidContent) {
                console.warn(`Content fetched from CID ${options.fromCid} is not a mergeable object, skipping merge.`);
            }
            else {
                console.warn(`No content fetched or content was null for CID ${options.fromCid}.`);
            }
        }
        catch (error) {
            console.error(`Failed to fetch or process content for CID ${options.fromCid}:`, error);
            // Decide how to handle fetch/processing errors
        }
    }
    static async loadConfigFromDns(options) {
        console.debug('Loading config from DNS');
        let domain = location.hostname;
        if (typeof options.fromDns === 'string') {
            domain = options.fromDns;
        }
        domain = CoercePrefix(domain, '_config.');
        try {
            console.log(`Attempting DNS lookup for domain: ${domain}`);
            const txtData = await dnsLookup(domain, 'TXT');
            console.log(`DNS TXT record data found: ${txtData}`);
            // Example CID extraction logic (adapt to your TXT record format)
            // This looks for IPFS CIDs (v0 'Qm...' or v1 'b...') potentially after 'ipfs://' or '/'
            const cidMatch = txtData.match(/(?:ipfs:\/\/|\/|^)([a-zA-Z0-9]{40,})$/);
            if (cidMatch && cidMatch[1]) {
                options.fromCid = cidMatch[1];
                console.log(`Extracted CID from DNS: ${options.fromCid}`);
            }
            else {
                console.warn(`Could not extract a valid CID format from DNS TXT data: "${txtData}"`);
            }
        }
        catch (error) {
            console.error(`DNS lookup for ${domain} failed:`, error);
            // Decide if failure is critical or recoverable
        }
    }
    static handleError(error) {
        this.onError.next(error);
        for (const fnc of this.onErrorFnc) {
            try {
                fnc(error);
            }
            catch (e) {
                console.error('Error in onErrorFnc', e);
            }
        }
    }
    static handleRequestError(response) {
        this.onRequestError.next(response);
        for (const fnc of this.onRequestErrorFnc) {
            try {
                fnc(response);
            }
            catch (e) {
                console.error('Error in onRequestErrorFnc', e);
            }
        }
    }
    static async loadConfig(url, required, schema) {
        let config;
        let response;
        try {
            response = await fetch(url);
        }
        catch (error) {
            const message = `Could not fetch config from '${url}': ${error.message}`;
            if (required) {
                this.handleError(error);
                this.showError(message);
                throw new Error(message);
            }
            else {
                console.warn(message);
                return null;
            }
        }
        if (!response.ok) {
            let message = `Config request results in non ok response for '${url}': (${response.status}) ${response.statusText}`;
            switch (response.status) {
                case 404:
                    message = `Config not found at '${url}'`;
                    break;
                case 405:
                    message = `Config service is not started yet. Wait 30s and try again.`;
                    break;
                case 401:
                    message = `Unauthorized to fetch config from '${url}'`;
                    break;
            }
            if (required) {
                this.handleRequestError(response);
                this.showError(message);
                throw new Error(message);
            }
            else {
                console.warn(message);
                return null;
            }
        }
        try {
            config = await response.json();
        }
        catch (error) {
            const message = `Could not parse config from '${url}' to a json object: ${error.message}`;
            if (required) {
                this.handleError(error);
                this.showError(message);
                throw new Error(message);
            }
            else {
                console.warn(message);
                return null;
            }
        }
        if (schema) {
            try {
                config = await schema.validateAsync(config);
            }
            catch (error) {
                const message = `Config from '${url}' is not valid: ${error.message}`;
                if (required) {
                    this.handleError(error);
                    this.showError(message);
                    throw new Error(message);
                }
                else {
                    console.warn(message);
                    return null;
                }
            }
        }
        return config;
    }
    static LoadConfigDefaultFromUrlParam(param = 'config') {
        const queryString = window.location.search;
        const urlParams = new URLSearchParams(queryString);
        const configFromParams = {};
        for (const configParam of urlParams.getAll('config')) {
            try {
                const split = configParam.split(';');
                if (split.length === 2) {
                    const keyPath = split[0];
                    const value = split[1];
                    SetObjectValue(configFromParams, keyPath, value);
                }
            }
            catch (e) {
                console.warn(`Parsing of url config param failed for '${configParam}': ${e.message}`);
            }
        }
        return configFromParams;
    }
    static async SideLoad(url, propertyPath, required, schema) {
        if (!this.Config) {
            throw new Error('Config side load is only possible after the initial config load.');
        }
        const config = await this.loadConfig(url, required, schema);
        SetObjectValue(this.Config, propertyPath, config);
        console.debug(`Side loaded config for '${propertyPath}' successful`, this.Config);
    }
    static Get(path, defaultValue, config = this.Config) {
        if (!config) {
            throw new Error('config not loaded');
        }
        let configValue = config;
        if (typeof path !== 'string') {
            throw new Error('The config property path is not a string');
        }
        for (const fragment of path.split('.')) {
            if (configValue && typeof configValue === 'object' && fragment in configValue) {
                configValue = configValue[fragment];
            }
            else {
                if (defaultValue !== undefined) {
                    return defaultValue;
                }
                console.warn(`Config with path '${path}' not found`);
                return undefined;
            }
        }
        return configValue;
    }
    static showError(message) {
        const hasUl = document.getElementById('rxap-config-error') !== null;
        const ul = document.getElementById('rxap-config-error') ?? document.createElement('ul');
        ul.id = 'rxap-config-error';
        ul.style.position = 'fixed';
        ul.style.bottom = '16px';
        ul.style.right = '16px';
        ul.style.backgroundColor = 'white';
        ul.style.padding = '32px';
        ul.style.zIndex = '99999999';
        ul.style.color = 'black';
        const messageLi = document.createElement('li');
        messageLi.innerText = message;
        ul.appendChild(messageLi);
        const refreshHintLi = document.createElement('li');
        refreshHintLi.innerText = 'Please refresh the page to try again.';
        ul.appendChild(refreshHintLi);
        const autoRefreshHintLi = document.createElement('li');
        autoRefreshHintLi.innerText = 'The page will refresh automatically in 30 seconds.';
        ul.appendChild(autoRefreshHintLi);
        if (!hasUl) {
            document.body.appendChild(ul);
        }
        setTimeout(() => location.reload(), 30000);
    }
    setLocalConfig(config) {
        localStorage.setItem(ConfigService.LocalStorageKey, JSON.stringify(config));
    }
    clearLocalConfig() {
        localStorage.removeItem(ConfigService.LocalStorageKey);
    }
    get(propertyPath, defaultValue) {
        return ConfigService.Get(propertyPath, defaultValue, this.config);
    }
    getOrThrow(propertyPath, defaultValue) {
        const value = ConfigService.Get(propertyPath, defaultValue, this.config);
        if (value === undefined) {
            throw new Error(`Could not find config in path '${propertyPath}'`);
        }
        return value;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: ConfigService, deps: [{ token: RXAP_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: ConfigService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: ConfigService, decorators: [{
            type: Injectable,
            args: [{
                    providedIn: 'root',
                }]
        }], ctorParameters: () => [{ type: undefined, decorators: [{
                    type: Optional
                }, {
                    type: Inject,
                    args: [RXAP_CONFIG]
                }] }] });

function ProvideConfig(config = {}) {
    return {
        provide: RXAP_CONFIG,
        useValue: config,
    };
}

// region 
// endregion

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

export { ConfigLoaderService, ConfigService, ConfigTestingService, ProvideConfig, RXAP_CONFIG, dnsLookup, dnsResolver, fetchCidContent, fetchCidContentAsJson, fetchCidContentViaHttp, fetchContentViaHttp, raceSuccess };
//# sourceMappingURL=rxap-config.mjs.map
