from puya.awst import wtypes
from puya.awst.nodes import Expression, IntrinsicCall
from puya.errors import InternalError
from puya.parse import SourceLocation

def bytes_len(expr: Expression, loc: SourceLocation | None = None) -> IntrinsicCall:
    loc = loc or expr.source_location
    return IntrinsicCall(
        op_code="len",
        stack_args=[expr],
        wtype=wtypes.uint64_wtype,
        source_location=loc,
    )

def concat(
    a: Expression,
    b: Expression,
    loc: SourceLocation,
    result_type: wtypes.WType = wtypes.bytes_wtype,
) -> IntrinsicCall:
    return IntrinsicCall(
        op_code="concat",
        stack_args=[a, b],
        wtype=result_type,
        source_location=loc,
    )

def itob(expr: Expression, loc: SourceLocation | None = None) -> IntrinsicCall:
    return itob_as(expr, wtypes.bytes_wtype, loc)

def itob_as(
    expr: Expression, wtype: wtypes.WType, loc: SourceLocation | None = None
) -> IntrinsicCall:
    return IntrinsicCall(
        op_code="itob",
        stack_args=[expr],
        wtype=wtype,
        source_location=loc or expr.source_location,
    )

def txn(immediate: str, wtype: wtypes.WType, location: SourceLocation) -> IntrinsicCall:
    return IntrinsicCall(
        op_code="txn",
        immediates=[immediate],
        wtype=wtype,
        source_location=location,
    )

def log(to_log: Expression, loc: SourceLocation) -> IntrinsicCall:
    return IntrinsicCall(
        op_code="log",
        stack_args=[to_log],
        wtype=wtypes.void_wtype,
        source_location=loc,
    )

def select(
    *, condition: Expression, false: Expression, true: Expression, loc: SourceLocation
) -> IntrinsicCall:
    if false.wtype != true.wtype:
        raise InternalError(f"false WType ({false}) does not match true WType ({true})", loc)
    return IntrinsicCall(
        op_code="select",
        stack_args=[
            false,
            true,
            condition,
        ],
        wtype=false.wtype,
        source_location=loc,
    )

def extract(
    value: Expression,
    start: int,
    length: int = 0,
    loc: SourceLocation | None = None,
    result_type: wtypes.WType = wtypes.bytes_wtype,
) -> IntrinsicCall:
    return IntrinsicCall(
        op_code="extract",
        immediates=[start, length],
        wtype=result_type,
        stack_args=[value],
        source_location=loc or value.source_location,
    )

def extract3(
    value: Expression,
    start: Expression,
    length: Expression,
    loc: SourceLocation | None = None,
    result_type: wtypes.WType = wtypes.bytes_wtype,
) -> IntrinsicCall:
    return IntrinsicCall(
        op_code="extract3",
        wtype=result_type,
        stack_args=[value, start, length],
        source_location=loc or value.source_location,
    )

def zero_address(
    loc: SourceLocation, *, as_type: wtypes.WType = wtypes.account_wtype
) -> IntrinsicCall:
    return IntrinsicCall(
        wtype=as_type,
        op_code="global",
        immediates=["ZeroAddress"],
        source_location=loc,
    )


"""
Used to map algopy/_gen.pyi stubs to AWST.
Referenced by both scripts/generate_stubs.py and src/puya/awst_build/eb/intrinsics.py
"""

from collections.abc import Sequence, Set
from functools import cached_property

import attrs

from puya.awst import wtypes
from puya.errors import InternalError
from puyapy.awst_build import pytypes

@attrs.frozen
class PropertyOpMapping:
    op_code: str
    immediate: str
    typ: pytypes.PyType = attrs.field(
        validator=attrs.validators.not_(attrs.validators.in_([pytypes.NoneType]))
    )
    wtype: wtypes.WType = attrs.field(init=False)

    @wtype.default
    def _wtype(self) -> wtypes.WType:
        return self.typ.checked_wtype(location=None)

@attrs.frozen
class FunctionOpMapping:
    op_code: str
    immediates: Sequence[str | int | type[str | int]] = attrs.field(
        default=(), converter=tuple[str | int | type[str | int], ...]
    )
    """A list of immediate literals, or expected type"""
    args: Sequence[Sequence[pytypes.PyType] | int] = attrs.field(
        default=(), converter=tuple[Sequence[pytypes.PyType] | int, ...]
    )
    """A list of allowed argument types, or an index into the immediates sequence"""

    @cached_property
    def literal_arg_positions(self) -> Set[int]:
        return {idx for idx, arg in enumerate(self.args) if isinstance(arg, int)}

    def __attrs_post_init__(self) -> None:
        for idx, imm in enumerate(self.immediates):
            if isinstance(imm, type) and idx not in self.args:
                raise InternalError("expected immediate type to be reference by arg index")

        for idx, arg in enumerate(self.args):
            if isinstance(arg, int):
                try:
                    immediate = self.immediates[arg]
                except IndexError:
                    raise InternalError(
                        f"intrinsic {self.op_code!r} argument {idx}: immediates index out of range"
                    ) from None
                if immediate not in (str, int):
                    raise InternalError(
                        f"intrinsic {self.op_code!r} argument {idx}:"
                        " immediate index does not point to a type"
                    )
            else:
                if not arg:
                    raise InternalError(
                        f"intrinsic {self.op_code!r} argument {idx}: no stack input types provided"
                    )
                if (pytypes.BigUIntType.is_type_or_supertype(*arg)) and (
                    pytypes.UInt64Type.is_type_or_supertype(*arg)
                ):
                    raise InternalError(
                        f"intrinsic {self.op_code!r} argument {idx}: overlap in integer types"
                    )

@attrs.frozen(kw_only=True)
class OpMappingWithOverloads:
    arity: int = attrs.field(validator=attrs.validators.ge(0))
    result: pytypes.PyType = pytypes.NoneType
    """Types output by TEAL op"""
    result_wtype: wtypes.WType = attrs.field(init=False)
    overloads: Sequence[FunctionOpMapping] = attrs.field(
        validator=attrs.validators.min_len(1), converter=tuple[FunctionOpMapping, ...]
    )

    @result_wtype.default
    def _result_wtype(self) -> wtypes.WType:
        return self.result.checked_wtype(location=None)

    @arity.validator
    def _arity_matches(self, _attribute: object, arity: int) -> None:
        if any(len(o.args) != arity for o in self.overloads):
            raise InternalError("arity does not match all overloads")


from collections.abc import Sequence

from puya import log
from puya.awst.nodes import Contract, LogicSignature, RootNode
from puya.program_refs import ContractReference, LogicSigReference
from puya.utils import make_path_relative_to_cwd
from puyapy.awst_build.context import ASTConversionContext
from puyapy.awst_build.module import ModuleASTConverter
from puyapy.parse import TYPESHED_PATH, ParseResult, SourceDiscoveryMechanism

logger = log.get_logger(__name__)

def transform_ast(
    parse_result: ParseResult,
) -> tuple[Sequence[RootNode], Sequence[ContractReference | LogicSigReference]]:
    ctx = ASTConversionContext(parse_result=parse_result)
    user_modules = []

    for module_name, src in parse_result.ordered_modules.items():
        module_rel_path = make_path_relative_to_cwd(src.path)
        if src.node.is_stub:
            if module_name in ("abc", "typing", "collections.abc"):
                logger.debug(f"Skipping stdlib stub {module_rel_path}")  # TODO: lowercase
            elif module_name.startswith("algopy"):
                logger.debug(f"Skipping algopy stub {module_rel_path}")
            elif src.path.is_relative_to(TYPESHED_PATH):
                logger.debug(f"Skipping typeshed stub {module_rel_path}")
            else:
                logger.warning(f"Skipping stub: {module_rel_path}")
        else:
            logger.debug(f"Discovered user module {module_name} at {module_rel_path}")
            module_ctx = ctx.for_module(src.path)
            user_modules.append((src, ModuleASTConverter(module_ctx, src.node)))

    compilation_set = list[ContractReference | LogicSigReference]()
    awst = list[RootNode]()
    for src, converter in user_modules:
        logger.debug(f"Building AWST for module {src.name}")
        root_nodes = converter.convert()
        awst.extend(root_nodes)
        if src.discovery_mechanism != SourceDiscoveryMechanism.dependency:
            for root_node in root_nodes:
                if isinstance(root_node, Contract | LogicSignature):
                    compilation_set.append(root_node.id)
    return awst, compilation_set


import typing
from collections.abc import Callable

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

from puya import log
from puya.algo_constants import MAX_SCRATCH_SLOT_NUMBER
from puya.awst.nodes import AWST, LogicSignature, RootNode, StateTotals
from puya.errors import CodeError, InternalError
from puya.program_refs import LogicSigReference
from puya.utils import coalesce
from puyapy.awst_build import constants, pytypes
from puyapy.awst_build.arc4_client import ARC4ClientASTVisitor
from puyapy.awst_build.base_mypy_visitor import (
    BaseMyPyExpressionVisitor,
    BaseMyPyStatementVisitor,
)
from puyapy.awst_build.context import ASTConversionModuleContext
from puyapy.awst_build.contract import ContractASTConverter
from puyapy.awst_build.exceptions import UnsupportedASTError
from puyapy.awst_build.subroutine import FunctionASTConverter
from puyapy.awst_build.utils import (
    extract_bytes_literal_from_mypy,
    fold_binary_expr,
    fold_unary_expr,
    get_decorators_by_fullname,
    get_subroutine_decorator_inline_arg,
    get_unaliased_fullname,
)
from puyapy.models import ConstantValue, ContractClassOptions

logger = log.get_logger(__name__)

DeferredRootNode: typing.TypeAlias = Callable[[ASTConversionModuleContext], RootNode | None]

StatementResult: typing.TypeAlias = list[DeferredRootNode]

