import { AccAddress } from '@initia/initia.js';
import { StrKey } from '@stellar/stellar-sdk';
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 {
    bytesToHexPrefixed,
    hexToBytes,
    hexZeroPad,
    isBase58,
    isHexString,
} from '@layerzerolabs/common-encoding-utils';
import { addressToHex, bigintToAddress, parseTonAddress } from '@layerzerolabs/common-ton';
import type {
    Base58String,
    ChainsByEncoding,
    ChainType,
    InferChainNamesForChainType,
    InferChainTypeForChainName,
    InitiaString,
    NativeAddress,
    ResolvedEncodingForChainName,
    StellarString,
} from '@layerzerolabs/layerzero-definitions';
import type { ChainNativeAddress } from '@layerzerolabs/layerzero-definitions';
import { ChainName } from '@layerzerolabs/layerzero-definitions';
import { StaticChainConfigs } from '@layerzerolabs/static-chain-info';
import type { MaybeUndefinedMapped } from '@layerzerolabs/typescript-utils';

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

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

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 extends NormalizedHexString | undefined>(
        address: Address,
    ) => MaybeUndefinedMapped<Address, ChainNativeAddress<T>>;
    normalizedToNativeString: <Address extends NormalizedHexString | undefined>(
        address: Address,
    ) => MaybeUndefinedMapped<Address, ResolvedEncodingForChainName<T>>;
    nativeToNormalized: <Address extends NativeOrString<T> | undefined>(
        nativeAddress: Address,
    ) => MaybeUndefinedMapped<Address, NormalizedHexString>;
    nativeToBytes: <Address extends NativeOrString<T> | undefined>(
        nativeAddress: Address,
    ) => MaybeUndefinedMapped<Address, Uint8Array>;
    nativeToBytes32Hex: <Address extends NativeOrString<T> | undefined>(
        nativeAddress: Address,
    ) => MaybeUndefinedMapped<Address, HexString>;
    nativeToBytes32: <Address extends NativeOrString<T> | undefined>(
        nativeAddress: Address,
    ) => MaybeUndefinedMapped<Address, Uint8Array>;
    validateNative: (
        nativeAddress: string | undefined,
    ) => 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 | undefined) => NormalizedHexString;
    parseNativeToUnpaddedHex: (nativeAddress: string | undefined) => 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>;
    if (chainName === ChainName.STELLAR)
        return stellarAddressParser(chainName) as unknown as AddressParser<T>;
    if (chainName === ChainName.STARKNET)
        return starknetAddressParser(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);
    };
}

