import {SwapData, ChainSwapType} from "@atomiqlabs/base";
import {TimelockRefundHandler} from "./handlers/refund/TimelockRefundHandler";
import {BigNumberish, cairo, CairoOption, CairoOptionVariant, hash} from "starknet";
import {replaceBigInts, Serialized, toBigInt, toHex} from "../../utils/Utils";
import {
    StringToPrimitiveType
} from "abi-wan-kanabi/dist/kanabi";
import {EscrowManagerAbi} from "./EscrowManagerAbi";
import {IClaimHandler} from "./handlers/claim/ClaimHandlers";

const FLAG_PAY_OUT: bigint = 0x01n;
const FLAG_PAY_IN: bigint = 0x02n;
const FLAG_REPUTATION: bigint = 0x04n;

export type StarknetSwapDataType = StringToPrimitiveType<typeof EscrowManagerAbi, "escrow_manager::structs::escrow::EscrowData">;

/**
 * Represents a success hook/action to be executed upon claim of the swap
 *
 * @category Swaps
 */
export type StarknetSuccessAction = {
    executionHash: string,
    executionExpiry: bigint,
    executionFee: bigint
}

function successActionEquals(a?: StarknetSuccessAction, b?: StarknetSuccessAction): boolean {
    if(a!=null && b!=null) {
        return a.executionHash.toLowerCase()===b.executionHash.toLowerCase() &&
            a.executionExpiry === b.executionExpiry &&
            a.executionFee === b.executionFee;
    }
    return a === b;
}

export type StarknetSwapDataCtorArgs = {
    offerer: string,
    claimer: string,
    token: string,
    refundHandler: string,
    claimHandler: string,
    payOut: boolean,
    payIn: boolean,
    reputation: boolean,
    sequence: bigint,
    claimData: string,
    refundData: string,
    amount: bigint,
    feeToken: string,
    securityDeposit: bigint,
    claimerBounty: bigint,
    kind: ChainSwapType,
    extraData?: string,
    successAction?: StarknetSuccessAction
};

function isSerializedData(obj: any): obj is ({type: "strk"} & Serialized<StarknetSwapData>) {
    return obj.type==="strk";
}

/**
 * Represents swap data for executing PrTLC (on-chain) or HTLC (lightning) based swaps
 *
 * @category Swaps
 */
export class StarknetSwapData extends SwapData {

    /**
     *
     * @param value
     * @private
     */
    private static toFlags(value: number | bigint | string): {payOut: boolean, payIn: boolean, reputation: boolean, sequence: bigint} {
        const val = toBigInt(value);
        return {
            sequence: val >> 64n,
            payOut: (val & FLAG_PAY_OUT) === FLAG_PAY_OUT,
            payIn: (val & FLAG_PAY_IN) === FLAG_PAY_IN,
            reputation: (val & FLAG_REPUTATION) === FLAG_REPUTATION
        }
    }

    /**
     *
     * @private
     */
    private getFlags(): bigint {
        return (this.sequence << 64n) +
            (this.payOut ? FLAG_PAY_OUT : 0n) +
            (this.payIn ? FLAG_PAY_IN : 0n) +
            (this.reputation ? FLAG_REPUTATION : 0n);
    }

    offerer: string;
    claimer: string;
    token: string;

    refundHandler: string;
    claimHandler: string;

    //Flags
    payOut: boolean;
    payIn: boolean;
    reputation: boolean;
    sequence: bigint;

    claimData: string;
    refundData: string;

    amount: bigint;

    feeToken: string;
    securityDeposit: bigint;
    claimerBounty: bigint;

    extraData?: string;

    successAction?: StarknetSuccessAction;

    kind: ChainSwapType;

    /**
     * Creates a new swap data based on the provided arguments
     *
     * @param args
     */
    constructor(args: StarknetSwapDataCtorArgs);

    /**
     * Deserializes the spv vault data from its serialized implementation (returned from {@link StarknetSwapData.serialize})
     *
     * @param data
     */
    constructor(data: Serialized<StarknetSwapData> & {type: "strk"});

