import { AccAddress } from '@initia/initia.js';
import type { Address } from '@ton/ton';
import bs58 from 'bs58';

import type { HexString, NormalizedHexString } from '@layerzerolabs/common-chain-model';
import { normalizeHex } from '@layerzerolabs/common-chain-model';
import { addressToHex, bigintToAddress, parseTonAddress } from '@layerzerolabs/common-ton';
import {
    bytesToHexPrefixed,
    hexToBytes,
    hexZeroPad,
    isBase58,
    isHexString,
} from '@layerzerolabs/common-utils';
import type {
    Base58String,
    ChainsByEncoding,
    ChainType,
    InferChainNamesForChainType,
    InferChainTypeForChainName,
    InitiaString,
    NativeAddress,
    ResolvedEncodingForChainName,
} from '@layerzerolabs/layerzero-definitions';
import type { ChainNativeAddress } from '@layerzerolabs/layerzero-definitions';
import { ChainName } from '@layerzerolabs/layerzero-definitions';
import { StaticChainConfigs } from '@layerzerolabs/static-chain-info';

type NativeOrString<T extends ChainName> = ChainNativeAddress<T> | ResolvedEncodingForChainName<T>;

export enum AddressEncoding {
    HEX = 'hex',
    BASE58 = 'base58',
    TON = 'ton',
    INITIA = 'initia',
}

export type ExtensionByChainType = {
    ton: {
        tonToNormalized: (tonAddress: Address) => NormalizedHexString;
    };
};

export type InferExtensionByChainName<T extends ChainName> =
    InferChainTypeForChainName<T> extends keyof ExtensionByChainType
        ? ExtensionByChainType[InferChainTypeForChainName<T>]
        : {};

export type AddressParser<T extends ChainName> = {
    normalizedToNative: (address: NormalizedHexString) => ChainNativeAddress<T>;
    normalizedToNativeString: (address: NormalizedHexString) => ResolvedEncodingForChainName<T>;
    nativeToNormalized: (nativeAddress: NativeOrString<T>) => NormalizedHexString;
    nativeToBytes: (nativeAddress: NativeOrString<T>) => Uint8Array;
    nativeToBytes32Hex: (nativeAddress: NativeOrString<T>) => HexString;
    nativeToBytes32: (nativeAddress: NativeOrString<T>) => Uint8Array;
    validateNative: (nativeAddress: string) => nativeAddress is ResolvedEncodingForChainName<T>;
    /**
     * Parse a native address to a string if it is valid.
     * Throws an error if the native address is invalid.
     * @param nativeAddress - The native address to parse.
     * @returns The parsed native address.
     */
    parseNative: (nativeAddress: string) => NormalizedHexString;
    parseNativeToHex: (nativeAddress: string) => HexString;
    encoding: AddressEncoding;
} & InferExtensionByChainName<T>;

export function addressParser<T extends ChainName>(chainName: T): AddressParser<T> {
    // FIXME: figure out a way to not have to cast here
    if (chainName === ChainName.TON)
        return tonAddressParser(chainName) as unknown as AddressParser<T>;
    if (chainName === ChainName.SOLANA)
        return base58AddressParser(chainName) as unknown as AddressParser<T>;
    if (chainName === ChainName.INITIA)
        return initiaAddressParser(chainName) as unknown as AddressParser<T>;
    return hexAddressParser(chainName) as unknown as AddressParser<T>;
}

export namespace addressParser {
    export const normalizedToBytes32Hex = (address: NormalizedHexString) => {
        return hexZeroPad(address as unknown as HexString, 32);
    };
    export const normalizedToUnpaddedString = (address: NormalizedHexString) => {
        return address.toString() as HexString;
    };
    export const normalizedToBytes32 = (address: NormalizedHexString) => {
        return hexToBytes(normalizedToBytes32Hex(address));
    };
    export const normalizedToBigInt = (address: NormalizedHexString) => {
        return BigInt(address as unknown as HexString);
    };
    export const parseNative = (nativeAddress: NativeAddress) => {
        return addressParser(nativeAddress.chainName).parseNative(nativeAddress.nativeAddress);
    };
}

