package p.studio.compiler.pbs.semantics;

import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.NameTable;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.*;

public final class PbsDeclarationSemanticsValidator {
    private static final String ATTR_HOST = "Host";
    private static final String ATTR_CAPABILITY = "Capability";
    private static final String ATTR_BUILTIN_TYPE = "BuiltinType";
    private static final String ATTR_BUILTIN_CONST = "BuiltinConst";
    private static final String ATTR_INTRINSIC_CALL = "IntrinsicCall";
    private static final String ATTR_INIT_ALLOWED = "InitAllowed";
    private static final String ATTR_ASSET_LOWERING = "AssetLowering";

    private static final Set<String> RESERVED_ATTRIBUTES = Set.of(
            ATTR_HOST,
            ATTR_CAPABILITY,
            ATTR_BUILTIN_TYPE,
            ATTR_BUILTIN_CONST,
            ATTR_INTRINSIC_CALL,
            ATTR_INIT_ALLOWED,
            ATTR_ASSET_LOWERING);

    private final NameTable nameTable;
    private final PbsConstSemanticsValidator constSemanticsValidator = new PbsConstSemanticsValidator();
    private final PbsGlobalSemanticsValidator globalSemanticsValidator = new PbsGlobalSemanticsValidator();

    public PbsDeclarationSemanticsValidator(final NameTable nameTable) {
        this.nameTable = nameTable == null ? new NameTable() : nameTable;
    }