    constructor(
        data: StarknetSwapDataCtorArgs | (Serialized<StarknetSwapData> & {type: "strk"})
    ) {
        super();
        if(!isSerializedData(data)) {
            this.offerer = data.offerer;
            this.claimer = data.claimer;
            this.token = data.token;
            this.refundHandler = data.refundHandler;
            this.claimHandler = data.claimHandler;
            this.payOut = data.payOut;
            this.payIn = data.payIn;
            this.reputation = data.reputation;
            this.sequence = data.sequence;
            this.claimData = data.claimData;
            this.refundData = data.refundData;
            this.amount = data.amount;
            this.feeToken = data.feeToken;
            this.securityDeposit = data.securityDeposit;
            this.claimerBounty = data.claimerBounty;
            this.kind = data.kind;
            this.extraData = data.extraData;
            this.successAction = data.successAction;
        } else {
            this.offerer = data.offerer;
            this.claimer = data.claimer;
            this.token = data.token;
            this.refundHandler = data.refundHandler;
            this.claimHandler = data.claimHandler;
            this.payOut = data.payOut;
            this.payIn = data.payIn;
            this.reputation = data.reputation;
            this.sequence = BigInt(data.sequence);
            this.claimData = data.claimData;
            this.refundData = data.refundData;
            this.amount = BigInt(data.amount);
            this.feeToken = data.feeToken;
            this.securityDeposit = BigInt(data.securityDeposit);
            this.claimerBounty = BigInt(data.claimerBounty);
            this.kind = data.kind;
            this.extraData = data.extraData;
            this.successAction = data.successAction==null || Array.isArray(data.successAction) ? undefined : {
                executionHash: data.successAction.executionHash,
                executionExpiry: BigInt(data.successAction.executionExpiry),
                executionFee: BigInt(data.successAction.executionFee),
            }
        }
    }

    /**
     * @inheritDoc
     */
    getOfferer(): string {
        return this.offerer;
    }

    /**
     * @inheritDoc
     */
    setOfferer(newOfferer: string) {
        this.offerer = newOfferer;
        this.payIn = true;
    }

    /**
     * @inheritDoc
     */
    getClaimer(): string {
        return this.claimer;
    }

    /**
     * @inheritDoc
     */
    setClaimer(newClaimer: string) {
        this.claimer = newClaimer;
        this.payIn = false;
        this.payOut = true;
        this.reputation = false;
    }

    /**
     * @inheritDoc
     */
    serialize(): Serialized<StarknetSwapData> & {type: "strk"} {
        return {
            type: "strk",
            offerer: this.offerer,
            claimer: this.claimer,
            token: this.token,
            refundHandler: this.refundHandler,
            claimHandler: this.claimHandler,
            payOut: this.payOut,
            payIn: this.payIn,
            reputation: this.reputation,
            sequence: this.sequence?.toString(10),
            claimData: this.claimData,
            refundData: this.refundData,
            amount: this.amount?.toString(10),
            feeToken: this.feeToken,
            securityDeposit: this.securityDeposit?.toString(10),
            claimerBounty: this.claimerBounty?.toString(10),
            kind: this.kind,
            extraData: this.extraData,
            successAction: this.successAction==null ? undefined : {
                executionHash: this.successAction.executionHash,
                executionExpiry: this.successAction.executionExpiry.toString(10),
                executionFee: this.successAction.executionFee.toString(10)
            }
        }
    }

    /**
     * @inheritDoc
     */
    getAmount(): bigint {
        return this.amount;
    }

    /**
     * @inheritDoc
     */
    getToken(): string {
        return this.token;
    }

    /**
     * @inheritDoc
     */
    isToken(token: string): boolean {
        return this.token.toLowerCase()===token.toLowerCase();
    }

    /**
     * @inheritDoc
     */
    getType(): ChainSwapType {
        return this.kind;
    }

    /**
     * @inheritDoc
     */
    getExpiry(): bigint {
        return TimelockRefundHandler.getExpiry(this);
    }

    /**
     * @inheritDoc
     */
    isPayIn(): boolean {
        return this.payIn;
    }

    /**
     * @inheritDoc
     */
    isPayOut(): boolean {
        return this.payOut;
    }

    /**
     * @inheritDoc
     */
    isTrackingReputation(): boolean {
        return this.reputation;
    }