function assertNative<T extends ChainName>(
    nativeAddress: string | undefined,
    expectedChainName: T,
): asserts 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,
        );
    }
}

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 => {
            if (nativeAddress === undefined) {
                return false;
            }
            try {
                parseTonAddress(nativeAddress);
                return true;
            } catch {
                return false;
            }
        },
        normalizedToNative: (address) => {
            if (address === undefined) return undefined as any;
            const numericalValue = addressParser.normalizedToBigInt(address);
            return {
                // FIXME this is wrong
                nativeAddress: bigintToAddress(numericalValue).toString() as HexString,
                chainName,
            };
        },
        normalizedToNativeString: (address) => {
            if (address === undefined) return undefined as any;
            return addressParser(chainName).normalizedToNative(address).nativeAddress;
        },
        nativeToNormalized: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            const address = getAddress(nativeAddress);
            assertNative(address, chainName);
            return normalizeHex(addressToHex(address));
        },
        nativeToBytes32Hex: (nativeAddress) => {
            // naturally ton is 32 bytes, pad it anyway
            if (nativeAddress === undefined) return undefined as any;
            return hexZeroPad(
                addressParser(chainName).nativeToNormalized(nativeAddress) as unknown as HexString,
                32,
            );
        },
        nativeToBytes: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            return hexToBytes(addressParser(chainName).nativeToBytes32Hex(nativeAddress));
        },
        nativeToBytes32: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            return hexToBytes(addressParser(chainName).nativeToBytes32Hex(nativeAddress));
        },
        parseNative: (nativeAddress) => {
            assertNative(nativeAddress, chainName);
            return addressParser(chainName).nativeToNormalized(nativeAddress);
        },
        parseNativeToUnpaddedHex: (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 => {
            if (nativeAddress === undefined) {
                return false;
            }
            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) => {
            if (address === undefined) {
                return undefined as any;
            }
            return {
                nativeAddress: AccAddress.fromHex(address as unknown as HexString) as InitiaString,
                chainName,
            };
        },
        normalizedToNativeString: (address) => {
            if (address === undefined) return undefined as any;
            return addressParser(chainName).normalizedToNative(address).nativeAddress;
        },
        nativeToNormalized: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            const address = getAddress(nativeAddress);
            assertNative(address, chainName);
            if (isHexString(address)) {
                return normalizeHex(address);
            }
            return normalizeHex(AccAddress.toHex(nativeAddress.toString()) as HexString);
        },
        nativeToBytes32Hex: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            // naturally ton is 32 bytes, pad it anyway
            return hexZeroPad(
                addressParser(chainName).nativeToNormalized(nativeAddress) as unknown as HexString,
                32,
            );
        },
        nativeToBytes: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            return hexToBytes(addressParser(chainName).nativeToBytes32Hex(nativeAddress));
        },
        nativeToBytes32: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            return hexToBytes(addressParser(chainName).nativeToBytes32Hex(nativeAddress));
        },
        parseNative: (nativeAddress) => {
            assertNative(nativeAddress, chainName);
            return addressParser(chainName).nativeToNormalized(nativeAddress);
        },
        parseNativeToUnpaddedHex: (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 => {
            if (nativeAddress === undefined) return false;
            return isBase58(nativeAddress);
        },
        normalizedToNative: (address) => {
            if (address === undefined) return undefined as any;
            return {
                nativeAddress: bs58.encode(
                    hexToBytes(address as unknown as HexString),
                ) as Base58String,
                chainName,
            };
        },
        normalizedToNativeString: (address) => {
            if (address === undefined) return undefined as any;
            return addressParser(chainName).normalizedToNative(address).nativeAddress;
        },
        nativeToNormalized: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            const address = getAddress(nativeAddress);
            assertNative(address, chainName);
            const hex = bytesToHexPrefixed(bs58.decode(address));
            return normalizeHex(hex);
        },
        nativeToBytes32Hex: (nativeAddress) => {
            // naturally solana is 32 bytes, pad it anyway
            if (nativeAddress === undefined) return undefined as any;
            return hexZeroPad(
                addressParser(chainName).nativeToNormalized(nativeAddress) as unknown as HexString,
                32,
            );
        },
        nativeToBytes: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            const address = getAddress(nativeAddress);
            assertNative(address, chainName);
            return bs58.decode(address);
        },
        nativeToBytes32: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            return hexToBytes(addressParser(chainName).nativeToBytes32Hex(nativeAddress));
        },
        parseNative: (nativeAddress) => {
            assertNative(nativeAddress, chainName);
            return addressParser(chainName).nativeToNormalized(nativeAddress);
        },
        parseNativeToUnpaddedHex: (nativeAddress) => {
            return addressParser(chainName).parseNative(nativeAddress) as unknown as HexString;
        },
        encoding: AddressEncoding.BASE58,
    };
};

