import typing

import mypy.nodes
import mypy.visitor
from immutabledict import immutabledict

from puya import log
from puya.avm import OnCompletionAction
from puya.awst.nodes import (
    ABIMethodArgConstantDefault,
    ABIMethodArgDefault,
    ABIMethodArgMemberDefault,
    ARC4ABIMethodConfig,
    ARC4BareMethodConfig,
    ARC4CreateOption,
    Expression,
)
from puya.errors import CodeError, InternalError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.context import ASTConversionModuleContext
from puyapy.awst_build.eb.interface import NodeBuilder
from puyapy.awst_build.subroutine import ExpressionASTConverter, require_instance_builder
from puyapy.awst_build.utils import extract_decorator_args, get_unaliased_fullname
from puyapy.models import ARC4ABIMethodData, ARC4BareMethodData

logger = log.get_logger(__name__)

def get_arc4_baremethod_data(
    context: ASTConversionModuleContext,
    decorator: mypy.nodes.Expression,
    func_def: mypy.nodes.FuncDef,
) -> ARC4BareMethodData:
    dec_loc = context.node_location(decorator, func_def.info)
    pytype, func_types = _get_func_types(context, func_def, dec_loc)
    if func_types != {"output": pytypes.NoneType}:
        logger.error("bare methods should have no arguments or return values", location=dec_loc)

    named_args = _extract_decorator_named_args(context, decorator, dec_loc)
    evaluated_args = {n: _parse_decorator_arg(context, n, a) for n, a in named_args.items()}

    create = _extract_create_option(evaluated_args, dec_loc)
    allowed_completion_types = _extract_allow_actions_option(evaluated_args, dec_loc)
    if evaluated_args:
        logger.error(
            f"unexpected parameters: {', '.join(map(str, evaluated_args))}", location=dec_loc
        )

    return ARC4BareMethodData(
        member_name=func_def.name,
        pytype=pytype,
        config=ARC4BareMethodConfig(
            allowed_completion_types=allowed_completion_types,
            create=create,
            source_location=dec_loc,
        ),
        source_location=dec_loc,
    )

_READONLY = "readonly"
_CLIENT_DEFAULTS = "default_args"
_ALLOWED_ACTIONS = "allow_actions"
_CREATE_OPTIONS = "create"
_NAME_OVERRIDE = "name"

def get_arc4_abimethod_data(
    context: ASTConversionModuleContext,
    decorator: mypy.nodes.Expression,
    func_def: mypy.nodes.FuncDef,
) -> ARC4ABIMethodData:
    dec_loc = context.node_location(decorator, func_def.info)
    pytype, func_types = _get_func_types(context, func_def, dec_loc)

    named_args = _extract_decorator_named_args(context, decorator, dec_loc)
    evaluated_args = {n: _parse_decorator_arg(context, n, a) for n, a in named_args.items()}

    create = _extract_create_option(evaluated_args, dec_loc)
    allowed_completion_types = _extract_allow_actions_option(evaluated_args, dec_loc)
    # map "name" param
    name = func_def.name
    match evaluated_args.pop(_NAME_OVERRIDE, None):
        case None:
            pass
        case str(name):
            pass
        case invalid_name:
            context.error(f"invalid name option: {invalid_name}", dec_loc)

    # map "readonly" param
    default_readonly = False
    match evaluated_args.pop(_READONLY, default_readonly):
        case bool(readonly):
            pass
        case invalid_readonly_option:
            context.error(f"invalid readonly option: {invalid_readonly_option}", dec_loc)
            readonly = default_readonly

    # map "default_args" param
    default_args = dict[str, ABIMethodArgDefault]()
    match evaluated_args.pop(_CLIENT_DEFAULTS, {}):
        case {**options}:
            method_arg_names = func_types.keys() - {"output"}
            for parameter, value in options.items():
                if parameter not in method_arg_names:
                    context.error(
                        f"{parameter!r} is not a parameter of {func_def.fullname}",
                        dec_loc,
                    )
                else:
                    # if it's in method_arg_names, it's a str
                    assert isinstance(parameter, str)
                    if isinstance(value, str):
                        default_args[parameter] = ABIMethodArgMemberDefault(name=value)
                    elif isinstance(value, Expression):
                        default_args[parameter] = ABIMethodArgConstantDefault(value=value)
                    else:
                        context.error(f"invalid default_args value: {value!r}", dec_loc)
        case invalid_default_args_option:
            context.error(f"invalid default_args option: {invalid_default_args_option}", dec_loc)

    config = ARC4ABIMethodConfig(
        source_location=dec_loc,
        allowed_completion_types=allowed_completion_types,
        create=create,
        name=name,
        readonly=readonly,
        default_args=immutabledict(default_args),
    )
    return ARC4ABIMethodData(
        member_name=func_def.name,
        pytype=pytype,
        config=config,
        signature=func_types,
        source_location=dec_loc,
    )

def _get_func_types(
    context: ASTConversionModuleContext, func_def: mypy.nodes.FuncDef, location: SourceLocation
) -> tuple[pytypes.FuncType, dict[str, pytypes.PyType]]:
    if func_def.type is None:
        raise CodeError("typing error", location)
    func_type = context.type_to_pytype(
        func_def.type, source_location=context.node_location(func_def, module_src=func_def.info)
    )
    if not isinstance(func_type, pytypes.FuncType):
        raise InternalError(
            f"unexpected type result for ABI function definition type: {type(func_type).__name__}",
            location,
        )

    def require_arg_name(arg: pytypes.FuncArg) -> str:
        if arg.name is None:
            raise CodeError(
                "positional only arguments are not supported with ARC-4 methods", location
            )
        return arg.name

    result = {require_arg_name(arg): arg.type for arg in func_type.args}
    if "output" in result:
        # https://github.com/algorandfoundation/ARCs/blob/main/assets/arc-0032/application.schema.json
        raise CodeError(
            "for compatibility with ARC-32, ARC-4 methods cannot have an argument named output",
            location,
        )
    result["output"] = func_type.ret_type
    return func_type, result

def _extract_decorator_named_args(
    context: ASTConversionModuleContext, decorator: mypy.nodes.Expression, location: SourceLocation
) -> dict[str, mypy.nodes.Expression]:
    result = {}
    for name, value in extract_decorator_args(decorator, location):
        if name is None:
            logger.error("unexpected positional argument", location=context.node_location(value))
        elif name in result:
            logger.error("duplicate named argument", location=context.node_location(value))
        else:
            result[name] = value
    return result

def _parse_decorator_arg(
    context: ASTConversionModuleContext, name: str, value: mypy.nodes.Expression
) -> object:
    visitor = _ARC4DecoratorArgEvaluator(context, name)
    return value.accept(visitor)

class _ARC4DecoratorArgEvaluator(mypy.visitor.NodeVisitor[object]):
    def __init__(self, context: ASTConversionModuleContext, arg_name: str):
        self.context = context
        self.arg_name = arg_name

    def __getattribute__(self, name: str) -> object:
        attr = super().__getattribute__(name)
        if name.startswith("visit_") and not attr.__module__.startswith("puyapy."):
            return self._not_supported
        return attr

    def _not_supported(self, o: mypy.nodes.Context) -> typing.Never:
        raise CodeError("unexpected argument type", self.context.node_location(o))

    def _resolve_constant_reference(self, expr: mypy.nodes.RefExpr) -> object:
        try:
            return self.context.constants[expr.fullname]
        except KeyError:
            raise CodeError(
                f"Unresolved module constant: {expr.fullname}", self.context.node_location(expr)
            ) from None

    @typing.override
    def visit_call_expr(self, o: mypy.nodes.CallExpr) -> Expression:
        return _parse_expression(self.context, o)

    @typing.override
    def visit_str_expr(self, o: mypy.nodes.StrExpr) -> str:
        return o.value

    @typing.override
    def visit_name_expr(self, o: mypy.nodes.NameExpr) -> object:
        if self.arg_name == _READONLY:
            if o.fullname == "builtins.True":
                return True
            if o.fullname == "builtins.False":
                return False
        elif self.arg_name == _CLIENT_DEFAULTS:
            if isinstance(o.node, mypy.nodes.Decorator):
                return o.name  # assume abimethod
        return self._resolve_constant_reference(o)

    @typing.override
    def visit_member_expr(self, o: mypy.nodes.MemberExpr) -> object:
        if self.arg_name == _ALLOWED_ACTIONS and isinstance(o.expr, mypy.nodes.RefExpr):
            unaliased_base_fullname = get_unaliased_fullname(o.expr)
            if unaliased_base_fullname == pytypes.OnCompleteActionType.name:
                try:
                    OnCompletionAction[o.name]
                except KeyError:
                    raise CodeError(
                        f"unable to resolve constant value for {unaliased_base_fullname}.{o.name}",
                        self.context.node_location(o),
                    ) from None
                else:
                    return o.name

        return self._resolve_constant_reference(o)

    @typing.override
    def visit_unary_expr(self, o: mypy.nodes.UnaryExpr) -> object:
        if self.arg_name == _READONLY:
            operand = o.expr.accept(self)
            if o.op == "not":
                return not operand
        elif self.arg_name == _CLIENT_DEFAULTS:
            return _parse_expression(self.context, o)
        self._not_supported(o)

    @typing.override
    def visit_list_expr(self, o: mypy.nodes.ListExpr) -> object:
        if self.arg_name == _ALLOWED_ACTIONS:
            return [item.accept(self) for item in o.items]
        self._not_supported(o)

    @typing.override
    def visit_tuple_expr(self, o: mypy.nodes.TupleExpr) -> object:
        if self.arg_name == _ALLOWED_ACTIONS:
            return tuple(item.accept(self) for item in o.items)
        elif self.arg_name == _CLIENT_DEFAULTS:
            return _parse_expression(self.context, o)
        self._not_supported(o)

    @typing.override
    def visit_dict_expr(self, o: mypy.nodes.DictExpr) -> dict[object, object]:
        return {key.accept(self) if key else None: value.accept(self) for key, value in o.items}

def _parse_expression(
    context: ASTConversionModuleContext, node: mypy.nodes.Expression
) -> Expression:
    converter = _ConstantExpressionASTConverter(context)
    node_builder = node.accept(converter)
    instance_builder = require_instance_builder(node_builder)
    return instance_builder.resolve()

class _ConstantExpressionASTConverter(ExpressionASTConverter):
    @typing.override
    def resolve_local_type(self, var_name: str, expr_loc: SourceLocation) -> pytypes.PyType | None:
        raise CodeError("local variables not supported in decorators", expr_loc)

    @typing.override
    def builder_for_self(self, expr_loc: SourceLocation) -> NodeBuilder:
        raise InternalError("self variable outside of method", expr_loc)

    @typing.override
    def visit_super_expr(self, o: mypy.nodes.SuperExpr) -> typing.Never:
        raise CodeError("super expressions not supported in decorators", self._location(o))

    @typing.override
    def visit_assignment_expr(self, o: mypy.nodes.AssignmentExpr) -> typing.Never:
        raise CodeError("assignment expressions not supported in decorators", self._location(o))