    /**
     * @inheritDoc
     */
    getEscrowHash(): string {
        const amountValue = cairo.uint256("0x"+this.amount.toString(16));
        const securityDepositValue = cairo.uint256("0x"+this.securityDeposit.toString(16));
        const claimerBountyValue = cairo.uint256("0x"+this.claimerBounty.toString(16));
        const elements = [
            this.offerer,
            this.claimer,
            this.token,
            this.refundHandler,
            this.claimHandler,
            this.getFlags(),
            this.claimData,
            this.refundData,
            amountValue.low,
            amountValue.high,
            this.feeToken,
            securityDepositValue.low,
            securityDepositValue.high,
            claimerBountyValue.low,
            claimerBountyValue.high
        ];
        if(this.successAction!=null) {
            elements.push(this.successAction.executionHash);
            elements.push(this.successAction.executionExpiry);
            const feeValue = cairo.uint256("0x"+this.successAction.executionFee.toString(16));
            elements.push(feeValue.low, feeValue.high);
        }

        let escrowHash = hash.computePoseidonHashOnElements(elements);
        if(escrowHash.startsWith("0x")) escrowHash = escrowHash.slice(2);
        return escrowHash.padStart(64, "0");
    }

    /**
     * @inheritDoc
     */
    getClaimHash(): string {
        let hash = this.claimData;
        if(hash.startsWith("0x")) hash = hash.slice(2);
        return hash.padStart(64, "0");
    }

    /**
     * @inheritDoc
     */
    getSequence(): bigint {
        return this.sequence;
    }

    /**
     * @inheritDoc
     */
    getConfirmationsHint(): number | null {
        if(this.extraData==null) return null;
        if(this.extraData.length!=84) return null;
        return parseInt(this.extraData.slice(80), 16);
    }

    /**
     * @inheritDoc
     */
    getNonceHint(): bigint | null {
        if(this.extraData==null) return null;
        if(this.extraData.length!=84) return null;
        return BigInt("0x"+this.extraData.slice(64, 80));
    }

    /**
     * @inheritDoc
     */
    getTxoHashHint(): string | null {
        if(this.extraData==null) return null;
        if(this.extraData.length!=84) return null;
        return this.extraData.slice(0, 64);
    }

    /**
     * @inheritDoc
     */
    getExtraData(): string | null {
        return this.extraData ?? null;
    }

    /**
     * @inheritDoc
     */
    setExtraData(extraData: string): void {
        this.extraData = extraData;
    }

    /**
     * @inheritDoc
     */
    getSecurityDeposit() {
        return this.securityDeposit;
    }

    /**
     * @inheritDoc
     */
    getClaimerBounty() {
        return this.claimerBounty;
    }

    /**
     * @inheritDoc
     */
    getTotalDeposit() {
        return this.claimerBounty < this.securityDeposit ? this.securityDeposit : this.claimerBounty;
    }

    /**
     * @inheritDoc
     */
    getDepositToken() {
        return this.feeToken;
    }

    /**
     * @inheritDoc
     */
    isDepositToken(token: string): boolean {
        if(!token.startsWith("0x")) token = "0x"+token;
        return toHex(this.feeToken)===toHex(token);
    }

    /**
     * @inheritDoc
     */
    isClaimer(address: string) {
        if(!address.startsWith("0x")) address = "0x"+address;
        return toHex(this.claimer)===toHex(address);
    }

    /**
     * @inheritDoc
     */
    isOfferer(address: string) {
        if(!address.startsWith("0x")) address = "0x"+address;
        return toHex(this.offerer)===toHex(address);
    }

    /**
     * Checks whether the passed address is specified as a claim handler for the swap
     *
     * @param address
     */
    isClaimHandler(address: string): boolean {
        if(!address.startsWith("0x")) address = "0x"+address;
        return toHex(this.claimHandler)===toHex(address);
    }

    /**
     * Checks if the passed data match the swap's claim data
     *
     * @param data
     */
    isClaimData(data: string): boolean {
        if(!data.startsWith("0x")) data = "0x"+data;
        return toHex(this.claimData)===toHex(data);
    }