@attrs.frozen(kw_only=True)
class _LogicSigDecoratorInfo:
    name_override: str | None
    avm_version: int | None

_BUILTIN_INHERITABLE: typing.Final = frozenset(
    ("builtins.object", "abc.ABC", *mypy.types.PROTOCOL_NAMES)
)

class ModuleASTConverter(
    BaseMyPyStatementVisitor[StatementResult], BaseMyPyExpressionVisitor[ConstantValue]
):
    """This does basic validation, and traversal of valid module scope elements, collecting
    and folding constants."""

    def __init__(self, context: ASTConversionModuleContext, module: mypy.nodes.MypyFile):
        super().__init__(context)
        self.module_name: typing.Final = module.fullname
        self._pre_parse_result = list[tuple[mypy.nodes.Context, StatementResult]]()
        for node in module.defs:
            with self.context.log_exceptions(fallback_location=node):
                items = node.accept(self)
                self._pre_parse_result.append((node, items))

    def convert(self) -> AWST:
        awst = []
        for location, deferrals in self._pre_parse_result:
            with self.context.log_exceptions(fallback_location=location):
                for deferred in deferrals:
                    awst_node = deferred(self.context)
                    if awst_node is not None:
                        awst.append(awst_node)
        return awst

    # Supported Statements

    def empty_statement(self, _stmt: mypy.nodes.Statement) -> StatementResult:
        return []

    def visit_function(
        self, func_def: mypy.nodes.FuncDef, decorator: mypy.nodes.Decorator | None
    ) -> StatementResult:
        self._precondition(
            func_def.abstract_status == mypy.nodes.NOT_ABSTRACT,
            "module level functions should not be classified as abstract by mypy",
            decorator or func_def,
        )
        dec_by_fullname = get_decorators_by_fullname(self.context, decorator) if decorator else {}
        source_location = self._location(decorator or func_def)
        logicsig_dec = dec_by_fullname.pop(constants.LOGICSIG_DECORATOR, None)
        if logicsig_dec:
            for dec_fullname, dec in dec_by_fullname.items():
                self._error(f'Unsupported logicsig decorator "{dec_fullname}"', dec)
            info = self._process_logic_sig_decorator(logicsig_dec)

            def deferred(ctx: ASTConversionModuleContext) -> RootNode:
                program = FunctionASTConverter.convert(
                    ctx, func_def, source_location, inline=False
                )
                ctx.register_pytype(pytypes.LogicSigType, alias=func_def.fullname)
                return LogicSignature(
                    id=LogicSigReference(func_def.fullname),
                    program=program,
                    short_name=coalesce(info.name_override, program.name),
                    docstring=func_def.docstring,
                    source_location=self._location(logicsig_dec),
                    avm_version=info.avm_version,
                )

            return [deferred]
        subroutine_dec = dec_by_fullname.pop(constants.SUBROUTINE_HINT, None)
        if subroutine_dec is None:
            self._error(
                f"free functions must be annotated with @{constants.SUBROUTINE_HINT_ALIAS}",
                func_def,
            )
            inline = None
        else:
            inline = get_subroutine_decorator_inline_arg(self.context, subroutine_dec)
        abimethod_dec = dec_by_fullname.pop(constants.ABIMETHOD_DECORATOR, None)
        if abimethod_dec is not None:
            self._error("free functions cannot be ARC4 ABI methods", abimethod_dec)
        # any further decorators are unsupported
        for dec_fullname, dec in dec_by_fullname.items():
            self._error(f'unsupported function decorator "{dec_fullname}"', dec)

        return [
            lambda ctx: FunctionASTConverter.convert(ctx, func_def, source_location, inline=inline)
        ]

    def _process_logic_sig_decorator(
        self, decorator: mypy.nodes.Expression
    ) -> _LogicSigDecoratorInfo:
        name_override = None
        avm_version = None
        match decorator:
            case mypy.nodes.NameExpr():
                pass
            case mypy.nodes.CallExpr(arg_names=arg_names, args=args):
                for arg_name, arg in zip(arg_names, args, strict=True):
                    match arg_name:
                        case "name":
                            name_const = arg.accept(self)
                            if isinstance(name_const, str):
                                name_override = name_const
                            else:
                                self.context.error("expected a str", arg)
                        case "avm_version":
                            version_const = arg.accept(self)
                            if isinstance(version_const, int):
                                avm_version = version_const
                            else:
                                self.context.error("expected an int", arg)
                        case _:
                            self.context.error("unexpected argument", arg)
            case _:
                self.context.error(
                    f"invalid {constants.LOGICSIG_DECORATOR_ALIAS} usage", decorator
                )
        return _LogicSigDecoratorInfo(name_override=name_override, avm_version=avm_version)

    def visit_class_def(self, cdef: mypy.nodes.ClassDef) -> StatementResult:
        self.check_fatal_decorators(cdef.decorators)
        cdef_loc = self._location(cdef)
        match cdef.analyzed:
            case None:
                pass
            case mypy.nodes.TypedDictExpr():
                self._unsupported(cdef, "TypedDict classes are not supported")
            case mypy.nodes.NamedTupleExpr():
                return _process_named_tuple(self.context, cdef)
            case unrecognised_analysis_expression:
                self.context.warning(
                    "Analyzed class expression of type"
                    f" {type(unrecognised_analysis_expression).__name__},"
                    " please report this issue and check the compilation results carefully",
                    cdef_loc,
                )
        for decorator in cdef.decorators:
            self._error(
                (
                    "Unsupported decorator "
                    "(note: *all* class decorators are currently unsupported)"
                ),
                location=decorator,
            )
        info: mypy.nodes.TypeInfo = cdef.info
        if info.bad_mro:
            self._error("Bad MRO", location=cdef_loc)
            return []
        if info.metaclass_type and info.metaclass_type.type.fullname not in (
            "abc.ABCMeta",
            "typing._ProtocolMeta",
            "typing_extensions._ProtocolMeta",
            constants.CLS_ARC4_STRUCT_META,
            constants.STRUCT_META,
        ):
            self._error(
                f"Unsupported metaclass: {info.metaclass_type.type.fullname}",
                location=cdef_loc,
            )
            return []

        direct_base_types = [
            self.context.require_ptype(ti.fullname, cdef_loc)
            for ti in info.direct_base_classes()
            if ti.fullname not in _BUILTIN_INHERITABLE
        ]
        mro_types = [
            self.context.require_ptype(ti.fullname, cdef_loc)
            for ti in info.mro[1:]
            if ti.fullname not in _BUILTIN_INHERITABLE
        ]
        # create a static type, but don't register it yet,
        # it might end up being a struct instead
        static_type = pytypes.StaticType(
            name=cdef.fullname, bases=direct_base_types, mro=mro_types
        )
        for struct_base in (pytypes.StructBaseType, pytypes.ARC4StructBaseType):
            # note that since these struct bases aren't protocols, any subclasses
            # cannot be protocols
            if struct_base < static_type:
                if direct_base_types != [struct_base]:
                    self._error(
                        f"{struct_base} classes must only inherit directly from {struct_base}",
                        cdef_loc,
                    )
                return _process_struct(self.context, struct_base, cdef)

        if pytypes.ContractBaseType < static_type:
            module_name = cdef.info.module_name
            class_name = cdef.name
            assert "." not in class_name
            assert cdef.fullname == f"{module_name}.{class_name}"
            contract_type = pytypes.ContractType(
                module_name=module_name,
                class_name=class_name,
                bases=direct_base_types,
                mro=mro_types,
                source_location=cdef_loc,
            )
            self.context.register_pytype(contract_type)

            class_options = _process_contract_class_options(self.context, self, cdef)
            converter = ContractASTConverter(self.context, cdef, class_options, contract_type)
            return [converter.build]

        if info.is_protocol:
            self.context.register_pytype(static_type)
            if pytypes.ARC4ClientBaseType in direct_base_types:
                ARC4ClientASTVisitor.visit(self.context, cdef)
            else:
                logger.debug(
                    f"Skipping further processing of protocol class {cdef.fullname}",
                    location=cdef_loc,
                )
            return []

        logger.error(
            f"Unsupported class declaration."
            f" Contract classes must inherit either directly"
            f" or indirectly from {pytypes.ContractBaseType}.",
            location=cdef_loc,
        )
        return []

    def visit_operator_assignment_stmt(
        self, stmt: mypy.nodes.OperatorAssignmentStmt
    ) -> StatementResult:
        match stmt.lvalue:
            case mypy.nodes.NameExpr(name="__all__"):
                return self.empty_statement(stmt)
            case _:
                self._unsupported(stmt)

    def visit_if_stmt(self, stmt: mypy.nodes.IfStmt) -> StatementResult:
        for expr, block in zip(stmt.expr, stmt.body, strict=True):
            if self._evaluate_compile_time_constant_condition(expr):
                return block.accept(self)
        # if we didn't return, we end up here, which means the user code
        # should evaluate to the else body (if present)
        if stmt.else_body:
            return stmt.else_body.accept(self)
        else:
            return []

    def visit_assignment_stmt(self, stmt: mypy.nodes.AssignmentStmt) -> StatementResult:
        stmt_loc = self._location(stmt)
        self._precondition(
            bool(stmt.lvalues), "assignment statements should have at least one lvalue", stmt_loc
        )
        self._precondition(
            not stmt.invalid_recursive_alias,
            "assignment statement with invalid_recursive_alias",
            stmt_loc,
        )
        lvalues = stmt.lvalues
        if not self._check_assignment_lvalues(lvalues):
            return []
        if stmt.is_alias_def:
            match stmt.rvalue:
                case mypy.nodes.RefExpr(
                    is_alias_rvalue=True, node=mypy.nodes.TypeInfo(fullname=alias_fullname)
                ):
                    maybe_aliased_pytype = self.context.lookup_pytype(alias_fullname)
                    if maybe_aliased_pytype is None:
                        self.context.error(
                            f"Unknown type for type alias: {alias_fullname}", stmt_loc
                        )
                        return []
                    aliased_pytype = maybe_aliased_pytype
                case mypy.nodes.IndexExpr(
                    analyzed=mypy.nodes.TypeAliasExpr(
                        node=mypy.nodes.TypeAlias(alias_tvars=[], target=alias_type)
                    )
                ):
                    aliased_pytype = self.context.type_to_pytype(
                        alias_type, source_location=stmt_loc
                    )
                case _:
                    self._error("Unsupported type-alias format", stmt_loc)
                    return []
            for lvalue in lvalues:
                self.context.register_pytype(aliased_pytype, alias=lvalue.fullname)
            # We don't include type aliases in AWST since they're Python specific
            return []
        if any(lvalue.is_special_form for lvalue in lvalues):
            self._error("Unsupported type-form", stmt_loc)

        constant_value = stmt.rvalue.accept(self)
        for lvalue in lvalues:
            self.context.constants[lvalue.fullname] = constant_value

        return []

    def _check_assignment_lvalues(
        self, lvalues: list[mypy.nodes.Lvalue]
    ) -> typing.TypeGuard[list[mypy.nodes.NameExpr]]:
        """Does some pre-condition checks, including that all lvalues are simple (ie name-exprs),
        hence the TypeGuard return type. If it returns True, then we should try and handle the
        assignment."""
        result = True
        for lvalue in lvalues:
            if not isinstance(lvalue, mypy.nodes.NameExpr):
                self._error(
                    "Only straight-forward assignment targets supported at module level", lvalue
                )
                result = False
            else:
                if len(lvalues) > 1:
                    self._precondition(
                        not lvalue.is_special_form, "special form with multiple lvalues", lvalue
                    )
                if lvalue.name == "__all__":
                    # Special notation to denote the public members of a file, we don't need to
                    # store this as we don't validate star-imports, mypy does, hence the False.
                    # We check inside functions if this is attempted to be referenced and produce
                    # a specific error message.
                    result = False
                    if len(lvalues) > 1:
                        self._error("Multi-assignment with __all__ not supported", lvalue)
                    break
                fullname = ".".join((self.module_name, lvalue.name))
                # fullname might be unset if this is in
                # a conditional branch that's !TYPE_CHECKING
                if lvalue.fullname:
                    self._precondition(
                        lvalue.fullname == fullname,
                        f"assignment to module const - expected fullname of {fullname},"
                        f" but mypy had {lvalue.fullname}",
                        lvalue,
                    )
                else:
                    # fix it up
                    lvalue._fullname = fullname  # noqa: SLF001
        return result

    def visit_block(self, block: mypy.nodes.Block) -> StatementResult:
        result = StatementResult()
        for stmt in block.body:
            items = stmt.accept(self)
            result.extend(items)
        return result

    # Expressions

    def visit_name_expr(self, expr: mypy.nodes.NameExpr) -> ConstantValue:
        match expr.name:
            case "True":
                return True
            case "False":
                return False
        # TODO: is the GDEF check always valid?
        if not isinstance(expr.node, mypy.nodes.Var) or expr.kind != mypy.nodes.GDEF:
            self._unsupported(
                expr,
                "references to anything other than module-level constants "
                "are not supported at module level",
            )
        value = self.context.constants.get(expr.fullname)
        if value is None:
            raise CodeError(
                f"not a known constant value: {expr.name}"
                f" (qualified source name: {expr.fullname})",
                self._location(expr),
            )
        return value

    def visit_unary_expr(self, expr: mypy.nodes.UnaryExpr) -> ConstantValue:
        value = expr.expr.accept(self)
        return fold_unary_expr(self._location(expr), expr.op, value)

    def visit_op_expr(self, expr: mypy.nodes.OpExpr) -> ConstantValue:
        left_value = expr.left.accept(self)
        right_value = expr.right.accept(self)
        return fold_binary_expr(self._location(expr), expr.op, left_value, right_value)

    def visit_comparison_expr(self, expr: mypy.nodes.ComparisonExpr) -> ConstantValue:
        match (expr.operators, expr.operands):
            case ([op], [expr_left, expr_right]):
                lhs = expr_left.accept(self)
                rhs = expr_right.accept(self)
                return fold_binary_expr(self._location(expr), op, lhs, rhs)
            case _:
                self._unsupported(
                    expr, details="chained comparisons not supported at module level"
                )

    def visit_int_expr(self, expr: mypy.nodes.IntExpr) -> ConstantValue:
        return expr.value

    def visit_str_expr(self, expr: mypy.nodes.StrExpr) -> ConstantValue:
        return expr.value

    def visit_bytes_expr(self, expr: mypy.nodes.BytesExpr) -> ConstantValue:
        return extract_bytes_literal_from_mypy(expr)

    def visit_member_expr(self, expr: mypy.nodes.MemberExpr) -> ConstantValue:
        if (
            isinstance(expr.node, mypy.nodes.Var)
            and expr.kind == mypy.nodes.GDEF
            and expr.fullname
            and isinstance(expr.expr, mypy.nodes.RefExpr)
            and isinstance(expr.expr.node, mypy.nodes.MypyFile)
        ):
            try:
                return self.context.constants[expr.fullname]
            except KeyError as ex:
                self._unsupported(
                    expr, details="could not resolve external module constant", ex=ex
                )
        else:
            self._unsupported(expr)

    def visit_call_expr(self, expr: mypy.nodes.CallExpr) -> ConstantValue:
        # unfortunately, mypy doesn't preserve f-string identification info,
        # they get translated into either a str.join or str.format call at the top level
        # References:
        # https://github.com/python/mypy/blob/cb813259c3b9dff6aaa8686793cf6a0634cf1f69/mypy/fastparse.py#L1528
        # https://github.com/python/mypy/blob/cb813259c3b9dff6aaa8686793cf6a0634cf1f69/mypy/fastparse.py#L1550
        match expr:
            case mypy.nodes.CallExpr(
                callee=mypy.nodes.MemberExpr(expr=mypy.nodes.StrExpr(value=joiner), name="join"),
                args=[mypy.nodes.ListExpr() as args_list],
            ):
                args = [arg.accept(self) for arg in args_list.items]
                assert all(isinstance(x, str) for x in args)
                result = joiner.join(map(str, args))
                return result
            case mypy.nodes.CallExpr(
                callee=mypy.nodes.MemberExpr(
                    expr=mypy.nodes.StrExpr(value=format_str), name="format"
                )
            ):
                args = [arg.accept(self) for arg in expr.args]
                return format_str.format(*args)
            case mypy.nodes.CallExpr(
                callee=mypy.nodes.MemberExpr(
                    expr=mypy.nodes.StrExpr(value=str_value), name="encode"
                ),
                args=[mypy.nodes.StrExpr(value=encoding)],
                arg_names=[("encoding" | None)],
            ):
                return str_value.encode(encoding=encoding)
            case mypy.nodes.CallExpr(
                callee=mypy.nodes.MemberExpr(
                    expr=mypy.nodes.StrExpr(value=str_value), name="encode"
                ),
                args=[],
            ):
                return str_value.encode()
        return self._unsupported(expr)

    def visit_conditional_expr(self, expr: mypy.nodes.ConditionalExpr) -> ConstantValue:
        if self._evaluate_compile_time_constant_condition(expr.cond):
            return expr.if_expr.accept(self)
        else:
            return expr.else_expr.accept(self)

    def _evaluate_compile_time_constant_condition(self, expr: mypy.nodes.Expression) -> bool:
        from mypy import reachability

        kind = reachability.infer_condition_value(expr, self.context.mypy_options)
        if kind == reachability.TRUTH_VALUE_UNKNOWN:
            try:
                result = expr.accept(self)
            except UnsupportedASTError as ex:
                self._unsupported(
                    expr,
                    details="only constant valued conditions supported at module level",
                    ex=ex,
                )
            kind = reachability.ALWAYS_TRUE if result else reachability.ALWAYS_FALSE
        if kind in (reachability.ALWAYS_TRUE, reachability.MYPY_FALSE):
            return True
        elif kind in (reachability.ALWAYS_FALSE, reachability.MYPY_TRUE):
            return False
        else:
            raise InternalError(
                f"Unexpected reachability value: {kind}", location=self._location(expr)
            )

    def _unsupported(
        self,
        node: mypy.nodes.Node,
        details: str = "not supported at module level",
        ex: Exception | None = None,
    ) -> typing.Never:
        raise UnsupportedASTError(node, self._location(node), details=details) from ex

    # Unsupported Statements

    def visit_expression_stmt(self, stmt: mypy.nodes.ExpressionStmt) -> StatementResult:
        if isinstance(stmt.expr, mypy.nodes.StrExpr):
            # ignore any doc-strings at module level
            return []
        else:
            return self._unsupported(stmt)

    def visit_while_stmt(self, stmt: mypy.nodes.WhileStmt) -> StatementResult:
        return self._unsupported(stmt)

    def visit_for_stmt(self, stmt: mypy.nodes.ForStmt) -> StatementResult:
        return self._unsupported(stmt)

    def visit_break_stmt(self, stmt: mypy.nodes.BreakStmt) -> StatementResult:
        return self._unsupported(stmt)

    def visit_continue_stmt(self, stmt: mypy.nodes.ContinueStmt) -> StatementResult:
        return self._unsupported(stmt)

    def visit_assert_stmt(self, stmt: mypy.nodes.AssertStmt) -> StatementResult:
        return self._unsupported(stmt)

    def visit_del_stmt(self, stmt: mypy.nodes.DelStmt) -> StatementResult:
        return self._unsupported(stmt)

    def visit_match_stmt(self, stmt: mypy.nodes.MatchStmt) -> StatementResult:
        return self._unsupported(stmt)

    def visit_type_alias_stmt(self, stmt: mypy.nodes.TypeAliasStmt) -> StatementResult:
        return self._unsupported(stmt)

    # the remaining statements (below) are invalid at the module lexical scope,
    # mypy should have caught these errors already
    def visit_return_stmt(self, stmt: mypy.nodes.ReturnStmt) -> StatementResult:
        raise InternalError("encountered return statement at module level", self._location(stmt))

    # Unsupported Expressions

    def visit_super_expr(self, expr: mypy.nodes.SuperExpr) -> ConstantValue:
        return self._unsupported(expr)

    def visit_index_expr(self, expr: mypy.nodes.IndexExpr) -> ConstantValue:
        return self._unsupported(expr)

    def visit_ellipsis(self, expr: mypy.nodes.EllipsisExpr) -> ConstantValue:
        return self._unsupported(expr)

    def visit_dict_expr(self, expr: mypy.nodes.DictExpr) -> ConstantValue:
        return self._unsupported(expr)

    def visit_list_expr(self, expr: mypy.nodes.ListExpr) -> ConstantValue:
        return self._unsupported(expr)

    def visit_tuple_expr(self, expr: mypy.nodes.TupleExpr) -> ConstantValue:
        return self._unsupported(expr)

    def visit_list_comprehension(self, expr: mypy.nodes.ListComprehension) -> ConstantValue:
        return self._unsupported(expr)

    def visit_slice_expr(self, expr: mypy.nodes.SliceExpr) -> ConstantValue:
        return self._unsupported(expr)

    def visit_assignment_expr(self, o: mypy.nodes.AssignmentExpr) -> ConstantValue:
        return self._unsupported(o)

    def visit_lambda_expr(self, expr: mypy.nodes.LambdaExpr) -> ConstantValue:
        return self._unsupported(expr)

