import abc
import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst import wtypes
from puya.awst.nodes import (
    AppAccountStateExpression,
    Expression,
    ExpressionStatement,
    IntegerConstant,
    StateDelete,
    StateExists,
    StateGet,
    StateGetEx,
    Statement,
)
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder, GenericTypeBuilder
from puyapy.awst_build.eb._bytes_backed import BytesBackedInstanceExpressionBuilder
from puyapy.awst_build.eb._utils import dummy_value
from puyapy.awst_build.eb.bool import BoolExpressionBuilder
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import (
    InstanceBuilder,
    NodeBuilder,
    StorageProxyConstructorArgs,
    StorageProxyConstructorResult,
    TypeBuilder,
)
from puyapy.awst_build.eb.storage._storage import (
    StorageProxyDefinitionBuilder,
    parse_storage_proxy_constructor_args,
)
from puyapy.awst_build.eb.storage._value_proxy import ValueProxyExpressionBuilder
from puyapy.awst_build.eb.tuple import TupleExpressionBuilder
from puyapy.awst_build.utils import get_arg_mapping

logger = log.get_logger(__name__)

class LocalStateTypeBuilder(TypeBuilder[pytypes.StorageProxyType]):
    def __init__(self, typ: pytypes.PyType, location: SourceLocation) -> None:
        assert isinstance(typ, pytypes.StorageProxyType)
        assert typ.generic == pytypes.GenericLocalStateType
        self._typ = typ
        super().__init__(typ, location)

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        return _init(args, arg_names, location, result_type=self._typ)

