import {
    ChainData,
    BitcoinNetwork,
    ChainType,
    StorageObject,
    IStorageManager, Messenger, ChainInitializer, BtcRelay
} from "@atomiqlabs/base";
import {SmartChainAssets, SmartChainAssetTickers} from "../SmartChainAssets";
import {NostrMessenger} from "@atomiqlabs/messenger-nostr";
import {Swapper, SwapperOptions} from "./Swapper";
import {CustomPriceProvider} from "../prices/providers/CustomPriceProvider";
import {BitcoinTokens, BtcToken, SCToken, Token} from "../types/Token";
import {SwapType} from "../enums/SwapType";
import {SwapTypeMapping} from "../utils/SwapUtils";
import {RedundantSwapPrice, RedundantSwapPriceAssets} from "../prices/RedundantSwapPrice";
import {LocalStorageManager} from "../storage-browser/LocalStorageManager";
import {SingleSwapPrice} from "../prices/SingleSwapPrice";
import {CustomPriceFunction} from "../types/CustomPriceFunction";
import {MempoolApi, MempoolBitcoinRpc, MempoolBtcRelaySynchronizer} from "@atomiqlabs/btc-mempool";

//Helper types
/**
 * Token definitions for a specific chain
 */
type TypedChainTokens<T extends ChainInitializer<any, any, any>> = {
    [val in keyof T["tokens"]]: SCToken<T["chainId"]>
};
/**
 * Token resolver for a specific chain
 */
type TypedChainTokenResolver<T extends ChainInitializer<any, any, any>> = {
    getToken: (address: string) => SCToken<T["chainId"]>
};

type ChainTypeDict<T extends ChainInitializer<any, any, any>> = {[K in T["chainId"]]: T["chainType"]};
type ToMultichain<T extends readonly ChainInitializer<any, any, any>[]> =
    (T extends readonly [infer First extends ChainInitializer<any, any, any>, ...infer Rest extends ChainInitializer<any, any, any>[]]
        ? ChainTypeDict<First> & ToMultichain<Rest>
        : {});

type TokensDict<T extends ChainInitializer<any, any, any>> = {
    [K in T["chainId"]]: TypedChainTokens<T>
};
type GetAllTokens<T extends readonly ChainInitializer<any, any, any>[]> =
    (T extends readonly [infer First extends ChainInitializer<any, any, any>, ...infer Rest extends ChainInitializer<any, any, any>[]]
        ? TokensDict<First> & GetAllTokens<Rest>
        : unknown);

type TokenResolverDict<T extends ChainInitializer<any, any, any>> = {
    [K in T["chainId"]]: TypedChainTokenResolver<T>
};

type OptionsDict<T extends ChainInitializer<any, any, any>> = {[K in T["chainId"]]: T["options"]};
type GetAllOptions<T extends readonly ChainInitializer<any, any, any>[]> =
    (T extends readonly [infer First extends ChainInitializer<any, any, any>, ...infer Rest extends ChainInitializer<any, any, any>[]]
        ? OptionsDict<First> & GetAllOptions<Rest>
        : unknown);

//Exported types
/**
 * Configuration options for creating a Swapper instance
 *
 * @category Core
 */
export type TypedSwapperOptions<T extends readonly ChainInitializer<any, any, any>[]> = {
    /**
     * A dictionary with chain-specific settings, like RPC URLs and others
     */
    chains: GetAllOptions<T>,
    /**
     * A function callback to retrieve a specific named storage container for key-value persistency. If not present, the
     *  default browser Local Storage adapter is used. When you use the SDK in non-browser based environments you need
     *  to provide this callback such that the SDK is able to use a custom storage adapter.
     *
     * @param storageName Name of the container to retrieve
     */
    chainStorageCtor?: <T extends StorageObject>(storageName: string) => IStorageManager<T>,
    /**
     * Maximum allowed pricing difference between intermediary (LP) returned rate and current market rate. In
     *  parts-per-million, e.g. 2% is written as `20000`.
     */
    pricingFeeDifferencePPM?: bigint,
    /**
     * Optional pricing function to use instead of default pricing APIs. See {@link CustomPriceFunction} for details.
     */
    getPriceFn?: CustomPriceFunction,
    /**
     * Mempool API instance or URLs to use, if not provided the defaults will be used for the specified bitcoin network
     */
    mempoolApi?: MempoolApi | MempoolBitcoinRpc | string | string[],
    /**
     * Data propagation messenger instance to use for broadcasting data to watchtowers. By default, a Nostr-based
     *  messenger is used.
     */
    messenger?: Messenger,
} & SwapperOptions;