def _process_contract_class_options(
    context: ASTConversionModuleContext,
    expr_visitor: mypy.visitor.ExpressionVisitor[ConstantValue],
    cdef: mypy.nodes.ClassDef,
) -> ContractClassOptions:
    name_override: str | None = None
    scratch_slot_reservations = set[int]()
    state_totals = None
    avm_version = None
    for kw_name, kw_expr in cdef.keywords.items():
        with context.log_exceptions(kw_expr):
            match kw_name:
                case "name":
                    name_value = kw_expr.accept(expr_visitor)
                    if isinstance(name_value, str):
                        name_override = name_value
                    else:
                        context.error("unexpected argument type", kw_expr)
                case "scratch_slots":
                    if isinstance(kw_expr, mypy.nodes.TupleExpr | mypy.nodes.ListExpr):
                        slot_items = kw_expr.items
                    else:
                        slot_items = [kw_expr]
                    for item_expr in slot_items:
                        slots = _map_scratch_space_reservation(context, expr_visitor, item_expr)
                        if not slots:
                            context.error("range is empty", item_expr)
                        elif (min(slots) < 0) or (max(slots) > MAX_SCRATCH_SLOT_NUMBER):
                            context.error(
                                "invalid scratch slot reservation - range must fall"
                                f" entirely between 0 and {MAX_SCRATCH_SLOT_NUMBER}",
                                item_expr,
                            )
                        else:
                            scratch_slot_reservations.update(slots)
                case "state_totals":
                    if not isinstance(kw_expr, mypy.nodes.CallExpr):
                        context.error("unexpected argument type", kw_expr)
                    else:
                        arg_map = dict[str, int]()
                        for arg_name, arg in zip(kw_expr.arg_names, kw_expr.args, strict=True):
                            if arg_name is None:
                                context.error("unexpected positional argument", arg)
                            else:
                                arg_value = arg.accept(expr_visitor)
                                if not isinstance(arg_value, int):
                                    context.error("unexpected argument type", arg)
                                else:
                                    arg_map[arg_name] = arg_value
                        state_totals = StateTotals(**arg_map)
                case "avm_version":
                    version_value = kw_expr.accept(expr_visitor)
                    if isinstance(version_value, int):
                        avm_version = version_value
                    else:
                        context.error("unexpected argument type", kw_expr)
                case "metaclass":
                    context.error("metaclass option is unsupported", kw_expr)
                case _:
                    context.error("unrecognised class option", kw_expr)
    return ContractClassOptions(
        name_override=name_override,
        scratch_slot_reservations=scratch_slot_reservations,
        state_totals=state_totals,
        avm_version=avm_version,
    )