const validateNative = <T extends ChainName>(
    nativeAddress: string,
    expectedChainName: T,
): nativeAddress is ResolvedEncodingForChainName<T> => {
    if (!addressParser(expectedChainName).validateNative(nativeAddress)) {
        throw new Error(
            'Cannot convert: ' +
                nativeAddress +
                ' - The native address does not match the expected format for the chain: ' +
                expectedChainName,
        );
    }
    return true;
};

const getAddress = <T extends ChainName>(nativeAddress: NativeOrString<T>) => {
    return typeof nativeAddress === 'object' ? nativeAddress.nativeAddress : nativeAddress;
};

export const tonAddressParser: (
    chainName: InferChainNamesForChainType<ChainType.TON>,
) => AddressParser<InferChainNamesForChainType<ChainType.TON>> = (chainName) => {
    return {
        validateNative: (nativeAddress): nativeAddress is HexString => {
            try {
                parseTonAddress(nativeAddress);
                return true;
            } catch {
                return false;
            }
        },
        normalizedToNative: (address) => {
            const numericalValue = addressParser.normalizedToBigInt(address);
            return {
                // FIXME this is wrong
                nativeAddress: bigintToAddress(numericalValue).toString() as HexString,
                chainName,
            };
        },
        normalizedToNativeString: (address) => {
            return addressParser(chainName).normalizedToNative(address).nativeAddress;
        },
        nativeToNormalized: (nativeAddress) => {
            const address = getAddress(nativeAddress);
            validateNative(address, chainName);
            return normalizeHex(addressToHex(address));
        },
        nativeToBytes32Hex: (nativeAddress) => {
            // naturally ton is 32 bytes, pad it anyway
            return hexZeroPad(
                addressParser(chainName).nativeToNormalized(nativeAddress) as unknown as HexString,
                32,
            );
        },
        nativeToBytes: (nativeAddress) => {
            return hexToBytes(addressParser(chainName).nativeToBytes32Hex(nativeAddress));
        },
        nativeToBytes32: (nativeAddress) => {
            return hexToBytes(addressParser(chainName).nativeToBytes32Hex(nativeAddress));
        },
        parseNative: (nativeAddress) => {
            if (!validateNative(nativeAddress, chainName)) {
                throw new Error('Invalid native address: ' + nativeAddress);
            }
            return addressParser(chainName).nativeToNormalized(nativeAddress);
        },
        parseNativeToHex: (nativeAddress) => {
            return addressParser(chainName).parseNative(nativeAddress) as unknown as HexString;
        },
        tonToNormalized: (tonAddress) => {
            return addressParser(chainName).parseNative(addressToHex(tonAddress));
        },
        encoding: AddressEncoding.TON,
    };
};

export const initiaAddressParser: (
    chainName: ChainsByEncoding[InitiaString],
) => AddressParser<ChainsByEncoding[InitiaString]> = (chainName) => {
    return {
        validateNative: (nativeAddress): nativeAddress is InitiaString => {
            try {
                // Both formats are native for the chain and are used kind of randomly.
                return (
                    isHexString(nativeAddress) ||
                    /^(init)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38,66})$/.test(nativeAddress)
                );
            } catch {
                return false;
            }
        },
        normalizedToNative: (address) => {
            return {
                nativeAddress: AccAddress.fromHex(address as unknown as HexString) as InitiaString,
                chainName,
            };
        },
        normalizedToNativeString: (address) => {
            return addressParser(chainName).normalizedToNative(address).nativeAddress;
        },
        nativeToNormalized: (nativeAddress) => {
            const address = getAddress(nativeAddress);
            validateNative(address, chainName);
            if (isHexString(address)) {
                return normalizeHex(address);
            }
            return normalizeHex(AccAddress.toHex(nativeAddress.toString()) as HexString);
        },
        nativeToBytes32Hex: (nativeAddress) => {
            return hexZeroPad(
                addressParser(chainName).nativeToNormalized(nativeAddress) as unknown as HexString,
                32,
            );
        },
        nativeToBytes: (nativeAddress) => {
            return hexToBytes(addressParser(chainName).nativeToBytes32Hex(nativeAddress));
        },
        nativeToBytes32: (nativeAddress) => {
            return hexToBytes(addressParser(chainName).nativeToBytes32Hex(nativeAddress));
        },
        parseNative: (nativeAddress) => {
            if (!validateNative(nativeAddress, chainName)) {
                throw new Error('Invalid native address: ' + nativeAddress);
            }
            return addressParser(chainName).nativeToNormalized(nativeAddress);
        },
        parseNativeToHex: (nativeAddress) => {
            return addressParser(chainName).parseNative(nativeAddress) as unknown as HexString;
        },
        encoding: AddressEncoding.INITIA,
    };
};

