from typing import Any, assert_never, cast

from erc7730.common.abi import ABIDataType
from erc7730.common.output import OutputAdder
from erc7730.convert.resolved.v2.constants import ConstantProvider
from erc7730.convert.resolved.v2.enums import get_enum, get_enum_id
from erc7730.convert.resolved.v2.values import resolve_path_or_constant_value
from erc7730.model.input.path import DescriptorPathStr
from erc7730.model.input.v2.display import (
    InputAddressNameParameters,
    InputCallDataParameters,
    InputDateParameters,
    InputEncryptionParameters,
    InputEnumParameters,
    InputFieldParameters,
    InputInteroperableAddressNameParameters,
    InputMapReference,
    InputNftNameParameters,
    InputTokenAmountParameters,
    InputTokenTickerParameters,
    InputUnitParameters,
)
from erc7730.model.paths import ContainerPath, DataPath
from erc7730.model.paths.path_ops import data_or_container_path_concat
from erc7730.model.resolved.display import ResolvedValue
from erc7730.model.resolved.metadata import EnumDefinition
from erc7730.model.resolved.v2.display import (
    ResolvedAddressNameParameters,
    ResolvedCallDataParameters,
    ResolvedDateParameters,
    ResolvedEncryptionParameters,
    ResolvedEnumParameters,
    ResolvedFieldParameters,
    ResolvedInteroperableAddressNameParameters,
    ResolvedNftNameParameters,
    ResolvedTokenAmountParameters,
    ResolvedTokenTickerParameters,
    ResolvedUnitParameters,
)
from erc7730.model.types import Address, HexStr, Id, MixedCaseAddress


def resolve_field_parameters(
    prefix: DataPath,
    params: InputFieldParameters | None,
    enums: dict[Id, EnumDefinition],
    constants: ConstantProvider,
    out: OutputAdder,
) -> ResolvedFieldParameters | None:
    match params:
        case None:
            return None
        case InputAddressNameParameters():
            return resolve_address_name_parameters(prefix, params, constants, out)
        case InputInteroperableAddressNameParameters():
            return resolve_interoperable_address_name_parameters(prefix, params, constants, out)
        case InputCallDataParameters():
            return resolve_calldata_parameters(prefix, params, constants, out)
        case InputTokenAmountParameters():
            return resolve_token_amount_parameters(prefix, params, constants, out)
        case InputTokenTickerParameters():
            return resolve_token_ticker_parameters(prefix, params, constants, out)
        case InputNftNameParameters():
            return resolve_nft_parameters(prefix, params, constants, out)
        case InputDateParameters():
            return resolve_date_parameters(prefix, params, constants, out)
        case InputUnitParameters():
            return resolve_unit_parameters(prefix, params, constants, out)
        case InputEnumParameters():
            return resolve_enum_parameters(prefix, params, enums, constants, out)
        case _:
            assert_never(params)


def resolve_address_name_parameters(
    prefix: DataPath, params: InputAddressNameParameters, constants: ConstantProvider, out: OutputAdder
) -> ResolvedAddressNameParameters | None:
    sender_address: list[Address] | None = None
    if (sender_addr_input := params.senderAddress) is not None:
        # InputMapReference is passed through to resolved model for runtime resolution
        if isinstance(sender_addr_input, InputMapReference):
            # Map references in senderAddress cannot be resolved at conversion time
            out.warning(
                title="Unresolved map reference",
                message="Map reference in senderAddress cannot be resolved at conversion time and will be dropped.",
            )
            sender_address = None
        else:
            resolved_sender = constants.resolve_or_none(sender_addr_input, out)
            if resolved_sender is None:
                sender_address = None
            elif isinstance(resolved_sender, str):
                sender_address = [Address(resolved_sender)]
            elif isinstance(resolved_sender, list):
                sender_address = [Address(addr) for addr in resolved_sender]
            else:
                raise Exception("Invalid senderAddress type")

    return ResolvedAddressNameParameters(
        types=constants.resolve_or_none(params.types, out),
        sources=constants.resolve_or_none(params.sources, out),
        senderAddress=sender_address,
    )


def resolve_interoperable_address_name_parameters(
    prefix: DataPath, params: InputInteroperableAddressNameParameters, constants: ConstantProvider, out: OutputAdder
) -> ResolvedInteroperableAddressNameParameters | None:
    sender_address: list[Address] | None = None
    if (sender_addr_input := params.senderAddress) is not None:
        # InputMapReference is passed through - similar to address_name
        if isinstance(sender_addr_input, InputMapReference):
            out.warning(
                title="Unresolved map reference",
                message="Map reference in senderAddress cannot be resolved at conversion time and will be dropped.",
            )
            sender_address = None
        else:
            resolved_sender = constants.resolve_or_none(sender_addr_input, out)
            if resolved_sender is None:
                sender_address = None
            elif isinstance(resolved_sender, str):
                sender_address = [Address(resolved_sender)]
            elif isinstance(resolved_sender, list):
                sender_address = [Address(addr) for addr in resolved_sender]
            else:
                raise Exception("Invalid senderAddress type")

    return ResolvedInteroperableAddressNameParameters(
        types=constants.resolve_or_none(params.types, out),
        sources=constants.resolve_or_none(params.sources, out),
        senderAddress=sender_address,
    )