def _process_dataclass_like_fields(
    context: ASTConversionModuleContext, cdef: mypy.nodes.ClassDef, base_type: pytypes.PyType
) -> dict[str, pytypes.PyType] | None:
    fields = dict[str, pytypes.PyType]()
    has_error = False
    for stmt in cdef.defs.body:
        stmt_loc = context.node_location(stmt)
        match stmt:
            case mypy.nodes.ExpressionStmt(expr=mypy.nodes.StrExpr()):
                # ignore class docstring, already extracted
                # TODO: should we capture field "docstrings"?
                pass
            case mypy.nodes.AssignmentStmt(
                lvalues=[mypy.nodes.NameExpr(name=field_name)],
                rvalue=mypy.nodes.TempNode(),
                type=mypy.types.Type() as mypy_type,
            ):
                pytype = context.type_to_pytype(mypy_type, source_location=stmt_loc)
                fields[field_name] = pytype
                if isinstance((maybe_err := pytype.wtype), str):
                    logger.error(maybe_err, location=stmt_loc)
                    has_error = True
            case mypy.nodes.SymbolNode(name=symbol_name) if (
                cdef.info.names[symbol_name].plugin_generated
            ):
                pass
            case mypy.nodes.PassStmt():
                pass
            case _:
                logger.error(
                    f"unsupported syntax for {base_type} member declaration", location=stmt_loc
                )
                has_error = True
    return fields if not has_error else None

def _process_struct(
    context: ASTConversionModuleContext, base: pytypes.PyType, cdef: mypy.nodes.ClassDef
) -> StatementResult:
    fields = _process_dataclass_like_fields(context, cdef, base)
    if fields is None:
        return []
    cls_loc = context.node_location(cdef)
    frozen = cdef.info.metadata["dataclass"]["frozen"]
    assert isinstance(frozen, bool)
    struct_typ = pytypes.StructType(
        base=base,
        name=cdef.fullname,
        desc=cdef.docstring,
        fields=fields,
        frozen=frozen,
        source_location=cls_loc,
    )
    context.register_pytype(struct_typ)
    return []

def _process_named_tuple(
    context: ASTConversionModuleContext, cdef: mypy.nodes.ClassDef
) -> StatementResult:
    fields = _process_dataclass_like_fields(context, cdef, pytypes.NamedTupleBaseType)
    if fields is None:
        return []
    cls_loc = context.node_location(cdef)
    named_tuple_type = pytypes.NamedTupleType(
        name=cdef.fullname,
        desc=cdef.docstring,
        fields=fields,
        source_location=cls_loc,
    )
    context.register_pytype(named_tuple_type)
    return []

def _map_scratch_space_reservation(
    context: ASTConversionModuleContext,
    expr_visitor: mypy.visitor.ExpressionVisitor[ConstantValue],
    expr: mypy.nodes.Expression,
) -> list[int]:
    def get_int_arg(arg_expr: mypy.nodes.Expression, *, error_msg: str) -> int:
        const_value = arg_expr.accept(expr_visitor)
        if isinstance(const_value, int):
            return const_value
        raise CodeError(error_msg, context.node_location(arg_expr))

    expr_loc = context.node_location(expr)
    match expr:
        case mypy.nodes.CallExpr(
            callee=mypy.nodes.RefExpr() as callee, args=args
        ) if get_unaliased_fullname(callee) == constants.URANGE:
            if not args:
                raise CodeError("Expected at least one argument to urange", expr_loc)
            if len(args) > 3:
                raise CodeError("Expected at most three arguments to urange", expr_loc)
            int_args = [
                get_int_arg(
                    arg_expr,
                    error_msg=(
                        "Unexpected argument for urange:"
                        " expected an in integer literal or module constant reference"
                    ),
                )
                for arg_expr in args
            ]
            if len(int_args) == 3 and int_args[-1] == 0:
                raise CodeError("urange arg 3 must not be zero", context.node_location(args[-1]))
            return list(range(*int_args))
        case _:
            return [
                get_int_arg(
                    expr,
                    error_msg=(
                        "Unexpected value:"
                        " Expected int literal, module constant reference, or urange expression"
                    ),
                )
            ]


from __future__ import annotations

import abc
import typing
from collections.abc import Callable, Mapping, Sequence
from functools import cached_property

import attrs
import typing_extensions
from immutabledict import immutabledict

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 puya.program_refs import ContractReference
from puya.utils import lazy_setdefault, unique
from puyapy.awst_build import constants

if typing.TYPE_CHECKING:
    from mypy.nodes import ArgKind

    from puyapy.awst_build.intrinsic_models import (
        OpMappingWithOverloads,
        PropertyOpMapping,
    )

logger = log.get_logger(__name__)

ErrorMessage = typing.NewType("ErrorMessage", str)

