import { ApiPromise } from "@polkadot/api";
import { H256, AccountId } from "@polkadot/types/interfaces";
import { BlockNumber } from "@polkadot/types/interfaces/runtime";
import { Network } from "bitcoinjs-lib";
import { BitcoinUnit, Currency, MonetaryAmount } from "@interlay/monetary-js";
import { isKeyringPair } from "@polkadot/api/util";
import { InterbtcPrimitivesReplaceReplaceRequest, BitcoinAddress } from "@polkadot/types/lookup";

import {
    storageKeyToNthInner,
    getTxProof,
    parseReplaceRequest,
    ensureHashEncoded,
    newMonetaryAmount,
    newVaultCurrencyPair,
    newVaultId,
} from "../utils";
import { FeeAPI } from "./fee";
import { TransactionAPI } from "./transaction";
import { ElectrsAPI } from "../external";
import { CollateralCurrency, CollateralUnit, ReplaceRequestExt, WrappedCurrency } from "../types";
import { VaultsAPI } from "../parachain";

/**
 * @category BTC Bridge
 */
export interface ReplaceAPI {
    /**
     * @returns The minimum amount of btc that is accepted for replace requests; any lower values would
     * risk the bitcoin client to reject the payment
     */
    getDustValue(): Promise<MonetaryAmount<WrappedCurrency, BitcoinUnit>>;
    /**
     * @returns The time difference in number of blocks between when a replace request is created
     * and required completion time by a vault. The replace period has an upper limit
     * to prevent griefing of vault collateral.
     */
    getReplacePeriod(): Promise<BlockNumber>;
    /**
     * @returns An array containing the replace requests
     */
    list(): Promise<ReplaceRequestExt[]>;
    /**
     * @returns A mapping from the replace request ID to the replace request object
     */
    map(): Promise<Map<H256, ReplaceRequestExt>>;
    /**
     * @param replaceId The ID of the replace request to fetch
     * @returns A replace request object
     */
    getRequestById(replaceId: H256 | string): Promise<ReplaceRequestExt>;
    /**
     * @param amount Amount issued, denoted in Bitcoin, to have replaced by another vault
     * @param collateralCurrency Collateral currency to have replaced
     */
    request(
        amount: MonetaryAmount<WrappedCurrency, BitcoinUnit>,
        collateralCurrency: CollateralCurrency
    ): Promise<void>;
    /**
     * Wihdraw a replace request
     * @param amount The amount of wrapped tokens to withdraw from the amount
     * requested to have replaced.
     * @param collateralCurrency Collateral currency of the request
     */
    withdraw(
        amount: MonetaryAmount<WrappedCurrency, BitcoinUnit>,
        collateralCurrency: CollateralCurrency
    ): Promise<void>;
    /**
     * Accept a replace request
     * @param oldVault ID of the old vault that to be (possibly partially) replaced
     * @param amount Amount of issued tokens to be replaced
     * @param collateral The collateral for replacement
     * @param btcAddress The address that old-vault should transfer the btc to
     */
    accept(
        oldVault: AccountId,
        amount: MonetaryAmount<WrappedCurrency, BitcoinUnit>,
        collateral: MonetaryAmount<Currency<CollateralUnit>, CollateralUnit>,
        btcAddress: string
    ): Promise<void>;
    /**
     * Execute a replace request
     * @remarks If `txId` is not set, the `merkleProof` and `rawTx` must both be set.
     *
     * @param requestId The ID generated by the replace request transaction
     * @param btcTxId Bitcoin transaction ID
     */
    execute(requestId: string, btcTxId: string): Promise<void>;
    /**
     * Fetch the replace requests associated with a vault. In the returned requests,
     * the vault is either the replaced or the replacing one.
     *
     * @param vaultAccountId The AccountId of the vault used to filter replace requests
     * @returns An array with replace requests involving said vault
     */
    mapReplaceRequests(vaultAccountId: AccountId): Promise<ReplaceRequestExt[]>;
}

export class DefaultReplaceAPI implements ReplaceAPI {
    constructor(
        private api: ApiPromise,
        private btcNetwork: Network,
        private electrsAPI: ElectrsAPI,
        private wrappedCurrency: WrappedCurrency,
        private feeAPI: FeeAPI,
        private vaultsAPI: VaultsAPI,
        private transactionAPI: TransactionAPI
    ) {}

    async request(
        amount: MonetaryAmount<WrappedCurrency, BitcoinUnit>,
        collateralCurrency: CollateralCurrency
    ): Promise<void> {
        const amountAtomicUnit = this.api.createType("Balance", amount.str.Satoshi());
        // Assumes the calling account is the `vaultId`
        const vaultAccount = this.transactionAPI.getAccount();
        if (vaultAccount === undefined) {
            return Promise.reject("Vault account must be set in the replace API");
        }
        const vaultAccountId = isKeyringPair(vaultAccount) ? vaultAccount.address : vaultAccount.toString();
        const vaultId = newVaultId(this.api, vaultAccountId, collateralCurrency, this.wrappedCurrency);
        const requestTx = this.api.tx.replace.requestReplace(vaultId.currencies, amountAtomicUnit);
        await this.transactionAPI.sendLogged(requestTx, this.api.events.replace.RequestReplace);
    }

    async withdraw(
        amount: MonetaryAmount<WrappedCurrency, BitcoinUnit>,
        collateralCurrency: CollateralCurrency
    ): Promise<void> {
        const amountAtomicUnit = this.api.createType("Balance", amount.str.Satoshi());
        const vaultCurrencyPair = newVaultCurrencyPair(this.api, collateralCurrency, this.wrappedCurrency);
        const requestTx = this.api.tx.replace.withdrawReplace(vaultCurrencyPair, amountAtomicUnit);
        await this.transactionAPI.sendLogged(requestTx, this.api.events.replace.WithdrawReplace, true);
    }