    public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) {
        validate(ast, SourceKind.PROJECT, diagnostics);
    }

    public void validate(
            final PbsAst.File ast,
            final SourceKind sourceKind,
            final DiagnosticSink diagnostics) {
        validate(ast, sourceKind, ModuleId.none(), ReadOnlyList.empty(), diagnostics);
    }

    public void validate(
            final PbsAst.File ast,
            final SourceKind sourceKind,
            final ModuleId currentModuleId,
            final ReadOnlyList<PbsFrontendCompiler.ImportedGlobalSurface> importedGlobals,
            final DiagnosticSink diagnostics) {
        final var binder = new PbsNamespaceBinder(nameTable, diagnostics);
        final var rules = new PbsDeclarationRuleValidator(nameTable, diagnostics);
        final var interfaceModule = sourceKind == SourceKind.SDK_INTERFACE;

        for (final var topDecl : ast.topDecls()) {
            if (topDecl instanceof PbsAst.FunctionDecl functionDecl) {
                if (interfaceModule) {
                    reportInterfaceNonDeclarativeDecl(
                            functionDecl.span(),
                            "Top-level functions are not allowed in interface modules",
                            diagnostics);
                }
                validateCallable(
                        PbsCallableScope.topLevelFunctions(),
                        functionDecl.name(),
                        functionDecl.parameters(),
                        functionDecl.returnKind(),
                        functionDecl.returnType(),
                        functionDecl.resultErrorType(),
                        functionDecl.span(),
                        false,
                        binder,
                        rules);
                binder.registerVisibleTopLevelSurface(functionDecl.name(), functionDecl.span(), "function");
                continue;
            }

            if (topDecl instanceof PbsAst.StructDecl structDecl) {
                binder.registerType(structDecl.name(), structDecl.span(), "struct");
                if (interfaceModule && (structDecl.hasBody() || !structDecl.methods().isEmpty() || !structDecl.ctors().isEmpty())) {
                    reportInterfaceNonDeclarativeDecl(
                            structDecl.span(),
                            "Interface modules must not declare executable struct bodies",
                            diagnostics);
                }
                validateStructDeclaration(structDecl, binder, rules, diagnostics);
                continue;
            }

            if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
                binder.registerType(serviceDecl.name(), serviceDecl.span(), "service");
                binder.registerValue(serviceDecl.name(), serviceDecl.span(), "service singleton");
                binder.registerVisibleTopLevelSurface(serviceDecl.name(), serviceDecl.span(), "service");
                validateServiceDeclaration(serviceDecl, binder, rules, interfaceModule, diagnostics);
                continue;
            }

            if (topDecl instanceof PbsAst.ContractDecl contractDecl) {
                binder.registerType(contractDecl.name(), contractDecl.span(), "contract");
                if (interfaceModule) {
                    for (final var signature : contractDecl.signatures()) {
                        validateReservedAttributeTarget(signature.attributes(), "contract signature", diagnostics);
                    }
                }
                validateContractDeclaration(contractDecl, binder, rules);
                continue;
            }

            if (topDecl instanceof PbsAst.HostDecl hostDecl) {
                binder.registerHostOwner(hostDecl.name(), hostDecl.span(), "host owner");
                validateHostDeclaration(hostDecl, binder, rules, interfaceModule, diagnostics);
                continue;
            }

            if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) {
                binder.registerType(builtinTypeDecl.name(), builtinTypeDecl.span(), "builtin type");
                validateBuiltinTypeDeclaration(builtinTypeDecl, binder, rules, interfaceModule, diagnostics);
                continue;
            }

            if (topDecl instanceof PbsAst.ErrorDecl errorDecl) {
                binder.registerType(errorDecl.name(), errorDecl.span(), "error");
                rules.validateErrorDeclaration(errorDecl);
                continue;
            }

            if (topDecl instanceof PbsAst.EnumDecl enumDecl) {
                binder.registerType(enumDecl.name(), enumDecl.span(), "enum");
                rules.validateEnumDeclaration(enumDecl);
                continue;
            }

            if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) {
                binder.registerType(callbackDecl.name(), callbackDecl.span(), "callback");
                rules.validateCallbackDeclaration(callbackDecl);
                continue;
            }

            if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
                binder.registerValue(globalDecl.name(), globalDecl.span(), "global");
                binder.registerVisibleTopLevelSurface(globalDecl.name(), globalDecl.span(), "global");
                rules.validateGlobalDeclaration(globalDecl);
                continue;
            }

            if (topDecl instanceof PbsAst.ConstDecl constDecl) {
                binder.registerValue(constDecl.name(), constDecl.span(), "const");
                binder.registerVisibleTopLevelSurface(constDecl.name(), constDecl.span(), "const");
                final var allowMissingInitializer = validateConstDeclarationAttributes(constDecl, interfaceModule, diagnostics);
                rules.validateConstDeclaration(constDecl, !allowMissingInitializer);
                continue;
            }

            if (topDecl instanceof PbsAst.ImplementsDecl implementsDecl) {
                if (interfaceModule) {
                    reportInterfaceNonDeclarativeDecl(
                            implementsDecl.span(),
                            "Interface modules must not declare executable 'implements' blocks",
                            diagnostics);
                }
                validateImplementsDeclaration(implementsDecl, binder, rules);
            }
        }

        constSemanticsValidator.validate(ast, diagnostics);
        globalSemanticsValidator.validate(ast, currentModuleId, importedGlobals, diagnostics);
        if (interfaceModule) {
            PbsBuiltinLayoutResolver.resolve(ast, diagnostics);
        }
    }

    private void validateStructDeclaration(
            final PbsAst.StructDecl structDecl,
            final PbsNamespaceBinder binder,
            final PbsDeclarationRuleValidator rules,
            final DiagnosticSink diagnostics) {
        final var ownerNameId = nameTable.register(structDecl.name());
        rules.validateTypeSurfaces(
                structDecl.fields().stream().map(PbsAst.StructField::typeRef).toList(),
                "struct '%s'".formatted(structDecl.name()),
                false);
        final var methodScope = PbsCallableScope.structMethods(ownerNameId);
        for (final var method : structDecl.methods()) {
            validateCallable(
                    methodScope,
                    method.name(),
                    method.parameters(),
                    method.returnKind(),
                    method.returnType(),
                    method.resultErrorType(),
                    method.span(),
                    true,
                    binder,
                    rules);
        }

        final var ctorScope = PbsCallableScope.structCtors(ownerNameId);
        for (final var ctor : structDecl.ctors()) {
            rules.validateParameters(ctor.parameters(), "ctor '%s'".formatted(ctor.name()));
            rules.validateTypeSurfaces(
                    ctor.parameters().stream().map(PbsAst.Parameter::typeRef).toList(),
                    "ctor '%s'".formatted(ctor.name()),
                    true);

            binder.registerCtor(ctorScope, ctor.name(), PbsCallableShape.ctorShapeKey(ctor.parameters()), ctor.span());
            if (PbsCtorReturnScanner.containsReturnStatement(ctor.body())) {
                p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                        PbsSemanticsErrors.E_SEM_INVALID_RETURN_INSIDE_CTOR.name(),
                        "Constructors cannot contain 'return' statements",
                        ctor.span());
            }
        }
    }

    private void validateServiceDeclaration(
            final PbsAst.ServiceDecl serviceDecl,
            final PbsNamespaceBinder binder,
            final PbsDeclarationRuleValidator rules,
            final boolean interfaceModule,
            final DiagnosticSink diagnostics) {
        final var methodScope = PbsCallableScope.serviceMethods(nameTable.register(serviceDecl.name()));
        for (final var method : serviceDecl.methods()) {
            validateCallable(
                    methodScope,
                    method.name(),
                    method.parameters(),
                    method.returnKind(),
                    method.returnType(),
                    method.resultErrorType(),
                    method.span(),
                    true,
                    binder,
                    rules);
            if (interfaceModule) {
                validateSdkExecutableServiceMethodBody(method, diagnostics);
            }
        }
    }

    private void validateSdkExecutableServiceMethodBody(
            final PbsAst.FunctionDecl method,
            final DiagnosticSink diagnostics) {
        validateSdkExecutableBlock(method.body(), diagnostics);
    }

    private void validateSdkExecutableBlock(
            final PbsAst.Block block,
            final DiagnosticSink diagnostics) {
        if (block == null) {
            return;
        }
        for (final var statement : block.statements()) {
            validateSdkExecutableStatement(statement, diagnostics);
        }
        if (block.tailExpression() != null) {
            reportUnsupportedSdkExecutableConstruct(
                    block.tailExpression().span(),
                    "SDK executable service bodies do not support block tail expressions in the initial restricted subset",
                    diagnostics);
        }
    }

    private void validateSdkExecutableStatement(
            final PbsAst.Statement statement,
            final DiagnosticSink diagnostics) {
        switch (statement) {
            case PbsAst.LetStatement letStatement ->
                    validateSdkExecutableExpression(letStatement.initializer(), diagnostics);
            case PbsAst.AssignStatement assignStatement ->
                    validateSdkExecutableExpression(assignStatement.value(), diagnostics);
            case PbsAst.ReturnStatement returnStatement ->
                    validateSdkExecutableExpression(returnStatement.value(), diagnostics);
            case PbsAst.ExpressionStatement expressionStatement ->
                    validateSdkExecutableExpression(expressionStatement.expression(), diagnostics);
            case PbsAst.IfStatement ignored ->
                    reportUnsupportedSdkExecutableConstruct(
                            statement.span(),
                            "SDK executable service bodies do not support if statements in the initial restricted subset",
                            diagnostics);
            case PbsAst.ForStatement ignored ->
                    reportUnsupportedSdkExecutableConstruct(
                            statement.span(),
                            "SDK executable service bodies do not support for statements in the initial restricted subset",
                            diagnostics);
            case PbsAst.WhileStatement ignored ->
                    reportUnsupportedSdkExecutableConstruct(
                            statement.span(),
                            "SDK executable service bodies do not support while statements in the initial restricted subset",
                            diagnostics);
            case PbsAst.BreakStatement ignored ->
                    reportUnsupportedSdkExecutableConstruct(
                            statement.span(),
                            "SDK executable service bodies do not support break statements in the initial restricted subset",
                            diagnostics);
            case PbsAst.ContinueStatement ignored ->
                    reportUnsupportedSdkExecutableConstruct(
                            statement.span(),
                            "SDK executable service bodies do not support continue statements in the initial restricted subset",
                            diagnostics);
        }
    }

    private void validateSdkExecutableExpression(
            final PbsAst.Expression expression,
            final DiagnosticSink diagnostics) {
        if (expression == null) {
            return;
        }
        switch (expression) {
            case PbsAst.IdentifierExpr ignored -> {
            }
            case PbsAst.IntLiteralExpr ignored -> {
            }
            case PbsAst.FloatLiteralExpr ignored -> {
            }
            case PbsAst.BoundedLiteralExpr ignored -> {
            }
            case PbsAst.StringLiteralExpr ignored -> {
            }
            case PbsAst.BoolLiteralExpr ignored -> {
            }
            case PbsAst.ThisExpr ignored -> {
            }
            case PbsAst.UnitExpr ignored -> {
            }
            case PbsAst.NoneExpr ignored -> {
            }
            case PbsAst.UnaryExpr unaryExpr ->
                    validateSdkExecutableExpression(unaryExpr.expression(), diagnostics);
            case PbsAst.BinaryExpr binaryExpr -> {
                validateSdkExecutableExpression(binaryExpr.left(), diagnostics);
                validateSdkExecutableExpression(binaryExpr.right(), diagnostics);
            }
            case PbsAst.CallExpr callExpr -> {
                validateSdkExecutableExpression(callExpr.callee(), diagnostics);
                for (final var argument : callExpr.arguments()) {
                    validateSdkExecutableExpression(argument, diagnostics);
                }
            }
            case PbsAst.MemberExpr memberExpr ->
                    validateSdkExecutableExpression(memberExpr.receiver(), diagnostics);
            case PbsAst.GroupExpr groupExpr ->
                    validateSdkExecutableExpression(groupExpr.expression(), diagnostics);
            case PbsAst.NewExpr newExpr -> {
                for (final var argument : newExpr.arguments()) {
                    validateSdkExecutableExpression(argument, diagnostics);
                }
            }
            case PbsAst.TupleExpr tupleExpr -> {
                for (final var item : tupleExpr.items()) {
                    validateSdkExecutableExpression(item.expression(), diagnostics);
                }
            }
            default -> reportUnsupportedSdkExecutableConstruct(
                    expression.span(),
                    "SDK executable service bodies use an unsupported expression form in the initial restricted subset",
                    diagnostics);
        }
    }

    private void reportUnsupportedSdkExecutableConstruct(
            final Span span,
            final String message,
            final DiagnosticSink diagnostics) {
        p.studio.compiler.source.diagnostics.Diagnostics.error(
                diagnostics,
                PbsSemanticsErrors.E_SEM_SDK_EXEC_SERVICE_UNSUPPORTED_CONSTRUCT.name(),
                message,
                span);
    }

    private void validateContractDeclaration(
            final PbsAst.ContractDecl contractDecl,
            final PbsNamespaceBinder binder,
            final PbsDeclarationRuleValidator rules) {
        final var signatureScope = PbsCallableScope.contractSignatures(nameTable.register(contractDecl.name()));
        for (final var signature : contractDecl.signatures()) {
            validateCallable(
                    signatureScope,
                    signature.name(),
                    signature.parameters(),
                    signature.returnKind(),
                    signature.returnType(),
                    signature.resultErrorType(),
                    signature.span(),
                    false,
                    binder,
                    rules);
        }
    }

    private void validateHostDeclaration(
            final PbsAst.HostDecl hostDecl,
            final PbsNamespaceBinder binder,
            final PbsDeclarationRuleValidator rules,
            final boolean interfaceModule,
            final DiagnosticSink diagnostics) {
        if (!interfaceModule) {
            reportInterfaceNonDeclarativeDecl(
                    hostDecl.span(),
                    "'declare host' is valid only in interface-module sources",
                    diagnostics);
            return;
        }

        final var signatureScope = PbsCallableScope.hostMethods(nameTable.register(hostDecl.name()));
        for (final var signature : hostDecl.signatures()) {
            validateCallable(
                    signatureScope,
                    signature.name(),
                    signature.parameters(),
                    signature.returnKind(),
                    signature.returnType(),
                    signature.resultErrorType(),
                    signature.span(),
                    false,
                    binder,
                    rules);
            validateHostSignatureAttributes(signature, diagnostics);
        }
    }

    private void validateBuiltinTypeDeclaration(
            final PbsAst.BuiltinTypeDecl builtinTypeDecl,
            final PbsNamespaceBinder binder,
            final PbsDeclarationRuleValidator rules,
            final boolean interfaceModule,
            final DiagnosticSink diagnostics) {
        if (!interfaceModule) {
            reportInterfaceNonDeclarativeDecl(
                    builtinTypeDecl.span(),
                    "'declare builtin type' is valid only in interface-module sources",
                    diagnostics);
            return;
        }

        validateBuiltinTypeAttribute(builtinTypeDecl, diagnostics);

        final var fieldTypes = new ArrayList<PbsAst.TypeRef>(builtinTypeDecl.fields().size());
        for (final var field : builtinTypeDecl.fields()) {
            fieldTypes.add(field.typeRef());
            validateBuiltinFieldAttributes(field, diagnostics);
        }
        rules.validateTypeSurfaces(
                fieldTypes,
                "builtin type '%s'".formatted(builtinTypeDecl.name()),
                false);

        final var methodScope = PbsCallableScope.builtinMethods(nameTable.register(builtinTypeDecl.name()));
        for (final var signature : builtinTypeDecl.signatures()) {
            validateCallable(
                    methodScope,
                    signature.name(),
                    signature.parameters(),
                    signature.returnKind(),
                    signature.returnType(),
                    signature.resultErrorType(),
                    signature.span(),
                    false,
                    binder,
                    rules);
            validateIntrinsicCallAttributes(signature, diagnostics);
        }
    }

    private void validateImplementsDeclaration(
            final PbsAst.ImplementsDecl implementsDecl,
            final PbsNamespaceBinder binder,
            final PbsDeclarationRuleValidator rules) {
        final var ownerNameId = nameTable.register(implementsDecl.ownerName() + ":" + implementsDecl.contractName());
        final var scope = PbsCallableScope.implementsMethods(ownerNameId);
        for (final var method : implementsDecl.methods()) {
            validateCallable(
                    scope,
                    method.name(),
                    method.parameters(),
                    method.returnKind(),
                    method.returnType(),
                    method.resultErrorType(),
                    method.span(),
                    false,
                    binder,
                    rules);
        }
    }

    private boolean validateConstDeclarationAttributes(
            final PbsAst.ConstDecl constDecl,
            final boolean interfaceModule,
            final DiagnosticSink diagnostics) {
        final var builtinConstAttributes = attributesNamed(constDecl.attributes(), ATTR_BUILTIN_CONST);
        for (final var attribute : constDecl.attributes()) {
            if (!isReservedAttribute(attribute.name())) {
                continue;
            }
            if (ATTR_BUILTIN_CONST.equals(attribute.name())) {
                continue;
            }
            reportInvalidReservedAttributeTarget(
                    attribute,
                    "Attribute '%s' is not valid on const declarations".formatted(attribute.name()),
                    diagnostics);
        }

        if (!interfaceModule && !builtinConstAttributes.isEmpty()) {
            for (final var attribute : builtinConstAttributes) {
                reportInvalidReservedAttributeTarget(
                        attribute,
                        "'BuiltinConst' is valid only in interface-module const declarations",
                        diagnostics);
            }
        }

        if (builtinConstAttributes.size() > 1) {
            for (int i = 1; i < builtinConstAttributes.size(); i++) {
                reportDuplicateReservedAttribute(builtinConstAttributes.get(i), ATTR_BUILTIN_CONST, diagnostics);
            }
        }

        if (builtinConstAttributes.isEmpty()) {
            return false;
        }

        validateBuiltinConstAttributeShape(builtinConstAttributes.getFirst(), diagnostics);
        if (constDecl.initializer() != null) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
                    "Const declaration '%s' with BuiltinConst metadata must omit initializer".formatted(constDecl.name()),
                    constDecl.span());
        }
        return interfaceModule;
    }

    private void validateHostSignatureAttributes(
            final PbsAst.FunctionSignature signature,
            final DiagnosticSink diagnostics) {
        final var hostAttributes = attributesNamed(signature.attributes(), ATTR_HOST);
        final var initAllowedAttributes = attributesNamed(signature.attributes(), ATTR_INIT_ALLOWED);
        final var assetLoweringAttributes = attributesNamed(signature.attributes(), ATTR_ASSET_LOWERING);
        for (final var attribute : signature.attributes()) {
            if (!isReservedAttribute(attribute.name())) {
                continue;
            }
            if (ATTR_HOST.equals(attribute.name())
                    || ATTR_CAPABILITY.equals(attribute.name())
                    || ATTR_INIT_ALLOWED.equals(attribute.name())
                    || ATTR_ASSET_LOWERING.equals(attribute.name())) {
                continue;
            }
            reportInvalidReservedAttributeTarget(
                    attribute,
                    "Attribute '%s' is not valid on host signatures".formatted(attribute.name()),
                    diagnostics);
        }

        if (hostAttributes.isEmpty()) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MISSING_REQUIRED_RESERVED_ATTRIBUTE.name(),
                    "Host signature '%s' must carry exactly one Host attribute".formatted(signature.name()),
                    signature.span());
            return;
        }

        if (hostAttributes.size() > 1) {
            for (int i = 1; i < hostAttributes.size(); i++) {
                reportDuplicateReservedAttribute(hostAttributes.get(i), ATTR_HOST, diagnostics);
            }
        }

        if (initAllowedAttributes.size() > 1) {
            for (int i = 1; i < initAllowedAttributes.size(); i++) {
                reportDuplicateReservedAttribute(initAllowedAttributes.get(i), ATTR_INIT_ALLOWED, diagnostics);
            }
        }

        if (assetLoweringAttributes.size() > 1) {
            for (int i = 1; i < assetLoweringAttributes.size(); i++) {
                reportDuplicateReservedAttribute(assetLoweringAttributes.get(i), ATTR_ASSET_LOWERING, diagnostics);
            }
        }

        validateHostAttributeShape(hostAttributes.getFirst(), diagnostics);
        if (!initAllowedAttributes.isEmpty()) {
            validateInitAllowedAttributeShape(initAllowedAttributes.getFirst(), diagnostics);
        }
        if (!assetLoweringAttributes.isEmpty()) {
            validateAssetLoweringAttributeShape(assetLoweringAttributes.getFirst(), signature, diagnostics);
        }
    }

    private void validateBuiltinTypeAttribute(
            final PbsAst.BuiltinTypeDecl builtinTypeDecl,
            final DiagnosticSink diagnostics) {
        final var builtinTypeAttributes = attributesNamed(builtinTypeDecl.attributes(), ATTR_BUILTIN_TYPE);
        for (final var attribute : builtinTypeDecl.attributes()) {
            if (!isReservedAttribute(attribute.name())) {
                continue;
            }
            if (ATTR_BUILTIN_TYPE.equals(attribute.name())) {
                continue;
            }
            reportInvalidReservedAttributeTarget(
                    attribute,
                    "Attribute '%s' is not valid on builtin type declarations".formatted(attribute.name()),
                    diagnostics);
        }

        if (builtinTypeAttributes.isEmpty()) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MISSING_REQUIRED_RESERVED_ATTRIBUTE.name(),
                    "Builtin type '%s' must carry exactly one BuiltinType attribute".formatted(builtinTypeDecl.name()),
                    builtinTypeDecl.span());
            return;
        }

        if (builtinTypeAttributes.size() > 1) {
            for (int i = 1; i < builtinTypeAttributes.size(); i++) {
                reportDuplicateReservedAttribute(builtinTypeAttributes.get(i), ATTR_BUILTIN_TYPE, diagnostics);
            }
        }

        validateBuiltinTypeAttributeShape(builtinTypeAttributes.getFirst(), diagnostics);
    }

    private void validateBuiltinFieldAttributes(
            final PbsAst.BuiltinFieldDecl field,
            final DiagnosticSink diagnostics) {
        for (final var attribute : field.attributes()) {
            if (!isReservedAttribute(attribute.name())) {
                continue;
            }
            reportInvalidReservedAttributeTarget(
                    attribute,
                    "Attribute '%s' is not valid on builtin fields".formatted(attribute.name()),
                    diagnostics);
        }
    }

    private void validateIntrinsicCallAttributes(
            final PbsAst.FunctionSignature signature,
            final DiagnosticSink diagnostics) {
        final var intrinsicAttributes = attributesNamed(signature.attributes(), ATTR_INTRINSIC_CALL);
        for (final var attribute : signature.attributes()) {
            if (!isReservedAttribute(attribute.name())) {
                continue;
            }
            if (ATTR_INTRINSIC_CALL.equals(attribute.name())) {
                continue;
            }
            reportInvalidReservedAttributeTarget(
                    attribute,
                    "Attribute '%s' is not valid on builtin method signatures".formatted(attribute.name()),
                    diagnostics);
        }

        if (intrinsicAttributes.size() > 1) {
            for (int i = 1; i < intrinsicAttributes.size(); i++) {
                reportDuplicateReservedAttribute(intrinsicAttributes.get(i), ATTR_INTRINSIC_CALL, diagnostics);
            }
        }

        if (!intrinsicAttributes.isEmpty()) {
            validateIntrinsicCallAttributeShape(intrinsicAttributes.getFirst(), diagnostics);
        }
    }

    private void validateHostAttributeShape(
            final PbsAst.Attribute attribute,
            final DiagnosticSink diagnostics) {
        final var args = validateNamedArguments(attribute, Set.of("module", "name", "version"), Set.of(), diagnostics);
        if (args == null) {
            return;
        }

        validateRequiredStringArgument(attribute, args, "module", false, diagnostics);
        validateRequiredStringArgument(attribute, args, "name", false, diagnostics);
        validateRequiredIntArgument(attribute, args, "version", true, diagnostics);
    }

    private void validateBuiltinTypeAttributeShape(
            final PbsAst.Attribute attribute,
            final DiagnosticSink diagnostics) {
        final var args = validateNamedArguments(attribute, Set.of("name", "version"), Set.of(), diagnostics);
        if (args == null) {
            return;
        }

        validateRequiredStringArgument(attribute, args, "name", false, diagnostics);
        validateRequiredIntArgument(attribute, args, "version", true, diagnostics);
    }

    private void validateBuiltinConstAttributeShape(
            final PbsAst.Attribute attribute,
            final DiagnosticSink diagnostics) {
        final var args = validateNamedArguments(attribute, Set.of("target", "name", "version"), Set.of(), diagnostics);
        if (args == null) {
            return;
        }

        validateRequiredStringArgument(attribute, args, "target", false, diagnostics);
        validateRequiredStringArgument(attribute, args, "name", false, diagnostics);
        validateRequiredIntArgument(attribute, args, "version", true, diagnostics);
    }

    private void validateIntrinsicCallAttributeShape(
            final PbsAst.Attribute attribute,
            final DiagnosticSink diagnostics) {
        final var args = validateNamedArguments(attribute, Set.of("name"), Set.of("version"), diagnostics);
        if (args == null) {
            return;
        }

        validateRequiredStringArgument(attribute, args, "name", false, diagnostics);
        if (args.containsKey("version")) {
            validateRequiredIntArgument(attribute, args, "version", true, diagnostics);
        }
    }

    private void validateInitAllowedAttributeShape(
            final PbsAst.Attribute attribute,
            final DiagnosticSink diagnostics) {
        final var args = validateNamedArguments(attribute, Set.of(), Set.of(), diagnostics);
        if (args == null) {
            return;
        }
        if (!args.isEmpty()) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
                    "InitAllowed does not accept arguments",
                    attribute.span());
        }
    }

    private void validateAssetLoweringAttributeShape(
            final PbsAst.Attribute attribute,
            final PbsAst.FunctionSignature signature,
            final DiagnosticSink diagnostics) {
        final var args = validateNamedArguments(attribute, Set.of("param"), Set.of(), diagnostics);
        if (args == null) {
            return;
        }
        validateRequiredIntArgument(attribute, args, "param", false, diagnostics);
        final var value = args.get("param");
        if (!(value instanceof PbsAst.AttributeIntValue intValue)) {
            return;
        }
        final var paramIndex = intValue.value();
        if (paramIndex < 0 || paramIndex >= signature.parameters().size()) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
                    "AssetLowering param index %d is out of range for host signature '%s'".formatted(paramIndex, signature.name()),
                    attribute.span());
            return;
        }
        final var parameter = signature.parameters().get((int) paramIndex);
        final var parameterType = unwrapGroup(parameter.typeRef());
        if (parameterType == null
                || parameterType.kind() != PbsAst.TypeRefKind.SIMPLE
                || !"Addressable".equals(parameterType.name())) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
                    "AssetLowering target parameter '%s' must have Addressable type".formatted(parameter.name()),
                    attribute.span());
        }
    }

    private Map<String, PbsAst.AttributeValue> validateNamedArguments(
            final PbsAst.Attribute attribute,
            final Set<String> requiredNames,
            final Set<String> optionalNames,
            final DiagnosticSink diagnostics) {
        final var allowedNames = new HashSet<String>();
        allowedNames.addAll(requiredNames);
        allowedNames.addAll(optionalNames);

        final var arguments = new HashMap<String, PbsAst.AttributeValue>();
        var malformed = false;
        for (final var argument : attribute.arguments()) {
            if (!allowedNames.contains(argument.name())) {
                malformed = true;
                continue;
            }
            if (arguments.putIfAbsent(argument.name(), argument.value()) != null) {
                malformed = true;
            }
        }

        for (final var requiredName : requiredNames) {
            if (!arguments.containsKey(requiredName)) {
                malformed = true;
            }
        }

        if (malformed) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
                    "Malformed %s attribute argument set".formatted(attribute.name()),
                    attribute.span());
            return null;
        }
        return arguments;
    }

    private void validateRequiredStringArgument(
            final PbsAst.Attribute attribute,
            final Map<String, PbsAst.AttributeValue> arguments,
            final String argumentName,
            final boolean positiveOnly,
            final DiagnosticSink diagnostics) {
        final var value = arguments.get(argumentName);
        if (!(value instanceof PbsAst.AttributeStringValue stringValue)) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
                    "Malformed %s.%s attribute argument".formatted(attribute.name(), argumentName),
                    attribute.span());
            return;
        }

        if (isStringLiteralEmpty(stringValue.value())) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
                    "Attribute %s.%s must be a non-empty string".formatted(attribute.name(), argumentName),
                    attribute.span());
        }

        if (positiveOnly && stringValue.value().isBlank()) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
                    "Attribute %s.%s must be a positive string literal".formatted(attribute.name(), argumentName),
                    attribute.span());
        }
    }

    private void validateRequiredIntArgument(
            final PbsAst.Attribute attribute,
            final Map<String, PbsAst.AttributeValue> arguments,
            final String argumentName,
            final boolean positive,
            final DiagnosticSink diagnostics) {
        final var value = arguments.get(argumentName);
        if (!(value instanceof PbsAst.AttributeIntValue intValue)) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
                    "Malformed %s.%s attribute argument".formatted(attribute.name(), argumentName),
                    attribute.span());
            return;
        }

        final var numericValue = intValue.value();
        final boolean invalid = positive ? numericValue <= 0 : numericValue < 0;
        if (invalid) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
                    "Attribute %s.%s has invalid numeric value".formatted(attribute.name(), argumentName),
                    attribute.span());
        }
    }

    private boolean isStringLiteralEmpty(final String lexeme) {
        if (lexeme == null) {
            return true;
        }
        final var trimmed = lexeme.trim();
        if (trimmed.length() >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
            return trimmed.length() == 2;
        }
        return trimmed.isEmpty();
    }

    private PbsAst.TypeRef unwrapGroup(final PbsAst.TypeRef typeRef) {
        if (typeRef == null || typeRef.kind() != PbsAst.TypeRefKind.GROUP) {
            return typeRef;
        }
        return unwrapGroup(typeRef.inner());
    }

    private List<PbsAst.Attribute> attributesNamed(
            final ReadOnlyList<PbsAst.Attribute> attributes,
            final String name) {
        final var matches = new ArrayList<PbsAst.Attribute>();
        for (final var attribute : attributes) {
            if (name.equals(attribute.name())) {
                matches.add(attribute);
            }
        }
        return matches;
    }

    private boolean isReservedAttribute(final String name) {
        return RESERVED_ATTRIBUTES.contains(name);
    }

    private void validateReservedAttributeTarget(
            final ReadOnlyList<PbsAst.Attribute> attributes,
            final String targetSurface,
            final DiagnosticSink diagnostics) {
        for (final var attribute : attributes) {
            if (!isReservedAttribute(attribute.name())) {
                continue;
            }
            reportInvalidReservedAttributeTarget(
                    attribute,
                    "Attribute '%s' is not valid on %s".formatted(attribute.name(), targetSurface),
                    diagnostics);
        }
    }

    private void reportInvalidReservedAttributeTarget(
            final PbsAst.Attribute attribute,
            final String message,
            final DiagnosticSink diagnostics) {
        p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                PbsSemanticsErrors.E_SEM_INVALID_RESERVED_ATTRIBUTE_TARGET.name(),
                message,
                attribute.span());
    }

    private void reportDuplicateReservedAttribute(
            final PbsAst.Attribute attribute,
            final String attributeName,
            final DiagnosticSink diagnostics) {
        p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name(),
                "Duplicate %s attribute on the same declaration surface".formatted(attributeName),
                attribute.span());
    }

    private void reportInterfaceNonDeclarativeDecl(
            final Span span,
            final String message,
            final DiagnosticSink diagnostics) {
        p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
                PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name(),
                message,
                span);
    }

    private void validateCallable(
            final PbsCallableScope scope,
            final String callableName,
            final ReadOnlyList<PbsAst.Parameter> parameters,
            final PbsAst.ReturnKind returnKind,
            final PbsAst.TypeRef returnType,
            final PbsAst.TypeRef resultErrorType,
            final Span span,
            final boolean allowSelf,
            final PbsNamespaceBinder binder,
            final PbsDeclarationRuleValidator rules) {
        rules.validateCallableHeader(callableName, parameters, returnKind, returnType, resultErrorType, span, allowSelf);
        binder.registerCallable(
                scope,
                callableName,
                PbsCallableShape.callableShapeKey(parameters, returnKind, returnType, resultErrorType),
                span);
    }
}