@attrs.frozen(kw_only=True, str=False, order=False)
class PyType(abc.ABC):
    name: str
    """The canonical fully qualified type name"""
    generic: PyType | None = None
    """The generic type that this type was parameterised from, if any."""
    bases: tuple[PyType, ...] = attrs.field(default=(), converter=tuple["PyType", ...])
    """Direct base classes. probably excluding the implicit builtins.object?"""
    mro: tuple[PyType, ...] = attrs.field(default=(), converter=tuple["PyType", ...])
    """All base cases, in Method Resolution Order"""

    @bases.validator
    def _bases_validate(self, _attribute: object, bases: tuple[PyType, ...]) -> None:
        if len(set(bases)) != len(bases):
            raise InternalError(f"Duplicate bases in {self}: [{', '.join(map(str, bases))}]")

    @mro.validator
    def _mro_validate(self, _attribute: object, mro: tuple[PyType, ...]) -> None:
        bases_missing_from_mro = set(self.bases).difference(mro)
        if bases_missing_from_mro:
            raise InternalError(
                f"Bases missing from MRO in {self}:"
                f" [{', '.join(map(str, bases_missing_from_mro))}]"
            )

    @cached_property
    def _friendly_name(self) -> str:
        """User-friendly fully-qualified name.

        Basically just strips "builtins." from types, and removes the private part
        of algopy names.
        """
        import re

        result, _ = re.subn(r"\bbuiltins\.", "", self.name)
        result, _ = re.subn(r"algopy\._[^.]+", "algopy", result)
        result = result.replace(NoneType.name, "None")
        return result

    def __str__(self) -> str:
        return self._friendly_name

    @property
    @abc.abstractmethod
    def wtype(self) -> wtypes.WType | ErrorMessage:
        """The WType that this type represents, if any."""

    def checked_wtype(self, location: SourceLocation | None) -> wtypes.WType:
        match self.wtype:
            case wtypes.WType() as wtype:
                return wtype
            case str(msg):
                raise CodeError(msg, location)
            case _:
                typing.assert_never(self.wtype)

    def parameterise(
        self,
        args: Sequence[PyType],  # noqa: ARG002
        source_location: SourceLocation | None,
    ) -> PyType:
        """Produce parameterised type.
        Throws if not a generic type of if a parameterised generic type."""
        if self.generic:
            raise CodeError(f"type already has parameters: {self}", source_location)
        raise CodeError(f"not a generic type: {self}", source_location)

    @typing.final
    def __lt__(self, other: object) -> bool:
        if not isinstance(other, PyType):
            return NotImplemented
        # self < other -> self is a supertype of other
        return self in other.mro

    @typing.final
    def __le__(self, other: object) -> bool:
        if not isinstance(other, PyType):
            return NotImplemented
        return self == other or self < other

    @typing.final
    def __gt__(self, other: object) -> bool:
        if not isinstance(other, PyType):
            return NotImplemented
        raise TypeError("types are partial ordered")

    @typing.final
    def __ge__(self, other: object) -> bool:
        if not isinstance(other, PyType):
            return NotImplemented
        raise TypeError("types are partial ordered")

    def is_type_or_subtype(self, *of_any: PyType) -> bool:
        return any(of <= self for of in of_any)

    def is_type_or_supertype(self, *of_any: PyType) -> bool:
        return any(self <= of for of in of_any)

class RuntimeType(PyType, abc.ABC):
    @typing.override
    @property
    @abc.abstractmethod
    def wtype(self) -> wtypes.WType: ...

_builtins_registry: typing.Final = dict[str, PyType]()

_TPyType = typing_extensions.TypeVar("_TPyType", bound=PyType, default=PyType)

def _register_builtin(typ: _TPyType, *, alias: str | None = None) -> _TPyType:
    name = alias or typ.name
    existing_entry = _builtins_registry.get(name)
    if existing_entry is None:
        _builtins_registry[name] = typ
    elif existing_entry is typ:
        logger.debug(f"Duplicate registration of {typ}")
    else:
        raise InternalError(f"Duplicate mapping of {name}")
    return typ

def builtins_registry() -> dict[str, PyType]:
    """Get a copy of the builtins registry"""
    return _builtins_registry.copy()

# https://typing.readthedocs.io/en/latest/spec/literal.html#legal-and-illegal-parameterizations
# We don't support enums as typing.Literal parameters. mypy encodes these as str values with
# an additional "fallback" data member, but we don't need that complication.
# mypy.types.LiteralValue also includes float, noting its invalid as a parameterization, but
# we exclude this here.
# None types are also encoded as their own type, but we have them as values here.
TypingLiteralValue: typing.TypeAlias = int | bytes | str | bool | None
_TypeArgs: typing.TypeAlias = tuple[PyType, ...]
_Parameterise = typing_extensions.TypeAliasType(
    "_Parameterise",
    Callable[["_GenericType[_TPyType]", _TypeArgs, SourceLocation | None], _TPyType],
    type_params=(_TPyType,),
)

@typing.final
@attrs.frozen(order=False)
class _GenericType(PyType, abc.ABC, typing.Generic[_TPyType]):
    """Represents a typing.Generic type with unknown parameters"""

    _parameterise: _Parameterise[_TPyType]
    _instance_cache: dict[_TypeArgs, _TPyType] = attrs.field(factory=dict, eq=False)

    def __attrs_post_init__(self) -> None:
        _register_builtin(self)

    @typing.override
    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage("generic type usage requires parameters")

    @typing.override
    def parameterise(
        self, args: Sequence[PyType], source_location: SourceLocation | None
    ) -> _TPyType:
        return lazy_setdefault(
            self._instance_cache,
            key=tuple(args),
            default=lambda args_: self._parameterise(self, args_, source_location),
        )

def _parameterise_type_type(
    self: _GenericType,  # noqa: ARG001
    args: _TypeArgs,
    source_location: SourceLocation | None,
) -> TypeType:
    try:
        (arg,) = args
    except ValueError:
        raise CodeError(
            f"expected a single type parameter, got {len(args)} parameters", source_location
        ) from None
    return TypeType(typ=arg)

GenericTypeType: typing.Final[PyType] = _GenericType(
    name="builtins.type",
    parameterise=_parameterise_type_type,
)

@typing.final
@attrs.frozen(order=False)
class TypeType(PyType):
    typ: PyType
    generic: PyType = attrs.field(default=GenericTypeType, init=False)
    name: str = attrs.field(init=False)

    @name.default
    def _name_default(self) -> str:
        return f"{self.generic.name}[{self.typ.name}]"

    @typing.override
    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage("type objects are not usable as values")

@typing.final
@attrs.frozen(order=False)
class TypingLiteralType(PyType):
    value: TypingLiteralValue
    source_location: SourceLocation | None
    name: str = attrs.field(init=False)
    generic: None = attrs.field(default=None, init=False)
    bases: tuple[PyType, ...] = attrs.field(default=(), init=False)
    mro: tuple[PyType, ...] = attrs.field(default=(), init=False)

    @name.default
    def _name_default(self) -> str:
        return f"typing.Literal[{self.value!r}]"

    @typing.override
    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage(f"{self} is not usable as a value")

def _flatten_nested_unions(types: Sequence[PyType]) -> tuple[PyType, ...]:
    result = list[PyType]()
    for t in types:
        if isinstance(t, UnionType):
            result.extend(t.types)
        else:
            result.append(t)
    return tuple(unique(result))

@typing.final
@attrs.frozen(order=False)
class UnionType(PyType):
    types: tuple[PyType, ...] = attrs.field(
        converter=_flatten_nested_unions, validator=attrs.validators.min_len(2)
    )
    name: str = attrs.field(init=False)
    generic: None = attrs.field(default=None, init=False)
    bases: tuple[PyType, ...] = attrs.field(default=(), init=False)
    mro: tuple[PyType, ...] = attrs.field(default=(), init=False)
    source_location: SourceLocation

    @name.default
    def _name(self) -> str:
        return " | ".join(t.name for t in self.types)

    @typing.override
    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage("type unions are unsupported at this location")

@attrs.frozen(order=False)
class _BaseType(PyType):
    """Type that is only usable as a base type"""

    @typing.override
    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage(f"{self} is only usable as a base type")

    def __attrs_post_init__(self) -> None:
        _register_builtin(self)

@attrs.frozen(kw_only=True, order=False)
class TupleLikeType(PyType, abc.ABC):
    items: tuple[PyType, ...]
    source_location: SourceLocation | None

def _parameterise_tuple(
    self: _GenericType[TupleType], args: _TypeArgs, source_location: SourceLocation | None
) -> TupleType:
    name = f"{self.name}[{', '.join(pyt.name for pyt in args)}]"
    return TupleType(name=name, items=tuple(args), source_location=source_location)

GenericTupleType: typing.Final = _GenericType(
    name="builtins.tuple",
    parameterise=_parameterise_tuple,
)

@attrs.frozen(kw_only=True, order=False)
class TupleType(TupleLikeType):
    generic: PyType | None = attrs.field(default=GenericTupleType, init=False)
    bases: tuple[PyType, ...] = attrs.field(default=(), init=False)
    mro: tuple[PyType, ...] = attrs.field(default=(), init=False)

    @property
    def wtype(self) -> wtypes.WTuple | ErrorMessage:
        item_wtypes = []
        for i in self.items:
            match i.wtype:
                case wtypes.WType() as wtype:
                    item_wtypes.append(wtype)
                case ErrorMessage() as err:
                    return err
                case other:
                    typing.assert_never(other)
        return wtypes.WTuple(
            types=item_wtypes,
            source_location=self.source_location,
        )

NamedTupleBaseType: typing.Final[PyType] = _BaseType(name="typing.NamedTuple")

@attrs.frozen(kw_only=True, order=False)
class NamedTupleType(TupleType, RuntimeType):
    fields: immutabledict[str, PyType] = attrs.field(converter=immutabledict)
    items: tuple[PyType, ...] = attrs.field(init=False)
    generic: None = attrs.field(default=None, init=False)
    bases: tuple[PyType, ...] = attrs.field(default=(NamedTupleBaseType,), init=False)
    mro: tuple[PyType, ...] = attrs.field(default=(NamedTupleBaseType,), init=False)
    desc: str | None = None
    wtype: wtypes.WTuple = attrs.field(init=False)

    @items.default
    def _items(self) -> tuple[PyType, ...]:
        return tuple(self.fields.values())

    @wtype.default
    def _wtype(self) -> wtypes.WTuple:
        unnamed_type = super().wtype
        if isinstance(unnamed_type, str):
            raise CodeError(unnamed_type, self.source_location)
        return attrs.evolve(
            unnamed_type,
            name=self.name,
            names=tuple(self.fields),
            source_location=self.source_location,
            desc=self.desc,
        )