    /**
     * @inheritDoc
     */
    equals(other: StarknetSwapData): boolean {
        return other.offerer.toLowerCase()===this.offerer.toLowerCase() &&
            other.claimer.toLowerCase()===this.claimer.toLowerCase() &&
            other.token.toLowerCase()===this.token.toLowerCase() &&
            other.refundHandler.toLowerCase()===this.refundHandler.toLowerCase() &&
            other.claimHandler.toLowerCase()===this.claimHandler.toLowerCase() &&
            other.payIn===this.payIn &&
            other.payOut===this.payOut &&
            other.reputation===this.reputation &&
            this.sequence === other.sequence &&
            other.claimData.toLowerCase()===this.claimData.toLowerCase() &&
            other.refundData.toLowerCase()===this.refundData.toLowerCase() &&
            other.amount === this.amount &&
            other.securityDeposit === this.securityDeposit &&
            other.claimerBounty === this.claimerBounty &&
            successActionEquals(other.successAction, this.successAction)
    }

    /**
     * Serializes the swap data into starknet.js struct representation
     */
    toEscrowStruct(): StarknetSwapDataType {
        return {
            offerer: this.offerer,
            claimer: this.claimer,
            token: this.token,
            refund_handler: this.refundHandler,
            claim_handler: this.claimHandler,
            flags: this.getFlags(),
            claim_data: this.claimData,
            refund_data: this.refundData,
            amount: cairo.uint256(toBigInt(this.amount)),
            fee_token: this.feeToken,
            security_deposit: cairo.uint256(toBigInt(this.securityDeposit)),
            claimer_bounty: cairo.uint256(toBigInt(this.claimerBounty)),
            success_action: new CairoOption(
                this.successAction==null ? CairoOptionVariant.None : CairoOptionVariant.Some,
                this.successAction==null ? undefined : {
                    hash: this.successAction.executionHash,
                    expiry: this.successAction.executionExpiry,
                    fee: cairo.uint256(this.successAction.executionFee)
                }
            ) as StarknetSwapDataType["success_action"]
        }
    }

    /**
     * Deserializes swap data from the provided felt252 array,
     *
     * @param span a felt252 array of length 16 or more
     * @param claimHandlerImpl Claim handler implementation to parse the swap type, this is checked
     *  for internally and this throws an error if the passed `claimHandlerImpl` doesn't match the
     *  claim handler address in the passed swap data
     */
    static fromSerializedFeltArray(span: BigNumberish[], claimHandlerImpl: IClaimHandler<any, any>) {
        if(span.length < 16) throw new Error("Invalid length of serialized starknet swap data!");
        const offerer = toHex(span.shift()!);
        const claimer = toHex(span.shift()!);
        const token = toHex(span.shift()!);
        const refundHandler = toHex(span.shift()!);
        const claimHandler = toHex(span.shift()!);
        const {payOut, payIn, reputation, sequence} = StarknetSwapData.toFlags(span.shift()!);
        const claimData = toHex(span.shift()!);
        const refundData = toHex(span.shift()!);
        const amount = toBigInt({low: span.shift()!, high: span.shift()!});
        const feeToken = toHex(span.shift()!);
        const securityDeposit = toBigInt({low: span.shift()!, high: span.shift()!});
        const claimerBounty = toBigInt({low: span.shift()!, high: span.shift()!});
        const hasSuccessAction = toBigInt(span.shift()!) === 0n;
        let successAction: StarknetSuccessAction | undefined = undefined;
        if(hasSuccessAction) {
            if(span.length < 4) throw new Error("Invalid length of serialized starknet swap data!");
            successAction = {
                executionHash: toHex(span.shift()!),
                executionExpiry: toBigInt(span.shift()!),
                executionFee: toBigInt({low: span.shift()!, high: span.shift()!})
            }
        }

        const swapData = new StarknetSwapData({
            offerer,
            claimer,
            token,
            refundHandler,
            claimHandler,
            payOut,
            payIn,
            reputation,
            sequence,
            claimData,
            refundData,
            amount,
            feeToken,
            securityDeposit,
            claimerBounty,
            kind: claimHandlerImpl.getType(),
            successAction
        });

        if(!swapData.isClaimHandler(claimHandlerImpl.address))
            throw new Error(`Invalid swap handler impl passed! Passed: ${claimHandlerImpl.address}, actual: ${swapData.claimHandler}`);

        return swapData;
    }

    /**
     * @inheritDoc
     */
    hasSuccessAction(): boolean {
        return this.successAction != null;
    }

    /**
     * @inheritDoc
     */
    getEscrowStruct(): any {
        return replaceBigInts(this.toEscrowStruct());
    }

}

SwapData.deserializers["strk"] = StarknetSwapData;