/**
 * Token resolvers for all chains, resolve tokens based on their address
 *
 * @category Core
 */
export type TypedTokenResolvers<T extends readonly ChainInitializer<any, any, any>[]> =
    (T extends readonly [infer First extends ChainInitializer<any, any, any>, ...infer Rest extends ChainInitializer<any, any, any>[]]
        ? TokenResolverDict<First> & TypedTokenResolvers<Rest>
        : unknown);

/**
 * All tokens including Bitcoin tokens
 *
 * @category Core
 */
export type TypedTokens<T extends readonly ChainInitializer<any, ChainType, any>[]> = GetAllTokens<T> & {
    BITCOIN: {
        BTC: BtcToken<false>,
        BTCLN: BtcToken<true>
    }
};

/**
 * Type alias for a Swapper instance with typed chain support
 *
 * @category Core
 */
export type TypedSwapper<T extends readonly ChainInitializer<any, ChainType, any>[]> = Swapper<ToMultichain<T>>;

/**
 * Type alias for a specific swap type
 *
 * @category Core
 */
export type TypedSwap<
    T extends ChainInitializer<any, ChainType, any>,
    S extends SwapType
> = SwapTypeMapping<T["chainType"]>[S];

const registries: {[key in BitcoinNetwork]?: string} = {
    [BitcoinNetwork.MAINNET]: "https://api.github.com/repos/adambor/SolLightning-registry/contents/registry-mainnet.json?ref=main",
    [BitcoinNetwork.TESTNET]: "https://api.github.com/repos/adambor/SolLightning-registry/contents/registry.json?ref=main",
    [BitcoinNetwork.TESTNET4]: "https://api.github.com/repos/adambor/SolLightning-registry/contents/registry-testnet4.json?ref=main"
}

const trustedIntermediaries: {[key in BitcoinNetwork]?: string} = {
    [BitcoinNetwork.MAINNET]: "https://node3.gethopa.com:34100",
    [BitcoinNetwork.TESTNET]: "https://node3.gethopa.com:24100"
}

const mempoolUrls: {[key in BitcoinNetwork]?: string[]} = {
    [BitcoinNetwork.MAINNET]: [
        "https://mempool.space/api/",
        // "https://mempool.holdings/api/",
        "https://mempool.fra.mempool.space/api/",
        "https://mempool.va1.mempool.space/api/",
        "https://mempool.tk7.mempool.space/api/"
    ],
    [BitcoinNetwork.TESTNET]: [
        "https://mempool.space/testnet/api/",
        // "https://mempool.holdings/testnet/api/",
        "https://mempool.fra.mempool.space/testnet/api/",
        "https://mempool.va1.mempool.space/testnet/api/",
        "https://mempool.tk7.mempool.space/testnet/api/"
    ],
    [BitcoinNetwork.TESTNET4]: [
        "https://mempool.space/testnet4/api/",
        // "https://mempool.holdings/testnet4/api/",
        "https://mempool.fra.mempool.space/testnet4/api/",
        "https://mempool.va1.mempool.space/testnet4/api/",
        "https://mempool.tk7.mempool.space/testnet4/api/"
    ]
}

const nostrUrls: string[] = [
    "wss://relay.damus.io", "wss://nostr.einundzwanzig.space", "wss://relay01.lnfi.network/", "wss://relay.puresignal.news/", "wss://relay.fountain.fm/", "wss://sendit.nosflare.com/"
];

/**
 * Factory class for creating and initializing Swapper instances with typed chain support
 *
 * @category Core
 */
export class SwapperFactory<T extends readonly ChainInitializer<any, ChainType, any>[]> {

    /**
     * All available tokens for the atomiq SDK
     */
    Tokens: TypedTokens<T> = {
        BITCOIN: BitcoinTokens
    } as any;
    /**
     * Token resolvers for various smart chains supported by the SDK, allow fetching tokens based on their addresses
     */
    TokenResolver: TypedTokenResolvers<T> = {} as any;

    private smartChainTokens: SCToken[] = [];