def _extract_create_option(
    evaluated_args: dict[str, object], location: SourceLocation
) -> ARC4CreateOption:
    default_value = ARC4CreateOption.disallow
    option_name = evaluated_args.pop(_CREATE_OPTIONS, default_value.name)
    try:
        return ARC4CreateOption[option_name]  # type: ignore[misc]
    except KeyError:
        logger.error(  # noqa: TRY400
            f"invalid create option value: {option_name}", location=location
        )
        return default_value

def _extract_allow_actions_option(
    evaluated_args: dict[str, object], location: SourceLocation
) -> list[OnCompletionAction]:
    allowed_completion_types = []
    match evaluated_args.pop(_ALLOWED_ACTIONS, None):
        case None:
            pass
        case []:
            logger.error("empty allow_actions", location=location)
        case [*allow_actions]:
            for a in allow_actions:
                oca = _allowed_oca(a)
                if oca is None:
                    logger.error(f"invalid allow action: {a}", location=location)
                elif oca in allowed_completion_types:
                    logger.error(f"duplicate value in allow_actions: {a}", location=location)
                else:
                    allowed_completion_types.append(oca)
        case invalid_allow_actions_option:
            logger.error(
                f"invalid allow_actions option: {invalid_allow_actions_option}", location=location
            )
    # defaults set last in case of one or more errors above
    return allowed_completion_types or [OnCompletionAction.NoOp]

def _allowed_oca(name: object) -> OnCompletionAction | None:
    if not isinstance(name, str):
        return None
    try:
        result = OnCompletionAction[name]
    except KeyError:
        return None
    if result is OnCompletionAction.ClearState:
        return None
    return result


import re
import typing
from collections.abc import Callable, Iterable

import attrs
import mypy.nodes
import mypy.visitor

from puya import log
from puya.avm import TransactionType
from puya.awst import wtypes
from puya.errors import CodeError, InternalError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes

__all__ = [
    "arc4_to_pytype",
    "pytype_to_arc4",
    "pytype_to_arc4_pytype",
]

logger = log.get_logger(__name__)

def _is_arc4_struct(typ: pytypes.PyType) -> typing.TypeGuard[pytypes.StructType]:
    if not (pytypes.ARC4StructBaseType < typ):
        return False
    if not isinstance(typ, pytypes.StructType):
        raise InternalError(
            f"Type inherits from {pytypes.ARC4StructBaseType!r}"
            f" but structure type is {type(typ).__name__!r}"
        )
    return True

@attrs.frozen
class _DecoratorData:
    fullname: str
    args: list[tuple[str | None, mypy.nodes.Expression]]
    source_location: SourceLocation

def pytype_to_arc4_pytype(
    pytype: pytypes.PyType,
    on_error: Callable[[pytypes.PyType], pytypes.PyType],
) -> pytypes.PyType:
    match pytype:
        case pytypes.BoolType:
            return pytypes.ARC4BoolType
        case pytypes.NamedTupleType():
            return pytypes.StructType(
                base=pytypes.ARC4StructBaseType,
                desc=pytype.desc,
                name=pytype.name,
                fields={
                    name: pytype_to_arc4_pytype(t, on_error) for name, t in pytype.fields.items()
                },
                frozen=True,
                source_location=pytype.source_location,
            )
        case pytypes.TupleType():
            return pytypes.GenericARC4TupleType.parameterise(
                [pytype_to_arc4_pytype(t, on_error) for t in pytype.items], pytype.source_location
            )
        case pytypes.NoneType | pytypes.GroupTransactionType():
            return pytype

    if pytypes.UInt64Type <= pytype:
        return pytypes.ARC4UIntN_Aliases[64]
    elif pytypes.BigUIntType <= pytype:
        return pytypes.ARC4UIntN_Aliases[512]
    elif pytypes.BytesType <= pytype:
        return pytypes.ARC4DynamicBytesType
    elif pytypes.StringType <= pytype:
        return pytypes.ARC4StringType
    elif pytype.is_type_or_subtype(
        pytypes.ApplicationType, pytypes.AssetType, pytypes.AccountType
    ) or isinstance(pytype.wtype, wtypes.ARC4Type):
        return pytype
    else:
        return on_error(pytype)

_UINT_REGEX = re.compile(r"^uint(?P<n>[0-9]+)$")
_UFIXED_REGEX = re.compile(r"^ufixed(?P<n>[0-9]+)x(?P<m>[0-9]+)$")
_FIXED_ARRAY_REGEX = re.compile(r"^(?P<type>.+)\[(?P<size>[0-9]+)]$")
_DYNAMIC_ARRAY_REGEX = re.compile(r"^(?P<type>.+)\[]$")
_TUPLE_REGEX = re.compile(r"^\((?P<types>.+)\)$")
_ARC4_PYTYPE_MAPPING = {
    "bool": pytypes.ARC4BoolType,
    "string": pytypes.ARC4StringType,
    "account": pytypes.AccountType,
    "application": pytypes.ApplicationType,
    "asset": pytypes.AssetType,
    "void": pytypes.NoneType,
    "txn": pytypes.GroupTransactionTypes[None],
    **{t.name: pytypes.GroupTransactionTypes[t] for t in TransactionType},
    "address": pytypes.ARC4AddressType,
    "byte": pytypes.ARC4ByteType,
    "byte[]": pytypes.ARC4DynamicBytesType,
}

def arc4_to_pytype(typ: str, location: SourceLocation | None = None) -> pytypes.PyType:
    if known_typ := _ARC4_PYTYPE_MAPPING.get(typ):
        return known_typ
    if uint := _UINT_REGEX.match(typ):
        n = int(uint.group("n"))
        n_typ = pytypes.TypingLiteralType(value=n, source_location=None)
        if n <= 64:
            return pytypes.GenericARC4UIntNType.parameterise([n_typ], location)
        else:
            return pytypes.GenericARC4BigUIntNType.parameterise([n_typ], location)
    if ufixed := _UFIXED_REGEX.match(typ):
        n, m = map(int, ufixed.group("n", "m"))
        n_typ = pytypes.TypingLiteralType(value=n, source_location=None)
        m_typ = pytypes.TypingLiteralType(value=m, source_location=None)
        if n <= 64:
            return pytypes.GenericARC4UFixedNxMType.parameterise([n_typ, m_typ], location)
        else:
            return pytypes.GenericARC4BigUFixedNxMType.parameterise([n_typ, m_typ], location)
    if fixed_array := _FIXED_ARRAY_REGEX.match(typ):
        arr_type, size_str = fixed_array.group("type", "size")
        size = int(size_str)
        size_typ = pytypes.TypingLiteralType(value=size, source_location=None)
        element_type = arc4_to_pytype(arr_type, location)
        return pytypes.GenericARC4StaticArrayType.parameterise([element_type, size_typ], location)
    if dynamic_array := _DYNAMIC_ARRAY_REGEX.match(typ):
        arr_type = dynamic_array.group("type")
        element_type = arc4_to_pytype(arr_type, location)
        return pytypes.GenericARC4DynamicArrayType.parameterise([element_type], location)
    if tuple_match := _TUPLE_REGEX.match(typ):
        tuple_types = [
            arc4_to_pytype(x, location) for x in split_tuple_types(tuple_match.group("types"))
        ]
        return pytypes.GenericARC4TupleType.parameterise(tuple_types, location)
    raise CodeError(f"unknown ARC4 type '{typ}'", location)

def pytype_to_arc4(typ: pytypes.PyType, loc: SourceLocation | None = None) -> str:
    def on_error(bad_type: pytypes.PyType) -> typing.Never:
        raise CodeError(
            f"not an ARC4 type or native equivalent: {bad_type}",
            loc or getattr(bad_type, "source_location", None),
        )

    arc4_pytype = pytype_to_arc4_pytype(typ, on_error)
    match arc4_pytype:
        case pytypes.NoneType:
            return "void"
        case pytypes.AssetType:
            return "asset"
        case pytypes.AccountType:
            return "account"
        case pytypes.ApplicationType:
            return "application"
        case pytypes.TransactionRelatedType(transaction_type=transaction_type):
            return transaction_type.name if transaction_type else "txn"
    wtype = arc4_pytype.wtype
    if not isinstance(wtype, wtypes.ARC4Type):
        raise CodeError(f"not an ARC4 type or native equivalent: {wtype}", loc)
    return wtype.arc4_name

def split_tuple_types(types: str) -> Iterable[str]:
    """Splits inner tuple types into individual elements.

    e.g. "uint64,(uint8,string),bool" becomes ["uint64", "(uint8,string)", "bool"]
    """
    tuple_level = 0
    last_idx = 0
    for idx, tok in enumerate(types):
        if tok == "(":
            tuple_level += 1
        elif tok == ")":
            tuple_level -= 1
        if tok == "," and tuple_level == 0:
            yield types[last_idx:idx]
            last_idx = idx + 1
    yield types[last_idx:]


import abc
import typing

import mypy.checker
import mypy.nodes
import mypy.visitor

from puya.errors import CodeError, InternalError
from puya.parse import SourceLocation
from puyapy.awst_build.context import ASTConversionModuleContext
from puyapy.awst_build.exceptions import UnsupportedASTError
from puyapy.awst_build.utils import refers_to_fullname

class _BaseMyPyVisitor:
    def __init__(self, context: ASTConversionModuleContext):
        self.context = context

    def _location(self, node: mypy.nodes.Context) -> SourceLocation:
        return self.context.node_location(node)

    def _error(self, msg: str, location: mypy.nodes.Context | SourceLocation) -> None:
        self.context.error(msg, location)

    def _precondition(
        self,
        condition: bool,  # noqa: FBT001
        /,
        msg: str,
        location: mypy.nodes.Context | SourceLocation,
    ) -> None:
        if not condition:
            raise InternalError(
                msg,
                self._location(location) if isinstance(location, mypy.nodes.Context) else location,
            )

    def _unsupported_node(self, node: mypy.nodes.Context, details: str) -> typing.Never:
        raise UnsupportedASTError(node, self._location(node), details=details)