export const base58AddressParser = (
    chainName: ChainsByEncoding[Base58String],
): AddressParser<ChainsByEncoding[Base58String]> => {
    return {
        validateNative: (nativeAddress): nativeAddress is Base58String => {
            return isBase58(nativeAddress);
        },
        normalizedToNative: (address) => {
            return {
                nativeAddress: bs58.encode(
                    hexToBytes(address as unknown as HexString),
                ) as Base58String,
                chainName,
            };
        },
        normalizedToNativeString: (address) => {
            return addressParser(chainName).normalizedToNative(address).nativeAddress;
        },
        nativeToNormalized: (nativeAddress) => {
            const address = getAddress(nativeAddress);
            validateNative(address, chainName);
            const hex = bytesToHexPrefixed(bs58.decode(address));
            return normalizeHex(hex);
        },
        nativeToBytes32Hex: (nativeAddress) => {
            // naturally solana is 32 bytes, pad it anyway
            return hexZeroPad(
                addressParser(chainName).nativeToNormalized(nativeAddress) as unknown as HexString,
                32,
            );
        },
        nativeToBytes: (nativeAddress) => {
            const address = getAddress(nativeAddress);
            validateNative(address, chainName);
            return bs58.decode(address);
        },
        nativeToBytes32: (nativeAddress) => {
            return hexToBytes(addressParser(chainName).nativeToBytes32Hex(nativeAddress));
        },
        parseNative: (nativeAddress) => {
            if (!validateNative(nativeAddress, chainName)) {
                throw new Error('Invalid native address: ' + nativeAddress);
            }
            return addressParser(chainName).nativeToNormalized(nativeAddress);
        },
        parseNativeToHex: (nativeAddress) => {
            return addressParser(chainName).parseNative(nativeAddress) as unknown as HexString;
        },
        encoding: AddressEncoding.BASE58,
    };
};

export const hexAddressParser = (
    chainName: ChainsByEncoding[HexString],
): AddressParser<ChainsByEncoding[HexString]> => {
    const nativeByteLength = StaticChainConfigs.getAddressSizeInBytes(chainName as ChainName);

    return {
        validateNative: (nativeAddress): nativeAddress is HexString => {
            return isHexString(nativeAddress);
        },
        normalizedToNative: (address) => {
            return {
                // we could have EIP-55 here, but omitted for now
                nativeAddress: hexZeroPad(address as unknown as HexString, nativeByteLength),
                chainName,
            };
        },
        normalizedToNativeString: (address) => {
            return hexZeroPad(address as unknown as HexString, nativeByteLength);
        },
        nativeToNormalized: (nativeAddress) => {
            const address = getAddress(nativeAddress);
            validateNative(address, chainName);
            return normalizeHex(address);
        },
        nativeToBytes: (nativeAddress) => {
            const address = getAddress(nativeAddress);
            validateNative(address, chainName);
            return hexToBytes(address);
        },
        nativeToBytes32Hex: (nativeAddress) => {
            const address = getAddress(nativeAddress);
            validateNative(address, chainName);
            return hexZeroPad(address, 32);
        },
        nativeToBytes32: (nativeAddress) => {
            return hexToBytes(addressParser(chainName).nativeToBytes32Hex(nativeAddress));
        },
        parseNative: (nativeAddress) => {
            // Specific case to make tron a little less flaky, handle the 41 prefix
            if (chainName === ChainName.TRON) {
                nativeAddress = nativeAddress.startsWith('41')
                    ? `0x${nativeAddress.slice(2)}`
                    : nativeAddress;
            }

            if (!validateNative(nativeAddress, chainName)) {
                throw new Error('Invalid native address: ' + nativeAddress);
            }
            return addressParser(chainName).nativeToNormalized(nativeAddress);
        },
        parseNativeToHex: (nativeAddress) => {
            return addressParser(chainName).parseNative(nativeAddress) as unknown as HexString;
        },
        encoding: AddressEncoding.HEX,
    };
};
