import type {
    Assign,
    Prettify,
} from "viem"
import {
    type Address,
    type Client,
    type Hex,
    concat,
    encodeFunctionData,
    toHex,
} from "viem"
import {
    type SmartAccount,
    type SmartAccountImplementation,
    entryPoint07Abi,
    entryPoint07Address,
    toSmartAccount
} from "viem/account-abstraction"
import { readContract } from "viem/actions"
import { getAccountNonce } from "../../actions/public/getAccountNonce"
import { decode7579Calls } from "../../utils/decode7579Calls"
import { encode7579Calls } from "../../utils/encode7579Calls"

/**
 * The account creation ABI for Biconomy Smart Account (from the biconomy SmartAccountFactory)
 */

/**
 * Default addresses for Biconomy Smart Account
 */
const BICONOMY_ADDRESSES: {
    CHARTER_FACTORY_ADDRESS: Address
    K1_VALIDATOR_ADDRESS: Address
} = {
    CHARTER_FACTORY_ADDRESS: "0xF6044Df1977DebF1069dE44a3Bf002d8738E25D6",
    K1_VALIDATOR_ADDRESS: "0x9Bf4eF98187990262579B0C98764683A824dDc5b"
}

export type ToNexusSmartAccountParameters = Prettify<{
    client: Client
    version: "1.0.0"
    address?: Address | undefined
    entryPoint?: {
        address: Address
        version: "0.7"
    }
    index?: bigint
    factoryAddress?: Address
    validatorAddress?: Address
    attesters?: Address[]
    threshold?: number
    /** 
     * Validator data containing the public key X and Y coordinates
     */
    validatorData?: {
        pubKeyX: bigint
        pubKeyY: bigint
    }
    /**
     * Hash of the authenticator ID
     */
    authenticatorIdHash?: Hex
    /**
     * Array of executor modules with their configuration data
     */
    executors?: { module: Address; data: Hex }[]
    /**
     * Hook module configuration
     */
    hook?: {
        module: Address
        data: Hex
    }
    /**
     * Array of fallback modules with their configuration data
     */
    fallbacks?: { module: Address; data: Hex }[]
    /**
     * Array of pre-validation hooks
     */
    preValidationHooks?: { hookType: bigint; module: Address; data: Hex }[]
    /**
     * Address of the registry contract
     */
    registryAddress?: Address
}>

export type NexusSmartAccountImplementation = Assign<
    SmartAccountImplementation<typeof entryPoint07Abi, "0.7">,
    { sign: NonNullable<SmartAccountImplementation["sign"]> }
>

export type ToNexusSmartAccountReturnType = Prettify<
    SmartAccount<NexusSmartAccountImplementation>
>