class BaseMyPyStatementVisitor[_TStatement](
    _BaseMyPyVisitor,
    mypy.visitor.StatementVisitor[_TStatement],
    abc.ABC,
):
    # ~~~ things we can just ignore ~~~ #
    @typing.override
    def visit_pass_stmt(self, o: mypy.nodes.PassStmt) -> _TStatement:
        return self.empty_statement(o)

    @typing.override
    def visit_import(self, o: mypy.nodes.Import) -> _TStatement:
        return self.empty_statement(o)

    @typing.override
    def visit_import_from(self, o: mypy.nodes.ImportFrom) -> _TStatement:
        return self.empty_statement(o)

    @typing.override
    def visit_import_all(self, o: mypy.nodes.ImportAll) -> _TStatement:
        return self.empty_statement(o)

    @abc.abstractmethod
    def empty_statement(self, stmt: mypy.nodes.Statement) -> _TStatement: ...

    # ~~~ simplify function (decorated, overloaded, normal) visitation ~~~ #

    def check_fatal_decorators(self, exprs: list[mypy.nodes.Expression]) -> None:
        for dec_expr in exprs:
            if isinstance(dec_expr, mypy.nodes.CallExpr):
                dec_expr = dec_expr.callee
            if isinstance(dec_expr, mypy.nodes.RefExpr):
                if refers_to_fullname(
                    dec_expr, "typing.no_type_check", "typing_extensions.no_type_check"
                ):
                    raise CodeError(
                        "no_type_check is not supported -"
                        " type checking is required for compilation",
                        self._location(dec_expr),
                    )
            else:
                self.context.warning(
                    "Unable to determine full name of expression", self._location(dec_expr)
                )

    @typing.final
    @typing.override
    def visit_func_def(self, fdef: mypy.nodes.FuncDef) -> _TStatement:
        self._precondition(
            not fdef.is_decorated,
            "Decorated functions should have been visited via visit_decorator",
            fdef,
        )
        self._precondition(not fdef.is_property, "Property function that is not decorated??", fdef)
        return self._do_function(fdef, decorator=None)

    @typing.final
    @typing.override
    def visit_decorator(self, dec: mypy.nodes.Decorator) -> _TStatement:
        self.check_fatal_decorators(dec.decorators)
        if mypy.checker.is_property(dec):
            self._unsupported_node(dec, "property decorator/descriptor not supported currently")
        return self._do_function(dec.func, dec)

    @typing.final
    @typing.override
    def visit_overloaded_func_def(self, o: mypy.nodes.OverloadedFuncDef) -> _TStatement:
        # This could either be a @typing.overload, in which case o.impl will contain
        # the actual function, or it could be a @property with a setter and/or a deleter,
        # in which case o.impl will be None
        if mypy.checker.is_property(o):
            self._unsupported_node(o, "property decorator/descriptor not supported currently")
        if o.impl:
            self.context.warning(
                "@typing.overload() should not be required, "
                "and may not function exactly as intended",
                o,
            )
            return o.impl.accept(self)
        else:
            # typing.overload sequences should always have an implementation,
            # unless they're in a stub file - but we don't process those,
            # so we shouldn't get here
            raise CodeError(
                "An overloaded function outside a stub file must have an implementation",
                self._location(o),
            )

    def _do_function(
        self,
        fdef: mypy.nodes.FuncDef,
        decorator: mypy.nodes.Decorator | None,
    ) -> _TStatement:
        self._precondition(
            not fdef.is_mypy_only, "function is defined in TYPE_CHECKING block", fdef
        )  # we shouldn't get here
        if fdef.is_generator:
            self._unsupported_node(fdef, "generator functions are not supported")
        if fdef.is_coroutine or fdef.is_awaitable_coroutine or fdef.is_async_generator:
            self._unsupported_node(fdef, "async functions are not supported")
        if fdef.dataclass_transform_spec is not None:
            self._unsupported_node(fdef, "data class transforms (PEP-681) are not supported ")
        return self.visit_function(fdef, decorator)

    @abc.abstractmethod
    def visit_function(
        self,
        fdef: mypy.nodes.FuncDef,
        decorator: mypy.nodes.Decorator | None,
    ) -> _TStatement: ...

    # ~~~ unsupported scope modifiers ~~~ #
    @typing.override
    def visit_global_decl(self, stmt: mypy.nodes.GlobalDecl) -> typing.Never:
        self._unsupported_node(stmt, "global variables must be immutable")

    # TODO: do we reject nonlocal here too? are nested functions in/out?
    @typing.override
    def visit_nonlocal_decl(self, o: mypy.nodes.NonlocalDecl) -> typing.Never:
        self._unsupported_node(o, "nested functions are not supported")

    # ~~~ raising and handling exceptions unsupported ~~~ #
    @typing.override
    def visit_raise_stmt(self, stmt: mypy.nodes.RaiseStmt) -> typing.Never:
        self._unsupported_node(stmt, "exception raising and exception handling not supported")

    @typing.override
    def visit_try_stmt(self, stmt: mypy.nodes.TryStmt) -> typing.Never:
        self._unsupported_node(stmt, "exception raising and exception handling not supported")

    @typing.override
    def visit_with_stmt(self, stmt: mypy.nodes.WithStmt) -> typing.Never:
        self._unsupported_node(
            stmt,
            "context managers are redundant due to a lack of exception support",
        )

class BaseMyPyExpressionVisitor[_TExpression](
    _BaseMyPyVisitor,
    mypy.visitor.ExpressionVisitor[_TExpression],
    abc.ABC,
):
    # ~~~ things that we support but shouldn't encounter via visitation ~~~ #
    @typing.override
    def visit_star_expr(self, expr: mypy.nodes.StarExpr) -> _TExpression:
        # star expression examples (non-exhaustive):
        #  head, *tail = my_list
        #  my_func(first, *rest)
        #  [prepend, *existing]
        raise InternalError(
            "star expressions should be handled at a higher level", self._location(expr)
        )

    @typing.override
    def visit_dictionary_comprehension(
        self, expr: mypy.nodes.DictionaryComprehension
    ) -> typing.Never:
        self._unsupported_node(expr, "dictionaries are not supported")

    @typing.override
    def visit_set_expr(self, expr: mypy.nodes.SetExpr) -> typing.Never:
        self._unsupported_node(expr, "sets are not supported")

    @typing.override
    def visit_set_comprehension(self, expr: mypy.nodes.SetComprehension) -> typing.Never:
        self._unsupported_node(expr, "sets are not supported")

    # ~~~ math we don't support (yet?) ~~~ #
    @typing.override
    def visit_float_expr(self, expr: mypy.nodes.FloatExpr) -> typing.Never:
        self._unsupported_node(expr, "floating point math is not supported")

    @typing.override
    def visit_complex_expr(self, expr: mypy.nodes.ComplexExpr) -> typing.Never:
        self._unsupported_node(expr, "complex math is not supported")

    # ~~~ generator functions unsupported ~~~ #
    @typing.override
    def visit_generator_expr(self, expr: mypy.nodes.GeneratorExpr) -> typing.Never:
        self._unsupported_node(expr, "generator functions are not supported")

    @typing.override
    def visit_yield_expr(self, expr: mypy.nodes.YieldExpr) -> typing.Never:
        self._unsupported_node(expr, "generator functions are not supported")

    @typing.override
    def visit_yield_from_expr(self, o: mypy.nodes.YieldFromExpr) -> typing.Never:
        self._unsupported_node(o, "generator functions are not supported")

    # ~~~ async/await functions unsupported ~~~ #
    @typing.override
    def visit_await_expr(self, o: mypy.nodes.AwaitExpr) -> typing.Never:
        self._unsupported_node(o, "async/await is not supported")

    # ~~~ analysis-only expressions, should never show up ~~~ #
    @typing.override
    def visit_temp_node(self, expr: mypy.nodes.TempNode) -> _TExpression:
        # "dummy node"
        raise InternalError(
            "Placeholder expression node encountered, should be handled at a higher level",
            self._location(expr),
        )

    def __analysis_only(self, expr: mypy.nodes.Expression) -> typing.Never:
        raise InternalError(
            f"Can't compile analysis-only expression of type {type(expr).__name__}",
            self._location(expr),
        )

    @typing.override
    def visit_cast_expr(self, expr: mypy.nodes.CastExpr) -> _TExpression:
        # NOTE: only appears as CallExpr.analyzed
        return self.__analysis_only(expr)

    @typing.override
    def visit_assert_type_expr(self, expr: mypy.nodes.AssertTypeExpr) -> _TExpression:
        # NOTE: only appears as CallExpr.analyzed
        return self.__analysis_only(expr)

    @typing.override
    def visit_enum_call_expr(self, expr: mypy.nodes.EnumCallExpr) -> _TExpression:
        # NOTE: only appears as CallExpr.analyzed
        return self.__analysis_only(expr)

    @typing.override
    def visit__promote_expr(self, expr: mypy.nodes.PromoteExpr) -> _TExpression:
        # NOTE: only appears as CallExpr.analyzed
        return self.__analysis_only(expr)

    @typing.override
    def visit_namedtuple_expr(self, expr: mypy.nodes.NamedTupleExpr) -> _TExpression:
        # NOTE: only appears as (ClassDef|CallExpr).analyzed
        return self.__analysis_only(expr)

    @typing.override
    def visit_newtype_expr(self, expr: mypy.nodes.NewTypeExpr) -> _TExpression:
        # NOTE: only appears as CallExpr.analyzed
        return self.__analysis_only(expr)

    @typing.override
    def visit_type_alias_expr(self, expr: mypy.nodes.TypeAliasExpr) -> _TExpression:
        # NOTE: only appears as (IndexExpr|CallExpr|OpExpr).analyzed
        return self.__analysis_only(expr)

    @typing.override
    def visit_type_application(self, expr: mypy.nodes.TypeApplication) -> _TExpression:
        # NOTE: only appears as IndexExpr.analyzed
        return self.__analysis_only(expr)

    @typing.override
    def visit_type_var_expr(self, expr: mypy.nodes.TypeVarExpr) -> _TExpression:
        # NOTE: appears as CallExpr.analyzed OR as SymbolTableNode.node
        return self.__analysis_only(expr)

    @typing.override
    def visit_paramspec_expr(self, expr: mypy.nodes.ParamSpecExpr) -> _TExpression:
        # NOTE: only appears as CallExpr.analyzed
        return self.__analysis_only(expr)

    @typing.override
    def visit_type_var_tuple_expr(self, expr: mypy.nodes.TypeVarTupleExpr) -> _TExpression:
        # NOTE: only appears as CallExpr.analyzed
        return self.__analysis_only(expr)

    @typing.override
    def visit_typeddict_expr(self, expr: mypy.nodes.TypedDictExpr) -> _TExpression:
        # NOTE: only appears as (ClassDef|CallExpr).analyzed
        return self.__analysis_only(expr)

    @typing.override
    def visit_reveal_expr(self, expr: mypy.nodes.RevealExpr) -> _TExpression:
        # NOTE: only appears as CallExpr.analyzed
        return self.__analysis_only(expr)


ARC4_CONTRACT_BASE = "algopy.arc4.ARC4Contract"
CONTRACT_BASE = "algopy._contract.Contract"
STRUCT_META = "algopy._struct._StructMeta"
SUBROUTINE_HINT = "algopy._hints.subroutine"
LOGICSIG_DECORATOR = "algopy._logic_sig.logicsig"
LOGICSIG_DECORATOR_ALIAS = "algopy.logicsig"
SUBROUTINE_HINT_ALIAS = "algopy.subroutine"
ABIMETHOD_DECORATOR = "algopy.arc4.abimethod"
ABIMETHOD_DECORATOR_ALIAS = ABIMETHOD_DECORATOR
BAREMETHOD_DECORATOR = "algopy.arc4.baremethod"
BAREMETHOD_DECORATOR_ALIAS = BAREMETHOD_DECORATOR
APPROVAL_METHOD = "approval_program"
CLEAR_STATE_METHOD = "clear_state_program"
ALGOPY_OP_PREFIX = "algopy.op."
URANGE = "algopy._unsigned_builtins.urange"
CLS_ARC4_STRUCT_META = "algopy.arc4._StructMeta"
CLS_ARC4_ABI_CALL = "algopy.arc4.abi_call"