def resolve_calldata_parameters(
    prefix: DataPath, params: InputCallDataParameters, constants: ConstantProvider, out: OutputAdder
) -> ResolvedCallDataParameters | None:
    # Resolve callee - can be path, constant, or map reference
    callee_resolved = None
    if params.callee is not None and isinstance(params.callee, InputMapReference):
        out.warning(
            title="Unresolved map reference",
            message="Map reference in callee cannot be resolved at conversion time and will be dropped.",
        )
        return None
    elif params.callee is not None or params.calleePath is not None:
        callee_resolved = resolve_path_or_constant_value(
            prefix=prefix,
            input_path=params.calleePath,
            input_value=params.callee,
            abi_type=ABIDataType.ADDRESS,
            constants=constants,
            out=out,
        )
        if callee_resolved is None:
            return None

    # Resolve selector
    selector_resolved = None
    if params.selector is not None and isinstance(params.selector, InputMapReference):
        out.warning(
            title="Unresolved map reference",
            message="Map reference in selector cannot be resolved at conversion time and will be dropped.",
        )
    elif params.selector is not None or params.selectorPath is not None:
        selector_resolved = resolve_path_or_constant_value(
            prefix=prefix,
            input_path=params.selectorPath,
            input_value=params.selector,
            abi_type=ABIDataType.STRING,
            constants=constants,
            out=out,
        )

    # Resolve amount
    amount_resolved = None
    if params.amount is not None and isinstance(params.amount, InputMapReference):
        out.warning(
            title="Unresolved map reference",
            message="Map reference in amount cannot be resolved at conversion time and will be dropped.",
        )
    elif params.amount is not None or params.amountPath is not None:
        amount_resolved = resolve_path_or_constant_value(
            prefix=prefix,
            input_path=params.amountPath,
            input_value=params.amount,
            abi_type=ABIDataType.UINT,
            constants=constants,
            out=out,
        )

    # Resolve spender
    spender_resolved = None
    if params.spender is not None and isinstance(params.spender, InputMapReference):
        out.warning(
            title="Unresolved map reference",
            message="Map reference in spender cannot be resolved at conversion time and will be dropped.",
        )
    elif params.spender is not None or params.spenderPath is not None:
        spender_resolved = resolve_path_or_constant_value(
            prefix=prefix,
            input_path=params.spenderPath,
            input_value=params.spender,
            abi_type=ABIDataType.ADDRESS,
            constants=constants,
            out=out,
        )

    return ResolvedCallDataParameters(
        callee=cast(ResolvedValue, callee_resolved),
        selector=selector_resolved,
        amount=amount_resolved,
        spender=spender_resolved,
    )


def resolve_token_amount_parameters(
    prefix: DataPath, params: InputTokenAmountParameters, constants: ConstantProvider, out: OutputAdder
) -> ResolvedTokenAmountParameters | None:
    # Resolve token into a ResolvedValue (path or constant), like v1.
    token_value = params.token
    token_resolved = None
    if token_value is not None and isinstance(token_value, InputMapReference):
        # Map reference needs runtime resolution and is dropped in resolved model
        out.warning(
            title="Unresolved map reference",
            message="Map reference in token cannot be resolved at conversion time and will be dropped.",
        )
    else:
        token_resolved = resolve_path_or_constant_value(
            prefix=prefix,
            input_path=params.tokenPath,
            input_value=token_value,
            abi_type=ABIDataType.ADDRESS,
            constants=constants,
            out=out,
        )

    input_addresses = cast(
        list[DescriptorPathStr | MixedCaseAddress] | MixedCaseAddress | None,
        constants.resolve_or_none(params.nativeCurrencyAddress, out),
    )
    resolved_addresses: list[Address] | None
    if input_addresses is None:
        resolved_addresses = None
    elif isinstance(input_addresses, list):
        resolved_addresses = []
        for input_address in input_addresses:
            if (resolved_address := constants.resolve(input_address, out)) is None:
                return None
            resolved_addresses.append(Address(resolved_address))
    elif isinstance(input_addresses, str):
        resolved_addresses = [Address(input_addresses)]
    else:
        raise Exception("Invalid nativeCurrencyAddress type")

    input_threshold = cast(HexStr | int | None, constants.resolve_or_none(params.threshold, out))
    resolved_threshold: HexStr | None
    if input_threshold is not None:
        if isinstance(input_threshold, int):
            resolved_threshold = "0x" + input_threshold.to_bytes(byteorder="big", signed=False).hex()
        else:
            resolved_threshold = input_threshold
    else:
        resolved_threshold = None

    # Resolve chainId - can be int, descriptor path, or map reference
    chain_id_value = params.chainId
    resolved_chain_id: int | None = None
    if chain_id_value is not None and not isinstance(chain_id_value, InputMapReference):
        if isinstance(chain_id_value, int):
            resolved_chain_id = chain_id_value
        else:
            # Descriptor path
            resolved_value: Any = constants.resolve(chain_id_value, out)
            if isinstance(resolved_value, int):
                resolved_chain_id = resolved_value

    return ResolvedTokenAmountParameters(
        token=token_resolved,
        nativeCurrencyAddress=resolved_addresses,
        threshold=resolved_threshold,
        message=constants.resolve_or_none(params.message, out),
        chainId=resolved_chain_id,
        chainIdPath=params.chainIdPath,
    )