class LocalStateGenericTypeBuilder(GenericTypeBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        return _init(args, arg_names, location, result_type=None)

def _init(
    args: Sequence[NodeBuilder],
    arg_names: list[str | None],
    location: SourceLocation,
    *,
    result_type: pytypes.StorageProxyType | None,
) -> InstanceBuilder:
    type_arg_name = "type_"
    key_arg_name = "key"
    descr_arg_name = "description"
    arg_mapping, _ = get_arg_mapping(
        required_positional_names=[type_arg_name],
        optional_kw_only=[key_arg_name, descr_arg_name],
        args=args,
        arg_names=arg_names,
        call_location=location,
        raise_on_missing=True,
    )

    match arg_mapping[type_arg_name].pytype:
        case pytypes.TypeType(typ=content):
            pass
        case _:
            raise CodeError("first argument must be a type reference", location)
    if result_type is None:
        result_type = pytypes.GenericLocalStateType.parameterise([content], location)
    elif result_type.content != content:
        logger.error(
            "explicit type annotation does not match first argument"
            " - suggest to remove the explicit type annotation,  it shouldn't be required",
            location=location,
        )

    typed_args = parse_storage_proxy_constructor_args(
        arg_mapping,
        key_wtype=wtypes.state_key,
        key_arg_name=key_arg_name,
        descr_arg_name=descr_arg_name,
        location=location,
    )

    if typed_args.key is None:
        return StorageProxyDefinitionBuilder(typed_args, result_type, location)
    return _LocalStateExpressionBuilderFromConstructor(typed_args, result_type)

class LocalStateExpressionBuilder(
    BytesBackedInstanceExpressionBuilder[pytypes.StorageProxyType], bytes_member="key"
):
    def __init__(self, expr: Expression, typ: pytypes.PyType, member_name: str | None = None):
        assert isinstance(typ, pytypes.StorageProxyType)
        assert typ.generic == pytypes.GenericLocalStateType
        self.member_name: typing.Final = member_name
        super().__init__(typ, expr)

    @typing.override
    def index(self, index: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        expr = _build_field(self, index, location)
        return _Value(self.pytype.content, expr)

    @typing.override
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        exists_expr = StateExists(
            field=_build_field(self, item, location), source_location=location
        )
        return BoolExpressionBuilder(exists_expr)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        match name:
            case "get":
                return _Get(self, location)
            case "maybe":
                return _Maybe(self, location)
        return super().member_access(name, location)

    @typing.override
    def iterate(self) -> typing.Never:
        raise CodeError("cannot iterate account states", self.source_location)

    @typing.override
    def iterable_item_type(self) -> typing.Never:
        self.iterate()

    @typing.override
    def slice_index(
        self,
        begin_index: InstanceBuilder | None,
        end_index: InstanceBuilder | None,
        stride: InstanceBuilder | None,
        location: SourceLocation,
    ) -> InstanceBuilder:
        raise CodeError("cannot slice account states", self.source_location)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        raise CodeError("cannot determine if a LocalState is empty or not", location)

def _build_field(
    self: LocalStateExpressionBuilder,
    index: NodeBuilder,
    location: SourceLocation,
) -> AppAccountStateExpression:
    # TODO: maybe resolve literal should allow functions, so we can validate
    #       constant values inside e.g. conditional expressions, not just plain constants
    #       like we check below with matching on IntegerConstant
    index_expr = expect.argument_of_type_else_dummy(
        index,
        # UInt64 comes first since we want to resolve int literals,
        # str literals for account not currently supported
        pytypes.UInt64Type,
        pytypes.AccountType,
        resolve_literal=True,
    ).resolve()
    # https://developer.algorand.org/docs/get-details/dapps/smart-contracts/apps/#resource-availability
    # Note that the sender address is implicitly included in the array,
    # but doesn't count towards the limit of 4, so the <= 4 below is correct
    # and intended
    if isinstance(index_expr, IntegerConstant) and not (0 <= index_expr.value <= 4):
        logger.error(
            "account index should be between 0 and 4 inclusive",
            location=index.source_location,
        )
    if self.member_name:
        exists_assertion_message = f"check self.{self.member_name} exists for account"
    else:
        exists_assertion_message = "check LocalState exists for account"
    return AppAccountStateExpression(
        key=self.resolve(),
        account=index_expr,
        wtype=self.pytype.content_wtype,
        exists_assertion_message=exists_assertion_message,
        source_location=location,
    )

class _LocalStateExpressionBuilderFromConstructor(
    LocalStateExpressionBuilder, StorageProxyConstructorResult
):
    def __init__(self, args: StorageProxyConstructorArgs, typ: pytypes.StorageProxyType):
        assert args.key is not None
        super().__init__(args.key, typ, member_name=None)
        self._args = args

    @typing.override
    @property
    def args(self) -> StorageProxyConstructorArgs:
        return self._args

class _MemberFunction(FunctionBuilder, abc.ABC):
    def __init__(self, base: LocalStateExpressionBuilder, location: SourceLocation):
        super().__init__(location)
        self._base = base

class _Get(_MemberFunction):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        content_typ = self._base.pytype.content
        account_arg_name = "account"
        default_arg_name = "default"
        args_map, any_missing = get_arg_mapping(
            required_positional_names=[account_arg_name, default_arg_name],
            args=args,
            arg_names=arg_names,
            call_location=location,
            raise_on_missing=False,
        )
        if any_missing:
            return dummy_value(content_typ, location)
        item = args_map[account_arg_name]
        default_arg = expect.argument_of_type_else_dummy(args_map[default_arg_name], content_typ)
        account = _build_field(self._base, item, location)
        default = default_arg.resolve()
        expr = StateGet(field=account, default=default, source_location=location)
        return builder_for_instance(content_typ, expr)

class _Maybe(_MemberFunction):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        content_typ = self._base.pytype.content
        result_typ = pytypes.GenericTupleType.parameterise(
            [content_typ, pytypes.BoolType], location
        )
        arg = expect.exactly_one_arg(args, location, default=expect.default_fixed_value(None))
        if arg is None:
            return dummy_value(result_typ, location)

        field = _build_field(self._base, arg, location)
        app_local_get_ex = StateGetEx(field=field, source_location=location)
        return TupleExpressionBuilder(app_local_get_ex, result_typ)

class _Value(ValueProxyExpressionBuilder[pytypes.PyType, AppAccountStateExpression]):
    def delete(self, location: SourceLocation) -> Statement:
        return ExpressionStatement(StateDelete(field=self.resolve(), source_location=location))


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import algo_constants, log
from puya.awst import wtypes
from puya.awst.nodes import (
    BoolConstant,
    BytesAugmentedAssignment,
    BytesBinaryOperation,
    BytesBinaryOperator,
    CallArg,
    ConditionalExpression,
    Expression,
    PuyaLibCall,
    PuyaLibFunction,
    Statement,
    StringConstant,
    UInt64Constant,
)
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import intrinsic_factory, pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder
from puyapy.awst_build.eb._bytes_backed import (
    BytesBackedInstanceExpressionBuilder,
    BytesBackedTypeBuilder,
)
from puyapy.awst_build.eb._utils import compare_bytes, dummy_statement, dummy_value
from puyapy.awst_build.eb.bool import BoolExpressionBuilder
from puyapy.awst_build.eb.bytes import BytesExpressionBuilder
from puyapy.awst_build.eb.interface import (
    BuilderBinaryOp,
    BuilderComparisonOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
    StaticSizedCollectionBuilder,
)
from puyapy.awst_build.eb.uint64 import UInt64ExpressionBuilder

logger = log.get_logger(__name__)

class StringTypeBuilder(BytesBackedTypeBuilder):
    def __init__(self, location: SourceLocation):
        super().__init__(pytypes.StringType, location)

    @typing.override
    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        match literal.value:
            case str(value):
                try:
                    bytes_value = value.encode("utf8")
                except UnicodeEncodeError as ex:
                    logger.error(  # noqa: TRY400
                        f"invalid UTF-8 string (encoding error: {ex})",
                        location=literal.source_location,
                    )
                else:
                    if len(bytes_value) > algo_constants.MAX_BYTES_LENGTH:
                        logger.error(
                            "string constant exceeds max byte array length",
                            location=literal.source_location,
                        )
                expr = StringConstant(value=value, source_location=location)
                return StringExpressionBuilder(expr)
        return None

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.at_most_one_arg(args, location)
        match arg:
            case InstanceBuilder(pytype=pytypes.StrLiteralType):
                return arg.resolve_literal(converter=StringTypeBuilder(location))
            case None:
                str_const = StringConstant(value="", source_location=location)
                return StringExpressionBuilder(str_const)
            case other:
                return expect.not_this_type(
                    other, default=expect.default_dummy_value(self.produces())
                )

class StringExpressionBuilder(BytesBackedInstanceExpressionBuilder):
    def __init__(self, expr: Expression):
        super().__init__(pytypes.StringType, expr)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        match name:
            case "startswith":
                return _StringStartsOrEndsWith(self, location, at_start=True)
            case "endswith":
                return _StringStartsOrEndsWith(self, location, at_start=False)
            case "join":
                return _StringJoin(self, location)
            case _:
                return super().member_access(name, location)

    @typing.override
    def augmented_assignment(
        self, op: BuilderBinaryOp, rhs: InstanceBuilder, location: SourceLocation
    ) -> Statement:
        if op != BuilderBinaryOp.add:
            logger.error(f"unsupported operator for type: {op.value!r}", location=location)
            return dummy_statement(location)
        rhs = expect.argument_of_type_else_dummy(rhs, self.pytype, resolve_literal=True)
        return BytesAugmentedAssignment(
            target=self.resolve_lvalue(),
            op=BytesBinaryOperator.add,
            value=rhs.resolve(),
            source_location=location,
        )

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        if op != BuilderBinaryOp.add:
            return NotImplemented

        other = other.resolve_literal(converter=StringTypeBuilder(other.source_location))
        # defer to most derived if not equal
        if not (other.pytype <= pytypes.StringType):
            return NotImplemented

        lhs = self.resolve()
        rhs = other.resolve()
        if reverse:
            (lhs, rhs) = (rhs, lhs)
        return StringExpressionBuilder(
            BytesBinaryOperation(
                left=lhs,
                op=BytesBinaryOperator.add,
                right=rhs,
                source_location=location,
            )
        )

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        other = other.resolve_literal(converter=StringTypeBuilder(other.source_location))
        return compare_bytes(self=self, op=op, other=other, source_location=location)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        len_expr = intrinsic_factory.bytes_len(self.resolve(), location)
        len_builder = UInt64ExpressionBuilder(len_expr)
        return len_builder.bool_eval(location, negate=negate)

    @typing.override
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        item = expect.argument_of_type_else_dummy(item, pytypes.StringType, resolve_literal=True)
        is_substring_expr = PuyaLibCall(
            func=PuyaLibFunction.is_substring,
            args=[
                CallArg(value=item.resolve(), name=None),
                CallArg(value=self.resolve(), name=None),
            ],
            source_location=location,
        )
        return BoolExpressionBuilder(is_substring_expr)

    @typing.override
    def iterate(self) -> typing.Never:
        raise CodeError(
            "string iteration in not supported due to lack of UTF8 support in AVM",
            self.source_location,
        )

    @typing.override
    def iterable_item_type(self) -> typing.Never:
        self.iterate()

    @typing.override
    def index(self, index: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        raise CodeError(
            "string indexing in not supported due to lack of UTF8 support in AVM", location
        )

    @typing.override
    def slice_index(
        self,
        begin_index: InstanceBuilder | None,
        end_index: InstanceBuilder | None,
        stride: InstanceBuilder | None,
        location: SourceLocation,
    ) -> InstanceBuilder:
        raise CodeError(
            "string slicing in not supported due to lack of UTF8 support in AVM", location
        )

class _StringStartsOrEndsWith(FunctionBuilder):
    def __init__(self, base: StringExpressionBuilder, location: SourceLocation, *, at_start: bool):
        super().__init__(location)
        self._base = base
        self._at_start = at_start

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.exactly_one_arg_of_type_else_dummy(
            args, pytypes.StringType, location, resolve_literal=True
        )
        arg = arg.single_eval()
        this = self._base.single_eval()

        this_length = BytesExpressionBuilder(this.to_bytes(location)).length(location)
        arg_length = BytesExpressionBuilder(arg.to_bytes(location)).length(location)

        arg_length_gt_this_length = arg_length.compare(
            this_length, op=BuilderComparisonOp.gt, location=location
        )

        if self._at_start:
            extracted = intrinsic_factory.extract3(
                this.resolve(),
                start=UInt64Constant(source_location=location, value=0),
                length=arg_length.resolve(),
                result_type=wtypes.string_wtype,
            )
        else:
            extracted = intrinsic_factory.extract3(
                this.resolve(),
                start=this_length.binary_op(
                    arg_length, BuilderBinaryOp.sub, location, reverse=False
                ).resolve(),
                length=arg_length.resolve(),
                result_type=wtypes.string_wtype,
            )
        this_substr = StringExpressionBuilder(extracted)

        cond = ConditionalExpression(
            condition=arg_length_gt_this_length.resolve(),
            true_expr=BoolConstant(location, value=False),
            false_expr=this_substr.compare(arg, BuilderComparisonOp.eq, location).resolve(),
            source_location=location,
        )
        return BoolExpressionBuilder(cond)

class _StringJoin(FunctionBuilder):
    def __init__(self, base: StringExpressionBuilder, location: SourceLocation):
        super().__init__(location)
        self._base = base

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.exactly_one_arg(args, location, default=expect.default_none)
        if not isinstance(arg, StaticSizedCollectionBuilder):
            if arg is not None:  # if None, already have an error logged
                expect.not_this_type(arg, default=expect.default_none)
            return dummy_value(pytypes.StringType, location)
        items = [
            expect.argument_of_type_else_dummy(
                ib, pytypes.StringType, resolve_literal=True
            ).resolve()
            for ib in arg.iterate_static()
        ]
        sep = self._base.single_eval().resolve()
        joined_value: Expression | None = None
        for item_expr in items:
            if joined_value is None:
                joined_value = item_expr
            else:
                joined_value = intrinsic_factory.concat(
                    intrinsic_factory.concat(joined_value, sep, location),
                    item_expr,
                    location,
                    result_type=wtypes.string_wtype,
                )
        if joined_value is None:
            joined_value = StringConstant(value="", source_location=location)
        return StringExpressionBuilder(joined_value)


import typing
from collections.abc import Sequence

import mypy.nodes

from puya.awst.nodes import Expression
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder, TypeBuilder

class StructSubclassExpressionBuilder(TypeBuilder[pytypes.StructType]):
    def __init__(self, typ: pytypes.PyType, location: SourceLocation):
        assert isinstance(typ, pytypes.StructType)
        assert pytypes.StructBaseType < typ
        super().__init__(typ, location)

    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        raise NotImplementedError

def StructExpressionBuilder(expr: Expression, typ: pytypes.PyType) -> typing.Never:  # noqa: N802
    raise NotImplementedError


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst.nodes import (
    CallArg,
    ContractMethodTarget,
    SubroutineCallExpression,
    SubroutineTarget,
)
from puya.errors import CodeError, InternalError
from puya.parse import SourceLocation
from puya.program_refs import ContractReference
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder
from puyapy.awst_build.eb._utils import dummy_value
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder
from puyapy.awst_build.utils import get_arg_mapping
from puyapy.models import ContractFragmentMethod

logger = log.get_logger(__name__)

class SubroutineInvokerExpressionBuilder(FunctionBuilder):
    def __init__(
        self, target: SubroutineTarget, func_type: pytypes.FuncType, location: SourceLocation
    ):
        super().__init__(location)
        self.target = target
        self.func_type = func_type

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        result_pytyp = self.func_type.ret_type
        result_wtype = result_pytyp.wtype
        if isinstance(result_wtype, str):
            raise CodeError(
                f"unsupported return type for user function: {result_pytyp}", location=location
            )
        if any(arg_kind.is_star() for arg_kind in arg_kinds):
            logger.error(
                "argument unpacking at call site not currently supported", location=location
            )
            return dummy_value(result_pytyp, location)

        required_positional_names = list[str]()
        optional_positional_names = list[str]()
        required_kw_only = list[str]()
        optional_kw_only = list[str]()
        type_arg_map = dict[str, pytypes.FuncArg]()
        for idx, typ_arg in enumerate(self.func_type.args):
            if typ_arg.name is None and typ_arg.kind.is_named():
                raise InternalError("argument marked as named has no name", location)
            arg_map_name = typ_arg.name or str(idx)
            match typ_arg.kind:
                case mypy.nodes.ARG_POS:
                    required_positional_names.append(arg_map_name)
                case mypy.nodes.ARG_OPT:
                    optional_positional_names.append(arg_map_name)
                case mypy.nodes.ARG_NAMED:
                    required_kw_only.append(arg_map_name)
                case mypy.nodes.ARG_NAMED_OPT:
                    optional_kw_only.append(arg_map_name)
                case mypy.nodes.ARG_STAR | mypy.nodes.ARG_STAR2:
                    logger.error(
                        "functions with variadic arguments are not supported", location=location
                    )
                    return dummy_value(result_pytyp, location)
                case _:
                    typing.assert_never(typ_arg.kind)
            type_arg_map[arg_map_name] = typ_arg

        arg_map, any_missing = get_arg_mapping(
            required_positional_names=required_positional_names,
            optional_positional_names=optional_positional_names,
            required_kw_only=required_kw_only,
            optional_kw_only=optional_kw_only,
            args=args,
            arg_names=arg_names,
            call_location=location,
            raise_on_missing=False,
        )
        if any_missing:
            return dummy_value(result_pytyp, location)

        call_args = []
        # TODO: ideally, we would iterate arg_map, so the order is the same as the call site
        #       need to build map from arg to FuncArg then though to extract expected type(s)
        for arg_map_name, typ_arg in type_arg_map.items():
            arg_typ = typ_arg.type
            if isinstance(arg_typ, pytypes.UnionType):
                logger.error("union types are not supported in user functions", location=location)
                return dummy_value(result_pytyp, location)
            if isinstance(arg_typ, pytypes.LiteralOnlyType):
                logger.error(
                    f"unsupported type for user function argument: {arg_typ}", location=location
                )
                return dummy_value(result_pytyp, location)

            arg = arg_map[arg_map_name]
            if pytypes.ContractBaseType < arg_typ:
                if not (arg_typ <= arg.pytype):
                    logger.error("unexpected argument type", location=arg.source_location)
            else:
                arg = expect.argument_of_type_else_dummy(arg, arg_typ)
                passed_name = arg_map_name if arg_map_name in arg_names else None
                call_args.append(CallArg(name=passed_name, value=arg.resolve()))

        call_expr = SubroutineCallExpression(
            target=self.target,
            args=call_args,
            wtype=result_wtype,
            source_location=location,
        )
        return builder_for_instance(result_pytyp, call_expr)

class BaseClassSubroutineInvokerExpressionBuilder(SubroutineInvokerExpressionBuilder):
    def __init__(
        self,
        cref: ContractReference,
        method: ContractFragmentMethod,
        func_type: pytypes.FuncType,
        location: SourceLocation,
    ):
        target = ContractMethodTarget(cref=cref, member_name=method.member_name)
        super().__init__(target, func_type, location)
        self.cref: typing.Final = cref
        self.method: typing.Final = method


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst.nodes import TemplateVar
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder
from puyapy.awst_build.eb._utils import dummy_value
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder
from puyapy.awst_build.utils import get_arg_mapping

logger = log.get_logger(__name__)

class GenericTemplateVariableExpressionBuilder(FunctionBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        raise CodeError("TemplateVar usage requires type parameter", location)

class TemplateVariableExpressionBuilder(FunctionBuilder):
    def __init__(self, typ: pytypes.PyType, location: SourceLocation):
        assert isinstance(typ, pytypes.PseudoGenericFunctionType)
        self.result_type = typ.return_type
        super().__init__(location)

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        var_name_arg_name = "variable_name"
        prefix_arg_name = "prefix"
        arg_mapping, any_missing = get_arg_mapping(
            required_positional_names=[var_name_arg_name],
            optional_kw_only=[prefix_arg_name],
            args=args,
            arg_names=arg_names,
            call_location=location,
            raise_on_missing=False,
        )
        if any_missing:
            return dummy_value(self.result_type, location)

        var_name = expect.simple_string_literal(
            arg_mapping[var_name_arg_name], default=expect.default_fixed_value("")
        )

        if (prefix_arg := arg_mapping.get(prefix_arg_name)) is None:
            prefix_value = "TMPL_"
        else:
            prefix_value = expect.simple_string_literal(
                prefix_arg, default=expect.default_fixed_value("")
            )

        result_expr = TemplateVar(
            name=prefix_value + var_name,
            wtype=self.result_type.checked_wtype(location),
            source_location=location,
        )
        return builder_for_instance(self.result_type, result_expr)


from puyapy.awst_build.eb.transaction.group import (
    GroupTransactionExpressionBuilder,
    GroupTransactionTypeBuilder,
)
from puyapy.awst_build.eb.transaction.inner import (
    InnerTransactionExpressionBuilder,
    InnerTransactionTypeBuilder,
    SubmitInnerTransactionExpressionBuilder,
)
from puyapy.awst_build.eb.transaction.inner_params import (
    InnerTxnParamsExpressionBuilder,
    InnerTxnParamsTypeBuilder,
)

__all__ = [
    "GroupTransactionTypeBuilder",
    "GroupTransactionExpressionBuilder",
    "InnerTransactionTypeBuilder",
    "InnerTransactionExpressionBuilder",
    "InnerTxnParamsTypeBuilder",
    "InnerTxnParamsExpressionBuilder",
    "SubmitInnerTransactionExpressionBuilder",
]


import abc
import typing
from collections.abc import Sequence

import mypy.nodes

from puya.awst.nodes import Expression
from puya.awst.txn_fields import TxnField
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder, NotIterableInstanceExpressionBuilder
from puyapy.awst_build.eb._utils import constant_bool_and_error
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder
from puyapy.awst_build.eb.transaction.txn_fields import PYTHON_TXN_FIELDS, PythonTxnField

class BaseTransactionExpressionBuilder(NotIterableInstanceExpressionBuilder, abc.ABC):
    @typing.override
    @typing.final
    def to_bytes(self, location: SourceLocation) -> Expression:
        raise CodeError("cannot serialize transaction type", location)

    @abc.abstractmethod
    def get_field_value(
        self, field: TxnField, typ: pytypes.RuntimeType, location: SourceLocation
    ) -> InstanceBuilder: ...

    @abc.abstractmethod
    def get_array_field_value(
        self,
        field: TxnField,
        typ: pytypes.RuntimeType,
        index: InstanceBuilder,
        location: SourceLocation,
    ) -> InstanceBuilder: ...

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        field_data = PYTHON_TXN_FIELDS.get(name)
        if field_data is None:
            return super().member_access(name, location)
        if field_data.field.is_array:
            return _ArrayItem(base=self, field_data=field_data, location=location)
        else:
            return self.get_field_value(field_data.field, field_data.type, location)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        return constant_bool_and_error(value=True, location=location, negate=negate)

class _ArrayItem(FunctionBuilder):
    def __init__(
        self,
        base: BaseTransactionExpressionBuilder,
        field_data: PythonTxnField,
        location: SourceLocation,
    ):
        super().__init__(location)
        self.base = base
        self.field_data = field_data

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.exactly_one_arg_of_type_else_dummy(
            args,
            pytypes.UInt64Type,
            location,
            resolve_literal=True,
        )
        return self.base.get_array_field_value(
            self.field_data.field, self.field_data.type, arg, location
        )


import typing
from collections.abc import Sequence

import mypy.nodes

from puya import algo_constants, log
from puya.awst.nodes import Expression, GroupTransactionReference, IntrinsicCall, UInt64Constant
from puya.awst.txn_fields import TxnField
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import (
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
    TypeBuilder,
)
from puyapy.awst_build.eb.transaction.base import BaseTransactionExpressionBuilder

logger = log.get_logger(__name__)

class GroupTransactionTypeBuilder(TypeBuilder[pytypes.GroupTransactionType]):
    @typing.override
    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        match literal.value:
            case int(int_value):
                if int_value < 0:
                    logger.error(
                        "transaction group index should be non-negative",
                        location=literal.source_location,
                    )
                elif int_value >= algo_constants.MAX_TRANSACTION_GROUP_SIZE:
                    logger.error(
                        "transaction group index should be"
                        f" less than {algo_constants.MAX_TRANSACTION_GROUP_SIZE}",
                        location=literal.source_location,
                    )
                typ = self.produces()
                group_index = UInt64Constant(value=int_value, source_location=location)
                txn = GroupTransactionReference(
                    index=group_index, wtype=typ.wtype, source_location=location
                )
                return GroupTransactionExpressionBuilder(txn, typ)
        return None

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.exactly_one_arg(
            args, location, default=expect.default_dummy_value(pytypes.UInt64Type)
        )
        typ = self.produces()
        if arg.pytype == pytypes.IntLiteralType:  # exact type, exclude bool
            return arg.resolve_literal(GroupTransactionTypeBuilder(typ, location))
        group_index = expect.argument_of_type_else_dummy(arg, pytypes.UInt64Type).resolve()
        txn = GroupTransactionReference(
            index=group_index, wtype=typ.wtype, source_location=location
        )
        return GroupTransactionExpressionBuilder(txn, typ)

class GroupTransactionExpressionBuilder(BaseTransactionExpressionBuilder):
    def __init__(self, expr: Expression, typ: pytypes.PyType):
        assert isinstance(typ, pytypes.TransactionRelatedType)
        super().__init__(typ, expr)

    @typing.override
    def get_field_value(
        self, field: TxnField, typ: pytypes.RuntimeType, location: SourceLocation
    ) -> InstanceBuilder:
        assert not field.is_array
        assert typ.wtype.scalar_type == field.avm_type
        expr = IntrinsicCall(  # TODO: use (+rename) InnerTransactionField
            op_code="gtxns",
            immediates=[field.immediate],
            stack_args=[self.resolve()],
            wtype=typ.wtype,
            source_location=location,
        )
        return builder_for_instance(typ, expr)

    @typing.override
    def get_array_field_value(
        self,
        field: TxnField,
        typ: pytypes.RuntimeType,
        index: InstanceBuilder,
        location: SourceLocation,
    ) -> InstanceBuilder:
        assert field.is_array
        assert pytypes.UInt64Type <= index.pytype
        assert typ.wtype.scalar_type == field.avm_type
        expr = IntrinsicCall(
            op_code="gtxnsas",
            immediates=[field.immediate],
            stack_args=[self.resolve(), index.resolve()],
            wtype=typ.wtype,
            source_location=location,
        )
        return builder_for_instance(typ, expr)


import typing
from collections.abc import Sequence

import mypy.nodes

from puya.awst.nodes import Expression, InnerTransactionField, SubmitInnerTransaction
from puya.awst.txn_fields import TxnField
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder, TypeBuilder
from puyapy.awst_build.eb.transaction.base import BaseTransactionExpressionBuilder
from puyapy.awst_build.eb.tuple import TupleExpressionBuilder

class InnerTransactionTypeBuilder(TypeBuilder[pytypes.InnerTransactionResultType]):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> typing.Never:
        typ = self.produces()
        params_typ = pytypes.InnerTransactionFieldsetTypes[typ.transaction_type]
        raise CodeError(
            f"{typ} cannot be instantiated directly, create a {params_typ} and submit instead",
            location,
        )

class InnerTransactionExpressionBuilder(BaseTransactionExpressionBuilder):
    def __init__(self, expr: Expression, typ: pytypes.PyType):
        assert isinstance(typ, pytypes.InnerTransactionResultType)
        super().__init__(typ, expr)

    @typing.override
    def get_field_value(
        self, field: TxnField, typ: pytypes.RuntimeType, location: SourceLocation
    ) -> InstanceBuilder:
        expr = InnerTransactionField(
            itxn=self.resolve(),
            field=field,
            wtype=typ.wtype,
            source_location=location,
        )
        return builder_for_instance(typ, expr)

    @typing.override
    def get_array_field_value(
        self,
        field: TxnField,
        typ: pytypes.RuntimeType,
        index: InstanceBuilder,
        location: SourceLocation,
    ) -> InstanceBuilder:
        assert pytypes.UInt64Type <= index.pytype
        expr = InnerTransactionField(
            itxn=self.resolve(),
            field=field,
            array_index=index.resolve(),
            wtype=typ.wtype,
            source_location=location,
        )
        return builder_for_instance(typ, expr)

class SubmitInnerTransactionExpressionBuilder(FunctionBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg_exprs = []
        result_types = []
        for arg in args:
            match arg:
                case InstanceBuilder(
                    pytype=pytypes.InnerTransactionFieldsetType(transaction_type=txn_type)
                ):
                    arg_exprs.append(arg.resolve())
                    arg_result_type = pytypes.InnerTransactionResultTypes[txn_type]
                    result_types.append(arg_result_type)
                case other:
                    expect.not_this_type(other, default=expect.default_raise)
        result_typ = pytypes.GenericTupleType.parameterise(result_types, location)
        return TupleExpressionBuilder(
            SubmitInnerTransaction(itxns=arg_exprs, source_location=location),
            result_typ,
        )


import abc
import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst.nodes import (
    Copy,
    CreateInnerTransaction,
    Expression,
    SubmitInnerTransaction,
    UInt64Constant,
    UpdateInnerTransaction,
)
from puya.awst.txn_fields import TxnField
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import FunctionBuilder, NotIterableInstanceExpressionBuilder
from puyapy.awst_build.eb._utils import constant_bool_and_error
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder, TypeBuilder
from puyapy.awst_build.eb.none import NoneExpressionBuilder
from puyapy.awst_build.eb.transaction.inner import InnerTransactionExpressionBuilder
from puyapy.awst_build.eb.transaction.itxn_args import PYTHON_ITXN_ARGUMENTS

logger = log.get_logger(__name__)

class InnerTxnParamsTypeBuilder(TypeBuilder[pytypes.InnerTransactionFieldsetType]):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        transaction_fields = dict[TxnField, Expression]()
        transaction_fields[TxnField.Fee] = UInt64Constant(
            value=0, source_location=self.source_location
        )
        typ = self.produces()
        if typ.transaction_type is not None:
            transaction_fields[TxnField.TypeEnum] = UInt64Constant(
                value=typ.transaction_type.value,
                teal_alias=typ.transaction_type.name,
                source_location=self.source_location,
            )
        transaction_fields.update(_map_itxn_args(arg_names, args))

        create_expr = CreateInnerTransaction(
            fields=transaction_fields, wtype=typ.wtype, source_location=location
        )
        return InnerTxnParamsExpressionBuilder(typ, create_expr)

class InnerTxnParamsExpressionBuilder(
    NotIterableInstanceExpressionBuilder[pytypes.InnerTransactionFieldsetType]
):
    @typing.override
    def to_bytes(self, location: SourceLocation) -> Expression:
        raise CodeError("cannot serialize inner transaction fieldset", location)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        if name == "submit":
            return _Submit(self, location)
        elif name == "set":
            return _Set(self, location)
        elif name == "copy":
            return _Copy(self, location)
        return super().member_access(name, location)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        return constant_bool_and_error(value=True, location=location, negate=negate)

class _MemberFunction(FunctionBuilder, abc.ABC):
    def __init__(self, base: InnerTxnParamsExpressionBuilder, location: SourceLocation) -> None:
        super().__init__(location)
        self.base = base

class _Submit(_MemberFunction):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        expect.no_args(args, location)
        result_typ = pytypes.InnerTransactionResultTypes[self.base.pytype.transaction_type]
        submit_expr = SubmitInnerTransaction(itxns=[self.base.resolve()], source_location=location)
        return InnerTransactionExpressionBuilder(submit_expr, result_typ)

class _Copy(_MemberFunction):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        expect.no_args(args, location)
        copy_expr = Copy(value=self.base.resolve(), source_location=location)
        return InnerTxnParamsExpressionBuilder(self.base.pytype, copy_expr)

class _Set(_MemberFunction):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        transaction_fields = _map_itxn_args(arg_names, args)
        update_expr = UpdateInnerTransaction(
            itxn=self.base.resolve(), fields=transaction_fields, source_location=location
        )
        return NoneExpressionBuilder(update_expr)

def _map_itxn_args(
    arg_names: list[str | None], args: Sequence[NodeBuilder]
) -> dict[TxnField, Expression]:
    transaction_fields = dict[TxnField, Expression]()
    for arg_name, arg in zip(arg_names, args, strict=True):
        if arg_name is None:
            logger.error("unexpected positional argument", location=arg.source_location)
        elif (params := PYTHON_ITXN_ARGUMENTS.get(arg_name)) is None:
            logger.error("unrecognised keyword argument", location=arg.source_location)
        else:
            transaction_fields[params.field] = params.validate_and_convert(arg).resolve()
    return transaction_fields


from collections.abc import Mapping

import attrs
from immutabledict import immutabledict

from puya import log
from puya.awst.txn_fields import TxnField
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb.bytes import BytesExpressionBuilder
from puyapy.awst_build.eb.interface import (
    InstanceBuilder,
    NodeBuilder,
    StaticSizedCollectionBuilder,
)
from puyapy.awst_build.eb.tuple import TupleLiteralBuilder
from puyapy.awst_build.utils import resolve_literal

logger = log.get_logger(__name__)

@attrs.frozen(kw_only=True)
class PythonITxnArgument:
    field: TxnField
    type: pytypes.PyType
    """the primary type of the field - will be called to convert literals,
    expect for special cases explicitly overridden in literal_overrides
    for array fields, this is the type of the items"""
    additional_types: tuple[pytypes.PyType, ...] = ()
    """any extra types that are acceptable, will be carried through without validation
    (so don't put literals in here)"""
    literal_overrides: Mapping[pytypes.LiteralOnlyType, pytypes.PyType] = attrs.field(
        converter=immutabledict, factory=immutabledict
    )
    array_promote: bool = False
    """if the field is an array, accept individual arguments and convert to array format"""
    auto_serialize_bytes: bool = False
    """rely on to_bytes() - only valid if the type is pytypes.BytesType"""

    def __attrs_post_init__(self) -> None:
        assert self.field.is_inner_param
        assert self.type not in self.additional_types
        if self.array_promote:
            assert self.field.is_array
        if self.auto_serialize_bytes:
            assert self.type is pytypes.BytesType
            assert not self.additional_types
            assert not self.literal_overrides

    def validate_and_convert(self, builder: NodeBuilder) -> InstanceBuilder:
        if not self.field.is_array:
            dummy_pytype = self.type
        else:
            dummy_pytype = pytypes.GenericTupleType.parameterise(
                [self.type] * self.field.num_values, builder.source_location
            )

        dummy_default = expect.default_dummy_value(dummy_pytype)
        eb = expect.instance_builder(builder, default=dummy_default)
        if not self.field.is_array:
            return self._validate_and_convert_item(eb)
        elif isinstance(eb, StaticSizedCollectionBuilder):
            return TupleLiteralBuilder(
                [self._validate_and_convert_item(item) for item in eb.iterate_static()],
                eb.source_location,
            )
        elif self.array_promote:
            # TODO: split bytes literals >= 4096
            item = self._validate_and_convert_item(eb)
            return TupleLiteralBuilder([item], item.source_location)
        else:
            return expect.argument_of_type(
                eb, pytypes.VariadicTupleType(items=self.type), default=dummy_default
            )

    def _validate_and_convert_item(self, item: InstanceBuilder) -> InstanceBuilder:
        if self.field == TxnField.ApplicationArgs:
            if pytypes.AccountType <= item.pytype:
                logger.warning(
                    f"{item.pytype} will not be added to foreign array,"
                    f" use .bytes to suppress this warning",
                    location=item.source_location,
                )
            elif item.pytype.is_type_or_subtype(pytypes.AssetType, pytypes.ApplicationType):
                logger.warning(
                    f"{item.pytype} will not be added to foreign array,"
                    f" use .id to suppress this warning",
                    location=item.source_location,
                )

        if self.auto_serialize_bytes:
            return BytesExpressionBuilder(item.to_bytes(item.source_location))

        override_literal_type = self.literal_overrides.get(item.pytype)  # type: ignore[call-overload]
        if override_literal_type is not None:
            item = resolve_literal(item, override_literal_type)
        return expect.argument_of_type_else_dummy(
            item, self.type, *self.additional_types, resolve_literal=True
        )

PYTHON_ITXN_ARGUMENTS = {
    # type(:) TransactionType = ...,
    "type": PythonITxnArgument(
        field=TxnField.TypeEnum,
        type=pytypes.TransactionTypeType,
    ),
    # ## payment
    # receiver: Account | str = ...,
    "receiver": PythonITxnArgument(
        field=TxnField.Receiver,
        type=pytypes.AccountType,
    ),
    # amount: UInt64 | int = ...,
    "amount": PythonITxnArgument(
        field=TxnField.Amount,
        type=pytypes.UInt64Type,
    ),
    # close_remainder_to: Account | str = ...,
    "close_remainder_to": PythonITxnArgument(
        field=TxnField.CloseRemainderTo,
        type=pytypes.AccountType,
    ),
    # ## key registration
    # vote_key: Bytes | bytes = ...,
    "vote_key": PythonITxnArgument(
        field=TxnField.VotePK,
        type=pytypes.BytesType,
    ),
    # selection_key: Bytes | bytes = ...,
    "selection_key": PythonITxnArgument(
        field=TxnField.SelectionPK,
        type=pytypes.BytesType,
    ),
    # vote_first: UInt64 | int = ...,
    "vote_first": PythonITxnArgument(
        field=TxnField.VoteFirst,
        type=pytypes.UInt64Type,
    ),
    # vote_last: UInt64 | int = ...,
    "vote_last": PythonITxnArgument(
        field=TxnField.VoteLast,
        type=pytypes.UInt64Type,
    ),
    # vote_key_dilution: UInt64 | int = ...,
    "vote_key_dilution": PythonITxnArgument(
        field=TxnField.VoteKeyDilution,
        type=pytypes.UInt64Type,
    ),
    # non_participation: UInt64 | int | bool = ...,
    "non_participation": PythonITxnArgument(
        field=TxnField.Nonparticipation,
        type=pytypes.UInt64Type,
        additional_types=(pytypes.BoolType,),
    ),
    # state_proof_key: Bytes | bytes = ...,
    "state_proof_key": PythonITxnArgument(
        field=TxnField.StateProofPK,
        type=pytypes.BytesType,
    ),
    # ## asset config
    # config_asset: Asset | UInt64 | int = ...,
    "config_asset": PythonITxnArgument(
        field=TxnField.ConfigAsset,
        type=pytypes.AssetType,
        additional_types=(pytypes.UInt64Type,),
    ),
    # total: UInt64 | int = ...,
    "total": PythonITxnArgument(
        field=TxnField.ConfigAssetTotal,
        type=pytypes.UInt64Type,
    ),
    # unit_name: String | Bytes | str | bytes = ...,
    "unit_name": PythonITxnArgument(
        field=TxnField.ConfigAssetUnitName,
        type=pytypes.BytesType,
        additional_types=(pytypes.StringType,),
        literal_overrides={pytypes.StrLiteralType: pytypes.StringType},
    ),
    # asset_name: String | Bytes | str | bytes = ...,
    "asset_name": PythonITxnArgument(
        field=TxnField.ConfigAssetName,
        type=pytypes.BytesType,
        additional_types=(pytypes.StringType,),
        literal_overrides={pytypes.StrLiteralType: pytypes.StringType},
    ),
    # decimals: UInt64 | int = ...,
    "decimals": PythonITxnArgument(
        field=TxnField.ConfigAssetDecimals,
        type=pytypes.UInt64Type,
    ),
    # default_frozen: bool = ...,
    "default_frozen": PythonITxnArgument(
        field=TxnField.ConfigAssetDefaultFrozen,
        type=pytypes.BoolType,
    ),
    # url: String | Bytes | bytes | str = ...,
    "url": PythonITxnArgument(
        field=TxnField.ConfigAssetURL,
        type=pytypes.BytesType,
        additional_types=(pytypes.StringType,),
        literal_overrides={pytypes.StrLiteralType: pytypes.StringType},
    ),
    # metadata_hash: Bytes | bytes = ...,
    "metadata_hash": PythonITxnArgument(
        field=TxnField.ConfigAssetMetadataHash,
        type=pytypes.BytesType,
    ),
    # manager: Account | str = ...,
    "manager": PythonITxnArgument(
        field=TxnField.ConfigAssetManager,
        type=pytypes.AccountType,
    ),
    # reserve: Account | str = ...,
    "reserve": PythonITxnArgument(
        field=TxnField.ConfigAssetReserve,
        type=pytypes.AccountType,
    ),
    # freeze: Account | str = ...,
    "freeze": PythonITxnArgument(
        field=TxnField.ConfigAssetFreeze,
        type=pytypes.AccountType,
    ),
    # clawback: Account | str = ...,
    "clawback": PythonITxnArgument(
        field=TxnField.ConfigAssetClawback,
        type=pytypes.AccountType,
    ),
    # ## asset transfer
    # xfer_asset: Asset | UInt64 | int = ...,
    "xfer_asset": PythonITxnArgument(
        field=TxnField.XferAsset,
        type=pytypes.AssetType,
        additional_types=(pytypes.UInt64Type,),
    ),
    # asset_amount: UInt64 | int = ...,
    "asset_amount": PythonITxnArgument(
        field=TxnField.AssetAmount,
        type=pytypes.UInt64Type,
    ),
    # asset_sender: Account | str = ...,
    "asset_sender": PythonITxnArgument(
        field=TxnField.AssetSender,
        type=pytypes.AccountType,
    ),
    # asset_receiver: Account | str = ...,
    "asset_receiver": PythonITxnArgument(
        field=TxnField.AssetReceiver,
        type=pytypes.AccountType,
    ),
    # asset_close_to: Account | str = ...,
    "asset_close_to": PythonITxnArgument(
        field=TxnField.AssetCloseTo,
        type=pytypes.AccountType,
    ),
    # ## asset freeze
    # freeze_asset: Asset | UInt64 | int = ...,
    "freeze_asset": PythonITxnArgument(
        field=TxnField.FreezeAsset,
        type=pytypes.AssetType,
        additional_types=(pytypes.UInt64Type,),
    ),
    # freeze_account: Account | str = ...,
    "freeze_account": PythonITxnArgument(
        field=TxnField.FreezeAssetAccount,
        type=pytypes.AccountType,
    ),
    # frozen: bool = ...,
    "frozen": PythonITxnArgument(
        field=TxnField.FreezeAssetFrozen,
        type=pytypes.BoolType,
    ),
    # ## application call
    # app_id: Application | UInt64 | int = ...,
    "app_id": PythonITxnArgument(
        field=TxnField.ApplicationID,
        type=pytypes.ApplicationType,
        additional_types=(pytypes.UInt64Type,),
    ),
    # approval_program: Bytes | bytes | tuple[Bytes, ...] = ...,
    "approval_program": PythonITxnArgument(
        field=TxnField.ApprovalProgramPages,
        type=pytypes.BytesType,
        array_promote=True,
    ),
    # clear_state_program: Bytes | bytes | tuple[Bytes, ...] = ...,
    "clear_state_program": PythonITxnArgument(
        field=TxnField.ClearStateProgramPages,
        type=pytypes.BytesType,
        array_promote=True,
    ),
    # on_completion: OnCompleteAction | UInt64 | int = ...,
    "on_completion": PythonITxnArgument(
        field=TxnField.OnCompletion,
        type=pytypes.OnCompleteActionType,
        additional_types=(pytypes.UInt64Type,),
    ),
    # global_num_uint: UInt64 | int = ...,
    "global_num_uint": PythonITxnArgument(
        field=TxnField.GlobalNumUint,
        type=pytypes.UInt64Type,
    ),
    # global_num_bytes: UInt64 | int = ...,
    "global_num_bytes": PythonITxnArgument(
        field=TxnField.GlobalNumByteSlice,
        type=pytypes.UInt64Type,
    ),
    # local_num_uint: UInt64 | int = ...,
    "local_num_uint": PythonITxnArgument(
        field=TxnField.LocalNumUint,
        type=pytypes.UInt64Type,
    ),
    # local_num_bytes: UInt64 | int = ...,
    "local_num_bytes": PythonITxnArgument(
        field=TxnField.LocalNumByteSlice,
        type=pytypes.UInt64Type,
    ),
    # extra_program_pages: UInt64 | int = ...,
    "extra_program_pages": PythonITxnArgument(
        field=TxnField.ExtraProgramPages,
        type=pytypes.UInt64Type,
    ),
    # app_args: tuple[object, ...] = ...,
    "app_args": PythonITxnArgument(
        field=TxnField.ApplicationArgs,
        type=pytypes.BytesType,
        auto_serialize_bytes=True,
    ),
    # accounts: tuple[Account, ...] = ...,
    "accounts": PythonITxnArgument(
        field=TxnField.Accounts,
        type=pytypes.AccountType,
    ),
    # assets: tuple[Asset, ...] = ...,
    "assets": PythonITxnArgument(
        field=TxnField.Assets,
        type=pytypes.AssetType,
    ),
    # apps: tuple[Application, ...] = ...,
    "apps": PythonITxnArgument(
        field=TxnField.Applications,
        type=pytypes.ApplicationType,
    ),
    # ## shared
    # sender: Account | str = ...,
    "sender": PythonITxnArgument(
        field=TxnField.Sender,
        type=pytypes.AccountType,
    ),
    # fee: UInt64 | int = 0,
    "fee": PythonITxnArgument(
        field=TxnField.Fee,
        type=pytypes.UInt64Type,
    ),
    # note: String | Bytes | str | bytes = ...,
    "note": PythonITxnArgument(
        field=TxnField.Note,
        type=pytypes.BytesType,
        additional_types=(pytypes.StringType,),
        literal_overrides={pytypes.StrLiteralType: pytypes.StringType},
    ),
    # rekey_to: Account | str = ...,
    "rekey_to": PythonITxnArgument(
        field=TxnField.RekeyTo,
        type=pytypes.AccountType,
    ),
}


import attrs

from puya import log
from puya.awst.txn_fields import TxnField
from puyapy.awst_build import pytypes

logger = log.get_logger(__name__)

@attrs.frozen
class PythonTxnField:
    field: TxnField
    type: pytypes.RuntimeType

PYTHON_TXN_FIELDS = {
    "sender": PythonTxnField(TxnField.Sender, pytypes.AccountType),
    "fee": PythonTxnField(TxnField.Fee, pytypes.UInt64Type),
    "first_valid": PythonTxnField(TxnField.FirstValid, pytypes.UInt64Type),
    "first_valid_time": PythonTxnField(TxnField.FirstValidTime, pytypes.UInt64Type),
    "last_valid": PythonTxnField(TxnField.LastValid, pytypes.UInt64Type),
    "note": PythonTxnField(TxnField.Note, pytypes.BytesType),
    "lease": PythonTxnField(TxnField.Lease, pytypes.BytesType),
    "receiver": PythonTxnField(TxnField.Receiver, pytypes.AccountType),
    "amount": PythonTxnField(TxnField.Amount, pytypes.UInt64Type),
    "close_remainder_to": PythonTxnField(TxnField.CloseRemainderTo, pytypes.AccountType),
    "vote_key": PythonTxnField(TxnField.VotePK, pytypes.BytesType),
    "selection_key": PythonTxnField(TxnField.SelectionPK, pytypes.BytesType),
    "vote_first": PythonTxnField(TxnField.VoteFirst, pytypes.UInt64Type),
    "vote_last": PythonTxnField(TxnField.VoteLast, pytypes.UInt64Type),
    "vote_key_dilution": PythonTxnField(TxnField.VoteKeyDilution, pytypes.UInt64Type),
    "type_bytes": PythonTxnField(TxnField.Type, pytypes.BytesType),
    "type": PythonTxnField(TxnField.TypeEnum, pytypes.TransactionTypeType),
    "xfer_asset": PythonTxnField(TxnField.XferAsset, pytypes.AssetType),
    "asset_amount": PythonTxnField(TxnField.AssetAmount, pytypes.UInt64Type),
    "asset_sender": PythonTxnField(TxnField.AssetSender, pytypes.AccountType),
    "asset_receiver": PythonTxnField(TxnField.AssetReceiver, pytypes.AccountType),
    "asset_close_to": PythonTxnField(TxnField.AssetCloseTo, pytypes.AccountType),
    "group_index": PythonTxnField(TxnField.GroupIndex, pytypes.UInt64Type),
    "txn_id": PythonTxnField(TxnField.TxID, pytypes.BytesType),
    "app_id": PythonTxnField(TxnField.ApplicationID, pytypes.ApplicationType),
    "on_completion": PythonTxnField(TxnField.OnCompletion, pytypes.OnCompleteActionType),
    "num_app_args": PythonTxnField(TxnField.NumAppArgs, pytypes.UInt64Type),
    "num_accounts": PythonTxnField(TxnField.NumAccounts, pytypes.UInt64Type),
    "approval_program": PythonTxnField(TxnField.ApprovalProgram, pytypes.BytesType),
    "clear_state_program": PythonTxnField(TxnField.ClearStateProgram, pytypes.BytesType),
    "rekey_to": PythonTxnField(TxnField.RekeyTo, pytypes.AccountType),
    "config_asset": PythonTxnField(TxnField.ConfigAsset, pytypes.AssetType),
    "total": PythonTxnField(TxnField.ConfigAssetTotal, pytypes.UInt64Type),
    "decimals": PythonTxnField(TxnField.ConfigAssetDecimals, pytypes.UInt64Type),
    "default_frozen": PythonTxnField(TxnField.ConfigAssetDefaultFrozen, pytypes.BoolType),
    "unit_name": PythonTxnField(TxnField.ConfigAssetUnitName, pytypes.BytesType),
    "asset_name": PythonTxnField(TxnField.ConfigAssetName, pytypes.BytesType),
    "url": PythonTxnField(TxnField.ConfigAssetURL, pytypes.BytesType),
    "metadata_hash": PythonTxnField(TxnField.ConfigAssetMetadataHash, pytypes.BytesType),
    "manager": PythonTxnField(TxnField.ConfigAssetManager, pytypes.AccountType),
    "reserve": PythonTxnField(TxnField.ConfigAssetReserve, pytypes.AccountType),
    "freeze": PythonTxnField(TxnField.ConfigAssetFreeze, pytypes.AccountType),
    "clawback": PythonTxnField(TxnField.ConfigAssetClawback, pytypes.AccountType),
    "freeze_asset": PythonTxnField(TxnField.FreezeAsset, pytypes.AssetType),
    "freeze_account": PythonTxnField(TxnField.FreezeAssetAccount, pytypes.AccountType),
    "frozen": PythonTxnField(TxnField.FreezeAssetFrozen, pytypes.BoolType),
    "num_assets": PythonTxnField(TxnField.NumAssets, pytypes.UInt64Type),
    "num_apps": PythonTxnField(TxnField.NumApplications, pytypes.UInt64Type),
    "global_num_uint": PythonTxnField(TxnField.GlobalNumUint, pytypes.UInt64Type),
    "global_num_bytes": PythonTxnField(TxnField.GlobalNumByteSlice, pytypes.UInt64Type),
    "local_num_uint": PythonTxnField(TxnField.LocalNumUint, pytypes.UInt64Type),
    "local_num_bytes": PythonTxnField(TxnField.LocalNumByteSlice, pytypes.UInt64Type),
    "extra_program_pages": PythonTxnField(TxnField.ExtraProgramPages, pytypes.UInt64Type),
    "non_participation": PythonTxnField(TxnField.Nonparticipation, pytypes.BoolType),
    "num_logs": PythonTxnField(TxnField.NumLogs, pytypes.UInt64Type),
    "created_asset": PythonTxnField(TxnField.CreatedAssetID, pytypes.AssetType),
    "created_app": PythonTxnField(TxnField.CreatedApplicationID, pytypes.ApplicationType),
    "last_log": PythonTxnField(TxnField.LastLog, pytypes.BytesType),
    "state_proof_key": PythonTxnField(TxnField.StateProofPK, pytypes.BytesType),
    "num_approval_program_pages": PythonTxnField(
        TxnField.NumApprovalProgramPages, pytypes.UInt64Type
    ),
    "num_clear_state_program_pages": PythonTxnField(
        TxnField.NumClearStateProgramPages,
        pytypes.UInt64Type,
    ),
    "app_args": PythonTxnField(TxnField.ApplicationArgs, pytypes.BytesType),
    "accounts": PythonTxnField(TxnField.Accounts, pytypes.AccountType),
    "assets": PythonTxnField(TxnField.Assets, pytypes.AssetType),
    "apps": PythonTxnField(TxnField.Applications, pytypes.ApplicationType),
    "logs": PythonTxnField(TxnField.Logs, pytypes.BytesType),
    "approval_program_pages": PythonTxnField(TxnField.ApprovalProgramPages, pytypes.BytesType),
    "clear_state_program_pages": PythonTxnField(
        TxnField.ClearStateProgramPages, pytypes.BytesType
    ),
}


import itertools
import typing
from collections.abc import Sequence

import mypy.nodes

from puya import log
from puya.awst import wtypes
from puya.awst.nodes import (
    BinaryBooleanOperator,
    BooleanBinaryOperation,
    Expression,
    FieldExpression,
    IntegerConstant,
    IntrinsicCall,
    Lvalue,
    SliceExpression,
    Statement,
    TupleExpression,
    TupleItemExpression,
    UInt64Constant,
)
from puya.errors import CodeError
from puya.parse import SourceLocation
from puya.utils import clamp, positive_index
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import (
    FunctionBuilder,
    GenericTypeBuilder,
    InstanceExpressionBuilder,
)
from puyapy.awst_build.eb._literals import LiteralBuilderImpl
from puyapy.awst_build.eb._utils import constant_bool_and_error, dummy_value
from puyapy.awst_build.eb.bool import BoolExpressionBuilder
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import (
    BuilderBinaryOp,
    BuilderComparisonOp,
    BuilderUnaryOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
    StaticSizedCollectionBuilder,
    TypeBuilder,
)
from puyapy.awst_build.utils import determine_base_type, get_arg_mapping

logger = log.get_logger(__name__)

_TUPLE_MEMBERS: typing.Final = frozenset(("count", "index"))
_NAMED_TUPLE_MEMBERS: typing.Final = frozenset(("_asdict", "_replace"))
_NAMED_TUPLE_CLASS_MEMBERS: typing.Final = frozenset(("_make", "_fields", "_field_defaults"))

class GenericTupleTypeBuilder(GenericTypeBuilder):
    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        return _init(args, location)

class TupleTypeBuilder(TypeBuilder[pytypes.TupleType]):
    def __init__(self, typ: pytypes.PyType, location: SourceLocation):
        assert isinstance(typ, pytypes.TupleType)
        assert typ.generic == pytypes.GenericTupleType
        super().__init__(typ, location)

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        result = _init(args, location)
        # equality comparison okay because type constrained to TupleType
        if result.pytype != self.produces():
            raise CodeError("type mismatch between tuple parameters and argument types", location)
        return result

def _init(args: Sequence[NodeBuilder], location: SourceLocation) -> InstanceBuilder:
    arg = expect.at_most_one_arg(args, location)
    if arg is None:
        return TupleLiteralBuilder(items=[], location=location)

    match arg:
        case StaticSizedCollectionBuilder() as static_builder:
            return TupleLiteralBuilder(items=static_builder.iterate_static(), location=location)
        # TODO: maybe LiteralBuilderImpl should be split for str and bytes, so it can
        #       implement StaticSizedCollectionBuilder?
        case LiteralBuilder(value=bytes() | str() as bytes_or_str):
            return TupleLiteralBuilder(
                items=[
                    LiteralBuilderImpl(value=item, source_location=arg.source_location)
                    for item in bytes_or_str
                ],
                location=location,
            )
        case _:
            raise CodeError("unhandled argument type", arg.source_location)

class NamedTupleTypeBuilder(TypeBuilder[pytypes.NamedTupleType]):
    def __init__(self, typ: pytypes.PyType, location: SourceLocation):
        assert isinstance(typ, pytypes.NamedTupleType)
        super().__init__(typ, location)

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        pytype = self.produces()
        field_mapping, any_missing = get_arg_mapping(
            required_positional_names=list(pytype.fields),
            args=args,
            arg_names=arg_names,
            call_location=location,
            raise_on_missing=False,
        )
        if any_missing:
            return dummy_value(pytype, location)

        values = [
            expect.argument_of_type_else_dummy(field_mapping[field_name], field_type).resolve()
            for field_name, field_type in pytype.fields.items()
        ]
        expr = TupleExpression(
            items=values,
            wtype=pytype.wtype,
            source_location=location,
        )
        return TupleExpressionBuilder(expr, pytype)

    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        if name in _NAMED_TUPLE_CLASS_MEMBERS:
            raise CodeError("unsupported member access", location)
        return super().member_access(name, location)

class TupleLiteralBuilder(InstanceBuilder[pytypes.TupleType], StaticSizedCollectionBuilder):
    def __init__(self, items: Sequence[InstanceBuilder], location: SourceLocation):
        super().__init__(location)
        self._items = tuple(items)
        self._pytype = pytypes.GenericTupleType.parameterise([i.pytype for i in items], location)

    @typing.override
    @property
    def pytype(self) -> pytypes.TupleType:
        return self._pytype

    @typing.override
    def iterate_static(self) -> Sequence[InstanceBuilder]:
        return self._items

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> typing.Never:
        if name in _TUPLE_MEMBERS:
            raise CodeError("unsupported member access", location)
        raise CodeError("unrecognised member access", location)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        op = BuilderComparisonOp.eq if negate else BuilderComparisonOp.ne
        return _compare(
            self,
            TupleLiteralBuilder([], location),
            op,
            location,
        )

    @typing.override
    def to_bytes(self, location: SourceLocation) -> Expression:
        raise CodeError(f"cannot serialize {self.pytype}", location)

    @typing.override
    def resolve(self) -> TupleExpression:
        item_exprs = [i.resolve() for i in self.iterate_static()]
        return TupleExpression.from_items(item_exprs, self.source_location)

    @typing.override
    def resolve_lvalue(self) -> Lvalue:
        return self.resolve()

    @typing.override
    def resolve_literal(self, converter: TypeBuilder) -> InstanceBuilder:
        return self.try_resolve_literal(converter)

    @typing.override
    def try_resolve_literal(self, converter: TypeBuilder) -> InstanceBuilder:
        # even though this may contain literals, it's not homogenous, so we can't really
        # resolve with a single converter currently...?
        return self

    @typing.override
    def delete(self, location: SourceLocation) -> Statement:
        raise CodeError("cannot delete tuple literal", location)

    @typing.override
    def unary_op(self, op: BuilderUnaryOp, location: SourceLocation) -> InstanceBuilder:
        raise CodeError(f"bad operand type for unary {op.value}: 'tuple'", location)

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        return _compare(self, other, op, location)

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        match op:
            case BuilderBinaryOp.add:
                return _concat(self, other, location, reverse=reverse)
            case BuilderBinaryOp.mult:
                match other:
                    # can't handle non-simple literals here
                    case LiteralBuilder(value=int(mult_literal)):
                        return TupleLiteralBuilder(self._items * mult_literal, location)
                    case _:
                        raise CodeError("can't multiply sequence by non-int-literal", location)
            case _:
                return NotImplemented

    @typing.override
    def bool_binary_op(
        self, other: InstanceBuilder, op: BinaryBooleanOperator, location: SourceLocation
    ) -> InstanceBuilder:
        return super().bool_binary_op(other, op, location)

    @typing.override
    def augmented_assignment(
        self, op: BuilderBinaryOp, rhs: InstanceBuilder, location: SourceLocation
    ) -> Statement:
        raise CodeError("'tuple' is an illegal expression for augmented assignment", location)

    @typing.override
    def iterate(self) -> Expression:
        return self.resolve()

    @typing.override
    def iterable_item_type(self) -> pytypes.PyType:
        return _iterable_item_type(self.pytype, self.source_location)

    def _expr_builder(self) -> InstanceBuilder:
        # used to maintain semantic compatibility, we must resolve this so all elements
        # get evaluated, we can't handle literal indexing or literal containment specially
        return TupleExpressionBuilder(self.resolve(), self.pytype)

    @typing.override
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        return self._expr_builder().contains(item, location)

    @typing.override
    def index(self, index: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        return self._expr_builder().index(index, location)

    @typing.override
    def slice_index(
        self,
        begin_index: InstanceBuilder | None,
        end_index: InstanceBuilder | None,
        stride: InstanceBuilder | None,
        location: SourceLocation,
    ) -> InstanceBuilder:
        return self._expr_builder().slice_index(begin_index, end_index, stride, location)

    @typing.override
    def single_eval(self) -> InstanceBuilder:
        return TupleLiteralBuilder(
            items=[item.single_eval() for item in self._items], location=self.source_location
        )

class TupleExpressionBuilder(
    InstanceExpressionBuilder[pytypes.TupleType], StaticSizedCollectionBuilder
):
    def __init__(self, expr: Expression, typ: pytypes.PyType):
        assert isinstance(typ, pytypes.TupleType)
        super().__init__(typ, expr)

    @typing.override
    def to_bytes(self, location: SourceLocation) -> Expression:
        raise CodeError(f"cannot serialize {self.pytype}", location)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        if isinstance(self.pytype, pytypes.NamedTupleType):
            item_typ = self.pytype.fields.get(name)
            if item_typ is not None:
                item_expr = FieldExpression(
                    base=self.resolve(),
                    name=name,
                    source_location=location,
                )
                return builder_for_instance(item_typ, item_expr)
            elif name == "_replace":
                return _Replace(self, self.pytype, location)
            elif name in _NAMED_TUPLE_MEMBERS:
                raise CodeError("unsupported member access", location)
            elif name in _NAMED_TUPLE_CLASS_MEMBERS:
                type_builder = NamedTupleTypeBuilder(self.pytype, self.source_location)
                return type_builder.member_access(name, location)
        if name in _TUPLE_MEMBERS:
            raise CodeError("unsupported member access", location)
        raise CodeError("unrecognised member access", location)

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        match op:
            case BuilderBinaryOp.add:
                return _concat(self, other, location, reverse=reverse)
            case BuilderBinaryOp.mult:
                match other:
                    # can't handle non-simple literals here
                    case LiteralBuilder(value=int(mult_literal)):
                        items = tuple(self.iterate_static())
                        return TupleLiteralBuilder(items * mult_literal, location)
                    case _:
                        raise CodeError("can't multiply sequence by non-int-literal", location)
            case _:
                return NotImplemented

    @typing.override
    def index(self, index: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        # special handling of tuples, they can be indexed by int literal only,
        # mostly because they can be non-homogenous so we need to be able to resolve the
        # result type, but also we can statically validate that value
        match index:
            case LiteralBuilder(value=int(index_value)):
                pass
            case _:
                raise CodeError(
                    "tuples can only be indexed by int constants",
                    index.source_location,
                )
        try:
            item_typ = self.pytype.items[index_value]
        except IndexError as ex:
            raise CodeError("tuple index out of range", location) from ex
        item_expr = TupleItemExpression(
            base=self.resolve(),
            index=index_value,
            source_location=location,
        )
        return builder_for_instance(item_typ, item_expr)

    @typing.override
    def slice_index(
        self,
        begin_index: InstanceBuilder | None,
        end_index: InstanceBuilder | None,
        stride: InstanceBuilder | None,
        location: SourceLocation,
    ) -> InstanceBuilder:
        if stride is not None:
            raise CodeError("stride is not supported", location=stride.source_location)

        start_expr, start_idx = self._clamp_slice_index(begin_index)
        end_expr, end_idx = self._clamp_slice_index(end_index)
        slice_types = self.pytype.items[start_idx:end_idx]
        if not slice_types:
            raise CodeError("empty slices are not supported", location)

        updated_type = pytypes.GenericTupleType.parameterise(slice_types, location)
        updated_wtype = updated_type.checked_wtype(location)
        return TupleExpressionBuilder(
            SliceExpression(
                source_location=location,
                base=self.resolve(),
                begin_index=start_expr,
                end_index=end_expr,
                wtype=updated_wtype,
            ),
            updated_type,
        )

    def _clamp_slice_index(
        self, index: NodeBuilder | None
    ) -> tuple[IntegerConstant | None, int | None]:
        match index:
            case None:
                expr = None
                idx = None
            case LiteralBuilder(value=int(idx), source_location=start_loc):
                positive_idx = positive_index(idx, self.pytype.items)
                positive_idx_clamped = clamp(positive_idx, low=0, high=len(self.pytype.items) - 1)
                expr = UInt64Constant(value=positive_idx_clamped, source_location=start_loc)
            case _:
                raise CodeError(
                    "tuples can only be indexed with literal values", index.source_location
                )
        return expr, idx

    @typing.override
    def iterate(self) -> Expression:
        return self.resolve()

    @typing.override
    def iterate_static(self) -> Sequence[InstanceBuilder]:
        base = self.single_eval().resolve()
        return [
            builder_for_instance(
                item_type,
                TupleItemExpression(base=base, index=idx, source_location=self.source_location),
            )
            for idx, item_type in enumerate(self.pytype.items)
        ]

    @typing.override
    def iterable_item_type(self) -> pytypes.PyType:
        return _iterable_item_type(self.pytype, self.source_location)

    @typing.override
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        contains_expr = None
        if isinstance(item.pytype, pytypes.LiteralOnlyType):
            logger.error(
                "a Python literal is not valid at this location", location=self.source_location
            )
            return dummy_value(pytypes.BoolType, location)
        item = item.single_eval()
        # note: iterate_static single_evals the sequence and is the only usage of self
        for compare_to in self.iterate_static():
            eq_compare = item.compare(compare_to, BuilderComparisonOp.eq, location)
            if eq_compare is NotImplemented:
                eq_compare = compare_to.compare(item, BuilderComparisonOp.eq, location)
            if eq_compare is NotImplemented:
                # if types don't support comparison, then skip
                continue
            if contains_expr is None:
                contains_expr = eq_compare.resolve()
            else:
                contains_expr = BooleanBinaryOperation(
                    left=contains_expr,
                    op=BinaryBooleanOperator.or_,
                    right=eq_compare.resolve(),
                    source_location=location,
                )
        if contains_expr is None:
            return constant_bool_and_error(value=False, location=location)
        else:
            return BoolExpressionBuilder(contains_expr)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        op = BuilderComparisonOp.eq if negate else BuilderComparisonOp.ne
        return _compare(
            self,
            TupleLiteralBuilder([], location),
            op,
            location,
        )

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        return _compare(self, other, op, location)

class _Replace(FunctionBuilder):
    def __init__(
        self,
        instance: TupleExpressionBuilder,
        named_tuple_type: pytypes.NamedTupleType,
        location: SourceLocation,
    ):
        super().__init__(location)
        self.instance = instance
        self.named_tuple_type = named_tuple_type

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        pytype = self.named_tuple_type
        field_mapping, _ = get_arg_mapping(
            optional_kw_only=list(pytype.fields),
            args=args,
            arg_names=arg_names,
            call_location=location,
            raise_on_missing=False,
        )
        base_expr = self.instance.single_eval().resolve()
        items = list[Expression]()
        for idx, (field_name, field_pytype) in enumerate(pytype.fields.items()):
            new_value = field_mapping.get(field_name)
            if new_value is not None:
                item_builder = expect.argument_of_type_else_dummy(new_value, field_pytype)
                item = item_builder.resolve()
            else:
                item = TupleItemExpression(base=base_expr, index=idx, source_location=location)
            items.append(item)
        new_tuple = TupleExpression(items=items, wtype=pytype.wtype, source_location=location)
        return TupleExpressionBuilder(new_tuple, pytype)

def _compare(
    lhs: InstanceBuilder[pytypes.TupleType],
    rhs: InstanceBuilder,
    op: BuilderComparisonOp,
    location: SourceLocation,
) -> InstanceBuilder:
    if not isinstance(rhs.pytype, pytypes.TupleType):
        return NotImplemented

    match op:
        case BuilderComparisonOp.eq:
            chain_op = "&&"
            inverse = BuilderComparisonOp.ne
            result_if_both_empty = True
        case BuilderComparisonOp.ne:
            chain_op = "||"
            inverse = BuilderComparisonOp.eq
            result_if_both_empty = False
        case _:
            raise CodeError(
                f"the {op.value!r} operator is not currently supported with tuples", location
            )

    assert isinstance(lhs, StaticSizedCollectionBuilder)
    lhs_items = lhs.iterate_static()
    assert isinstance(rhs, StaticSizedCollectionBuilder)
    rhs_items = rhs.iterate_static()

    result_exprs = []
    for idx, (lhs_item, rhs_item) in enumerate(itertools.zip_longest(lhs_items, rhs_items)):
        if lhs_item is None:
            # if lhs is shorter than rhs, use the rhs item to compare against itself,
            # so that rhs is still fully evaluated to maintain semantic compatibility,
            # and we don't create typing errors trying to invent a different eval.
            # make sure it's evaluated once though, because it only appears on one side.
            # also, to make the comparison correct, we need to invert the overall op,
            # if we're doing tup1 == tup2, then things get &&'d together, and we need to use !=
            # if we're doing tup1 != tup2, then things get ||'d together, and we need to use ==
            lhs_item = rhs_item = rhs_item.single_eval()
            op_at = inverse
        elif rhs_item is None:
            rhs_item = lhs_item = lhs_item.single_eval()
            op_at = inverse
        else:
            op_at = op
        cmp_builder = lhs_item.compare(rhs_item, op=op_at, location=location)
        if cmp_builder is NotImplemented:
            cmp_builder = rhs_item.compare(lhs_item, op=op_at.reversed(), location=location)
        if cmp_builder is NotImplemented:
            raise CodeError(
                f"items at index {idx} do not support comparison with operator {op_at.value!r}",
                location,
            )
        result_exprs.append(cmp_builder.resolve())

    if not result_exprs:
        return LiteralBuilderImpl(value=result_if_both_empty, source_location=location)

    result = result_exprs[0]
    for result_expr in result_exprs[1:]:
        result = IntrinsicCall(
            op_code=chain_op,
            stack_args=[result, result_expr],
            wtype=wtypes.bool_wtype,
            source_location=location,
        )
    return BoolExpressionBuilder(result)

def _concat(
    this: InstanceBuilder[pytypes.TupleType],
    other: InstanceBuilder,
    location: SourceLocation,
    *,
    reverse: bool,
) -> InstanceBuilder:
    if not isinstance(other.pytype, pytypes.TupleType):
        raise CodeError("can only concatenate tuple with other tuples", location)
    other = typing.cast(InstanceBuilder[pytypes.TupleType], other)
    if not reverse:
        lhs, rhs = this, other
    else:
        lhs, rhs = other, this

    assert isinstance(lhs, StaticSizedCollectionBuilder)
    lhs_items = lhs.iterate_static()
    assert isinstance(rhs, StaticSizedCollectionBuilder)
    rhs_items = rhs.iterate_static()

    items = [*lhs_items, *rhs_items]
    return TupleLiteralBuilder(items, location)

def _iterable_item_type(
    pytype: pytypes.TupleType, source_location: SourceLocation
) -> pytypes.PyType:
    base_type = determine_base_type(*pytype.items, location=source_location)
    if isinstance(base_type, pytypes.UnionType):
        raise CodeError(
            "unable to iterate heterogeneous tuple without common base type", source_location
        )
    return base_type


import typing
from collections.abc import Sequence

import attrs
import mypy.nodes

from puya import log
from puya.awst import wtypes
from puya.awst.nodes import (
    Expression,
    NumericComparison,
    NumericComparisonExpression,
    ReinterpretCast,
    Statement,
    UInt64AugmentedAssignment,
    UInt64BinaryOperation,
    UInt64BinaryOperator,
    UInt64Constant,
    UInt64UnaryOperation,
    UInt64UnaryOperator,
)
from puya.parse import SourceLocation
from puyapy.awst_build import intrinsic_factory, pytypes
from puyapy.awst_build.eb import _expect as expect
from puyapy.awst_build.eb._base import NotIterableInstanceExpressionBuilder
from puyapy.awst_build.eb._utils import dummy_statement
from puyapy.awst_build.eb.bool import BoolExpressionBuilder
from puyapy.awst_build.eb.interface import (
    BuilderBinaryOp,
    BuilderComparisonOp,
    BuilderUnaryOp,
    InstanceBuilder,
    LiteralBuilder,
    NodeBuilder,
    TypeBuilder,
)

logger = log.get_logger(__name__)

class UInt64TypeBuilder(TypeBuilder):
    def __init__(self, location: SourceLocation):
        super().__init__(pytypes.UInt64Type, location)

    @typing.override
    def try_convert_literal(
        self, literal: LiteralBuilder, location: SourceLocation
    ) -> InstanceBuilder | None:
        match literal.value:
            case int(int_value):
                pytype = self.produces()
                if int_value < 0 or int_value.bit_length() > 64:
                    logger.error(f"invalid {pytype} value", location=literal.source_location)
                # take int() of the value since it could match a bool also
                expr = UInt64Constant(value=int(int_value), source_location=location)
                return UInt64ExpressionBuilder(expr)
        return None

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        arg = expect.at_most_one_arg_of_type(
            args, [pytypes.IntLiteralType, pytypes.BoolType], location
        )
        match arg:
            case InstanceBuilder(pytype=pytypes.IntLiteralType):
                return arg.resolve_literal(converter=UInt64TypeBuilder(location))
            case InstanceBuilder(pytype=pytypes.BoolType):
                return _upcast_bool(arg, location)
            case _:
                return UInt64ExpressionBuilder(UInt64Constant(value=0, source_location=location))

class UInt64ExpressionBuilder(NotIterableInstanceExpressionBuilder):
    def __init__(self, expr: Expression, enum_type: pytypes.UInt64EnumType | None = None):
        if enum_type is None:
            pytype = pytypes.UInt64Type
        else:
            pytype = enum_type
        super().__init__(pytype, expr)

    @typing.override
    def to_bytes(self, location: SourceLocation) -> Expression:
        return intrinsic_factory.itob(self.resolve(), location)

    @typing.override
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        cmp_expr = NumericComparisonExpression(
            lhs=self.resolve(),
            operator=NumericComparison.eq if negate else NumericComparison.ne,
            rhs=UInt64Constant(value=0, source_location=location),
            source_location=location,
        )
        return BoolExpressionBuilder(cmp_expr)

    @typing.override
    def unary_op(self, op: BuilderUnaryOp, location: SourceLocation) -> InstanceBuilder:
        match op:
            case BuilderUnaryOp.positive:
                # unary + is allowed, but for the current types it has no real impact
                # so just expand the existing expression to include the unary operator
                return UInt64ExpressionBuilder(
                    attrs.evolve(self.resolve(), source_location=location)
                )
            case BuilderUnaryOp.bit_invert:
                return UInt64ExpressionBuilder(
                    UInt64UnaryOperation(
                        expr=self.resolve(),
                        op=UInt64UnaryOperator.bit_invert,
                        source_location=location,
                    )
                )
            case _:
                return super().unary_op(op, location)

    @typing.override
    def compare(
        self, other: InstanceBuilder, op: BuilderComparisonOp, location: SourceLocation
    ) -> InstanceBuilder:
        other = _resolve_literal_and_upcast_bool(other)
        if not (pytypes.UInt64Type <= other.pytype):
            return NotImplemented
        cmp_expr = NumericComparisonExpression(
            source_location=location,
            lhs=self.resolve(),
            operator=NumericComparison(op.value),
            rhs=other.resolve(),
        )
        return BoolExpressionBuilder(cmp_expr)

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        uint64_op = _translate_uint64_math_operator(op, location)
        if uint64_op is None:
            return NotImplemented
        other = _resolve_literal_and_upcast_bool(other)
        if not (pytypes.UInt64Type <= other.pytype):
            return NotImplemented

        lhs = self.resolve()
        rhs = other.resolve()
        if reverse:
            (lhs, rhs) = (rhs, lhs)
        bin_op_expr = UInt64BinaryOperation(
            left=lhs, op=uint64_op, right=rhs, source_location=location
        )
        return UInt64ExpressionBuilder(bin_op_expr)

    @typing.override
    def augmented_assignment(
        self, op: BuilderBinaryOp, rhs: InstanceBuilder, location: SourceLocation
    ) -> Statement:
        uint64_op = _translate_uint64_math_operator(op, location)
        if uint64_op is None:
            logger.error(f"unsupported operator for type: {op.value!r}", location=location)
            return dummy_statement(location)
        rhs = _resolve_literal_and_upcast_bool(rhs)
        # note: don't check for enum types here, mypy errors anyway
        rhs = expect.argument_of_type_else_dummy(rhs, pytypes.UInt64Type)
        target = self.resolve_lvalue()
        return UInt64AugmentedAssignment(
            target=target, op=uint64_op, value=rhs.resolve(), source_location=location
        )

def _translate_uint64_math_operator(
    operator: BuilderBinaryOp, loc: SourceLocation
) -> UInt64BinaryOperator | None:
    if operator is BuilderBinaryOp.div:
        logger.error(
            (
                "To maintain semantic compatibility with Python, "
                "only the truncating division operator (//) is supported "
            ),
            location=loc,
        )
        # continue traversing code to generate any further errors
        operator = BuilderBinaryOp.floor_div
    try:
        return UInt64BinaryOperator(operator.value)
    except ValueError:
        return None

def _resolve_literal_and_upcast_bool(other: InstanceBuilder) -> InstanceBuilder:
    other = other.resolve_literal(converter=UInt64TypeBuilder(other.source_location))
    if other.pytype == pytypes.BoolType:
        return _upcast_bool(other)
    return other

def _upcast_bool(
    builder: InstanceBuilder, location: SourceLocation | None = None
) -> InstanceBuilder:
    assert builder.pytype == pytypes.BoolType
    expr = ReinterpretCast(
        expr=builder.resolve(),
        wtype=wtypes.uint64_wtype,
        source_location=location or builder.source_location,
    )
    return UInt64ExpressionBuilder(expr)


import enum
import typing
from collections.abc import Mapping, Sequence

import mypy.nodes

from puya.avm import OnCompletionAction, TransactionType
from puya.awst.nodes import UInt64Constant
from puya.errors import CodeError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder, TypeBuilder
from puyapy.awst_build.eb.uint64 import UInt64ExpressionBuilder

class _UInt64EnumTypeBuilder(TypeBuilder):
    enum_type: typing.ClassVar[pytypes.UInt64EnumType]
    enum_data: typing.ClassVar[Mapping[str, enum.IntEnum] | Mapping[str, int]]

    def __init_subclass__(
        cls,
        enum_type: pytypes.UInt64EnumType,
        enum_data: Mapping[str, enum.IntEnum] | Mapping[str, int],
        **kwargs: typing.Any,
    ):
        super().__init_subclass__(**kwargs)
        cls.enum_type = enum_type
        cls.enum_data = enum_data

    def __init__(self, location: SourceLocation):
        super().__init__(self.enum_type, location)

    @typing.override
    def call(
        self,
        args: Sequence[NodeBuilder],
        arg_kinds: list[mypy.nodes.ArgKind],
        arg_names: list[str | None],
        location: SourceLocation,
    ) -> InstanceBuilder:
        raise CodeError("cannot instantiate enumeration type", location)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        if (data := self.enum_data.get(name)) is None:
            return super().member_access(name, location)
        if isinstance(data, enum.IntEnum):
            teal_alias = data.name
            value = data.value
        else:
            teal_alias = None
            value = data

        const_expr = UInt64Constant(value=value, teal_alias=teal_alias, source_location=location)
        return UInt64ExpressionBuilder(const_expr, self.enum_type)

class OnCompletionActionTypeBuilder(
    _UInt64EnumTypeBuilder,
    enum_type=pytypes.OnCompleteActionType,
    enum_data={oca.name: oca for oca in OnCompletionAction},
):
    pass

class TransactionTypeTypeBuilder(
    _UInt64EnumTypeBuilder,
    enum_type=pytypes.TransactionTypeType,
    enum_data={
        "Payment": TransactionType.pay,
        "KeyRegistration": TransactionType.keyreg,
        "AssetConfig": TransactionType.acfg,
        "AssetTransfer": TransactionType.axfer,
        "AssetFreeze": TransactionType.afrz,
        "ApplicationCall": TransactionType.appl,
    },
):
    pass

class OpUpFeeSourceTypeBuilder(
    _UInt64EnumTypeBuilder,
    enum_type=pytypes.OpUpFeeSourceType,
    enum_data={
        "GroupCredit": 0,
        "AppAccount": 1,
        "Any": 2,
    },
):
    pass