import ast
import contextlib
import functools
from collections.abc import Iterator, Mapping, Sequence
from pathlib import Path

import attrs
import mypy.nodes
import mypy.options
import mypy.types

from puya import log
from puya.context import try_get_source
from puya.errors import CodeError, InternalError, log_exceptions
from puya.parse import SourceLocation
from puya.program_refs import ContractReference
from puya.utils import attrs_extend, unique
from puyapy.awst_build import pytypes
from puyapy.models import ConstantValue, ContractFragmentBase
from puyapy.parse import ParseResult, source_location_from_mypy

logger = log.get_logger(__name__)

@attrs.frozen(kw_only=True)
class ASTConversionContext:
    _parse_result: ParseResult
    constants: dict[str, ConstantValue] = attrs.field(factory=dict)
    _pytypes: dict[str, pytypes.PyType] = attrs.field(factory=pytypes.builtins_registry)
    _contract_fragments: dict[ContractReference, ContractFragmentBase] = attrs.field(factory=dict)

    @property
    def mypy_options(self) -> mypy.options.Options:
        return self._parse_result.mypy_options

    @property
    def contract_fragments(self) -> Mapping[ContractReference, ContractFragmentBase]:
        return self._contract_fragments

    def add_contract_fragment(self, fragment: ContractFragmentBase) -> None:
        assert (
            fragment.id not in self._contract_fragments
        ), "attempted to add contract fragment twice"
        self._contract_fragments[fragment.id] = fragment

    def for_module(self, module_path: Path) -> "ASTConversionModuleContext":
        return attrs_extend(ASTConversionModuleContext, self, module_path=module_path)

    def register_pytype(self, typ: pytypes.PyType, *, alias: str | None = None) -> None:
        name = alias or typ.name
        existing_entry = self._pytypes.get(name)

        if existing_entry is typ:
            logger.debug(f"Duplicate registration of {typ}")
        else:
            if existing_entry is not None:
                logger.error(f"Redefinition of type {name}")
            self._pytypes[name] = typ

    def lookup_pytype(self, name: str) -> pytypes.PyType | None:
        """Lookup type by the canonical fully qualified name"""
        return self._pytypes.get(name)

    def require_ptype(self, name: str, source_location: SourceLocation) -> pytypes.PyType:
        try:
            return self._pytypes[name]
        except KeyError:
            raise CodeError(f"Unknown type {name}", source_location) from None

@attrs.frozen(kw_only=True)
class ASTConversionModuleContext(ASTConversionContext):
    module_path: Path

    def node_location(
        self,
        node: mypy.nodes.Context,
        module_src: mypy.nodes.TypeInfo | None = None,
    ) -> SourceLocation:
        if not module_src:
            module_path = self.module_path
        else:
            module_name = module_src.module_name
            try:
                module_path = self._parse_result.ordered_modules[module_name].path
            except KeyError as ex:
                raise CodeError(f"could not find module '{module_name}'") from ex
        loc = source_location_from_mypy(file=module_path, node=node)
        # if not at start of file, try and expand to preceding comment lines,
        if loc.line > 1:
            prior_code = try_get_source(
                self._parse_result.sources_by_path,
                SourceLocation(file=module_path, line=1, end_line=loc.line - 1),
            )
            comment_lines_count = 0
            for line in reversed(prior_code or []):
                if not line.strip().startswith("#"):
                    break
                comment_lines_count += 1
            if comment_lines_count:
                loc = attrs.evolve(loc, comment_lines=comment_lines_count)
        # if multi-line, strip trailing blank/comment lines
        if loc.end_line != loc.line:
            lines = try_get_source(self._parse_result.sources_by_path, loc)
            if lines is not None:
                chop = 0
                for line in reversed(lines):
                    l_stripped = line.lstrip()
                    if l_stripped and not l_stripped.startswith("#"):
                        break
                    chop += 1
                if chop:
                    loc = attrs.evolve(loc, end_line=loc.end_line - chop, end_column=None)
        return loc

    def _maybe_convert_location(
        self, location: mypy.nodes.Context | SourceLocation
    ) -> SourceLocation:
        if isinstance(location, mypy.nodes.Context):
            return self.node_location(location)
        return location

    def error(self, msg: str, location: mypy.nodes.Context | SourceLocation) -> None:
        logger.error(msg, location=self._maybe_convert_location(location))

    def info(self, msg: str, location: mypy.nodes.Context | SourceLocation) -> None:
        logger.info(msg, location=self._maybe_convert_location(location))

    def warning(self, msg: str, location: mypy.nodes.Context | SourceLocation) -> None:
        logger.warning(msg, location=self._maybe_convert_location(location))

    @contextlib.contextmanager
    def log_exceptions(
        self, fallback_location: mypy.nodes.Context | SourceLocation
    ) -> Iterator[None]:
        with log_exceptions(self._maybe_convert_location(fallback_location)):
            yield

    def type_to_pytype(
        self,
        mypy_type: mypy.types.Type,
        *,
        source_location: mypy.nodes.Context | SourceLocation,
        in_type_args: bool = False,
    ) -> pytypes.PyType:
        return type_to_pytype(
            self._pytypes,
            mypy_type,
            source_location=self._maybe_convert_location(source_location),
            in_type_args=in_type_args,
        )

def type_to_pytype(
    registry: Mapping[str, pytypes.PyType],
    mypy_type: mypy.types.Type,
    *,
    source_location: SourceLocation,
    in_type_args: bool = False,
    in_func_sig: bool = False,
) -> pytypes.PyType:
    loc = source_location
    proper_type_or_alias: mypy.types.ProperType | mypy.types.TypeAliasType
    if isinstance(mypy_type, mypy.types.TypeAliasType):
        proper_type_or_alias = mypy_type
    else:
        proper_type_or_alias = mypy.types.get_proper_type(mypy_type)
    recurse = functools.partial(
        type_to_pytype,
        registry,
        source_location=loc,
        in_type_args=in_type_args,
        in_func_sig=in_func_sig,
    )
    match proper_type_or_alias:
        case mypy.types.TypeAliasType(alias=alias, args=args):
            if alias is None:
                raise InternalError("mypy type alias type missing alias reference", loc)
            result = registry.get(alias.fullname)
            if result is None:
                return recurse(mypy.types.get_proper_type(proper_type_or_alias))
            return _maybe_parameterise_pytype(registry, result, args, loc)
        # this is how variadic tuples are represented in mypy types...
        case mypy.types.Instance(type=mypy.nodes.TypeInfo(fullname="builtins.tuple"), args=args):
            try:
                (arg,) = args
            except ValueError:
                raise InternalError(
                    f"mypy tuple type as instance had unrecognised args: {args}", loc
                ) from None
            if not in_func_sig:
                raise CodeError("variadic tuples are not supported", loc)
            return pytypes.VariadicTupleType(items=recurse(arg))
        case mypy.types.Instance(args=args) as inst:
            fullname = inst.type.fullname
            result = registry.get(fullname)
            if result is None:
                if fullname.startswith("builtins."):
                    msg = f"Unsupported builtin type: {fullname.removeprefix('builtins.')}"
                else:
                    msg = f"Unknown type: {fullname}"
                raise CodeError(msg, loc)
            return _maybe_parameterise_pytype(registry, result, args, loc)
        case mypy.types.TupleType(items=items, partial_fallback=fallback):
            if not fallback.args:
                return recurse(fallback)
            generic = registry.get(fallback.type.fullname)
            if generic is None:
                raise CodeError(f"unknown tuple base type: {fallback.type.fullname}", loc)
            return _maybe_parameterise_pytype(registry, generic, items, loc)
        case mypy.types.LiteralType(fallback=fallback, value=literal_value) as mypy_literal_type:
            if not in_type_args:
                # this is a bit clumsy, but exists because for some reason, bool types
                # can be "narrowed" down to a typing.Literal. e.g. in the case of:
                #   assert a
                #   assert a or b
                # then the type of `a or b` becomes typing.Literal[True]
                return recurse(fallback)
            if mypy_literal_type.is_enum_literal():
                raise CodeError("typing literals of enum are not supported", loc)
            our_literal_value: pytypes.TypingLiteralValue
            if fallback.type.fullname == "builtins.bytes":  # WHY^2
                bytes_literal_value = ast.literal_eval("b" + repr(literal_value))
                assert isinstance(bytes_literal_value, bytes)
                our_literal_value = bytes_literal_value
            elif isinstance(literal_value, float):  # WHY
                raise CodeError("typing literals with float values are not supported", loc)
            else:
                our_literal_value = literal_value
            return pytypes.TypingLiteralType(value=our_literal_value, source_location=loc)
        case mypy.types.UnionType(items=items):
            types = unique(recurse(it) for it in items)
            if not types:
                return pytypes.NeverType
            elif len(types) == 1:
                return types[0]
            else:
                return pytypes.UnionType(types, loc)
        case mypy.types.NoneType() | mypy.types.PartialType(type=None):
            return pytypes.NoneType
        case mypy.types.UninhabitedType():
            return pytypes.NeverType
        case mypy.types.AnyType(type_of_any=type_of_any):
            msg = _type_of_any_to_error_message(type_of_any, loc)
            raise CodeError(msg, loc)
        case mypy.types.TypeType(item=inner_type):
            inner_pytype = recurse(inner_type)
            return pytypes.TypeType(inner_pytype)
        case mypy.types.FunctionLike() as func_like:
            if func_like.is_type_obj():
                # note sure if this will always work for overloads, but the only overloaded
                # constructor we have is arc4.StaticArray, so...
                ret_type = func_like.items[0].ret_type
                cls_typ = recurse(ret_type)
                return pytypes.TypeType(cls_typ)
            else:
                if not isinstance(func_like, mypy.types.CallableType):  # vs Overloaded
                    raise CodeError("references to overloaded functions are not supported", loc)
                ret_pytype = recurse(func_like.ret_type)
                func_args = []
                for at, name, kind in zip(
                    func_like.arg_types, func_like.arg_names, func_like.arg_kinds, strict=True
                ):
                    arg_pytype = type_to_pytype(
                        registry,
                        at,
                        source_location=loc,
                        in_type_args=in_type_args,
                        in_func_sig=True,
                    )
                    func_args.append(pytypes.FuncArg(type=arg_pytype, kind=kind, name=name))
                if func_like.bound_args:
                    logger.error("function type has bound arguments", location=loc)
                if func_like.definition is not None:
                    name = func_like.definition.fullname
                else:
                    name = repr(func_like)
                if func_like.def_extras.get("first_arg"):
                    _self_arg, *func_args = func_args
                return pytypes.FuncType(
                    name=name,
                    args=func_args,
                    ret_type=ret_pytype,
                )
        case _:
            raise CodeError(f"Unable to resolve mypy type {mypy_type!r} to known algopy type", loc)