@attrs.frozen(order=False)
class SequenceType(PyType, abc.ABC):
    items: PyType

@typing.final
@attrs.frozen(order=False)
class ArrayType(SequenceType, RuntimeType):
    size: int | None
    wtype: wtypes.WType
    # convenience accessors
    items_wtype: wtypes.WType

@typing.final
@attrs.frozen(order=False)
class StorageProxyType(RuntimeType):
    content: PyType
    wtype: wtypes.WType
    # convenience accessors
    content_wtype: wtypes.WType

@typing.final
@attrs.frozen(order=False)
class StorageMapProxyType(RuntimeType):
    generic: PyType
    key: PyType
    content: PyType
    wtype: wtypes.WType
    # convenience accessors
    key_wtype: wtypes.WType
    content_wtype: wtypes.WType

@typing.final
@attrs.frozen
class FuncArg:
    type: PyType
    name: str | None
    kind: ArgKind

@typing.final
@attrs.frozen(kw_only=True, order=False)
class FuncType(PyType):
    ret_type: PyType
    args: tuple[FuncArg, ...] = attrs.field(converter=tuple[FuncArg, ...])
    # static data
    generic: None = None
    bases: tuple[PyType, ...] = attrs.field(default=(), init=False)
    mro: tuple[PyType, ...] = attrs.field(default=(), init=False)

    @typing.override
    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage("function objects are not usable as values")

@typing.final
@attrs.frozen(order=False)
class StaticType(PyType):
    @typing.override
    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage(f"{self} is only usable as a type and cannot be instantiated")

@typing.final
@attrs.frozen(kw_only=True, order=False)
class ContractType(PyType):
    generic: None = attrs.field(default=None, init=False)
    module_name: str
    class_name: str
    name: ContractReference = attrs.field(init=False)
    source_location: SourceLocation

    @name.default
    def _name(self) -> ContractReference:
        return ContractReference(".".join((self.module_name, self.class_name)))

    @typing.override
    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage(f"{self} is only usable as a type and cannot be instantiated")

ObjectType: typing.Final[PyType] = _register_builtin(StaticType(name="builtins.object"))

@typing.final
@attrs.frozen(init=False, order=False)
class StructType(RuntimeType):
    fields: immutabledict[str, PyType] = attrs.field(
        converter=immutabledict, validator=[attrs.validators.min_len(1)]
    )
    frozen: bool
    wtype: wtypes.ARC4Struct | wtypes.WStructType
    source_location: SourceLocation | None
    generic: None = None
    desc: str | None = None

    @cached_property
    def names(self) -> tuple[str, ...]:
        return tuple(self.fields.keys())

    @cached_property
    def types(self) -> tuple[PyType, ...]:
        return tuple(self.fields.values())

    def __init__(
        self,
        *,
        base: PyType,
        name: str,
        desc: str | None,
        fields: Mapping[str, PyType],
        frozen: bool,
        source_location: SourceLocation | None,
    ):
        field_wtypes = {
            name: field_typ.checked_wtype(source_location) for name, field_typ in fields.items()
        }  # TODO: this is a bit of a kludge
        wtype_cls: type[wtypes.ARC4Struct | wtypes.WStructType]
        if base is ARC4StructBaseType:
            wtype_cls = wtypes.ARC4Struct
        elif base is StructBaseType:
            wtype_cls = wtypes.WStructType
        else:
            raise InternalError(f"Unknown struct base type: {base}", source_location)
        wtype = wtype_cls(
            fields=field_wtypes,
            name=name,
            desc=desc,
            frozen=frozen,
            source_location=source_location,
        )
        self.__attrs_init__(
            bases=[base],
            mro=[base],
            name=name,
            desc=desc,
            wtype=wtype,
            fields=fields,
            frozen=frozen,
            source_location=source_location,
        )

@typing.final
@attrs.frozen(order=False)
class _SimpleType(RuntimeType):
    wtype: wtypes.WType

    def __attrs_post_init__(self) -> None:
        _register_builtin(self)

@typing.final
@attrs.frozen(order=False)
class LiteralOnlyType(PyType):
    python_type: type[int | bytes | str]
    name: str = attrs.field(init=False)

    @name.default
    def _name(self) -> str:
        return ".".join((self.python_type.__module__, self.python_type.__qualname__))

    @typing.override
    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage(f"Python literals of type {self} cannot be used as runtime values")

NoneType: typing.Final[RuntimeType] = _SimpleType(name="types.NoneType", wtype=wtypes.void_wtype)
NeverType: typing.Final[PyType] = _SimpleType(name="typing.Never", wtype=wtypes.void_wtype)
IntLiteralType: typing.Final = _register_builtin(LiteralOnlyType(int))
StrLiteralType: typing.Final = _register_builtin(LiteralOnlyType(str))
BytesLiteralType: typing.Final = _register_builtin(LiteralOnlyType(bytes))
BoolType: typing.Final[RuntimeType] = _SimpleType(
    name="builtins.bool",
    wtype=wtypes.bool_wtype,
    bases=[IntLiteralType],
    mro=[IntLiteralType],
)

UInt64Type: typing.Final[RuntimeType] = _SimpleType(
    name="algopy._primitives.UInt64",
    wtype=wtypes.uint64_wtype,
)
BigUIntType: typing.Final[RuntimeType] = _SimpleType(
    name="algopy._primitives.BigUInt",
    wtype=wtypes.biguint_wtype,
)
BytesType: typing.Final[RuntimeType] = _SimpleType(
    name="algopy._primitives.Bytes",
    wtype=wtypes.bytes_wtype,
)
StringType: typing.Final[RuntimeType] = _SimpleType(
    name="algopy._primitives.String",
    wtype=wtypes.string_wtype,
)
AccountType: typing.Final[RuntimeType] = _SimpleType(
    name="algopy._reference.Account",
    wtype=wtypes.account_wtype,
)
AssetType: typing.Final[RuntimeType] = _SimpleType(
    name="algopy._reference.Asset",
    wtype=wtypes.asset_wtype,
)
ApplicationType: typing.Final[RuntimeType] = _SimpleType(
    name="algopy._reference.Application",
    wtype=wtypes.application_wtype,
)

@attrs.frozen(init=False, order=False)
class UInt64EnumType(RuntimeType):
    def __init__(self, name: str):
        self.__attrs_init__(
            name=name,
            bases=[UInt64Type],
            mro=[UInt64Type],
        )
        _register_builtin(self)

    @property
    def wtype(self) -> wtypes.WType:
        return wtypes.uint64_wtype

OnCompleteActionType: typing.Final = UInt64EnumType(
    name="algopy._constants.OnCompleteAction",
)
TransactionTypeType: typing.Final = UInt64EnumType(
    name="algopy._constants.TransactionType",
)
OpUpFeeSourceType: typing.Final = UInt64EnumType(
    name="algopy._util.OpUpFeeSource",
)

ARC4StringType: typing.Final[RuntimeType] = _SimpleType(
    name="algopy.arc4.String",
    wtype=wtypes.arc4_string_alias,
)
ARC4BoolType: typing.Final[RuntimeType] = _SimpleType(
    name="algopy.arc4.Bool",
    wtype=wtypes.arc4_bool_wtype,
)

@attrs.frozen(order=False)
class ARC4UIntNType(RuntimeType):
    bits: int
    wtype: wtypes.ARC4UIntN
    native_type: RuntimeType

def _require_int_literal(
    generic: _GenericType[_TPyType],
    type_arg: PyType,
    source_location: SourceLocation | None,
    *,
    position_qualifier: str = "",
) -> int:
    match type_arg:
        case TypingLiteralType(value=int(value)):
            return value
    if position_qualifier:
        position_qualifier = position_qualifier + " "
    raise CodeError(
        f"{generic} expects a typing.Literal[<int>] as a {position_qualifier}parameter",
        source_location,
    )

def _make_arc4_unsigned_int_parameterise(
    *, native_type: RuntimeType, max_bits: int | None = None
) -> _Parameterise:
    def parameterise(
        self: _GenericType, args: _TypeArgs, source_location: SourceLocation | None
    ) -> ARC4UIntNType:
        try:
            (bits_t,) = args
        except ValueError:
            raise CodeError(
                f"expected a single type parameter, got {len(args)} parameters", source_location
            ) from None
        bits = _require_int_literal(self, bits_t, source_location)
        if (max_bits is not None) and bits > max_bits:
            raise CodeError(f"max bit size of {self} is {max_bits}, got {bits}", source_location)

        name = f"{self.name}[{bits_t.name}]"
        return ARC4UIntNType(
            generic=self,
            name=name,
            bits=bits,
            native_type=native_type,
            wtype=wtypes.ARC4UIntN(n=bits, source_location=source_location),
        )

    return parameterise

GenericARC4UIntNType: typing.Final = _GenericType(
    name="algopy.arc4.UIntN",
    parameterise=_make_arc4_unsigned_int_parameterise(native_type=UInt64Type, max_bits=64),
)
GenericARC4BigUIntNType: typing.Final = _GenericType(
    name="algopy.arc4.BigUIntN",
    parameterise=_make_arc4_unsigned_int_parameterise(native_type=BigUIntType),
)

ARC4UIntN_Aliases: typing.Final = immutabledict[int, ARC4UIntNType](
    {
        (_bits := 2**_exp): _register_builtin(
            (GenericARC4UIntNType if _bits <= 64 else GenericARC4BigUIntNType).parameterise(
                [TypingLiteralType(value=_bits, source_location=None)], source_location=None
            ),
            alias=f"algopy.arc4.UInt{_bits}",
        )
        for _exp in range(3, 10)
    }
)
ARC4ByteType: typing.Final = _register_builtin(
    ARC4UIntNType(
        generic=None,
        name="algopy.arc4.Byte",
        wtype=wtypes.arc4_byte_alias,
        bits=8,
        bases=[ARC4UIntN_Aliases[8]],
        mro=[ARC4UIntN_Aliases[8]],
        native_type=UInt64Type,
    )
)