export async function toNexusSmartAccount(
    parameters: ToNexusSmartAccountParameters
): Promise<ToNexusSmartAccountReturnType> {
    const {
        client,
        index = 0n,
        address,
        factoryAddress = BICONOMY_ADDRESSES.CHARTER_FACTORY_ADDRESS,
        validatorAddress = BICONOMY_ADDRESSES.K1_VALIDATOR_ADDRESS,
        attesters = [],
        threshold = 0
    } = parameters

    const entryPoint = {
        address: parameters.entryPoint?.address ?? entryPoint07Address,
        abi: entryPoint07Abi,
        version: parameters.entryPoint?.version ?? "0.7"
    }

    let _accountAddress: Address | undefined = address

    const getFactoryArgs = async () => {
        // Carefully handle defaults only for undefined values
        const validatorData = parameters.validatorData === undefined
            ? { pubKeyX: 0n, pubKeyY: 0n }
            : parameters.validatorData
        
        const authenticatorIdHash = parameters.authenticatorIdHash === undefined
            ? "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex
            : parameters.authenticatorIdHash
        
        const executors = parameters.executors === undefined ? [] : parameters.executors
        
        const hook = parameters.hook === undefined
            ? { module: "0x0000000000000000000000000000000000000000" as Address, data: "0x" as Hex }
            : parameters.hook
        
        const fallbacks = parameters.fallbacks === undefined ? [] : parameters.fallbacks
        
        const preValidationHooks = parameters.preValidationHooks === undefined ? [] : parameters.preValidationHooks
        
        const registryConfig = {
            registry: parameters.registryAddress === undefined
                ? "0x0000000000000000000000000000000000000000" as Address
                : parameters.registryAddress,
            attesters: attesters.sort((left, right) =>
                left.toLowerCase().localeCompare(right.toLowerCase())
            ) as readonly Address[],
            threshold
        }

        return {
            factory: factoryAddress,
            factoryData: encodeFunctionData({
                abi: [
                    {
                        name: "createAccount",
                        type: "function",
                        stateMutability: "payable",
                        inputs: [
                            { type: "uint256", name: "index" },
                            { 
                                type: "tuple", 
                                name: "validatorData",
                                components: [
                                    { type: "uint256", name: "pubKeyX" },
                                    { type: "uint256", name: "pubKeyY" }
                                ] 
                            },
                            { type: "bytes32", name: "authenticatorIdHash" },
                            { 
                                type: "tuple[]", 
                                name: "executors",
                                components: [
                                    { type: "address", name: "module" },
                                    { type: "bytes", name: "data" }
                                ] 
                            },
                            { 
                                type: "tuple", 
                                name: "hook",
                                components: [
                                    { type: "address", name: "module" },
                                    { type: "bytes", name: "data" }
                                ] 
                            },
                            { 
                                type: "tuple[]", 
                                name: "fallbacks",
                                components: [
                                    { type: "address", name: "module" },
                                    { type: "bytes", name: "data" }
                                ] 
                            },
                            { 
                                type: "tuple[]", 
                                name: "preValidationHooks",
                                components: [
                                    { type: "uint256", name: "hookType" },
                                    { type: "address", name: "module" },
                                    { type: "bytes", name: "data" }
                                ] 
                            },
                            { 
                                type: "tuple", 
                                name: "registryConfig",
                                components: [
                                    { type: "address", name: "registry" },
                                    { type: "address[]", name: "attesters" },
                                    { type: "uint8", name: "threshold" }
                                ] 
                            }
                        ],
                        outputs: [{ type: "address", name: "" }]
                    }
                ],
                functionName: "createAccount",
                args: [
                    index,
                    validatorData,
                    authenticatorIdHash,
                    executors,
                    hook,
                    fallbacks,
                    preValidationHooks,
                    registryConfig
                ]
            })
        }
    }

    return toSmartAccount({
        client,
        entryPoint,
        getFactoryArgs,
        async getAddress() {
            if (_accountAddress) return _accountAddress

            // If we need to compute the address
            const computedAddress = await readContract(client, {
                address: factoryAddress,
                abi: [
                    {
                        name: "computeAccountAddress",
                        type: "function",
                        stateMutability: "view",
                        inputs: [
                            { type: "uint256", name: "index" },
                            { 
                                type: "tuple", 
                                name: "validatorData",
                                components: [
                                    { type: "uint256", name: "pubKeyX" },
                                    { type: "uint256", name: "pubKeyY" }
                                ] 
                            },
                            { type: "bytes32", name: "authenticatorIdHash" },
                            { 
                                type: "tuple[]", 
                                name: "executors",
                                components: [
                                    { type: "address", name: "module" },
                                    { type: "bytes", name: "data" }
                                ] 
                            },
                            { 
                                type: "tuple", 
                                name: "hook",
                                components: [
                                    { type: "address", name: "module" },
                                    { type: "bytes", name: "data" }
                                ] 
                            },
                            { 
                                type: "tuple[]", 
                                name: "fallbacks",
                                components: [
                                    { type: "address", name: "module" },
                                    { type: "bytes", name: "data" }
                                ] 
                            },
                            { 
                                type: "tuple[]", 
                                name: "preValidationHooks",
                                components: [
                                    { type: "uint256", name: "hookType" },
                                    { type: "address", name: "module" },
                                    { type: "bytes", name: "data" }
                                ] 
                            },
                            { 
                                type: "tuple", 
                                name: "registryConfig",
                                components: [
                                    { type: "address", name: "registry" },
                                    { type: "address[]", name: "attesters" },
                                    { type: "uint8", name: "threshold" }
                                ] 
                            }
                        ],
                        outputs: [{ type: "address", name: "expectedAddress" }]
                    }
                ],
                functionName: "computeAccountAddress",
                args: [
                    index,
                    parameters.validatorData === undefined 
                        ? { pubKeyX: 0n, pubKeyY: 0n } 
                        : parameters.validatorData,
                    parameters.authenticatorIdHash === undefined
                        ? "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex
                        : parameters.authenticatorIdHash,
                    parameters.executors === undefined ? [] : parameters.executors,
                    parameters.hook === undefined 
                        ? { module: "0x0000000000000000000000000000000000000000" as Address, data: "0x" as Hex }
                        : parameters.hook,
                    parameters.fallbacks === undefined ? [] : parameters.fallbacks,
                    parameters.preValidationHooks === undefined ? [] : parameters.preValidationHooks,
                    {
                        registry: parameters.registryAddress === undefined
                            ? "0x0000000000000000000000000000000000000000" as Address
                            : parameters.registryAddress,
                        attesters: attesters.sort((left, right) =>
                            left.toLowerCase().localeCompare(right.toLowerCase())
                        ) as readonly Address[],
                        threshold
                    }
                ]
            })

            _accountAddress = computedAddress
            return computedAddress
        },
        async getNonce(args) {
            const TIMESTAMP_ADJUSTMENT = 16777215n // max value for size 3
            const defaultedKey = (args?.key ?? 0n) % TIMESTAMP_ADJUSTMENT
            const defaultedValidationMode = "0x00"
            const key = concat([
                toHex(defaultedKey, { size: 3 }),
                defaultedValidationMode,
                validatorAddress
            ])

            const address = await this.getAddress()

            return getAccountNonce(client, {
                address,
                entryPointAddress: entryPoint.address,
                key: BigInt(key)
            })
        },
        encodeCalls: async (calls) => {
            return encode7579Calls({
                mode: {
                    type: calls.length > 1 ? "batchcall" : "call",
                    revertOnError: false,
                    selector: "0x",
                    context: "0x"
                },
                callData: calls
            })
        },
        async decodeCalls(callData) {
            return decode7579Calls(callData).callData
        },
        async getStubSignature() {
            const dynamicPart = validatorAddress.substring(2).padEnd(40, "0")
            return `0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000${dynamicPart}000000000000000000000000000000000000000000000000000000000000004181d4b4981670cb18f99f0b4a66446df1bf5b204d24cfcb659bf38ba27a4359b5711649ec2423c5e1247245eba2964679b6a1dbb85c992ae40b9b00c6935b02ff1b00000000000000000000000000000000000000000000000000000000000000` as Hex
        },
        async sign({ hash }) {
            return this.signMessage({ message: hash })
        },
        // README: Logic Moved to client side
        async signMessage() {
            throw new Error('Not implemented')
        },
        // README: Logic Moved to client side
        async signTypedData() {
            throw new Error('Not implemented')
        },
        async signUserOperation() {
            throw new Error('Not implemented')
        }
    })
}