def _maybe_parameterise_pytype(
    registry: Mapping[str, pytypes.PyType],
    maybe_generic: pytypes.PyType,
    mypy_type_args: Sequence[mypy.types.Type],
    loc: SourceLocation,
) -> pytypes.PyType:
    if not mypy_type_args:
        return maybe_generic
    if all(isinstance(t, mypy.types.TypeVarType | mypy.types.UnpackType) for t in mypy_type_args):
        return maybe_generic
    type_args_resolved = [
        type_to_pytype(registry, mta, source_location=loc, in_type_args=True)
        for mta in mypy_type_args
    ]
    result = maybe_generic.parameterise(type_args_resolved, loc)
    return result

def _type_of_any_to_error_message(type_of_any: int, source_location: SourceLocation) -> str:
    from mypy.types import TypeOfAny

    match type_of_any:
        case TypeOfAny.unannotated:
            msg = "type annotation is required at this location"
        case TypeOfAny.explicit | TypeOfAny.from_another_any:
            msg = "Any type is not supported"
        case TypeOfAny.from_unimported_type:
            msg = "unknown type from import"
        case TypeOfAny.from_omitted_generics:
            msg = "type parameters are required at this location"
        case TypeOfAny.from_error:
            msg = "typing error prevents type resolution"
        case TypeOfAny.special_form:
            msg = "unsupported type form"
        case TypeOfAny.implementation_artifact | TypeOfAny.suggestion_engine:
            msg = "mypy cannot handle this type form, try providing an explicit annotation"
        case _:
            logger.debug(f"Unknown TypeOfAny value: {type_of_any}", location=source_location)
            msg = "Any type is not supported"
    return msg


import abc
import contextlib
import typing
from collections.abc import Callable, Iterator, Sequence, Set

import attrs
import mypy.nodes
import mypy.types
import mypy.visitor

from puya import log
from puya.avm import OnCompletionAction
from puya.awst import (
    nodes as awst_nodes,
    wtypes,
)
from puya.errors import CodeError, InternalError
from puya.parse import SourceLocation
from puya.program_refs import ContractReference
from puya.utils import StableSet, set_add, unique
from puyapy.awst_build import constants, intrinsic_factory, pytypes
from puyapy.awst_build.arc4_decorators import get_arc4_abimethod_data, get_arc4_baremethod_data
from puyapy.awst_build.base_mypy_visitor import BaseMyPyStatementVisitor
from puyapy.awst_build.context import ASTConversionModuleContext
from puyapy.awst_build.subroutine import ContractMethodInfo, FunctionASTConverter
from puyapy.awst_build.utils import get_decorators_by_fullname, get_subroutine_decorator_inline_arg
from puyapy.models import (
    ARC4BareMethodData,
    ARC4MethodData,
    ContractClassOptions,
    ContractFragmentBase,
    ContractFragmentMethod,
    ContractFragmentStorage,
)

logger = log.get_logger(__name__)

_ContractMethodBuilder: typing.TypeAlias = Callable[
    [ASTConversionModuleContext], awst_nodes.ContractMethod
]

_INIT_METHOD = "__init__"
_ARC4_CONTRACT_BASE_CREF = ContractReference(constants.ARC4_CONTRACT_BASE)
_SYNTHETIC_LOCATION = SourceLocation(file=None, line=1)

class ContractASTConverter(BaseMyPyStatementVisitor[None]):
    def __init__(
        self,
        context: ASTConversionModuleContext,
        class_def: mypy.nodes.ClassDef,
        class_options: ContractClassOptions,
        typ: pytypes.ContractType,
    ):
        super().__init__(context=context)
        class_loc = self._location(class_def)
        fragment_mro = _build_resolved_mro(context, typ)
        if class_options.state_totals is None:
            base_with_defined = next(
                (b for b in fragment_mro if b.options and (b.options.state_totals is not None)),
                None,
            )
            if base_with_defined:
                logger.warning(
                    f"Contract extends base contract {base_with_defined.id} "
                    "with explicit state_totals, but does not define its own state_totals. "
                    "This could result in insufficient reserved state at run time.",
                    location=class_loc,
                )

        self.fragment: typing.Final = _ContractFragment(
            id=typ.name,
            source_location=class_loc,
            pytype=typ,
            mro=fragment_mro,
            is_abstract=_check_class_abstractness(context, class_def),
            options=class_options,
            docstring=class_def.docstring,
        )

        # TODO: validation for state proxies being non-conditional
        _build_symbols_and_state(context, self.fragment, class_def.info.names)

        self._deferred_methods = list[tuple[ContractFragmentMethod, _ContractMethodBuilder]]()
        # if the class has an __init__ method, we need to visit it first, so any storage
        # fields cane be resolved to a (static) key
        match class_def.info.names.get(_INIT_METHOD):
            case mypy.nodes.SymbolTableNode(node=mypy.nodes.Statement() as init_node):
                stmts = unique((init_node, *class_def.defs.body))
            case _:
                stmts = class_def.defs.body
        # note: we iterate directly and catch+log code errors here,
        #       since each statement should be somewhat independent given
        #       the constraints we place (e.g. if one function fails to convert,
        #       we can still keep trying to convert other functions to produce more valid errors)
        for stmt in stmts:
            with context.log_exceptions(fallback_location=stmt):
                stmt.accept(self)

        if (
            self.fragment.is_arc4
            and not self.fragment.is_abstract
            and not any(self.fragment.find_arc4_method_metadata(can_create=True))
        ):
            self._insert_default_arc4_create(self.fragment)
        context.add_contract_fragment(self.fragment)

    @staticmethod
    def _insert_default_arc4_create(fragment: "_ContractFragment") -> None:
        if any(fragment.find_arc4_method_metadata(bare=True, oca=OnCompletionAction.NoOp)):
            logger.error(
                "Non-abstract ARC4 contract has no methods that can be called"
                " to create the contract, but does have a NoOp bare method,"
                " so one couldn't be inserted."
                " In order to allow creating the contract add either"
                " an @abimethod or @baremethod"
                ' decorated method with create="require" or create="allow"',
                location=fragment.source_location,
            )
        else:
            default_create_name = "__algopy_default_create"
            while fragment.resolve_symbol(default_create_name):  # ensure uniqueness
                default_create_name = f"_{default_create_name}"
            default_create_config = awst_nodes.ARC4BareMethodConfig(
                create=awst_nodes.ARC4CreateOption.require,
                source_location=_SYNTHETIC_LOCATION,
            )
            fragment.add_method(
                ContractFragmentMethod(
                    member_name=default_create_name,
                    source_location=_SYNTHETIC_LOCATION,
                    metadata=ARC4BareMethodData(
                        member_name=default_create_name,
                        pytype=(
                            pytypes.FuncType(
                                name=".".join((fragment.id, default_create_name)),
                                args=(),
                                ret_type=pytypes.NoneType,
                            )
                        ),
                        config=default_create_config,
                        source_location=_SYNTHETIC_LOCATION,
                    ),
                    is_trivial=False,
                    synthetic=True,
                    inheritable=False,
                    implementation=awst_nodes.ContractMethod(
                        cref=fragment.id,
                        member_name=default_create_name,
                        args=[],
                        return_type=wtypes.void_wtype,
                        body=awst_nodes.Block(
                            body=[],
                            source_location=_SYNTHETIC_LOCATION,
                        ),
                        documentation=awst_nodes.MethodDocumentation(),
                        arc4_method_config=default_create_config,
                        source_location=_SYNTHETIC_LOCATION,
                        inline=True,
                    ),
                )
            )

    def build(self, context: ASTConversionModuleContext) -> awst_nodes.Contract | None:
        for method_fragment, method_builder in self._deferred_methods:
            with context.log_exceptions(fallback_location=method_fragment.source_location):
                method_fragment.implementation = method_builder(context)

        if self.fragment.is_abstract:
            return None

        approval_program = None
        approval_method = self.fragment.resolve_method(constants.APPROVAL_METHOD)
        if approval_method is None or approval_method.is_trivial:
            logger.error(
                "non-abstract contract class missing approval program",
                location=self.fragment.source_location,
            )
        elif approval_method.implementation is None:
            pass  # error during method construction, already logged
        else:
            approval_program = approval_method.implementation
            if self.fragment.resolve_method(_INIT_METHOD) is not None:
                approval_program = _insert_init_call_on_create(
                    self.fragment.id, approval_program.return_type
                )

        clear_method = self.fragment.resolve_method(constants.CLEAR_STATE_METHOD)
        if clear_method is None or clear_method.is_trivial:
            logger.error(
                "non-abstract contract class missing clear-state program",
                location=self.fragment.source_location,
            )
            clear_program = None
        else:
            clear_program = clear_method.implementation

        if approval_program is None or clear_program is None:
            return None

        return awst_nodes.Contract(
            id=self.fragment.id,
            name=self.fragment.options.name_override or self.fragment.pytype.class_name,
            method_resolution_order=[ancestor.id for ancestor in self.fragment.mro],
            approval_program=approval_program,
            clear_program=clear_program,
            methods=tuple(
                cm.implementation
                for cm in self.fragment.methods(include_overridden=True)
                if cm.implementation is not None
            ),
            app_state=tuple(
                state_decl.definition
                for state_decl in self.fragment.state()
                if state_decl.definition is not None
            ),
            description=self.fragment.docstring,
            source_location=self.fragment.source_location,
            reserved_scratch_space=self.fragment.reserved_scratch_space,
            state_totals=self.fragment.options.state_totals,
            avm_version=self.fragment.options.avm_version,
        )

    def empty_statement(self, _stmt: mypy.nodes.Statement) -> None:
        return None

    def visit_function(
        self, func_def: mypy.nodes.FuncDef, decorator: mypy.nodes.Decorator | None
    ) -> None:
        func_loc = self._location(func_def)
        method_name = func_def.name

        if func_def.is_class:
            raise CodeError("@classmethod not supported", func_loc)
        if func_def.is_static:
            raise CodeError(
                "@staticmethod not supported, use a module level function instead", func_loc
            )
        if func_def.type is None:
            raise CodeError("function is untyped", func_loc)
        if len(func_def.arguments) < 1:
            # since we checked we're only handling instance methods, should be at least one
            # argument to function - ie self
            logger.error(f"{method_name} should take a self parameter", location=func_loc)

        dec_by_fullname = get_decorators_by_fullname(self.context, decorator) if decorator else {}
        subroutine_dec = dec_by_fullname.pop(constants.SUBROUTINE_HINT, None)
        abimethod_dec = dec_by_fullname.pop(constants.ABIMETHOD_DECORATOR, None)
        baremethod_dec = dec_by_fullname.pop(constants.BAREMETHOD_DECORATOR, None)

        for unknown_dec_fullname, dec in dec_by_fullname.items():
            self._error(f'unsupported decorator "{unknown_dec_fullname}"', dec)

        # TODO: handle difference of subroutine vs abimethod and overrides???

        inline = None
        if subroutine_dec is not None:
            inline = get_subroutine_decorator_inline_arg(self.context, subroutine_dec)

        arc4_method_data: ARC4MethodData | None = None
        if method_name in (_INIT_METHOD, constants.APPROVAL_METHOD, constants.CLEAR_STATE_METHOD):
            for invalid_dec in (subroutine_dec, abimethod_dec, baremethod_dec):
                if invalid_dec is not None:
                    self._error("method should not be decorated", location=invalid_dec)
        elif method_name.startswith("__") and method_name.endswith("__"):
            raise CodeError(
                "methods starting and ending with a double underscore"
                ' (aka "dunder" methods) are reserved for the Python data model'
                " (https://docs.python.org/3/reference/datamodel.html)."
                " Of these methods, only __init__ is supported in contract classes",
                func_loc,
            )
        elif not self.fragment.is_arc4:
            if subroutine_dec is None:
                logger.error(
                    f"missing @{constants.SUBROUTINE_HINT_ALIAS} decorator", location=func_loc
                )
            for invalid_dec in (abimethod_dec, baremethod_dec):
                if invalid_dec is not None:
                    self._error(
                        f"decorator is only valid in subclasses of {pytypes.ARC4ContractBaseType}",
                        invalid_dec,
                    )
        else:
            if len(list(filter(None, (subroutine_dec, abimethod_dec, baremethod_dec)))) != 1:
                logger.error(
                    f"ARC-4 contract member functions"
                    f" (other than __init__ or approval / clear program methods)"
                    f" must be annotated with exactly one of"
                    f" @{constants.SUBROUTINE_HINT_ALIAS},"
                    f" @{constants.ABIMETHOD_DECORATOR_ALIAS},"
                    f" or @{constants.BAREMETHOD_DECORATOR_ALIAS}",
                    location=func_loc,
                )

            if abimethod_dec:
                arc4_method_data = get_arc4_abimethod_data(self.context, abimethod_dec, func_def)
            elif baremethod_dec:
                arc4_method_data = get_arc4_baremethod_data(self.context, baremethod_dec, func_def)
            else:
                arc4_method_data = None
            # TODO: validate against super-class configs??

        source_location = self._location(decorator or func_def)
        obj = ContractFragmentMethod(
            member_name=method_name,
            source_location=source_location,
            metadata=arc4_method_data,
            is_trivial=func_def.is_trivial_body,
            synthetic=False,
            inheritable=True,
            implementation=None,
        )
        self.fragment.add_method(obj)
        if obj.is_trivial:
            logger.debug(f"skipping trivial method {method_name}", location=func_loc)
        else:
            self._deferred_methods.append(
                (
                    obj,
                    lambda ctx: FunctionASTConverter.convert(
                        ctx,
                        func_def=func_def,
                        source_location=source_location,
                        inline=inline,
                        contract_method_info=ContractMethodInfo(
                            fragment=self.fragment,
                            contract_type=self.fragment.pytype,
                            arc4_method_config=(
                                arc4_method_data.config if arc4_method_data else None
                            ),
                            is_abstract=self.fragment.is_abstract,
                        ),
                    ),
                )
            )

    def visit_block(self, o: mypy.nodes.Block) -> None:
        raise InternalError("shouldn't get here", self._location(o))

    def visit_return_stmt(self, stmt: mypy.nodes.ReturnStmt) -> None:
        self._error("illegal Python syntax, return in class body", location=stmt)

    def visit_class_def(self, cdef: mypy.nodes.ClassDef) -> None:
        self._error("nested classes are not supported", location=cdef)

    def _unsupported_stmt(self, kind: str, stmt: mypy.nodes.Statement) -> None:
        self._error(f"{kind} statements are not supported in the class body", location=stmt)

    def visit_assignment_stmt(self, stmt: mypy.nodes.AssignmentStmt) -> None:
        # just pass on state forward-declarations, these will be picked up by gather state
        # everything else (ie any _actual_ assignments) is unsupported
        if not isinstance(stmt.rvalue, mypy.nodes.TempNode):
            self._unsupported_stmt("assignment", stmt)

    def visit_operator_assignment_stmt(self, stmt: mypy.nodes.OperatorAssignmentStmt) -> None:
        self._unsupported_stmt("operator assignment", stmt)

    def visit_expression_stmt(self, stmt: mypy.nodes.ExpressionStmt) -> None:
        if isinstance(stmt.expr, mypy.nodes.StrExpr):
            # ignore class docstring, already extracted
            # TODO: should we capture field "docstrings"?
            pass
        else:
            self._unsupported_stmt("expression statement", stmt)

    def visit_if_stmt(self, stmt: mypy.nodes.IfStmt) -> None:
        self._unsupported_stmt("if", stmt)

    def visit_while_stmt(self, stmt: mypy.nodes.WhileStmt) -> None:
        self._unsupported_stmt("while", stmt)

    def visit_for_stmt(self, stmt: mypy.nodes.ForStmt) -> None:
        self._unsupported_stmt("for", stmt)

    def visit_break_stmt(self, stmt: mypy.nodes.BreakStmt) -> None:
        self._unsupported_stmt("break", stmt)

    def visit_continue_stmt(self, stmt: mypy.nodes.ContinueStmt) -> None:
        self._unsupported_stmt("continue", stmt)

    def visit_assert_stmt(self, stmt: mypy.nodes.AssertStmt) -> None:
        self._unsupported_stmt("assert", stmt)

    def visit_del_stmt(self, stmt: mypy.nodes.DelStmt) -> None:
        self._unsupported_stmt("del", stmt)

    def visit_match_stmt(self, stmt: mypy.nodes.MatchStmt) -> None:
        self._unsupported_stmt("match", stmt)

    def visit_type_alias_stmt(self, stmt: mypy.nodes.TypeAliasStmt) -> None:
        self._unsupported_stmt("type", stmt)

