import {SignatureVerificationError, SwapDataVerificationError} from "@atomiqlabs/base";
import {toHex, tryWithRetries} from "../../../utils/Utils";
import {StarknetSwapModule} from "../StarknetSwapModule";
import {StarknetSwapData} from "../StarknetSwapData";
import {StarknetAction, StarknetGas, sumStarknetGas} from "../../base/StarknetAction";
import {StarknetSwapContract} from "../StarknetSwapContract";
import {IHandler} from "../handlers/IHandler";
import {BigNumberish} from "starknet";
import {StarknetTx} from "../../base/modules/StarknetTransactions";
import {StarknetSigner} from "../../wallet/StarknetSigner";
import {StarknetFees} from "../../base/modules/StarknetFees";

const Refund = [
    { name: 'Swap hash', type: 'felt' },
    { name: 'Timeout', type: 'timestamp' }
];

export class StarknetSwapRefund extends StarknetSwapModule {

    private static readonly GasCosts = {
        REFUND: {l1: 750, l2: 0},
        REFUND_PAY_OUT: {l1: 1250, l2: 0}
    };

    /**
     * Action for generic Refund instruction
     *
     * @param signer
     * @param swapData
     * @param witness
     * @param handlerGas
     * @constructor
     * @private
     */
    private Refund(
        signer: string,
        swapData: StarknetSwapData,
        witness: BigNumberish[],
        handlerGas?: StarknetGas
    ): StarknetAction {
        return new StarknetAction(signer, this.root,
            this.contract.populateTransaction.refund(swapData.toEscrowStruct(), witness),
            sumStarknetGas(swapData.payIn ? StarknetSwapRefund.GasCosts.REFUND_PAY_OUT : StarknetSwapRefund.GasCosts.REFUND, handlerGas)
        );
    }

    /**
     * Action for cooperative refunding with signature
     *
     * @param sender
     * @param swapData
     * @param timeout
     * @param signature
     * @constructor
     * @private
     */
    private RefundWithSignature(
        sender: string,
        swapData: StarknetSwapData,
        timeout: string,
        signature: BigNumberish[]
    ): StarknetAction {
        return new StarknetAction(sender, this.root,
            this.contract.populateTransaction.cooperative_refund(swapData.toEscrowStruct(), signature, BigInt(timeout)),
            swapData.payIn ? StarknetSwapRefund.GasCosts.REFUND_PAY_OUT : StarknetSwapRefund.GasCosts.REFUND
        );
    }

    constructor(root: StarknetSwapContract) {
        super(root);
    }

    public async signSwapRefund(
        signer: StarknetSigner,
        swapData: StarknetSwapData,
        authorizationTimeout: number
    ): Promise<{ prefix: string; timeout: string; signature: string }> {
        const authPrefix = "refund";
        const authTimeout = Math.floor(Date.now()/1000)+authorizationTimeout;

        const signature = await this.root.Signatures.signTypedMessage(signer, Refund, "Refund", {
            "Swap hash": "0x"+swapData.getEscrowHash(),
            "Timeout": toHex(authTimeout)
        });

        return {
            prefix: authPrefix,
            timeout: authTimeout.toString(10),
            signature: signature
        };
    }

    public async isSignatureValid(
        swapData: StarknetSwapData,
        timeout: string,
        prefix: string,
        signature: string
    ): Promise<null> {
        if(prefix!=="refund") throw new SignatureVerificationError("Invalid prefix");

        const expiryTimestamp = BigInt(timeout);
        const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));

        const isExpired = (expiryTimestamp - currentTimestamp) < BigInt(this.root.authGracePeriod);
        if(isExpired) throw new SignatureVerificationError("Authorization expired!");

        const valid = await this.root.Signatures.isValidSignature(signature, swapData.claimer, Refund, "Refund", {
            "Swap hash": "0x"+swapData.getEscrowHash(),
            "Timeout": toHex(expiryTimestamp)
        });

        if(!valid) {
            throw new SignatureVerificationError("Invalid signature!");
        }

        return null;
    }

    /**
     * Creates transactions required for refunding timed out swap
     *
     * @param signer
     * @param swapData swap data to refund
     * @param check whether to check if swap is already expired and refundable
     * @param feeRate fee rate to be used for the transactions
     * @param witnessData
     */
    public async txsRefund<T>(
        signer: string,
        swapData: StarknetSwapData,
        check?: boolean,
        feeRate?: string,
        witnessData?: T
    ): Promise<StarknetTx[]> {
        const refundHandler: IHandler<any, T> = this.root.refundHandlersByAddress[swapData.refundHandler.toLowerCase()];
        if(refundHandler==null) throw new Error("Invalid refund handler");

        if(check && !await tryWithRetries(() => this.root.isRequestRefundable(swapData.offerer.toString(), swapData), this.retryPolicy)) {
            throw new SwapDataVerificationError("Not refundable yet!");
        }

        feeRate ??= await this.root.Fees.getFeeRate();

        const {initialTxns, witness} = await refundHandler.getWitness(signer, swapData, witnessData, feeRate);

        const action = this.Refund(signer, swapData, witness, refundHandler.getGas(swapData));
        await action.addToTxs(initialTxns, feeRate);

        this.logger.debug("txsRefund(): creating refund transaction, swap: "+swapData.getClaimHash());

        return initialTxns;
    }

    /**
     * Creates transactions required for refunding the swap with authorization signature, also unwraps WSOL to SOL
     *
     * @param signer
     * @param swapData swap data to refund
     * @param timeout signature timeout
     * @param prefix signature prefix of the counterparty
     * @param signature signature of the counterparty
     * @param check whether to check if swap is committed before attempting refund
     * @param feeRate fee rate to be used for the transactions
     */
    public async txsRefundWithAuthorization(
        signer: string,
        swapData: StarknetSwapData,
        timeout: string,
        prefix: string,
        signature: string,
        check?: boolean,
        feeRate?: string
    ): Promise<StarknetTx[]> {
        if(check && !await tryWithRetries(() => this.root.isCommited(swapData), this.retryPolicy)) {
            throw new SwapDataVerificationError("Not correctly committed");
        }
        await tryWithRetries(
            () => this.isSignatureValid(swapData, timeout, prefix, signature),
            this.retryPolicy,
            (e) => e instanceof SignatureVerificationError
        );

        const action = this.RefundWithSignature(signer, swapData, timeout, JSON.parse(signature));

        feeRate ??= await this.root.Fees.getFeeRate();

        this.logger.debug("txsRefundWithAuthorization(): creating refund transaction, swap: "+swapData.getClaimHash()+
            " auth expiry: "+timeout+" signature: "+signature);

        return [await action.tx(feeRate)];
    }

    /**
     * Get the estimated solana transaction fee of the refund transaction, in the worst case scenario in case where the
     *  ATA needs to be initialized again (i.e. adding the ATA rent exempt lamports to the fee)
     */
    async getRefundFee(swapData: StarknetSwapData, feeRate?: string): Promise<bigint> {
        feeRate ??= await this.root.Fees.getFeeRate();
        return StarknetFees.getGasFee(swapData.payIn ? StarknetSwapRefund.GasCosts.REFUND_PAY_OUT.l1 : StarknetSwapRefund.GasCosts.REFUND.l1, feeRate);
    }

}