export const hexAddressParser = (
    chainName: ChainsByEncoding[HexString],
): AddressParser<ChainsByEncoding[HexString]> => {
    return {
        validateNative: (nativeAddress): nativeAddress is HexString => {
            if (nativeAddress === undefined) return false;
            return isHexString(nativeAddress);
        },
        normalizedToNative: (address) => {
            if (address === undefined) return undefined as any;
            // we fetch here instead of storing by chain because some chains may not have this defined,
            // so we should only throw on those chains if they try to use these methods that need the native byte length
            const nativeByteLength = StaticChainConfigs.getAddressSizeInBytes(chainName);
            return {
                // we could have EIP-55 here, but omitted for now
                nativeAddress: hexZeroPad(address as unknown as HexString, nativeByteLength),
                chainName,
            };
        },
        normalizedToNativeString: (address) => {
            if (address === undefined) return undefined as any;
            const nativeByteLength = StaticChainConfigs.getAddressSizeInBytes(chainName);
            return hexZeroPad(address as unknown as HexString, nativeByteLength);
        },
        nativeToNormalized: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            const address = getAddress(nativeAddress);
            assertNative(address, chainName);
            return normalizeHex(address);
        },
        nativeToBytes: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            const address = getAddress(nativeAddress);
            assertNative(address, chainName);
            return hexToBytes(address);
        },
        nativeToBytes32Hex: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            const address = getAddress(nativeAddress);
            assertNative(address, chainName);
            return hexZeroPad(address, 32);
        },
        nativeToBytes32: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            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 = nativeAddress.startsWith('41')
                    ? `0x${nativeAddress.slice(2)}`
                    : nativeAddress;
            }

            assertNative(nativeAddress, chainName);
            return addressParser(chainName).nativeToNormalized(nativeAddress);
        },
        parseNativeToUnpaddedHex: (nativeAddress) => {
            return addressParser(chainName).parseNative(nativeAddress) as unknown as HexString;
        },
        encoding: AddressEncoding.HEX,
    };
};