class _UserContractBase(ContractFragmentBase, abc.ABC):
    @property
    @abc.abstractmethod
    def options(self) -> ContractClassOptions | None: ...

@attrs.frozen
class _StaticContractBase(_UserContractBase):
    id: ContractReference
    methods_: dict[str, ContractFragmentMethod]
    mro: Sequence[ContractFragmentBase]
    symbols: dict[str, pytypes.PyType]
    options: None = None

    @typing.override
    def resolve_method(
        self, name: str, *, include_inherited: bool = True
    ) -> ContractFragmentMethod | None:
        return self.methods_.get(name)

    @typing.override
    def methods(
        self, *, include_inherited: bool = True, include_overridden: bool = False
    ) -> Iterator[ContractFragmentMethod]:
        yield from self.methods_.values()

    @typing.override
    def resolve_storage(
        self, name: str, *, include_inherited: bool = True
    ) -> ContractFragmentStorage | None:
        return None

    @typing.override
    def state(self, *, include_inherited: bool = True) -> Iterator[ContractFragmentStorage]:
        yield from ()

@attrs.frozen(kw_only=True)
class _ContractFragment(_UserContractBase):
    id: ContractReference
    source_location: SourceLocation
    pytype: pytypes.ContractType
    mro: Sequence[_UserContractBase]
    is_abstract: bool
    options: ContractClassOptions
    docstring: str | None
    _methods: dict[str, ContractFragmentMethod] = attrs.field(factory=dict, init=False)
    _state_defs: dict[str, ContractFragmentStorage] = attrs.field(factory=dict, init=False)
    symbols: dict[str, pytypes.PyType | None] = attrs.field(factory=dict, init=False)

    @property
    def is_arc4(self) -> bool:
        return pytypes.ARC4ContractBaseType in self.pytype.mro

    def add_method(self, method: ContractFragmentMethod) -> None:
        set_result = self._methods.setdefault(method.member_name, method)
        if set_result is not method:
            logger.info(
                f"previous definition of {method.member_name} was here",
                location=set_result.source_location,
            )
            logger.error(
                f"redefinition of {method.member_name}",
                location=method.source_location,
            )

    @typing.override
    def resolve_method(
        self, name: str, *, include_inherited: bool = True
    ) -> ContractFragmentMethod | None:
        with contextlib.suppress(KeyError):
            return self._methods[name]
        if include_inherited:
            for fragment in self.mro:
                method = fragment.resolve_method(name, include_inherited=False)
                if method and method.inheritable:
                    return method
        return None

    @typing.override
    def methods(
        self, *, include_inherited: bool = True, include_overridden: bool = False
    ) -> Iterator[ContractFragmentMethod]:
        yield from self._methods.values()
        if include_inherited:
            seen_names = set(self._methods.keys())
            for fragment in self.mro:
                for method in fragment.methods(include_inherited=False):
                    if method.inheritable and (
                        include_overridden or set_add(seen_names, method.member_name)
                    ):
                        yield method

    @typing.override
    def resolve_storage(
        self, name: str, *, include_inherited: bool = True
    ) -> ContractFragmentStorage | None:
        with contextlib.suppress(KeyError):
            return self._state_defs[name]
        if include_inherited:
            for fragment in self.mro:
                result = fragment.resolve_storage(name, include_inherited=False)
                if result is not None:
                    return result
        return None

    def add_state(self, decl: ContractFragmentStorage) -> None:
        existing = self.resolve_storage(decl.member_name)
        self._state_defs.setdefault(decl.member_name, decl)
        if existing is not None:
            logger.info(
                f"previous definition of {decl.member_name} was here",
                location=existing.source_location,
            )
            logger.error(
                f"redefinition of {decl.member_name}",
                location=decl.source_location,
            )

    @typing.override
    def state(self, *, include_inherited: bool = True) -> Iterator[ContractFragmentStorage]:
        result = self._state_defs
        if include_inherited:
            for ancestor in self.mro:
                result = {
                    s.member_name: s for s in ancestor.state(include_inherited=False)
                } | result
        yield from result.values()

    @property
    def reserved_scratch_space(self) -> Set[int]:
        return StableSet[int].from_iter(
            num
            for c in (self, *self.mro)
            if c.options and c.options.scratch_slot_reservations
            for num in c.options.scratch_slot_reservations
        )

