package p.studio.compiler.pbs;

import p.studio.compiler.messages.HostAdmissionContext;
import p.studio.compiler.models.IRReservedMetadata;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.diagnostics.Diagnostics;
import p.studio.compiler.source.diagnostics.RelatedSpan;
import p.studio.compiler.source.identifiers.HostBindingId;
import p.studio.compiler.source.tables.HostBindingTable;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.*;
import java.util.regex.Pattern;

public final class PbsHostAdmissionValidator {
    private static final Pattern CAPABILITY_NAME = Pattern.compile("^[a-z][a-z0-9_]*$");

    public ReadOnlyList<String> validate(
            final IRReservedMetadata metadata,
            final HostAdmissionContext context,
            final DiagnosticSink diagnostics) {
        final var knownCapabilities = normalizedSet(context.knownCapabilities());
        final var declaredCapabilities = normalizedSet(context.declaredCapabilities());
        final var requiredCapabilities = new LinkedHashSet<String>();
        final var hostBindingTable = new HostBindingTable();
        final var firstCapabilityByHostBinding = new HashMap<HostBindingId, FirstBindingCapability>();

        for (final var hostBinding : metadata.hostMethodBindings()) {
            final var normalizedCapability = normalize(hostBinding.requiredCapability());
            if (hostBinding(diagnostics, hostBinding)) continue;
            if (normalizedCapability(diagnostics, hostBinding, normalizedCapability)) continue;
            if (knowCapabilities(diagnostics, hostBinding, knownCapabilities, normalizedCapability)) continue;

            final var hostBindingId = hostBindingTable.register(
                    hostBinding.abiModule(),
                    hostBinding.abiMethod(),
                    hostBinding.abiVersion());
            final var firstBindingCapability = firstCapabilityByHostBinding.putIfAbsent(
                    hostBindingId,
                    new FirstBindingCapability(normalizedCapability, hostBinding.span()));

            if (firstBindingCapability(diagnostics, hostBinding, firstBindingCapability, normalizedCapability)) continue;
            if (declaredCapabilities(context, diagnostics, hostBinding, declaredCapabilities, normalizedCapability)) continue;

            requiredCapabilities.add(normalizedCapability);
        }

        return ReadOnlyList.wrap(requiredCapabilities.stream().toList());
    }

    private static boolean declaredCapabilities(HostAdmissionContext context, DiagnosticSink diagnostics, IRReservedMetadata.HostMethodBinding hostBinding, Set<String> declaredCapabilities, String normalizedCapability) {
        if (context.enforceDeclaredCapabilities() && !declaredCapabilities.contains(normalizedCapability)) {
            Diagnostics.error(
                    diagnostics,
                    PbsHostAdmissionErrors.E_HOST_CAPABILITY_NOT_DECLARED.name(),
                    "Capability '%s' required by host binding '%s.%s' is not declared in host admission context"
                            .formatted(normalizedCapability, hostBinding.ownerName(), hostBinding.sourceMethodName()),
                    hostBinding.span());
            return true;
        }
        return false;
    }

    private static boolean firstBindingCapability(DiagnosticSink diagnostics, IRReservedMetadata.HostMethodBinding hostBinding, FirstBindingCapability firstBindingCapability, String normalizedCapability) {
        if (firstBindingCapability != null && !firstBindingCapability.capability().equals(normalizedCapability)) {
            Diagnostics.error(
                    diagnostics,
                    PbsHostAdmissionErrors.E_HOST_INCONSISTENT_BINDING_CAPABILITY.name(),
                    "Canonical host binding (%s, %s, %d) has inconsistent capability metadata ('%s' vs '%s')"
                            .formatted(
                                    hostBinding.abiModule(),
                                    hostBinding.abiMethod(),
                                    hostBinding.abiVersion(),
                                    firstBindingCapability.capability(),
                                    normalizedCapability),
                    hostBinding.span(),
                    List.of(new RelatedSpan("First capability declaration for this binding is here", firstBindingCapability.span())));
            return true;
        }
        return false;
    }

    private static boolean knowCapabilities(DiagnosticSink diagnostics, IRReservedMetadata.HostMethodBinding hostBinding, Set<String> knownCapabilities, String normalizedCapability) {
        if (!knownCapabilities.contains(normalizedCapability)) {
            Diagnostics.error(
                    diagnostics,
                    PbsHostAdmissionErrors.E_HOST_UNKNOWN_CAPABILITY.name(),
                    "Host binding '%s.%s' references unknown capability '%s'"
                            .formatted(hostBinding.ownerName(), hostBinding.sourceMethodName(), normalizedCapability),
                    hostBinding.span());
            return true;
        }
        return false;
    }

    private static boolean normalizedCapability(DiagnosticSink diagnostics, IRReservedMetadata.HostMethodBinding hostBinding, String normalizedCapability) {
        if (normalizedCapability.isBlank()) {
            Diagnostics.error(
                    diagnostics,
                    PbsHostAdmissionErrors.E_HOST_MALFORMED_CAPABILITY_METADATA.name(),
                    "Host binding '%s.%s' has malformed Capability(name=...) metadata"
                            .formatted(hostBinding.ownerName(), hostBinding.sourceMethodName()),
                    hostBinding.span());
            return true;
        }

        if (!CAPABILITY_NAME.matcher(normalizedCapability).matches()) {
            Diagnostics.error(
                    diagnostics,
                    PbsHostAdmissionErrors.E_HOST_MALFORMED_CAPABILITY_METADATA.name(),
                    "Host binding '%s.%s' has malformed capability name '%s'"
                            .formatted(hostBinding.ownerName(), hostBinding.sourceMethodName(), normalizedCapability),
                    hostBinding.span());
            return true;
        }

        return false;
    }

    private static boolean hostBinding(DiagnosticSink diagnostics, IRReservedMetadata.HostMethodBinding hostBinding) {
        if (!hostBinding.capabilityDeclared()) {
            Diagnostics.error(
                    diagnostics,
                    PbsHostAdmissionErrors.E_HOST_MISSING_REQUIRED_CAPABILITY.name(),
                    "Host binding '%s.%s' requires Capability(name=...) metadata"
                            .formatted(hostBinding.ownerName(), hostBinding.sourceMethodName()),
                    hostBinding.span());
            return true;
        }
        return false;
    }

    private Set<String> normalizedSet(final ReadOnlyList<String> values) {
        final var normalized = new HashSet<String>();
        for (final var value : values) {
            final var capability = normalize(value);
            if (!capability.isBlank()) {
                normalized.add(capability);
            }
        }
        return normalized;
    }

    private String normalize(final String capability) {
        return capability == null ? "" : capability.trim().toLowerCase(Locale.ROOT);
    }

    private record FirstBindingCapability(
            String capability,
            p.studio.compiler.source.Span span) {
    }
}