    async accept(
        oldVault: AccountId,
        amount: MonetaryAmount<WrappedCurrency, BitcoinUnit>,
        collateral: MonetaryAmount<Currency<CollateralUnit>, CollateralUnit>,
        btcAddress: string
    ): Promise<void> {
        const parsedBtcAddress = this.api.createType<BitcoinAddress>("BitcoinAddress", btcAddress);
        const amountAtomicUnit = this.api.createType("Balance", amount.str.Satoshi());
        const collateralAtomicUnit = this.api.createType("Balance", collateral.toString(collateral.currency.rawBase));
        const vaultCurrencyPair = newVaultCurrencyPair(
            this.api,
            collateral.currency as CollateralCurrency,
            this.wrappedCurrency
        );
        const requestTx = this.api.tx.replace.acceptReplace(
            vaultCurrencyPair,
            oldVault,
            amountAtomicUnit,
            collateralAtomicUnit,
            parsedBtcAddress
        );
        await this.transactionAPI.sendLogged(requestTx, this.api.events.replace.AcceptReplace, true);
    }

    async execute(requestId: string, btcTxId: string): Promise<void> {
        const parsedRequestId = ensureHashEncoded(this.api, requestId);
        const txInclusionDetails = await getTxProof(this.electrsAPI, btcTxId);
        const tx = this.api.tx.replace.executeReplace(
            parsedRequestId,
            txInclusionDetails.merkleProof,
            txInclusionDetails.rawTx
        );
        await this.transactionAPI.sendLogged(tx, this.api.events.replace.ExecuteReplace, true);
    }

    async getDustValue(): Promise<MonetaryAmount<WrappedCurrency, BitcoinUnit>> {
        const dustSatoshi = await this.api.query.replace.replaceBtcDustValue();
        return newMonetaryAmount(dustSatoshi.toString(), this.wrappedCurrency);
    }

    async getReplacePeriod(): Promise<BlockNumber> {
        return await this.api.query.replace.replacePeriod();
    }

    async list(): Promise<ReplaceRequestExt[]> {
        const head = await this.api.rpc.chain.getFinalizedHead();
        const replaceRequests = await this.api.query.replace.replaceRequests.entriesAt(head);
        return await Promise.all(
            replaceRequests
                .filter((v) => v[1].isSome.valueOf)
                // Can be unwrapped because the filter removes `None` values
                .map(([id, req]) =>
                    parseReplaceRequest(
                        this.vaultsAPI,
                        req.unwrap(),
                        this.btcNetwork,
                        this.wrappedCurrency,
                        storageKeyToNthInner(id)
                    )
                )
        );
    }

    async map(): Promise<Map<H256, ReplaceRequestExt>> {
        const head = await this.api.rpc.chain.getFinalizedHead();
        const replaceRequests = await this.api.query.replace.replaceRequests.entriesAt(head);
        const replaceRequestMap = new Map<H256, ReplaceRequestExt>();
        await Promise.all(
            replaceRequests
                .filter((v) => v[1].isSome.valueOf)
                // Can be unwrapped because the filter removes `None` values
                .map(
                    ([id, req]) =>
                        new Promise<void>((resolve) => {
                            parseReplaceRequest(
                                this.vaultsAPI,
                                req.unwrap(),
                                this.btcNetwork,
                                this.wrappedCurrency,
                                storageKeyToNthInner(id)
                            ).then((replaceRequest) => {
                                replaceRequestMap.set(storageKeyToNthInner(id), replaceRequest);
                                resolve();
                            });
                        })
                )
        );
        return replaceRequestMap;
    }

    async getRequestById(replaceId: H256 | string): Promise<ReplaceRequestExt> {
        const head = await this.api.rpc.chain.getFinalizedHead();
        return parseReplaceRequest(
            this.vaultsAPI,
            await this.api.query.replace.replaceRequests.at(head, ensureHashEncoded(this.api, replaceId)),
            this.btcNetwork,
            this.wrappedCurrency,
            replaceId
        );
    }

    async mapReplaceRequests(vaultAccountId: AccountId): Promise<ReplaceRequestExt[]> {
        try {
            const [oldVaultReplaceRequests, newVaultReplaceRequests] = await Promise.all([
                this.getOldVaultReplaceRequests(vaultAccountId),
                this.getNewVaultReplaceRequests(vaultAccountId),
            ]);
            return [...oldVaultReplaceRequests, ...newVaultReplaceRequests];
        } catch (err) {
            return Promise.reject(new Error(`Error during replace request retrieval: ${err}`));
        }
    }

    async getOldVaultReplaceRequests(vaultAccountId: AccountId): Promise<ReplaceRequestExt[]> {
        return (await this.list()).filter(
            (request) => request.oldVault.accountId.toString() === vaultAccountId.toString()
        );
    }

    async getNewVaultReplaceRequests(vaultAccountId: AccountId): Promise<ReplaceRequestExt[]> {
        return (await this.list()).filter(
            (request) => request.newVault.accountId.toString() === vaultAccountId.toString()
        );
    }

    async parseRequestsAsync(
        requestPairs: [H256, InterbtcPrimitivesReplaceReplaceRequest][]
    ): Promise<[H256, ReplaceRequestExt][]> {
        return await Promise.all(
            requestPairs.map(
                ([id, req]) =>
                    new Promise<[H256, ReplaceRequestExt]>((resolve) => {
                        parseReplaceRequest(this.vaultsAPI, req, this.btcNetwork, this.wrappedCurrency, id).then(
                            (replaceRequest) => {
                                resolve([id, replaceRequest]);
                            }
                        );
                    })
            )
        );
    }
}