def resolve_token_ticker_parameters(
    prefix: DataPath, params: InputTokenTickerParameters, constants: ConstantProvider, out: OutputAdder
) -> ResolvedTokenTickerParameters | None:
    # Resolve chainId - can be int, descriptor path, or map reference
    chain_id_value = params.chainId
    resolved_chain_id: int | None = None
    if chain_id_value is not None and not isinstance(chain_id_value, InputMapReference):
        if isinstance(chain_id_value, int):
            resolved_chain_id = chain_id_value
        else:
            # Descriptor path
            resolved_value: Any = constants.resolve(chain_id_value, out)
            if isinstance(resolved_value, int):
                resolved_chain_id = resolved_value

    # Resolve and normalize chainIdPath using constants and the current prefix
    resolved_chain_id_path: DataPath | ContainerPath | None = None
    if params.chainIdPath is not None:
        relative_chain_id_path = constants.resolve_path(params.chainIdPath, out)
        if relative_chain_id_path is not None:
            resolved_chain_id_path = data_or_container_path_concat(prefix, relative_chain_id_path)

    return ResolvedTokenTickerParameters(
        chainId=resolved_chain_id,
        chainIdPath=resolved_chain_id_path,
    )


def resolve_nft_parameters(
    prefix: DataPath, params: InputNftNameParameters, constants: ConstantProvider, out: OutputAdder
) -> ResolvedNftNameParameters | None:
    # Resolve collection - can be path, constant, or map reference
    collection_value = params.collection
    if collection_value is not None and isinstance(collection_value, InputMapReference):
        # Map reference - needs runtime resolution
        out.warning(
            title="Unresolved map reference",
            message="Map reference in collection cannot be resolved at conversion time and will be dropped.",
        )
        return None
    else:
        collection_resolved = resolve_path_or_constant_value(
            prefix=prefix,
            input_path=params.collectionPath,
            input_value=collection_value,
            abi_type=ABIDataType.ADDRESS,
            constants=constants,
            out=out,
        )
        if collection_resolved is None:
            return None
        return ResolvedNftNameParameters(collection=collection_resolved)


def resolve_date_parameters(
    prefix: DataPath, params: InputDateParameters, constants: ConstantProvider, out: OutputAdder
) -> ResolvedDateParameters | None:
    return ResolvedDateParameters(encoding=constants.resolve(params.encoding, out))


def resolve_unit_parameters(
    prefix: DataPath, params: InputUnitParameters, constants: ConstantProvider, out: OutputAdder
) -> ResolvedUnitParameters | None:
    return ResolvedUnitParameters(
        base=constants.resolve(params.base, out),
        decimals=constants.resolve_or_none(params.decimals, out),
        prefix=constants.resolve_or_none(params.prefix, out),
    )


def resolve_enum_parameters(
    prefix: DataPath,
    params: InputEnumParameters,
    enums: dict[Id, EnumDefinition],
    constants: ConstantProvider,
    out: OutputAdder,
) -> ResolvedEnumParameters | None:
    if get_enum_id(params.ref, out) is None:
        return None
    if get_enum(params.ref, enums, out) is None:
        return None

    return ResolvedEnumParameters.model_validate({"$ref": str(params.ref)})


def resolve_encryption_parameters(
    prefix: DataPath, params: InputEncryptionParameters, constants: ConstantProvider, out: OutputAdder
) -> ResolvedEncryptionParameters | None:
    # Encryption parameters are passed through as-is
    return ResolvedEncryptionParameters(
        scheme=params.scheme,
        plaintextType=params.plaintextType,
        fallbackLabel=params.fallbackLabel,
    )