    constructor(readonly initializers: T) {
        this.initializers = initializers;
        initializers.forEach(initializer => {
            const addressMap: {[tokenAddress: string]: SCToken} = {};

            const tokens = (this.Tokens[initializer.chainId as keyof GetAllTokens<T>] = {} as any);

            for(let ticker in initializer.tokens) {
                const assetData = initializer.tokens[ticker] as any;
                const token: SCToken = {
                    chain: "SC",
                    chainId: initializer.chainId,
                    ticker,
                    name: SmartChainAssets[ticker as SmartChainAssetTickers]?.name ?? ticker,
                    decimals: assetData.decimals,
                    displayDecimals: assetData.displayDecimals,
                    address: assetData.address,
                    equals: (other: Token) => other.chainId===initializer.chainId && other.ticker===ticker && other.address===assetData.address,
                    toString: () => `${initializer.chainId}-${ticker}`
                };
                this.smartChainTokens.push(token);
                tokens[ticker] = addressMap[assetData.address] = token;
            }

            this.TokenResolver[initializer.chainId as keyof TypedTokenResolvers<T>] = {
                getToken: (address: string) => addressMap[address]
            } as any;
        });
    }

    /**
     * Returns a new swapper instance with the passed options.
     *
     * The swapper returned here is not yet initialized, be sure to call {@link Swapper.init}, before
     *  calling any other functions in the swapper instance.
     *
     * @param options Options for customizing the swapper instance
     */
    newSwapper(options: TypedSwapperOptions<T>): TypedSwapper<T> {
        options.bitcoinNetwork ??= BitcoinNetwork.MAINNET;
        options.storagePrefix ??= "atomiqsdk-"+options.bitcoinNetwork+"-";
        options.messenger ??= new NostrMessenger(options.bitcoinNetwork, nostrUrls);
        options.defaultTrustedIntermediaryUrl ??= trustedIntermediaries[options.bitcoinNetwork];
        options.registryUrl ??= registries[options.bitcoinNetwork];

        let bitcoinRpc: MempoolBitcoinRpc;
        if(options.mempoolApi!=null) {
            bitcoinRpc = options.mempoolApi instanceof MempoolBitcoinRpc ? options.mempoolApi : new MempoolBitcoinRpc(options.mempoolApi, options.bitcoinNetwork);
        } else {
            const urls = mempoolUrls[options.bitcoinNetwork];
            if(urls==null) throw new Error(`No pre-configured urls for ${BitcoinNetwork[options.bitcoinNetwork]} network were found, please explicitly pass mempoolApi parameter!`);
            bitcoinRpc = new MempoolBitcoinRpc(urls, options.bitcoinNetwork);
        }

        const pricingAssets: (RedundantSwapPriceAssets<ToMultichain<T>>[number] & {ticker: string, name: string})[] = [];
        Object.keys(SmartChainAssets).forEach((ticker) => {
            const chains: any = {};
            for(let {tokens, chainId} of this.initializers) {
                if(tokens[ticker]!=null) chains[chainId] = tokens[ticker];
            }
            const assetData = SmartChainAssets[ticker as SmartChainAssetTickers];
            pricingAssets.push({
                ...assetData.pricing,
                chains,
                ticker,
                name: assetData.name
            })
        });

        options.chainStorageCtor ??= <T extends StorageObject>(name: string) => new LocalStorageManager<T>(name);

        const chains: {[key in T[number]["chainId"]]: ChainData<any>} = {} as any;
        for(let {initializer, chainId} of this.initializers) {
            const chainOptions = options.chains[chainId as keyof GetAllOptions<T>];
            if(chainOptions==null) continue;
            chains[chainId as T[number]["chainId"]] = initializer(chainOptions, bitcoinRpc, options.bitcoinNetwork, options.chainStorageCtor) as any;
        }

        const swapPricing = options.getPriceFn!=null ?
            new SingleSwapPrice(options.pricingFeeDifferencePPM ?? 10000n, new CustomPriceProvider(pricingAssets.map(val => {
                return {
                    coinId: val.ticker,
                    chains: val.chains
                }
            }), options.getPriceFn)) :
            RedundantSwapPrice.createFromTokenMap<ToMultichain<T>>(options.pricingFeeDifferencePPM ?? 10000n, pricingAssets);

        return new Swapper<ToMultichain<T>>(
            bitcoinRpc,
            bitcoinRpc,
            (btcRelay: BtcRelay<any, any, any>) => new MempoolBtcRelaySynchronizer(btcRelay, bitcoinRpc),
            chains as any,
            swapPricing,
            this.smartChainTokens,
            options.messenger,
            options
        );
    }

    /**
     * Returns a new and already initialized swapper instance with the passed options. There is no need
     *  to call {@link Swapper.init} anymore.
     *
     * @param options Options for customizing the swapper instance
     */
    async newSwapperInitialized(options: TypedSwapperOptions<T>): Promise<TypedSwapper<T>> {
        const swapper = this.newSwapper(options);
        await swapper.init();
        return swapper;
    }

}