@attrs.frozen(order=False)
class ARC4UFixedNxMType(RuntimeType):
    generic: _GenericType
    bits: int
    precision: int
    wtype: wtypes.WType

def _make_arc4_unsigned_fixed_parameterise(*, max_bits: int | None = None) -> _Parameterise:
    def parameterise(
        self: _GenericType, args: _TypeArgs, source_location: SourceLocation | None
    ) -> ARC4UFixedNxMType:
        try:
            bits_t, precision_t = args
        except ValueError:
            raise CodeError(
                f"expected two type parameters, got {len(args)} parameters", source_location
            ) from None
        bits = _require_int_literal(self, bits_t, source_location, position_qualifier="first")
        precision = _require_int_literal(
            self, precision_t, source_location, position_qualifier="second"
        )
        if (max_bits is not None) and bits > max_bits:
            raise CodeError(f"max bit size of {self} is {max_bits}, got {bits}", source_location)

        name = f"{self.name}[{bits_t.name}, {precision_t.name}]"
        return ARC4UFixedNxMType(
            generic=self,
            name=name,
            bits=bits,
            precision=precision,
            wtype=wtypes.ARC4UFixedNxM(n=bits, m=precision, source_location=source_location),
        )

    return parameterise

GenericARC4UFixedNxMType: typing.Final = _GenericType(
    name="algopy.arc4.UFixedNxM",
    parameterise=_make_arc4_unsigned_fixed_parameterise(max_bits=64),
)
GenericARC4BigUFixedNxMType: typing.Final = _GenericType(
    name="algopy.arc4.BigUFixedNxM",
    parameterise=_make_arc4_unsigned_fixed_parameterise(),
)

def _parameterise_arc4_tuple(
    self: _GenericType[ARC4TupleType],  # noqa: ARG001
    args: _TypeArgs,
    source_location: SourceLocation | None,
) -> ARC4TupleType:
    item_wtypes = tuple(arg.checked_wtype(source_location) for arg in args)
    wtype = wtypes.ARC4Tuple(types=item_wtypes, source_location=source_location)
    return ARC4TupleType(items=tuple(args), wtype=wtype, source_location=source_location)

GenericARC4TupleType: typing.Final = _GenericType(
    name="algopy.arc4.Tuple",
    parameterise=_parameterise_arc4_tuple,
)

@typing.final
@attrs.frozen(kw_only=True, order=False)
class ARC4TupleType(TupleLikeType, RuntimeType):
    generic: _GenericType = attrs.field(default=GenericARC4TupleType, init=False)
    name: str = attrs.field(init=False)
    bases: tuple[PyType, ...] = attrs.field(default=(), init=False)
    mro: tuple[PyType, ...] = attrs.field(default=(), init=False)
    wtype: wtypes.ARC4Tuple

    @name.default
    def _name(self) -> str:
        return f"{self.generic.name}[{', '.join(pyt.name for pyt in self.items)}]"

CompiledContractType: typing.Final = _register_builtin(
    NamedTupleType(
        name="algopy._compiled.CompiledContract",
        fields={
            "approval_program": GenericTupleType.parameterise([BytesType, BytesType], None),
            "clear_state_program": GenericTupleType.parameterise([BytesType, BytesType], None),
            "extra_program_pages": UInt64Type,
            "global_uints": UInt64Type,
            "global_bytes": UInt64Type,
            "local_uints": UInt64Type,
            "local_bytes": UInt64Type,
        },
        source_location=None,
    )
)
CompiledLogicSigType: typing.Final = _register_builtin(
    NamedTupleType(
        name="algopy._compiled.CompiledLogicSig",
        fields={
            "account": AccountType,
        },
        source_location=None,
    )
)

@typing.final
@attrs.frozen(order=False)
class VariadicTupleType(SequenceType):
    items: PyType
    generic: _GenericType = attrs.field(default=GenericTupleType, init=False)
    bases: tuple[PyType, ...] = attrs.field(default=(), init=False)
    mro: tuple[PyType, ...] = attrs.field(default=(), init=False)
    name: str = attrs.field(init=False)

    @name.default
    def _name_factory(self) -> str:
        return f"{self.generic.name}[{self.items.name}, ...]"

    @typing.override
    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage("variadic tuples cannot be used as runtime values")

def _make_array_parameterise(
    typ: type[wtypes.WArray | wtypes.ARC4DynamicArray],
) -> _Parameterise[ArrayType]:
    def parameterise(
        self: _GenericType[ArrayType], args: _TypeArgs, source_location: SourceLocation | None
    ) -> ArrayType:
        try:
            (arg,) = args
        except ValueError:
            raise CodeError(
                f"expected a single type parameter, got {len(args)} parameters", source_location
            ) from None
        name = f"{self.name}[{arg.name}]"
        items_wtype = arg.checked_wtype(source_location)

        return ArrayType(
            generic=self,
            name=name,
            size=None,
            items=arg,
            wtype=typ(element_type=items_wtype, source_location=source_location),
            items_wtype=items_wtype,
        )

    return parameterise

GenericArrayType: typing.Final = _GenericType(
    name="algopy._array.Array",
    parameterise=_make_array_parameterise(wtypes.WArray),
)

GenericARC4DynamicArrayType: typing.Final = _GenericType(
    name="algopy.arc4.DynamicArray",
    parameterise=_make_array_parameterise(wtypes.ARC4DynamicArray),
)
ARC4DynamicBytesType: typing.Final = _register_builtin(
    ArrayType(
        name="algopy.arc4.DynamicBytes",
        wtype=wtypes.ARC4DynamicArray(
            element_type=ARC4ByteType.wtype,
            native_type=wtypes.bytes_wtype,
            source_location=None,
        ),
        size=None,
        items=ARC4ByteType,
        items_wtype=ARC4ByteType.wtype,
        bases=[GenericARC4DynamicArrayType.parameterise([ARC4ByteType], source_location=None)],
        mro=[GenericARC4DynamicArrayType.parameterise([ARC4ByteType], source_location=None)],
    )
)

def _parameterise_arc4_static_array(
    self: _GenericType[ArrayType], args: _TypeArgs, source_location: SourceLocation | None
) -> ArrayType:
    try:
        items, size_t = args
    except ValueError:
        raise CodeError(
            f"expected a single type parameter, got {len(args)} parameters", source_location
        ) from None
    size = _require_int_literal(self, size_t, source_location, position_qualifier="second")
    if size < 0:
        raise CodeError("array size should be non-negative", source_location)

    name = f"{self.name}[{items.name}, {size_t.name}]"
    items_wtype = items.checked_wtype(source_location)

    return ArrayType(
        generic=self,
        name=name,
        size=size,
        items=items,
        wtype=wtypes.ARC4StaticArray(
            element_type=items_wtype, array_size=size, source_location=source_location
        ),
        items_wtype=items_wtype,
    )

GenericARC4StaticArrayType: typing.Final = _GenericType(
    name="algopy.arc4.StaticArray",
    parameterise=_parameterise_arc4_static_array,
)
ARC4AddressType: typing.Final = _register_builtin(
    ArrayType(
        name="algopy.arc4.Address",
        wtype=wtypes.arc4_address_alias,
        size=32,
        generic=None,
        items=ARC4ByteType,
        items_wtype=ARC4ByteType.wtype,
        bases=[
            GenericARC4StaticArrayType.parameterise(
                [ARC4ByteType, TypingLiteralType(value=32, source_location=None)],
                source_location=None,
            )
        ],
        mro=[
            GenericARC4StaticArrayType.parameterise(
                [ARC4ByteType, TypingLiteralType(value=32, source_location=None)],
                source_location=None,
            )
        ],
    )
)

def _make_storage_parameterise(wtype: wtypes.WType) -> _Parameterise[StorageProxyType]:
    def parameterise(
        self: _GenericType[StorageProxyType],
        args: _TypeArgs,
        source_location: SourceLocation | None,
    ) -> StorageProxyType:
        try:
            (arg,) = args
        except ValueError:
            raise CodeError(
                f"expected a single type parameter, got {len(args)} parameters", source_location
            ) from None
        name = f"{self.name}[{arg.name}]"
        content_wtype = arg.checked_wtype(source_location)
        return StorageProxyType(
            generic=self,
            name=name,
            content=arg,
            wtype=wtype,
            content_wtype=content_wtype,
        )

    return parameterise

def _parameterise_storage_map(
    self: _GenericType[StorageMapProxyType],
    args: _TypeArgs,
    source_location: SourceLocation | None,
) -> StorageMapProxyType:
    try:
        key, content = args
    except ValueError:
        raise CodeError(
            f"expected two type parameters, got {len(args)} parameters", source_location
        ) from None
    name = f"{self.name}[{key.name}, {content.name}]"
    key_wtype = key.checked_wtype(source_location)
    content_wtype = content.checked_wtype(source_location)
    return StorageMapProxyType(
        generic=self,
        name=name,
        key=key,
        content=content,
        wtype=wtypes.box_key,
        key_wtype=key_wtype,
        content_wtype=content_wtype,
    )

GenericGlobalStateType: typing.Final = _GenericType(
    name="algopy._state.GlobalState",
    parameterise=_make_storage_parameterise(wtypes.state_key),
)
GenericLocalStateType: typing.Final = _GenericType(
    name="algopy._state.LocalState",
    parameterise=_make_storage_parameterise(wtypes.state_key),
)
GenericBoxType: typing.Final = _GenericType(
    name="algopy._box.Box",
    parameterise=_make_storage_parameterise(wtypes.box_key),
)
BoxRefType: typing.Final = _register_builtin(
    StorageProxyType(
        name="algopy._box.BoxRef",
        content=BytesType,
        content_wtype=BytesType.wtype,
        wtype=wtypes.box_key,
        generic=None,
    )
)