def _insert_init_call_on_create(
    current_contract: ContractReference, return_type: wtypes.WType
) -> awst_nodes.ContractMethod:
    call_init = awst_nodes.Block(
        comment="call __init__",
        body=[
            awst_nodes.ExpressionStatement(
                expr=awst_nodes.SubroutineCallExpression(
                    target=awst_nodes.InstanceMethodTarget(member_name=_INIT_METHOD),
                    args=[],
                    wtype=wtypes.void_wtype,
                    source_location=_SYNTHETIC_LOCATION,
                )
            )
        ],
        source_location=_SYNTHETIC_LOCATION,
    )
    call_init_on_create = awst_nodes.IfElse(
        condition=awst_nodes.Not(
            expr=intrinsic_factory.txn("ApplicationID", wtypes.bool_wtype, _SYNTHETIC_LOCATION),
            source_location=_SYNTHETIC_LOCATION,
        ),
        if_branch=call_init,
        else_branch=None,
        source_location=_SYNTHETIC_LOCATION,
    )
    return awst_nodes.ContractMethod(
        cref=current_contract,
        member_name="__algopy_entrypoint_with_init",
        args=[],
        arc4_method_config=None,
        return_type=return_type,
        documentation=awst_nodes.MethodDocumentation(),
        body=awst_nodes.Block(
            body=[
                call_init_on_create,
                awst_nodes.ReturnStatement(
                    value=awst_nodes.SubroutineCallExpression(
                        target=awst_nodes.InstanceMethodTarget(
                            member_name=constants.APPROVAL_METHOD,
                        ),
                        args=[],
                        wtype=return_type,
                        source_location=_SYNTHETIC_LOCATION,
                    ),
                    source_location=_SYNTHETIC_LOCATION,
                ),
            ],
            source_location=_SYNTHETIC_LOCATION,
        ),
        source_location=_SYNTHETIC_LOCATION,
    )

def _build_resolved_mro(
    context: ASTConversionModuleContext, contract_type: pytypes.ContractType
) -> list[_UserContractBase]:
    class_def_loc = contract_type.source_location
    contract_bases_mro = list[_UserContractBase]()
    for ancestor in contract_type.mro:
        if ancestor == pytypes.ContractBaseType:
            pass
        elif ancestor == pytypes.ARC4ContractBaseType:
            contract_bases_mro.append(_arc4_contract_fragment())
        elif isinstance(ancestor, pytypes.ContractType):
            ancestor_fragment = context.contract_fragments.get(ancestor.name)
            if isinstance(ancestor_fragment, _ContractFragment):
                contract_bases_mro.append(ancestor_fragment)
            else:
                raise CodeError(
                    f"contract type has non-contract base {ancestor.name}", class_def_loc
                )
        else:
            raise CodeError(f"base class {ancestor} is not a contract subclass", class_def_loc)
    return contract_bases_mro

def _arc4_contract_fragment() -> _UserContractBase:
    result = _StaticContractBase(id=_ARC4_CONTRACT_BASE_CREF, mro=(), methods_={}, symbols={})

    def add_program_method(
        name: str,
        body: Sequence[awst_nodes.Statement],
        *,
        return_type: pytypes.RuntimeType = pytypes.BoolType,
    ) -> None:
        result.symbols[name] = pytypes.FuncType(
            name=".".join((_ARC4_CONTRACT_BASE_CREF, name)),
            args=(),
            ret_type=return_type,
        )
        implementation = awst_nodes.ContractMethod(
            cref=_ARC4_CONTRACT_BASE_CREF,
            member_name=name,
            source_location=_SYNTHETIC_LOCATION,
            args=[],
            arc4_method_config=None,
            return_type=return_type.wtype,
            documentation=awst_nodes.MethodDocumentation(),
            body=awst_nodes.Block(body=body, source_location=_SYNTHETIC_LOCATION),
            inline=None,
        )
        result.methods_[name] = ContractFragmentMethod(
            member_name=name,
            source_location=_SYNTHETIC_LOCATION,
            metadata=None,
            is_trivial=False,
            synthetic=True,
            inheritable=True,
            implementation=implementation,
        )

    add_program_method(
        name=constants.APPROVAL_METHOD,
        body=[
            awst_nodes.ReturnStatement(
                value=awst_nodes.ARC4Router(source_location=_SYNTHETIC_LOCATION),
                source_location=_SYNTHETIC_LOCATION,
            )
        ],
    )
    add_program_method(
        name=constants.CLEAR_STATE_METHOD,
        body=[
            awst_nodes.ReturnStatement(
                value=awst_nodes.BoolConstant(value=True, source_location=_SYNTHETIC_LOCATION),
                source_location=_SYNTHETIC_LOCATION,
            )
        ],
    )

    return result

def _build_symbols_and_state(
    context: ASTConversionModuleContext,
    fragment: _ContractFragment,
    symtable: mypy.nodes.SymbolTable,
) -> None:
    cref = fragment.id
    for name, sym in symtable.items():
        node = sym.node
        assert node, f"mypy cross reference remains unresolved: member {name!r} of {cref!r}"
        node_loc = context.node_location(node)
        if isinstance(node, mypy.nodes.OverloadedFuncDef):
            node = node.impl
        if isinstance(node, mypy.nodes.Decorator):
            # we don't support any decorators that would change signature
            node = node.func
        pytyp = None
        if isinstance(node, mypy.nodes.Var | mypy.nodes.FuncDef) and node.type:
            with contextlib.suppress(CodeError):
                pytyp = context.type_to_pytype(node.type, source_location=node_loc)

        fragment.symbols[name] = pytyp
        if pytyp and not isinstance(pytyp, pytypes.FuncType):
            definition = None
            if isinstance(pytyp, pytypes.StorageProxyType):
                wtypes.validate_persistable(pytyp.content_wtype, node_loc)
                match pytyp.generic:
                    case pytypes.GenericLocalStateType:
                        kind = awst_nodes.AppStorageKind.account_local
                    case pytypes.GenericGlobalStateType:
                        kind = awst_nodes.AppStorageKind.app_global
                    case pytypes.GenericBoxType:
                        kind = awst_nodes.AppStorageKind.box
                    case None if pytyp == pytypes.BoxRefType:
                        kind = awst_nodes.AppStorageKind.box
                    case _:
                        raise InternalError(f"unhandled StorageProxyType: {pytyp}", node_loc)
            elif isinstance(pytyp, pytypes.StorageMapProxyType):
                wtypes.validate_persistable(pytyp.key_wtype, node_loc)
                wtypes.validate_persistable(pytyp.content_wtype, node_loc)
                if pytyp.generic != pytypes.GenericBoxMapType:
                    raise InternalError(f"unhandled StorageMapProxyType: {pytyp}", node_loc)
                kind = awst_nodes.AppStorageKind.box
            else:  # global state, direct
                wtype = pytyp.checked_wtype(node_loc)
                wtypes.validate_persistable(wtype, node_loc)
                key = awst_nodes.BytesConstant(
                    value=name.encode("utf8"),
                    encoding=awst_nodes.BytesEncoding.utf8,
                    source_location=node_loc,
                    wtype=wtypes.state_key,
                )
                kind = awst_nodes.AppStorageKind.app_global
                definition = awst_nodes.AppStorageDefinition(
                    source_location=node_loc,
                    member_name=name,
                    kind=kind,
                    storage_wtype=wtype,
                    key_wtype=None,
                    key=key,
                    description=None,
                )
            fragment.add_state(
                ContractFragmentStorage(
                    member_name=name,
                    kind=kind,
                    definition=definition,
                    source_location=node_loc,
                )
            )

def _check_class_abstractness(
    context: ASTConversionModuleContext, class_def: mypy.nodes.ClassDef
) -> bool:
    is_abstract = class_def.info.is_abstract
    # note: we don't support the metaclass= option, so we only need to check for
    # inheritance of abc.ABC and not  metaclass=abc.ABCMeta
    if is_abstract and not any(
        base.fullname == "abc.ABC" for base in class_def.info.direct_base_classes()
    ):
        context.warning(f"Class {class_def.fullname} is implicitly abstract", class_def)
    return is_abstract


import abc
import typing

import typing_extensions

from puya import log
from puya.awst.nodes import (
    BinaryBooleanOperator,
    CompileTimeConstantExpression,
    Expression,
    Lvalue,
    SingleEvaluation,
    Statement,
    TupleExpression,
    VarExpression,
)
from puya.errors import CodeError, InternalError
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import (
    BuilderBinaryOp,
    BuilderComparisonOp,
    BuilderUnaryOp,
    CallableBuilder,
    InstanceBuilder,
    NodeBuilder,
    TypeBuilder,
)

__all__ = [
    "FunctionBuilder",
    "GenericTypeBuilder",
    "InstanceExpressionBuilder",
    "NotIterableInstanceExpressionBuilder",
]

_TPyType_co = typing_extensions.TypeVar(
    "_TPyType_co", bound=pytypes.PyType, default=pytypes.PyType, covariant=True
)
_TExpression_co = typing_extensions.TypeVar(
    "_TExpression_co", bound=Expression, default=Expression, covariant=True
)

logger = log.get_logger(__name__)

class FunctionBuilder(CallableBuilder, abc.ABC):
    @property
    @typing.final
    def pytype(self) -> None:  # TODO: give function type
        return None

    @typing.override
    @typing.final
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        from puyapy.awst_build.eb._utils import constant_bool_and_error

        return constant_bool_and_error(value=True, location=location, negate=negate)

    @typing.override
    @typing.final
    def member_access(self, name: str, location: SourceLocation) -> typing.Never:
        raise CodeError("function attribute access is not supported", location)

class GenericTypeBuilder(CallableBuilder, abc.ABC):  # TODO: can we fold this with TypeBuilder?
    @typing.override
    @property
    def pytype(self) -> None:  # TODO, take this as an init argument
        return None

    @typing.override
    @typing.final
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        raise CodeError("generic type requires parameters", location)

    @typing.override
    @typing.final
    def bool_eval(self, location: SourceLocation, *, negate: bool = False) -> InstanceBuilder:
        from puyapy.awst_build.eb._utils import constant_bool_and_error

        return constant_bool_and_error(value=True, location=location, negate=negate)

class InstanceExpressionBuilder(
    InstanceBuilder[_TPyType_co], typing.Generic[_TPyType_co, _TExpression_co], abc.ABC
):
    def __init__(self, pytype: _TPyType_co, expr: _TExpression_co):
        super().__init__(expr.source_location)
        if expr.wtype != pytype.wtype:
            raise InternalError(
                f"invalid expression wtype {str(expr.wtype)!r} for Python type {str(pytype)!r}",
                expr.source_location,
            )
        self._pytype = pytype
        self.__expr = expr

    @typing.override
    @typing.final
    @property
    def pytype(self) -> _TPyType_co:
        return self._pytype

    @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:
        return self

    @typing.override
    @typing.final
    def resolve_lvalue(self) -> Lvalue:
        resolved = self.resolve()
        return _validate_lvalue(self._pytype, resolved)

    @typing.override
    def resolve(self) -> _TExpression_co:
        return self.__expr

    @typing.override
    def delete(self, location: SourceLocation) -> Statement:
        from puyapy.awst_build.eb._utils import dummy_statement

        logger.error(f"{self.pytype} is not valid as del target", location=location)
        return dummy_statement(location)

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        raise CodeError(f"unrecognised member of {self.pytype}: {name}", location)

    @typing.override
    def unary_op(self, op: BuilderUnaryOp, location: SourceLocation) -> InstanceBuilder:
        raise CodeError(f"{self.pytype} does not support unary {op.value!r} operator", location)

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

    @typing.override
    def binary_op(
        self,
        other: InstanceBuilder,
        op: BuilderBinaryOp,
        location: SourceLocation,
        *,
        reverse: bool,
    ) -> InstanceBuilder:
        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:
        from puyapy.awst_build.eb._utils import dummy_statement

        logger.error(f"{self.pytype} does not support augmented assignment", location=location)
        return dummy_statement(location)

    @typing.override
    def single_eval(self) -> InstanceBuilder:
        if not isinstance(
            self.__expr, VarExpression | CompileTimeConstantExpression | SingleEvaluation
        ):
            return builder_for_instance(self.pytype, SingleEvaluation(self.__expr))
        return self