export const stellarAddressParser: (
    chainName: InferChainNamesForChainType<ChainType.STELLAR>,
) => AddressParser<InferChainNamesForChainType<ChainType.STELLAR>> = (chainName) => {
    /**
     * Validate if a string is a valid Stellar address.
     *
     * According to SEP-0023 (https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md),
     * Stellar supports multiple address types encoded as base32 ASCII strings:
     * - Account addresses starting with 'G' (56 chars, Ed25519 public key)
     * - Contract addresses starting with 'C' (56 chars, contract ID)
     * - Muxed addresses starting with 'M' (69 chars, muxed account with ID)
     * - Liquidity pool addresses starting with 'L' (56 chars, pool ID)
     * - Claimable balance addresses starting with 'B' (56 chars, balance ID)
     * - Signed payload starting with 'P' (variable length)
     *
     * All addresses are stored in their native ASCII format to preserve type information.
     */
    const isValidStellarAddress = (address: string): boolean => {
        if (!address) return false;
        // Use the StrKey validation methods for all supported address types
        return (
            StrKey.isValidEd25519PublicKey(address) ||
            StrKey.isValidContract(address) ||
            StrKey.isValidMed25519PublicKey(address) ||
            StrKey.isValidLiquidityPool(address) ||
            StrKey.isValidClaimableBalance(address) ||
            StrKey.isValidSignedPayload(address)
        );
    };

    /**
     * Convert Stellar ASCII address to normalized hex by encoding the ASCII string as UTF-8 bytes.
     * This preserves the full address including type prefix and allows round-trip conversion.
     */
    const stellarTextEncoder = new TextEncoder();
    const stellarAsciiToNormalized = (address: string): NormalizedHexString => {
        // Convert ASCII address string to UTF-8 bytes, then to hex
        const bytes = stellarTextEncoder.encode(address);
        return normalizeHex(bytesToHexPrefixed(bytes));
    };

    /**
     * Convert normalized hex back to Stellar ASCII address by decoding UTF-8 bytes.
     */
    const stellarTextDecoder = new TextDecoder();
    const normalizedToStellarAscii = (normalizedHex: NormalizedHexString): string => {
        // Convert hex to bytes, then decode as UTF-8 string
        const bytes = hexToBytes(normalizedHex as unknown as HexString);
        return stellarTextDecoder.decode(bytes);
    };

    return {
        validateNative: (nativeAddress): nativeAddress is StellarString => {
            if (nativeAddress === undefined) return false;
            return isValidStellarAddress(nativeAddress);
        },
        normalizedToNative: (address) => {
            if (address === undefined) return undefined as any;
            // Decode normalized hex back to ASCII Stellar address
            const stellarAddress = normalizedToStellarAscii(address);
            return {
                nativeAddress: stellarAddress as StellarString,
                chainName,
            };
        },
        normalizedToNativeString: (address) => {
            if (address === undefined) return undefined as any;
            return normalizedToStellarAscii(address) as StellarString;
        },
        nativeToNormalized: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            const address = getAddress(nativeAddress);
            assertNative(address, chainName);
            // Convert ASCII address to normalized hex
            return stellarAsciiToNormalized(address);
        },
        nativeToBytes32Hex: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            const address = getAddress(nativeAddress);
            assertNative(address, chainName);
            // Decode the Stellar address to its raw 32-byte payload based on type
            let rawBytes: Buffer;
            if (StrKey.isValidEd25519PublicKey(address)) {
                // Account addresses (G...) - Ed25519 public key (32 bytes)
                rawBytes = StrKey.decodeEd25519PublicKey(address);
            } else if (StrKey.isValidContract(address)) {
                // Contract addresses (C...) - contract ID (32 bytes)
                rawBytes = StrKey.decodeContract(address);
            } else if (StrKey.isValidLiquidityPool(address)) {
                // Liquidity pool addresses (L...) - pool ID (32 bytes)
                rawBytes = StrKey.decodeLiquidityPool(address);
            } else if (StrKey.isValidClaimableBalance(address)) {
                // Claimable balance addresses (B...) - balance ID (33 bytes: 1-byte version + 32-byte hash)
                // Per SEP-0023: first byte is the claimable balance type (0x00 = V0), followed by 32-byte SHA256 hash
                const fullBytes = StrKey.decodeClaimableBalance(address);
                rawBytes = fullBytes.subarray(1); // Skip the first byte (version), return the 32-byte hash
            } else if (StrKey.isValidMed25519PublicKey(address)) {
                // Muxed addresses (M...) contain additional data beyond 32 bytes
                throw new Error(
                    'nativeToBytes32 is not supported for Stellar muxed addresses (M...)',
                );
            } else if (StrKey.isValidSignedPayload(address)) {
                // Signed payload (P...) has variable length
                throw new Error(
                    'nativeToBytes32 is not supported for Stellar signed payload addresses (P...)',
                );
            } else {
                throw new Error(`Unknown Stellar address type: ${address}`);
            }
            return bytesToHexPrefixed(rawBytes);
        },
        nativeToBytes: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            const address = getAddress(nativeAddress);
            assertNative(address, chainName);
            // Convert ASCII address to UTF-8 bytes
            return stellarTextEncoder.encode(address);
        },
        nativeToBytes32: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            // Use nativeToBytes32Hex and convert to Uint8Array
            const hex = addressParser(chainName).nativeToBytes32Hex(nativeAddress);
            return hexToBytes(hex);
        },
        parseNative: (nativeAddress) => {
            assertNative(nativeAddress, chainName);
            return addressParser(chainName).nativeToNormalized(nativeAddress);
        },
        parseNativeToUnpaddedHex: (nativeAddress) => {
            assertNative(nativeAddress, chainName);
            const normalized = addressParser(chainName).nativeToNormalized(nativeAddress);
            return normalized as unknown as HexString;
        },
        encoding: AddressEncoding.STELLAR,
    };
};

// Starknet addresses do not handle hex-zero padding, so we just return the address as is
export const starknetAddressParser = (
    chainName: ChainsByEncoding[HexString],
): AddressParser<ChainsByEncoding[HexString]> => {
    return {
        ...hexAddressParser(chainName),
        validateNative: (nativeAddress): nativeAddress is HexString => {
            if (nativeAddress === undefined) return false;
            return isHexString(nativeAddress);
        },
        parseNative: (nativeAddress) => {
            if (nativeAddress === undefined) return undefined as any;
            // We can't trust starknet.js strings, they may actually be bigints!
            // so just convert everything to hex first, then convert to normalized
            nativeAddress = `0x${BigInt(nativeAddress).toString(16)}`;
            assertNative(nativeAddress, chainName);
            return addressParser(chainName).nativeToNormalized(nativeAddress);
        },
        normalizedToNative: (address) => {
            if (address === undefined) return undefined as any;
            return {
                nativeAddress: address,
                chainName,
            };
        },
        normalizedToNativeString: (address) => {
            if (address === undefined) return undefined as any;
            return address;
        },
    };
};