GenericBoxMapType: typing.Final = _GenericType(
    name="algopy._box.BoxMap",
    parameterise=_parameterise_storage_map,
)

@attrs.frozen(order=False)
class TransactionRelatedType(PyType, abc.ABC):
    transaction_type: TransactionType | None
    """None implies "any" type, which may be considered as either union or an intersection"""

    def __attrs_post_init__(self) -> None:
        _register_builtin(self)

@typing.final
@attrs.frozen(order=False)
class GroupTransactionType(TransactionRelatedType):
    wtype: wtypes.WGroupTransaction

@typing.final
@attrs.frozen(order=False)
class InnerTransactionFieldsetType(TransactionRelatedType):
    wtype: wtypes.WInnerTransactionFields

@typing.final
@attrs.frozen(order=False)
class InnerTransactionResultType(TransactionRelatedType):
    wtype: wtypes.WInnerTransaction

GroupTransactionBaseType: typing.Final = GroupTransactionType(
    name="algopy.gtxn.TransactionBase",
    wtype=wtypes.WGroupTransaction(name="group_transaction_base", transaction_type=None),
    transaction_type=None,
)

def _make_gtxn_type(kind: TransactionType | None) -> GroupTransactionType:
    if kind is None:
        cls_name = "Transaction"
    else:
        cls_name = f"{_TXN_TYPE_NAMES[kind]}Transaction"
    stub_name = f"algopy.gtxn.{cls_name}"
    return GroupTransactionType(
        name=stub_name,
        transaction_type=kind,
        wtype=wtypes.WGroupTransaction.from_type(kind),
        bases=[GroupTransactionBaseType],
        mro=[GroupTransactionBaseType],
    )

def _make_itxn_fieldset_type(kind: TransactionType | None) -> InnerTransactionFieldsetType:
    if kind is None:
        cls_name = "InnerTransaction"
    else:
        cls_name = _TXN_TYPE_NAMES[kind]
    stub_name = f"algopy.itxn.{cls_name}"
    return InnerTransactionFieldsetType(
        name=stub_name,
        transaction_type=kind,
        wtype=wtypes.WInnerTransactionFields.from_type(kind),
    )

def _make_itxn_result_type(kind: TransactionType | None) -> InnerTransactionResultType:
    if kind is None:
        cls_name = "InnerTransactionResult"
    else:
        cls_name = f"{_TXN_TYPE_NAMES[kind]}InnerTransaction"
    stub_name = f"algopy.itxn.{cls_name}"
    return InnerTransactionResultType(
        name=stub_name,
        transaction_type=kind,
        wtype=wtypes.WInnerTransaction.from_type(kind),
    )

_TXN_TYPE_NAMES: typing.Final[Mapping[TransactionType, str]] = {
    TransactionType.pay: "Payment",
    TransactionType.keyreg: "KeyRegistration",
    TransactionType.acfg: "AssetConfig",
    TransactionType.axfer: "AssetTransfer",
    TransactionType.afrz: "AssetFreeze",
    TransactionType.appl: "ApplicationCall",
}

_all_txn_kinds: typing.Final[Sequence[TransactionType | None]] = [
    None,
    *TransactionType,
]
GroupTransactionTypes: typing.Final[Mapping[TransactionType | None, GroupTransactionType]] = {
    kind: _make_gtxn_type(kind) for kind in _all_txn_kinds
}
InnerTransactionFieldsetTypes: typing.Final[
    Mapping[TransactionType | None, InnerTransactionFieldsetType]
] = {kind: _make_itxn_fieldset_type(kind) for kind in _all_txn_kinds}
InnerTransactionResultTypes: typing.Final[
    Mapping[TransactionType | None, InnerTransactionResultType]
] = {kind: _make_itxn_result_type(kind) for kind in _all_txn_kinds}

@attrs.frozen(kw_only=True, order=False)
class _CompileTimeType(PyType):
    _wtype_error: str

    @typing.override
    @property
    def wtype(self) -> ErrorMessage:
        msg = self._wtype_error.format(self=self)
        return ErrorMessage(msg)

    def __attrs_post_init__(self) -> None:
        _register_builtin(self)

BytesBackedType: typing.Final[PyType] = _CompileTimeType(
    name="algopy._primitives.BytesBacked",
    wtype_error="{self} is not usable as a runtime type",
)

@attrs.frozen(kw_only=True, order=False)
class IntrinsicEnumType(PyType):
    generic: None = attrs.field(default=None, init=False)
    bases: tuple[PyType, ...] = attrs.field(
        default=(StrLiteralType,),  # strictly true, but not sure if we want this?
        init=False,
    )
    mro: tuple[PyType, ...] = attrs.field(default=(StrLiteralType,), init=False)
    members: immutabledict[str, str] = attrs.field(converter=immutabledict)

    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage(f"{self} is only valid as a literal argument to an algopy.op function")

def _make_intrinsic_enum_types() -> Sequence[IntrinsicEnumType]:
    from puyapy.awst_build.intrinsic_data import ENUM_CLASSES

    return [
        _register_builtin(
            IntrinsicEnumType(
                name="".join((constants.ALGOPY_OP_PREFIX, cls_name)),
                members=cls_members,
            )
        )
        for cls_name, cls_members in ENUM_CLASSES.items()
    ]

@attrs.frozen(kw_only=True, order=False)
class IntrinsicNamespaceType(PyType):
    generic: None = attrs.field(default=None, init=False)
    bases: tuple[PyType, ...] = attrs.field(default=(), init=False)
    mro: tuple[PyType, ...] = attrs.field(default=(), init=False)
    members: immutabledict[str, PropertyOpMapping | OpMappingWithOverloads] = attrs.field(
        converter=immutabledict
    )

    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage(f"{self} is a namespace type only and not usable at runtime")

def _make_intrinsic_namespace_types() -> Sequence[IntrinsicNamespaceType]:
    from puyapy.awst_build.intrinsic_data import NAMESPACE_CLASSES

    return [
        _register_builtin(
            IntrinsicNamespaceType(
                name="".join((constants.ALGOPY_OP_PREFIX, cls_name)),
                members=cls_members,
            )
        )
        for cls_name, cls_members in NAMESPACE_CLASSES.items()
    ]

OpEnumTypes: typing.Final = _make_intrinsic_enum_types()
OpNamespaceTypes: typing.Final = _make_intrinsic_namespace_types()

StateTotalsType: typing.Final[PyType] = _CompileTimeType(
    name="algopy._contract.StateTotals",
    wtype_error="{self} is only usable in a class options context",
)

urangeType: typing.Final[PyType] = _CompileTimeType(  # noqa: N816
    name=constants.URANGE,
    wtype_error="{self} is not usable at runtime",
)

def _parameterise_any_compile_time(
    self: _GenericType,
    args: _TypeArgs,
    source_location: SourceLocation | None,  # noqa: ARG001
) -> PyType:
    arg_names = [arg.name for arg in args]
    name = f"{self.name}[{', '.join(arg_names)}]"
    return _CompileTimeType(name=name, generic=self, wtype_error="{self} is not usable at runtime")

reversedGenericType: typing.Final[PyType] = _GenericType(  # noqa: N816
    name="builtins.reversed",
    parameterise=_parameterise_any_compile_time,
)
uenumerateGenericType: typing.Final[PyType] = _GenericType(  # noqa: N816
    name="algopy._unsigned_builtins.uenumerate",
    parameterise=_parameterise_any_compile_time,
)

LogicSigType: typing.Final[PyType] = _CompileTimeType(
    name="algopy._logic_sig.LogicSig",
    wtype_error="{self} is only usable in a static context",
)

ContractBaseType: typing.Final[PyType] = _BaseType(name=constants.CONTRACT_BASE)
ARC4ContractBaseType: typing.Final[PyType] = _BaseType(
    name=constants.ARC4_CONTRACT_BASE,
    bases=[ContractBaseType],
    mro=[ContractBaseType],
)
ARC4ClientBaseType: typing.Final[PyType] = _BaseType(name="algopy.arc4.ARC4Client")
ARC4StructBaseType: typing.Final[PyType] = _BaseType(name="algopy.arc4.Struct")
StructBaseType: typing.Final[PyType] = _BaseType(name="algopy._struct.Struct")

@typing.final
@attrs.frozen(order=False)
class PseudoGenericFunctionType(PyType):
    return_type: PyType
    generic: _GenericType[PseudoGenericFunctionType]
    bases: tuple[PyType, ...] = attrs.field(default=(), init=False)
    mro: tuple[PyType, ...] = attrs.field(default=(), init=False)

    @property
    def wtype(self) -> ErrorMessage:
        return ErrorMessage(f"{self} is not a value")

def _parameterise_pseudo_generic_function_type(
    self: _GenericType[PseudoGenericFunctionType],
    args: _TypeArgs,
    source_location: SourceLocation | None,
) -> PseudoGenericFunctionType:
    try:
        (arg,) = args
    except ValueError:
        raise CodeError(
            f"expected a single type parameter, got {len(args)} parameters", source_location
        ) from None
    name = f"{self.name}[{arg.name}]"
    return PseudoGenericFunctionType(generic=self, name=name, return_type=arg)

GenericABICallWithReturnType: typing.Final[PyType] = _GenericType(
    name="algopy.arc4._ABICallWithReturnProtocol",
    parameterise=_parameterise_pseudo_generic_function_type,
)
GenericTemplateVarType: typing.Final[PyType] = _GenericType(
    name="algopy._template_variables._TemplateVarMethod",
    parameterise=_parameterise_pseudo_generic_function_type,
)