class NotIterableInstanceExpressionBuilder(InstanceExpressionBuilder[_TPyType_co], abc.ABC):
    @typing.final
    @typing.override
    def contains(self, item: InstanceBuilder, location: SourceLocation) -> InstanceBuilder:
        return super().contains(item, location)

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

    @typing.final
    @typing.override
    def iterable_item_type(self) -> pytypes.PyType:
        return super().iterable_item_type()

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

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

def _validate_lvalue(typ: pytypes.PyType, resolved: Expression) -> Lvalue:
    if typ == pytypes.NoneType:
        raise CodeError(
            "None indicates an empty return and cannot be assigned",
            resolved.source_location,
        )
    if not isinstance(resolved, Lvalue):
        raise CodeError(
            "expression is not valid as an assignment target", resolved.source_location
        )
    if isinstance(resolved, TupleExpression):
        assert isinstance(typ, pytypes.TupleLikeType)
        for item_typ, item in zip(typ.items, resolved.items, strict=True):
            _validate_lvalue(item_typ, item)
    return resolved


import abc
import typing
from collections.abc import Sequence

import mypy.nodes
import typing_extensions

from puya.awst.nodes import Expression, ReinterpretCast
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, InstanceExpressionBuilder
from puyapy.awst_build.eb._utils import cast_to_bytes
from puyapy.awst_build.eb.bytes import BytesExpressionBuilder
from puyapy.awst_build.eb.factories import builder_for_instance
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder, TypeBuilder

_TPyType_co = typing_extensions.TypeVar(
    "_TPyType_co", bound=pytypes.PyType, default=pytypes.PyType, covariant=True
)

class BytesBackedTypeBuilder(TypeBuilder[_TPyType_co], abc.ABC):
    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        typ = self.produces()
        match name:
            case "from_bytes":
                return _FromBytes(typ, location)
            case _:
                return super().member_access(name, location)

class _FromBytes(FunctionBuilder):
    def __init__(self, result_type: pytypes.PyType, location: SourceLocation):
        super().__init__(location)
        self.result_type = result_type

    @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.BytesType, location, resolve_literal=True
        )
        result_expr = ReinterpretCast(
            expr=arg.resolve(),
            wtype=self.result_type.checked_wtype(location),
            source_location=location,
        )
        return builder_for_instance(self.result_type, result_expr)

class BytesBackedInstanceExpressionBuilder(InstanceExpressionBuilder[_TPyType_co], abc.ABC):
    _bytes_member: typing.ClassVar[str]

    def __init_subclass__(cls, *, bytes_member: str = "bytes", **kwargs: object):
        super().__init_subclass__(**kwargs)
        cls._bytes_member = bytes_member

    @typing.override
    def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
        if name == self._bytes_member:
            return self.bytes(location)
        else:
            return super().member_access(name, location)

    def bytes(self, location: SourceLocation) -> BytesExpressionBuilder:
        return BytesExpressionBuilder(self.to_bytes(location))

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


import typing
from collections.abc import Callable, Sequence
from itertools import zip_longest

from puya import log
from puya.parse import SourceLocation
from puyapy.awst_build import pytypes
from puyapy.awst_build.eb._utils import dummy_value
from puyapy.awst_build.eb.interface import InstanceBuilder, NodeBuilder
from puyapy.awst_build.utils import maybe_resolve_literal

_T = typing.TypeVar("_T")
_TBuilder = typing.TypeVar("_TBuilder", bound=NodeBuilder)

logger = log.get_logger(__name__)

def at_most_one_arg(
    args: Sequence[NodeBuilder], location: SourceLocation
) -> InstanceBuilder | None:
    if not args:
        return None
    first, *rest = args
    if rest:
        logger.error(f"expected at most 1 argument, got {len(args)}", location=location)
    return instance_builder(first, default=default_none)

def at_most_one_arg_of_type(
    args: Sequence[NodeBuilder], valid_types: Sequence[pytypes.PyType], location: SourceLocation
) -> InstanceBuilder | None:
    if not args:
        return None
    first, *rest = args
    if rest:
        logger.error(f"expected at most 1 argument, got {len(args)}", location=location)
    if isinstance(first, InstanceBuilder) and first.pytype.is_type_or_subtype(*valid_types):
        return first
    return not_this_type(first, default=default_none)

def default_raise(msg: str, location: SourceLocation) -> typing.Never:
    from puya.errors import CodeError

    raise CodeError(msg, location)

def default_fixed_value(value: _T) -> Callable[[str, SourceLocation], _T]:
    def defaulter(msg: str, location: SourceLocation) -> _T:  # noqa: ARG001
        return value

    return defaulter

default_none: typing.Final = default_fixed_value(None)

def default_dummy_value(
    pytype: pytypes.PyType,
) -> Callable[[str, SourceLocation], InstanceBuilder]:
    def defaulter(msg: str, location: SourceLocation) -> InstanceBuilder:  # noqa: ARG001
        return dummy_value(pytype, location)

    return defaulter

def not_this_type(node: NodeBuilder, default: Callable[[str, SourceLocation], _T]) -> _T:
    """Provide consistent error messages for unexpected types."""
    if isinstance(node.pytype, pytypes.UnionType):
        msg = "type unions are unsupported at this location"
    else:
        msg = "unexpected argument type"
    result = default(msg, node.source_location)
    logger.error(msg, location=node.source_location)
    return result

def at_least_one_arg(
    args: Sequence[_TBuilder],
    location: SourceLocation,
    *,
    default: Callable[[str, SourceLocation], _T],
) -> tuple[InstanceBuilder | _T, Sequence[_TBuilder]]:
    if not args:
        msg = "expected at least 1 argument, got 0"
        result = default(msg, location)
        logger.error(msg, location=location)
        return result, []
    first, *rest = args
    return instance_builder(first, default=default), rest

def exactly_one_arg(
    args: Sequence[NodeBuilder],
    location: SourceLocation,
    *,
    default: Callable[[str, SourceLocation], _T],
) -> InstanceBuilder | _T:
    if not args:
        msg = "expected 1 argument, got 0"
        result = default(msg, location)
        logger.error(msg, location=location)
        return result
    first, *rest = args
    if rest:
        logger.error(f"expected 1 argument, got {len(args)}", location=location)
    return instance_builder(first, default=default)

def exactly_one_arg_of_type(
    args: Sequence[NodeBuilder],
    expected: pytypes.PyType,
    location: SourceLocation,
    *,
    default: Callable[[str, SourceLocation], _T],
    resolve_literal: bool = False,
) -> InstanceBuilder | _T:
    if not args:
        msg = "expected 1 argument, got 0"
        result = default(msg, location)
        logger.error(msg, location=location)
        return result
    first, *rest = args
    if rest:
        logger.error(f"expected 1 argument, got {len(args)}", location=location)
    if resolve_literal:
        first = maybe_resolve_literal(first, expected)
    if isinstance(first, InstanceBuilder) and expected <= first.pytype:
        return first
    return not_this_type(first, default=default)

def exactly_one_arg_of_type_else_dummy(
    args: Sequence[NodeBuilder],
    pytype: pytypes.PyType,
    location: SourceLocation,
    *,
    resolve_literal: bool = False,
) -> InstanceBuilder:
    return exactly_one_arg_of_type(
        args,
        pytype,
        location,
        default=default_dummy_value(pytype),
        resolve_literal=resolve_literal,
    )

def no_args(args: Sequence[NodeBuilder], location: SourceLocation) -> None:
    if args:
        logger.error(f"expected 0 arguments, got {len(args)}", location=location)

def exactly_n_args_of_type_else_dummy(
    args: Sequence[NodeBuilder], pytype: pytypes.PyType, location: SourceLocation, num_args: int
) -> Sequence[InstanceBuilder]:
    if not exactly_n_args(args, location, num_args):
        dummy_args = [dummy_value(pytype, location)] * num_args
        args = [arg or default for arg, default in zip_longest(args, dummy_args)]
    arg_ebs = [argument_of_type_else_dummy(arg, pytype) for arg in args]
    return arg_ebs[:num_args]

def exactly_n_args(args: Sequence[NodeBuilder], location: SourceLocation, num_args: int) -> bool:
    if len(args) == num_args:
        return True
    logger.error(
        f"expected {num_args} argument{'' if num_args == 1 else 's'}, got {len(args)}",
        location=location,
    )
    return False

def argument_of_type(
    builder: NodeBuilder,
    target_type: pytypes.PyType,
    *additional_types: pytypes.PyType,
    resolve_literal: bool = False,
    default: Callable[[str, SourceLocation], _T],
) -> InstanceBuilder | _T:
    if resolve_literal:
        builder = maybe_resolve_literal(builder, target_type)

    if isinstance(builder, InstanceBuilder) and builder.pytype.is_type_or_subtype(
        target_type, *additional_types
    ):
        return builder
    return not_this_type(builder, default=default)

def argument_of_type_else_dummy(
    builder: NodeBuilder,
    target_type: pytypes.PyType,
    *additional_types: pytypes.PyType,
    resolve_literal: bool = False,
) -> InstanceBuilder:
    return argument_of_type(
        builder,
        target_type,
        *additional_types,
        resolve_literal=resolve_literal,
        default=default_dummy_value(target_type),
    )

def simple_string_literal(
    builder: NodeBuilder,
    *,
    default: Callable[[str, SourceLocation], _T],
) -> str | _T:
    from puyapy.awst_build.eb.interface import LiteralBuilder

    match builder:
        case LiteralBuilder(value=str(value)):
            return value
        case InstanceBuilder(pytype=pytypes.StrLiteralType):
            msg = "argument must be a simple str literal"
            result = default(msg, builder.source_location)
            logger.error(msg, location=builder.source_location)
            return result
        case other:
            return not_this_type(other, default=default)

def instance_builder(
    builder: NodeBuilder, *, default: Callable[[str, SourceLocation], _T]
) -> InstanceBuilder | _T:
    if isinstance(builder, InstanceBuilder):
        return builder
    msg = "expression is not a value"
    result = default(msg, builder.source_location)
    logger.error(msg, location=builder.source_location)
    return result