#include "fesa/io/abaqus/domain_mapper.hpp"

#include "fesa/model/shell_geometry.hpp"

#include <algorithm>
#include <array>
#include <cerrno>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace fesa {
namespace {

std::string uppercaseAscii(std::string value) {
    std::transform(
        value.begin(), value.end(), value.begin(), [](char character) {
            if (character >= 'a' && character <= 'z') {
                return static_cast<char>(character - 'a' + 'A');
            }
            return character;
        });
    return value;
}

bool equalName(const std::string& left, const std::string& right) {
    return uppercaseAscii(left) == uppercaseAscii(right);
}

std::vector<std::string> withoutTrailingEmpty(
    std::vector<std::string> fields) {
    while (!fields.empty() && fields.back().empty()) {
        fields.pop_back();
    }
    return fields;
}

struct RawNode {
    std::int64_t label;
    std::string labelText;
    std::array<double, 3> coordinates;
    SourceLocation location;
};

struct RawElement {
    enum class Type {
        b33,
        s4,
        s4r
    };

    std::int64_t label;
    std::string labelText;
    Type type;
    std::vector<std::int64_t> nodeLabels;
    SourceLocation location;
};

enum class ElementFamily {
    beam,
    shell
};

struct RawSet {
    std::string name;
    std::vector<std::int64_t> members;
    SourceLocation location;
};

struct RawSection {
    std::string elementSetName;
    std::string materialName;
    std::array<double, 5> properties;
    std::array<double, 3> firstAxis;
    std::vector<std::array<double, 2>> sectionPoints;
    SourceLocation location;
};

struct RawShellSection {
    std::string elementSetName;
    std::string materialName;
    double thickness;
    SourceLocation location;
};

struct RawPart {
    std::string name;
    SourceLocation location;
    std::vector<RawNode> nodes;
    std::vector<RawElement> elements;
    std::vector<RawSet> nodeSets;
    std::vector<RawSet> elementSets;
    std::vector<RawSection> sections;
    std::vector<RawShellSection> shellSections;
};

struct RawInstance {
    std::string name;
    std::string partName;
    SourceLocation location;
};

struct RawAssemblySet {
    bool isNodeSet;
    std::string name;
    std::string instanceName;
    std::vector<std::int64_t> members;
    SourceLocation location;
};

struct RawMaterial {
    std::string name;
    double youngsModulus{0.0};
    double poissonRatio{0.0};
    bool hasElastic{false};
    SourceLocation location;
    SourceLocation elasticLocation;
};

struct RawStep {
    SourceLocation location;
    bool hasStatic{false};
    std::array<double, 4> staticValues{};
    std::vector<BoundaryCondition> boundaries;
    std::vector<NodalLoad> loads;
};

struct MappingFailure {
    FailureCategory category;
    Diagnostic diagnostic;
};

class MappingContext {
public:
    explicit MappingContext(const ParsedInput& input) : input_{input} {}

    Result<Domain> run() {
        parseBlocks();
        if (!failure_) {
            finalizeModel();
        }
        if (failure_) {
            return Result<Domain>::failure(Status::failure(
                failure_->category, {std::move(failure_->diagnostic)}));
        }
        sortDiagnostics(definition_.warnings);
        return Domain::create(std::move(definition_));
    }

private:
    const KeywordParameter* parameter(
        const KeywordBlock& block,
        std::string_view name) const {
        const auto found = std::find_if(
            block.parameters.begin(),
            block.parameters.end(),
            [name](const KeywordParameter& candidate) {
                return candidate.name == name;
            });
        return found == block.parameters.end() ? nullptr : &*found;
    }

    bool fail(
        FailureCategory category,
        std::string code,
        const SourceLocation& location,
        std::string keyword,
        std::string entityIdentity,
        std::string message) {
        if (!failure_) {
            failure_ = MappingFailure{
                category,
                {Severity::error,
                 std::move(code),
                 location,
                 std::move(keyword),
                 std::move(entityIdentity),
                 std::move(message)}};
        }
        return false;
    }

    bool inputFailure(
        std::string code,
        const SourceLocation& location,
        std::string keyword,
        std::string entityIdentity,
        std::string message) {
        return fail(
            FailureCategory::input,
            std::move(code),
            location,
            std::move(keyword),
            std::move(entityIdentity),
            std::move(message));
    }

    bool modelFailure(
        std::string code,
        const SourceLocation& location,
        std::string keyword,
        std::string entityIdentity,
        std::string message) {
        return fail(
            FailureCategory::model,
            std::move(code),
            location,
            std::move(keyword),
            std::move(entityIdentity),
            std::move(message));
    }

    bool invalidKeywordLocation(
        const KeywordBlock& block,
        std::string message) {
        return inputFailure(
            "invalid-keyword-location",
            block.location,
            block.canonicalName,
            "",
            std::move(message));
    }

    bool validateParameters(
        const KeywordBlock& block,
        const std::vector<std::string_view>& allowed) {
        std::set<std::string> seen;
        for (const auto& candidate : block.parameters) {
            if (!seen.insert(candidate.name).second) {
                return inputFailure(
                    "duplicate-entity",
                    block.location,
                    block.canonicalName,
                    candidate.name,
                    "A keyword parameter may be declared only once.");
            }
            if (std::find(allowed.begin(), allowed.end(), candidate.name) ==
                allowed.end()) {
                return inputFailure(
                    "invalid-keyword-parameter",
                    block.location,
                    block.canonicalName,
                    candidate.name,
                    "The keyword parameter is outside the approved subset.");
            }
        }
        return true;
    }

    const std::string* requiredParameterValue(
        const KeywordBlock& block,
        std::string_view name) {
        const auto* found = parameter(block, name);
        if (found == nullptr || !found->value || found->value->empty()) {
            inputFailure(
                "invalid-keyword-parameter",
                block.location,
                block.canonicalName,
                std::string{name},
                "The keyword requires a non-empty parameter value.");
            return nullptr;
        }
        return &*found->value;
    }

    bool requireNoData(const KeywordBlock& block) {
        if (!block.data.empty()) {
            return inputFailure(
                "invalid-data-arity",
                block.data.front().location,
                block.canonicalName,
                "",
                "This keyword does not accept data rows.");
        }
        return true;
    }

    bool parseInteger(
        const std::string& text,
        std::int64_t& value,
        const SourceLocation& location,
        const std::string& keyword,
        bool positive) {
        if (text.empty()) {
            return inputFailure(
                "invalid-numeric-value", location, keyword, text,
                "A numeric field cannot be empty.");
        }
        char* end = nullptr;
        errno = 0;
        const long long parsed = std::strtoll(text.c_str(), &end, 10);
        if (errno == ERANGE || end != text.c_str() + text.size() ||
            (positive && parsed <= 0)) {
            return inputFailure(
                "invalid-numeric-value", location, keyword, text,
                "The field must be a valid positive base-10 integer.");
        }
        value = static_cast<std::int64_t>(parsed);
        return true;
    }

    bool tryPositiveInteger(const std::string& text, std::int64_t& value) const {
        if (text.empty()) {
            return false;
        }
        char* end = nullptr;
        errno = 0;
        const long long parsed = std::strtoll(text.c_str(), &end, 10);
        if (errno == ERANGE || end != text.c_str() + text.size() || parsed <= 0) {
            return false;
        }
        value = static_cast<std::int64_t>(parsed);
        return true;
    }

    bool parseDouble(
        const std::string& text,
        double& value,
        const SourceLocation& location,
        const std::string& keyword) {
        if (text.empty()) {
            return inputFailure(
                "invalid-numeric-value", location, keyword, text,
                "A numeric field cannot be empty.");
        }
        char* end = nullptr;
        errno = 0;
        const double parsed = std::strtod(text.c_str(), &end);
        if (errno == ERANGE || end != text.c_str() + text.size() ||
            !std::isfinite(parsed)) {
            return inputFailure(
                "invalid-numeric-value", location, keyword, text,
                "The field must be a finite floating-point value.");
        }
        value = parsed;
        return true;
    }

    bool parseModelDouble(
        const std::string& text,
        double& value,
        const SourceLocation& location,
        const std::string& keyword,
        const std::string& diagnosticCode,
        const std::string& message) {
        if (text.empty()) {
            return inputFailure(
                "invalid-numeric-value", location, keyword, text,
                "A numeric field cannot be empty.");
        }
        char* end = nullptr;
        errno = 0;
        const double parsed = std::strtod(text.c_str(), &end);
        if (end != text.c_str() + text.size()) {
            return inputFailure(
                "invalid-numeric-value", location, keyword, text,
                "The field must use floating-point syntax.");
        }
        if (errno == ERANGE || !std::isfinite(parsed)) {
            return modelFailure(
                diagnosticCode, location, keyword, text, message);
        }
        value = parsed;
        return true;
    }

    bool containsName(
        const std::vector<RawPart>& values,
        const std::string& name) const {
        return std::any_of(
            values.begin(), values.end(), [&name](const RawPart& value) {
                return equalName(value.name, name);
            });
    }

    bool containsName(
        const std::vector<RawInstance>& values,
        const std::string& name) const {
        return std::any_of(
            values.begin(), values.end(), [&name](const RawInstance& value) {
                return equalName(value.name, name);
            });
    }

    bool containsName(
        const std::vector<RawMaterial>& values,
        const std::string& name) const {
        return std::any_of(
            values.begin(), values.end(), [&name](const RawMaterial& value) {
                return equalName(value.name, name);
            });
    }

    bool containsSetName(
        const std::vector<RawSet>& sets,
        const std::string& name) const {
        const auto matches = [&name](const RawSet& set) {
            return equalName(set.name, name);
        };
        return std::any_of(sets.begin(), sets.end(), matches);
    }

    bool containsAssemblySetName(
        bool nodeSet,
        const std::string& name) const {
        return std::any_of(
            assemblySets_.begin(),
            assemblySets_.end(),
            [nodeSet, &name](const RawAssemblySet& set) {
                return set.isNodeSet == nodeSet && equalName(set.name, name);
            });
    }

    void parseBlocks() {
        definition_.sourcePath = input_.sourcePath;
        definition_.sourceContentIdentity = input_.sourceContentIdentity;

        for (std::size_t index = 0U;
             index < input_.blocks.size() && !failure_;
             ++index) {
            const auto& block = input_.blocks[index];
            if (inInstance_) {
                parseInstanceBlock(block);
            } else if (currentPart_) {
                parsePartBlock(block);
            } else if (inAssembly_) {
                parseAssemblyBlock(block);
            } else if (inStep_) {
                parseStepBlock(block);
            } else {
                parseTopLevelBlock(block, index);
            }
        }

        if (!failure_ &&
            (currentPart_ || inAssembly_ || inInstance_ || inStep_)) {
            const auto location = input_.blocks.empty()
                ? SourceLocation{input_.sourcePath, 0U}
                : input_.blocks.back().location;
            inputFailure(
                "unclosed-keyword-block",
                location,
                "",
                "",
                "A part, assembly, instance, or step block was not closed.");
        }
    }

    void parseTopLevelBlock(const KeywordBlock& block, std::size_t index) {
        if (stepSeen_) {
            if (block.canonicalName == "STEP") {
                parseStepStart(block);
            } else {
                invalidKeywordLocation(
                    block,
                    "The sole analysis step must be the final top-level block.");
            }
            return;
        }

        if (block.canonicalName != "ELASTIC") {
            materialEligible_.reset();
        }
        pendingSection_.reset();
        activeOutput_ = false;

        if (block.canonicalName == "HEADING") {
            if (index != 0U || headingSeen_ ||
                !validateParameters(block, {})) {
                if (!failure_) {
                    inputFailure(
                        "invalid-keyword-location",
                        block.location,
                        block.canonicalName,
                        "",
                        "HEADING is optional only as the first keyword.");
                }
                return;
            }
            headingSeen_ = true;
            for (std::size_t row = 0U; row < block.data.size(); ++row) {
                if (row != 0U) {
                    definition_.heading.push_back('\n');
                }
                for (std::size_t field = 0U;
                     field < block.data[row].fields.size();
                     ++field) {
                    if (field != 0U) {
                        definition_.heading.push_back(',');
                    }
                    definition_.heading += block.data[row].fields[field];
                }
            }
            return;
        }
        if (block.canonicalName == "PREPRINT") {
            if (!requireNoData(block)) {
                return;
            }
            addIgnoredWarning(block);
            return;
        }
        if (block.canonicalName == "PART") {
            parsePartStart(block);
            return;
        }
        if (block.canonicalName == "ASSEMBLY") {
            parseAssemblyStart(block);
            return;
        }
        if (block.canonicalName == "MATERIAL") {
            parseMaterial(block);
            return;
        }
        if (block.canonicalName == "ELASTIC") {
            parseElastic(block);
            return;
        }
        if (block.canonicalName == "BOUNDARY") {
            if (!assemblySeen_ || materials_.empty()) {
                invalidKeywordLocation(
                    block,
                    "Model boundary data follows the assembly and materials.");
                return;
            }
            modelBoundarySeen_ = true;
            parseBoundary(block, modelBoundaries_);
            return;
        }
        if (block.canonicalName == "STEP") {
            parseStepStart(block);
            return;
        }
        if (block.canonicalName == "END PART" ||
            block.canonicalName == "END ASSEMBLY" ||
            block.canonicalName == "END INSTANCE" ||
            block.canonicalName == "END STEP") {
            inputFailure(
                "invalid-keyword-location",
                block.location,
                block.canonicalName,
                "",
                "The closing keyword has no matching open block.");
            return;
        }
        rejectUnknown(block);
    }

    void parsePartStart(const KeywordBlock& block) {
        if (assemblySeen_ || !materials_.empty() || modelBoundarySeen_) {
            invalidKeywordLocation(
                block,
                "All part blocks must precede the sole assembly block.");
            return;
        }
        if (!validateParameters(block, {"NAME"}) || !requireNoData(block)) {
            return;
        }
        const auto* name = requiredParameterValue(block, "NAME");
        if (name == nullptr) {
            return;
        }
        if (containsName(parts_, *name)) {
            inputFailure(
                "duplicate-entity",
                block.location,
                block.canonicalName,
                *name,
                "Part names are unique under case-insensitive lookup.");
            return;
        }
        parts_.push_back({*name, block.location, {}, {}, {}, {}, {}, {}});
        currentPart_ = parts_.size() - 1U;
        partElementsSeen_ = false;
        partSetsSeen_ = false;
        partSectionsSeen_ = false;
        beamSectionContextActive_ = false;
    }

    void parsePartBlock(const KeywordBlock& block) {
        if (block.canonicalName == "END PART") {
            if (!validateParameters(block, {}) || !requireNoData(block)) {
                return;
            }
            const RawPart& part = parts_[*currentPart_];
            const bool shellPart = modelElementFamily_ == ElementFamily::shell;
            if (part.nodes.empty() || part.elements.empty() ||
                (!shellPart && part.sections.empty())) {
                invalidKeywordLocation(
                    block,
                    "A part closes only after node, element, and matching section blocks.");
                return;
            }
            currentPart_.reset();
            pendingSection_.reset();
            beamSectionContextActive_ = false;
            return;
        }
        if (block.canonicalName == "PART") {
            inputFailure(
                "invalid-keyword-location",
                block.location,
                block.canonicalName,
                "",
                "Nested part blocks are not supported.");
            return;
        }
        if (block.canonicalName == "ASSEMBLY") {
            inputFailure(
                "unsupported-nested-assembly",
                block.location,
                block.canonicalName,
                "",
                "An assembly cannot be nested in a part.");
            return;
        }

        RawPart& part = parts_[*currentPart_];
        if (block.canonicalName == "NODE") {
            beamSectionContextActive_ = false;
            if (partElementsSeen_ || partSetsSeen_ || partSectionsSeen_) {
                invalidKeywordLocation(
                    block,
                    "NODE blocks must precede element, set, and section blocks.");
                return;
            }
            pendingSection_.reset();
            parseNodes(block, part);
        } else if (block.canonicalName == "ELEMENT") {
            beamSectionContextActive_ = false;
            if (part.nodes.empty() || partSetsSeen_ || partSectionsSeen_) {
                invalidKeywordLocation(
                    block,
                    "ELEMENT blocks follow at least one NODE block and precede sets and sections.");
                return;
            }
            pendingSection_.reset();
            parseElements(block, part);
            partElementsSeen_ = !failure_;
        } else if (block.canonicalName == "NSET") {
            beamSectionContextActive_ = false;
            if (!partElementsSeen_ || partSectionsSeen_) {
                invalidKeywordLocation(
                    block,
                    "Part sets follow element blocks and precede beam sections.");
                return;
            }
            pendingSection_.reset();
            parseSet(block, true, part);
            partSetsSeen_ = !failure_;
        } else if (block.canonicalName == "ELSET") {
            beamSectionContextActive_ = false;
            if (!partElementsSeen_ || partSectionsSeen_) {
                invalidKeywordLocation(
                    block,
                    "Part sets follow element blocks and precede beam sections.");
                return;
            }
            pendingSection_.reset();
            parseSet(block, false, part);
            partSetsSeen_ = !failure_;
        } else if (block.canonicalName == "BEAM GENERAL SECTION") {
            if (!partElementsSeen_) {
                invalidKeywordLocation(
                    block,
                    "BEAM GENERAL SECTION follows the part mesh and optional sets.");
                return;
            }
            if (modelElementFamily_ == ElementFamily::shell) {
                inputFailure(
                    "unsupported-mixed-element-model", block.location,
                    block.canonicalName, "",
                    "Beam-section semantics cannot be mixed with shell elements.");
                return;
            }
            parseBeamSection(block, part);
            partSectionsSeen_ = !failure_;
            beamSectionContextActive_ = !failure_;
        } else if (block.canonicalName == "SHELL SECTION") {
            beamSectionContextActive_ = false;
            if (!partElementsSeen_) {
                invalidKeywordLocation(
                    block,
                    "SHELL SECTION follows the part mesh and optional sets.");
                return;
            }
            if (modelElementFamily_ == ElementFamily::beam) {
                inputFailure(
                    "unsupported-mixed-element-model", block.location,
                    block.canonicalName, "",
                    "Shell-section semantics cannot be mixed with beam elements.");
                return;
            }
            parseShellSection(block, part);
            partSectionsSeen_ = !failure_;
        } else if (block.canonicalName == "SECTION POINTS") {
            parseSectionPoints(block, part);
        } else if (block.canonicalName == "TRANSVERSE SHEAR STIFFNESS") {
            pendingSection_.reset();
            if (!beamSectionContextActive_) {
                inputFailure(
                    "invalid-keyword-location",
                    block.location,
                    block.canonicalName,
                    "",
                    "The ignored shear keyword still requires beam-section context.");
                return;
            }
            addIgnoredWarning(block);
        } else {
            pendingSection_.reset();
            beamSectionContextActive_ = false;
            rejectUnknown(block);
        }
    }

    void parseNodes(const KeywordBlock& block, RawPart& part) {
        if (!validateParameters(block, {}) || block.data.empty()) {
            if (!failure_) {
                inputFailure(
                    "invalid-data-arity", block.location, block.canonicalName,
                    "", "NODE requires at least one four-field data row.");
            }
            return;
        }
        for (const auto& row : block.data) {
            if (row.fields.size() != 4U) {
                inputFailure(
                    "invalid-data-arity", row.location, block.canonicalName,
                    "", "NODE rows require label, x, y, z.");
                return;
            }
            RawNode node{};
            node.labelText = row.fields[0];
            node.location = row.location;
            if (!parseInteger(
                    row.fields[0], node.label, row.location,
                    block.canonicalName, true)) {
                return;
            }
            if (std::any_of(
                    part.nodes.begin(), part.nodes.end(),
                    [&node](const RawNode& existing) {
                        return existing.label == node.label;
                    })) {
                inputFailure(
                    "duplicate-entity", row.location, block.canonicalName,
                    node.labelText, "Node labels are unique within a part.");
                return;
            }
            for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) {
                if (!parseDouble(
                        row.fields[coordinate + 1U],
                        node.coordinates[coordinate],
                        row.location,
                        block.canonicalName)) {
                    return;
                }
            }
            part.nodes.push_back(std::move(node));
        }
    }

    void parseElements(const KeywordBlock& block, RawPart& part) {
        if (!validateParameters(block, {"TYPE"})) {
            return;
        }
        const auto* type = requiredParameterValue(block, "TYPE");
        if (type == nullptr) {
            return;
        }
        RawElement::Type elementType{};
        ElementFamily elementFamily{};
        std::size_t expectedFieldCount = 0U;
        if (equalName(*type, "B33")) {
            elementType = RawElement::Type::b33;
            elementFamily = ElementFamily::beam;
            expectedFieldCount = 3U;
        } else if (equalName(*type, "S4")) {
            elementType = RawElement::Type::s4;
            elementFamily = ElementFamily::shell;
            expectedFieldCount = 5U;
        } else if (equalName(*type, "S4R")) {
            elementType = RawElement::Type::s4r;
            elementFamily = ElementFamily::shell;
            expectedFieldCount = 5U;
        } else {
            inputFailure(
                "unsupported-element-formulation",
                block.location,
                block.canonicalName,
                *type,
                "Only TYPE=B33, TYPE=S4, and TYPE=S4R belong to the approved element subsets.");
            return;
        }
        if (modelElementFamily_ && *modelElementFamily_ != elementFamily) {
            inputFailure(
                "unsupported-mixed-element-model",
                block.location,
                block.canonicalName,
                *type,
                "Beam and shell elements cannot be mixed in one model.");
            return;
        }
        modelElementFamily_ = elementFamily;
        if (block.data.empty()) {
            inputFailure(
                elementFamily == ElementFamily::shell
                    ? "invalid-shell-connectivity"
                    : "invalid-data-arity",
                block.location, block.canonicalName, "",
                "ELEMENT requires at least one row with the approved connectivity arity.");
            return;
        }
        for (const auto& row : block.data) {
            if (row.fields.size() != expectedFieldCount) {
                inputFailure(
                    elementFamily == ElementFamily::shell
                        ? "invalid-shell-connectivity"
                        : "invalid-data-arity",
                    row.location, block.canonicalName, "",
                    elementFamily == ElementFamily::shell
                        ? "S4 and S4R rows require a label and exactly four nodes."
                        : "B33 rows require label, node 1, node 2.");
                return;
            }
            RawElement element{};
            element.labelText = row.fields[0];
            element.type = elementType;
            element.location = row.location;
            if (!parseInteger(
                    row.fields[0], element.label, row.location,
                    block.canonicalName, true)) {
                return;
            }
            element.nodeLabels.reserve(expectedFieldCount - 1U);
            for (std::size_t field = 1U; field < expectedFieldCount; ++field) {
                std::int64_t nodeLabel = 0;
                if (!parseInteger(
                        row.fields[field], nodeLabel, row.location,
                        block.canonicalName, true)) {
                    return;
                }
                element.nodeLabels.push_back(nodeLabel);
            }
            if (elementFamily == ElementFamily::shell) {
                const std::set<std::int64_t> distinctNodes{
                    element.nodeLabels.begin(), element.nodeLabels.end()};
                if (distinctNodes.size() != element.nodeLabels.size()) {
                    inputFailure(
                        "invalid-shell-connectivity", row.location,
                        block.canonicalName, element.labelText,
                        "Shell connectivity requires four distinct source nodes.");
                    return;
                }
            }
            if (std::any_of(
                    part.elements.begin(), part.elements.end(),
                    [&element](const RawElement& existing) {
                        return existing.label == element.label;
                    })) {
                inputFailure(
                    "duplicate-entity", row.location, block.canonicalName,
                    element.labelText,
                    "Element labels are unique within a part.");
                return;
            }
            part.elements.push_back(std::move(element));
        }
    }

    void parseShellSection(const KeywordBlock& block, RawPart& part) {
        for (const auto& candidate : block.parameters) {
            if (candidate.name != "ELSET" && candidate.name != "MATERIAL") {
                inputFailure(
                    "unsupported-shell-section-option", block.location,
                    block.canonicalName, candidate.name,
                    "The shell-section option is outside the centered single-layer subset.");
                return;
            }
        }
        if (!validateParameters(block, {"ELSET", "MATERIAL"})) {
            return;
        }
        const auto* elementSet = requiredParameterValue(block, "ELSET");
        const auto* material = requiredParameterValue(block, "MATERIAL");
        if (elementSet == nullptr || material == nullptr) {
            return;
        }
        if (block.data.size() != 1U || block.data[0].fields.empty() ||
            block.data[0].fields.size() > 2U) {
            inputFailure(
                "unsupported-shell-section-option", block.location,
                block.canonicalName, *elementSet,
                "SHELL SECTION requires one thickness row with one optional integration-point field.");
            return;
        }
        double thickness = 0.0;
        if (!parseModelDouble(
                block.data[0].fields[0], thickness,
                block.data[0].location, block.canonicalName,
                "invalid-shell-thickness",
                "Shell thickness must be finite and positive.")) {
            return;
        }
        if (!(thickness > 0.0)) {
            modelFailure(
                "invalid-shell-thickness", block.data[0].location,
                block.canonicalName, *elementSet,
                "Shell thickness must be finite and positive.");
            return;
        }
        if (block.data[0].fields.size() == 2U) {
            std::int64_t ignoredIntegrationPoints = 0;
            if (!tryPositiveInteger(
                    block.data[0].fields[1], ignoredIntegrationPoints)) {
                inputFailure(
                    "unsupported-shell-section-option", block.data[0].location,
                    block.canonicalName, block.data[0].fields[1],
                    "The optional shell integration-point field must be a positive integer.");
                return;
            }
        }
        part.shellSections.push_back(
            {*elementSet, *material, thickness, block.location});
    }

    bool parseSetMembers(
        const KeywordBlock& block,
        bool generate,
        std::vector<std::int64_t>& members) {
        if (block.data.empty()) {
            return inputFailure(
                "invalid-data-arity", block.location, block.canonicalName,
                "", "A set requires at least one member row.");
        }
        for (const auto& data : block.data) {
            auto fields = withoutTrailingEmpty(data.fields);
            if (generate) {
                if (fields.size() != 3U) {
                    return inputFailure(
                        "invalid-data-arity", data.location,
                        block.canonicalName, "",
                        "GENERATE rows require first, last, increment.");
                }
                std::int64_t first = 0;
                std::int64_t last = 0;
                std::int64_t increment = 0;
                if (!parseInteger(
                        fields[0], first, data.location,
                        block.canonicalName, true) ||
                    !parseInteger(
                        fields[1], last, data.location,
                        block.canonicalName, true)) {
                    return false;
                }
                if (!parseInteger(
                        fields[2], increment, data.location,
                        block.canonicalName, false) ||
                    increment <= 0 || first > last ||
                    (last - first) % increment != 0) {
                    return inputFailure(
                        "invalid-set-range", data.location,
                        block.canonicalName, "",
                        "GENERATE requires an inclusive range reached by a positive increment.");
                }
                for (std::int64_t label = first; label <= last;) {
                    members.push_back(label);
                    if (label > last - increment) {
                        break;
                    }
                    label += increment;
                }
            } else {
                if (fields.empty() ||
                    std::any_of(
                        fields.begin(), fields.end(),
                        [](const std::string& field) { return field.empty(); })) {
                    return inputFailure(
                        "invalid-data-arity", data.location,
                        block.canonicalName, "",
                        "Explicit set rows require non-empty member labels.");
                }
                for (const auto& field : fields) {
                    std::int64_t label = 0;
                    if (!parseInteger(
                            field, label, data.location,
                            block.canonicalName, true)) {
                        return false;
                    }
                    members.push_back(label);
                }
            }
        }
        std::set<std::int64_t> unique;
        for (const auto member : members) {
            if (!unique.insert(member).second) {
                return inputFailure(
                    "duplicate-entity", block.location,
                    block.canonicalName, std::to_string(member),
                    "A set cannot repeat the same source member.");
            }
        }
        return true;
    }

    void parseSet(const KeywordBlock& block, bool nodeSet, RawPart& part) {
        const std::string_view nameParameter = nodeSet ? "NSET" : "ELSET";
        if (!validateParameters(block, {nameParameter, "GENERATE"})) {
            return;
        }
        const auto* name = requiredParameterValue(block, nameParameter);
        if (name == nullptr) {
            return;
        }
        const auto* generateParameter = parameter(block, "GENERATE");
        if (generateParameter != nullptr && generateParameter->value) {
            inputFailure(
                "invalid-keyword-parameter", block.location,
                block.canonicalName, "GENERATE",
                "GENERATE is a valueless flag.");
            return;
        }
        const auto& sets = nodeSet ? part.nodeSets : part.elementSets;
        if (containsSetName(sets, *name)) {
            inputFailure(
                "duplicate-entity", block.location, block.canonicalName,
                *name,
                "Set names are unique within their node-set or element-set namespace.");
            return;
        }
        RawSet set{*name, {}, block.location};
        if (!parseSetMembers(block, generateParameter != nullptr, set.members)) {
            return;
        }
        if (nodeSet) {
            part.nodeSets.push_back(std::move(set));
        } else {
            part.elementSets.push_back(std::move(set));
        }
    }

    void parseBeamSection(const KeywordBlock& block, RawPart& part) {
        pendingSection_.reset();
        if (!validateParameters(block, {"ELSET", "MATERIAL", "SECTION"})) {
            return;
        }
        const auto* elementSet = requiredParameterValue(block, "ELSET");
        const auto* material = requiredParameterValue(block, "MATERIAL");
        const auto* section = requiredParameterValue(block, "SECTION");
        if (elementSet == nullptr || material == nullptr || section == nullptr) {
            return;
        }
        if (!equalName(*section, "GENERAL")) {
            inputFailure(
                "unsupported-section-formulation", block.location,
                block.canonicalName, *section,
                "Only SECTION=GENERAL belongs to the approved subset.");
            return;
        }
        if (std::any_of(
                part.sections.begin(), part.sections.end(),
                [elementSet](const RawSection& existing) {
                    return equalName(existing.elementSetName, *elementSet);
                })) {
            inputFailure(
                "duplicate-entity", block.location, block.canonicalName,
                *elementSet,
                "An element set can receive only one beam section.");
            return;
        }
        if (block.data.size() != 2U || block.data[0].fields.size() != 5U ||
            block.data[1].fields.size() != 3U) {
            inputFailure(
                "invalid-data-arity", block.location, block.canonicalName,
                *elementSet,
                "A general section requires one five-field property row and one three-field axis row.");
            return;
        }
        RawSection raw{*elementSet, *material, {}, {}, {}, block.location};
        for (std::size_t property = 0U; property < 5U; ++property) {
            if (!parseModelDouble(
                    block.data[0].fields[property], raw.properties[property],
                    block.data[0].location, block.canonicalName,
                    "invalid-beam-property",
                    "Beam section properties must be finite.")) {
                return;
            }
        }
        for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) {
            if (!parseModelDouble(
                    block.data[1].fields[coordinate], raw.firstAxis[coordinate],
                    block.data[1].location, block.canonicalName,
                    "invalid-beam-guide-vector",
                    "The beam guide vector must be finite.")) {
                return;
            }
        }
        part.sections.push_back(std::move(raw));
        pendingSection_ = part.sections.size() - 1U;
    }

    void parseSectionPoints(const KeywordBlock& block, RawPart& part) {
        if (!pendingSection_ || !validateParameters(block, {}) ||
            block.data.empty()) {
            if (!failure_) {
                inputFailure(
                    "invalid-keyword-location", block.location,
                    block.canonicalName, "",
                    "SECTION POINTS must immediately follow a general section.");
            }
            return;
        }
        RawSection& section = part.sections[*pendingSection_];
        for (const auto& row : block.data) {
            if (row.fields.size() != 2U) {
                inputFailure(
                    "invalid-data-arity", row.location,
                    block.canonicalName, section.elementSetName,
                    "Section-point rows require x1 and x2.");
                return;
            }
            std::array<double, 2> point{};
            if (!parseDouble(
                    row.fields[0], point[0], row.location,
                    block.canonicalName) ||
                !parseDouble(
                    row.fields[1], point[1], row.location,
                    block.canonicalName)) {
                return;
            }
            if (std::find(
                    section.sectionPoints.begin(),
                    section.sectionPoints.end(), point) !=
                section.sectionPoints.end()) {
                inputFailure(
                    "duplicate-entity", row.location,
                    block.canonicalName, section.elementSetName,
                    "Section points must be unique within a section.");
                return;
            }
            section.sectionPoints.push_back(point);
        }
        pendingSection_.reset();
    }

    void parseAssemblyStart(const KeywordBlock& block) {
        if (assemblySeen_) {
            inputFailure(
                "unsupported-nested-assembly", block.location,
                block.canonicalName, "",
                "V0 accepts exactly one non-nested assembly.");
            return;
        }
        if (parts_.empty() || !materials_.empty() || modelBoundarySeen_) {
            invalidKeywordLocation(
                block,
                "The sole assembly follows all part blocks and precedes model data.");
            return;
        }
        if (!validateParameters(block, {"NAME"}) || !requireNoData(block)) {
            return;
        }
        if (requiredParameterValue(block, "NAME") == nullptr) {
            return;
        }
        assemblySeen_ = true;
        inAssembly_ = true;
        assemblySetSeen_ = false;
    }

    void parseAssemblyBlock(const KeywordBlock& block) {
        if (block.canonicalName == "END ASSEMBLY") {
            if (!validateParameters(block, {}) || !requireNoData(block)) {
                return;
            }
            if (instances_.empty()) {
                invalidKeywordLocation(
                    block,
                    "The assembly requires at least one identity instance.");
                return;
            }
            inAssembly_ = false;
            return;
        }
        if (block.canonicalName == "ASSEMBLY") {
            inputFailure(
                "unsupported-nested-assembly", block.location,
                block.canonicalName, "",
                "Nested or duplicate assembly blocks are unsupported.");
        } else if (block.canonicalName == "INSTANCE") {
            if (assemblySetSeen_) {
                invalidKeywordLocation(
                    block,
                    "All identity instances must precede assembly-level sets.");
                return;
            }
            parseInstanceStart(block);
        } else if (block.canonicalName == "NSET") {
            if (instances_.empty()) {
                invalidKeywordLocation(
                    block,
                    "Assembly sets follow at least one identity instance.");
                return;
            }
            assemblySetSeen_ = true;
            parseAssemblySet(block, true);
        } else if (block.canonicalName == "ELSET") {
            if (instances_.empty()) {
                invalidKeywordLocation(
                    block,
                    "Assembly sets follow at least one identity instance.");
                return;
            }
            assemblySetSeen_ = true;
            parseAssemblySet(block, false);
        } else {
            rejectUnknown(block);
        }
    }

    void parseInstanceStart(const KeywordBlock& block) {
        if (parameter(block, "DEPENDENT") != nullptr ||
            parameter(block, "INDEPENDENT") != nullptr) {
            inputFailure(
                "unsupported-instance-mesh-semantics", block.location,
                block.canonicalName, "",
                "Dependent and independent instance mesh semantics are unsupported.");
            return;
        }
        if (!validateParameters(block, {"NAME", "PART"})) {
            return;
        }
        const auto* name = requiredParameterValue(block, "NAME");
        const auto* part = requiredParameterValue(block, "PART");
        if (name == nullptr || part == nullptr) {
            return;
        }
        if (!block.data.empty()) {
            inputFailure(
                "unsupported-instance-transform", block.data.front().location,
                block.canonicalName, *name,
                "Instance translation or rotation data is unsupported.");
            return;
        }
        if (containsName(instances_, *name)) {
            inputFailure(
                "duplicate-entity", block.location, block.canonicalName,
                *name, "Instance names are globally unique.");
            return;
        }
        instances_.push_back({*name, *part, block.location});
        inInstance_ = true;
    }

    void parseInstanceBlock(const KeywordBlock& block) {
        if (block.canonicalName == "END INSTANCE") {
            if (!validateParameters(block, {}) || !requireNoData(block)) {
                return;
            }
            inInstance_ = false;
            return;
        }
        if (block.canonicalName == "ASSEMBLY") {
            inputFailure(
                "unsupported-nested-assembly", block.location,
                block.canonicalName, "",
                "An assembly cannot be nested in an instance.");
            return;
        }
        inputFailure(
            "unsupported-instance-mesh-semantics", block.location,
            block.canonicalName, instances_.back().name,
            "Instance-local mesh definitions are unsupported.");
    }

    void parseAssemblySet(const KeywordBlock& block, bool nodeSet) {
        const std::string_view nameParameter = nodeSet ? "NSET" : "ELSET";
        if (!validateParameters(
                block, {nameParameter, "INSTANCE", "GENERATE"})) {
            return;
        }
        const auto* name = requiredParameterValue(block, nameParameter);
        const auto* instance = requiredParameterValue(block, "INSTANCE");
        if (name == nullptr || instance == nullptr) {
            return;
        }
        const auto* generateParameter = parameter(block, "GENERATE");
        if (generateParameter != nullptr && generateParameter->value) {
            inputFailure(
                "invalid-keyword-parameter", block.location,
                block.canonicalName, "GENERATE",
                "GENERATE is a valueless flag.");
            return;
        }
        if (containsAssemblySetName(nodeSet, *name)) {
            inputFailure(
                "duplicate-entity", block.location, block.canonicalName,
                *name,
                "Assembly set names are unique within their node-set or element-set namespace.");
            return;
        }
        RawAssemblySet set{nodeSet, *name, *instance, {}, block.location};
        if (!parseSetMembers(block, generateParameter != nullptr, set.members)) {
            return;
        }
        assemblySets_.push_back(std::move(set));
    }

    void parseMaterial(const KeywordBlock& block) {
        if (!assemblySeen_ || modelBoundarySeen_) {
            invalidKeywordLocation(
                block,
                "Material definitions follow the assembly and precede model boundaries.");
            return;
        }
        if (!validateParameters(block, {"NAME"}) || !requireNoData(block)) {
            return;
        }
        const auto* name = requiredParameterValue(block, "NAME");
        if (name == nullptr) {
            return;
        }
        if (containsName(materials_, *name)) {
            inputFailure(
                "duplicate-entity", block.location, block.canonicalName,
                *name, "Material names are globally unique.");
            return;
        }
        materials_.push_back({*name, 0.0, 0.0, false, block.location, {}});
        materialEligible_ = materials_.size() - 1U;
    }

    void parseElastic(const KeywordBlock& block) {
        if (!materialEligible_) {
            inputFailure(
                "invalid-keyword-location", block.location,
                block.canonicalName, "",
                "ELASTIC must immediately follow MATERIAL.");
            return;
        }
        if (!validateParameters(block, {}) || block.data.size() != 1U ||
            block.data[0].fields.size() != 2U) {
            if (!failure_) {
                inputFailure(
                    "invalid-data-arity", block.location,
                    block.canonicalName, materials_[*materialEligible_].name,
                    "ELASTIC requires exactly one E, nu row.");
            }
            return;
        }
        RawMaterial& material = materials_[*materialEligible_];
        if (material.hasElastic) {
            inputFailure(
                "duplicate-entity", block.location, block.canonicalName,
                material.name, "A material accepts one ELASTIC definition.");
            return;
        }
        if (!parseModelDouble(
                block.data[0].fields[0], material.youngsModulus,
                block.data[0].location, block.canonicalName,
                modelElementFamily_ == ElementFamily::shell
                    ? "invalid-shell-material"
                    : "invalid-beam-property",
                "Elastic material values must be finite.") ||
            !parseModelDouble(
                block.data[0].fields[1], material.poissonRatio,
                block.data[0].location, block.canonicalName,
                modelElementFamily_ == ElementFamily::shell
                    ? "invalid-shell-material"
                    : "invalid-beam-property",
                "Elastic material values must be finite.")) {
            return;
        }
        material.hasElastic = true;
        material.elasticLocation = block.data[0].location;
    }

    void parseBoundary(
        const KeywordBlock& block,
        std::vector<BoundaryCondition>& destination) {
        if (!validateParameters(block, {}) || block.data.empty()) {
            if (!failure_) {
                inputFailure(
                    "invalid-data-arity", block.location,
                    block.canonicalName, "",
                    "BOUNDARY requires one or more data rows.");
            }
            return;
        }
        for (const auto& row : block.data) {
            if (row.fields.size() != 3U && row.fields.size() != 4U) {
                inputFailure(
                    "invalid-data-arity", row.location,
                    block.canonicalName, "",
                    "BOUNDARY rows require target, first DOF, last DOF, and optional value.");
                return;
            }
            if (row.fields[0].empty()) {
                inputFailure(
                    "unresolved-reference", row.location,
                    block.canonicalName, "",
                    "A boundary target cannot be empty.");
                return;
            }
            std::int64_t first = 0;
            std::int64_t last = 0;
            if (!parseInteger(
                    row.fields[1], first, row.location,
                    block.canonicalName, true) ||
                !parseInteger(
                    row.fields[2], last, row.location,
                    block.canonicalName, true)) {
                return;
            }
            if (first < 1 || first > 6 || last < first || last > 6) {
                inputFailure(
                    "invalid-dof", row.location, block.canonicalName,
                    row.fields[0], "Boundary DOFs must be an ordered range in 1..6.");
                return;
            }
            double value = 0.0;
            if (row.fields.size() == 4U &&
                !parseDouble(
                    row.fields[3], value, row.location,
                    block.canonicalName)) {
                return;
            }
            destination.push_back({
                row.fields[0],
                static_cast<int>(first),
                static_cast<int>(last),
                value,
                row.location});
        }
    }

    void parseCload(const KeywordBlock& block, std::vector<NodalLoad>& loads) {
        if (!validateParameters(block, {}) || block.data.empty()) {
            if (!failure_) {
                inputFailure(
                    "invalid-data-arity", block.location,
                    block.canonicalName, "", "CLOAD requires one or more rows.");
            }
            return;
        }
        for (const auto& row : block.data) {
            if (row.fields.size() != 3U || row.fields[0].empty()) {
                inputFailure(
                    "invalid-data-arity", row.location,
                    block.canonicalName, "",
                    "CLOAD rows require target, DOF, magnitude.");
                return;
            }
            std::int64_t dof = 0;
            double magnitude = 0.0;
            if (!parseInteger(
                    row.fields[1], dof, row.location,
                    block.canonicalName, true) ||
                !parseDouble(
                    row.fields[2], magnitude, row.location,
                    block.canonicalName)) {
                return;
            }
            if (dof < 1 || dof > 6) {
                inputFailure(
                    "invalid-dof", row.location, block.canonicalName,
                    row.fields[0], "CLOAD DOF must be in 1..6.");
                return;
            }
            loads.push_back({
                row.fields[0], static_cast<int>(dof), magnitude, row.location});
        }
    }

    void parseStepStart(const KeywordBlock& block) {
        if (stepSeen_) {
            inputFailure(
                "unsupported-multiple-step", block.location,
                block.canonicalName, "",
                "V0 accepts exactly one analysis step.");
            return;
        }
        if (!assemblySeen_ || materials_.empty()) {
            invalidKeywordLocation(
                block,
                "The sole step follows the complete assembly and material definitions.");
            return;
        }
        if (!validateParameters(block, {"NAME", "NLGEOM"}) ||
            !requireNoData(block)) {
            return;
        }
        const auto* nlgeom = parameter(block, "NLGEOM");
        if (nlgeom != nullptr) {
            if (!nlgeom->value || nlgeom->value->empty()) {
                inputFailure(
                    "invalid-keyword-parameter", block.location,
                    block.canonicalName, "NLGEOM",
                    "NLGEOM requires NO in the approved subset.");
                return;
            }
            if (!equalName(*nlgeom->value, "NO")) {
                if (modelElementFamily_ == ElementFamily::shell) {
                    inputFailure(
                        "unsupported-nonlinear-geometry", block.location,
                        block.canonicalName, *nlgeom->value,
                        "Only absent NLGEOM or NLGEOM=NO is supported.");
                } else {
                    modelFailure(
                        "unsupported-nonlinear-geometry", block.location,
                        block.canonicalName, *nlgeom->value,
                        "Only absent NLGEOM or NLGEOM=NO is supported.");
                }
                return;
            }
        }
        stepSeen_ = true;
        inStep_ = true;
        step_ = RawStep{block.location, false, {}, {}, {}};
        stepLoadSeen_ = false;
        stepNoOpSeen_ = false;
    }

    void parseStepBlock(const KeywordBlock& block) {
        if (block.canonicalName == "END STEP") {
            activeOutput_ = false;
            if (!validateParameters(block, {}) || !requireNoData(block)) {
                return;
            }
            if (!step_.hasStatic) {
                invalidKeywordLocation(
                    block,
                    "The sole step requires exactly one leading STATIC procedure.");
                return;
            }
            inStep_ = false;
            return;
        }
        if (block.canonicalName == "STEP") {
            inputFailure(
                "unsupported-multiple-step", block.location,
                block.canonicalName, "",
                "A second or nested step is unsupported.");
            return;
        }
        if (block.canonicalName == "STATIC") {
            activeOutput_ = false;
            if (!step_.boundaries.empty() || !step_.loads.empty() ||
                stepNoOpSeen_) {
                invalidKeywordLocation(
                    block,
                    "STATIC must be the first keyword in the sole step.");
                return;
            }
            parseStatic(block);
        } else if (block.canonicalName == "BOUNDARY") {
            activeOutput_ = false;
            if (!step_.hasStatic || stepLoadSeen_ || stepNoOpSeen_) {
                invalidKeywordLocation(
                    block,
                    "Step boundaries follow STATIC and precede loads and no-op requests.");
                return;
            }
            parseBoundary(block, step_.boundaries);
        } else if (block.canonicalName == "CLOAD") {
            activeOutput_ = false;
            if (!step_.hasStatic || stepNoOpSeen_) {
                invalidKeywordLocation(
                    block,
                    "CLOAD follows STATIC and boundaries and precedes no-op requests.");
                return;
            }
            parseCload(block, step_.loads);
            stepLoadSeen_ = !failure_;
        } else if (block.canonicalName == "RESTART") {
            activeOutput_ = false;
            if (!step_.hasStatic) {
                invalidKeywordLocation(
                    block,
                    "Step no-op requests follow STATIC, boundaries, and loads.");
                return;
            }
            stepNoOpSeen_ = true;
            if (requireNoData(block)) {
                addIgnoredWarning(block);
            }
        } else if (block.canonicalName == "OUTPUT") {
            if (!step_.hasStatic) {
                invalidKeywordLocation(
                    block,
                    "Step no-op requests follow STATIC, boundaries, and loads.");
                return;
            }
            stepNoOpSeen_ = true;
            parseOutputRoot(block);
        } else if (block.canonicalName == "NODE OUTPUT" ||
                   block.canonicalName == "ELEMENT OUTPUT" ||
                   block.canonicalName == "CONTACT OUTPUT") {
            parseOutputChild(block);
        } else {
            activeOutput_ = false;
            rejectUnknown(block);
        }
    }

    void parseStatic(const KeywordBlock& block) {
        if (step_.hasStatic || !validateParameters(block, {}) ||
            block.data.size() != 1U || block.data[0].fields.size() != 4U) {
            if (!failure_) {
                inputFailure(
                    "invalid-static-data", block.location,
                    block.canonicalName, "",
                    "STATIC requires exactly one row of four values.");
            }
            return;
        }
        for (std::size_t field = 0U; field < 4U; ++field) {
            if (!parseDouble(
                    block.data[0].fields[field], step_.staticValues[field],
                    block.data[0].location, block.canonicalName)) {
                return;
            }
            if (step_.staticValues[field] <= 0.0) {
                inputFailure(
                    "invalid-static-data", block.data[0].location,
                    block.canonicalName, "",
                    "All four STATIC fields must be positive.");
                return;
            }
        }
        if (step_.staticValues[2] > step_.staticValues[3]) {
            inputFailure(
                "invalid-static-data", block.data[0].location,
                block.canonicalName, "",
                "STATIC minimum increment cannot exceed maximum increment.");
            return;
        }
        step_.hasStatic = true;
    }

    void parseOutputRoot(const KeywordBlock& block) {
        const auto* field = parameter(block, "FIELD");
        const auto* history = parameter(block, "HISTORY");
        if ((field == nullptr) == (history == nullptr) ||
            (field != nullptr && field->value) ||
            (history != nullptr && history->value)) {
            inputFailure(
                "unsupported-keyword", block.location,
                block.canonicalName, "",
                "OUTPUT must select exactly FIELD or HISTORY.");
            return;
        }
        activeOutput_ = true;
        addIgnoredWarning(block);
    }

    void parseOutputChild(const KeywordBlock& block) {
        if (!activeOutput_) {
            inputFailure(
                "invalid-keyword-location", block.location,
                block.canonicalName, "",
                "Output variable keywords require an active OUTPUT request.");
            return;
        }
        addIgnoredWarning(block);
    }

    void addIgnoredWarning(const KeywordBlock& block) {
        // One warning per allowlisted keyword keeps no-op provenance stable;
        // subordinate variable rows remain attached to that keyword record.
        definition_.warnings.push_back({
            Severity::warning,
            "ignored-input-keyword",
            block.location,
            block.canonicalName,
            "",
            "The allowlisted Abaqus keyword is ignored without semantic effect."});
    }

    void rejectUnknown(const KeywordBlock& block) {
        if (block.canonicalName == "DLOAD" &&
            modelElementFamily_ == ElementFamily::shell) {
            inputFailure(
                "unsupported-distributed-load", block.location,
                block.canonicalName, "",
                "Distributed, pressure, gravity, body, edge, and follower loads are unsupported.");
            return;
        }
        inputFailure(
            "unsupported-keyword", block.location,
            block.canonicalName, "",
            "The keyword is outside the approved Abaqus subset.");
    }

    const RawPart* findPart(const std::string& name) const {
        const auto found = std::find_if(
            parts_.begin(), parts_.end(), [&name](const RawPart& part) {
                return equalName(part.name, name);
            });
        return found == parts_.end() ? nullptr : &*found;
    }

    const RawInstance* findInstance(const std::string& name) const {
        const auto found = std::find_if(
            instances_.begin(), instances_.end(),
            [&name](const RawInstance& instance) {
                return equalName(instance.name, name);
            });
        return found == instances_.end() ? nullptr : &*found;
    }

    std::optional<EntityIndex> findMaterialIndex(const std::string& name) const {
        for (std::size_t index = 0U; index < materials_.size(); ++index) {
            if (equalName(materials_[index].name, name)) {
                return static_cast<EntityIndex>(index);
            }
        }
        return std::nullopt;
    }

    const RawSet* findSet(
        const std::vector<RawSet>& sets,
        const std::string& name) const {
        const auto found = std::find_if(
            sets.begin(), sets.end(), [&name](const RawSet& set) {
                return equalName(set.name, name);
            });
        return found == sets.end() ? nullptr : &*found;
    }

    const RawNode* findNode(const RawPart& part, std::int64_t label) const {
        const auto found = std::find_if(
            part.nodes.begin(), part.nodes.end(),
            [label](const RawNode& node) { return node.label == label; });
        return found == part.nodes.end() ? nullptr : &*found;
    }

    const RawElement* findElement(
        const RawPart& part,
        std::int64_t label) const {
        const auto found = std::find_if(
            part.elements.begin(), part.elements.end(),
            [label](const RawElement& element) { return element.label == label; });
        return found == part.elements.end() ? nullptr : &*found;
    }

    void finalizeModel() {
        if (parts_.empty() || !assemblySeen_ || instances_.empty() ||
            materials_.empty() || !stepSeen_ || !step_.hasStatic) {
            const auto location = input_.blocks.empty()
                ? SourceLocation{input_.sourcePath, 0U}
                : input_.blocks.back().location;
            inputFailure(
                "invalid-model-cardinality", location, "", "",
                "The model requires part, assembly, identity instance, material, and one STATIC step.");
            return;
        }

        finalizeMaterials();
        if (failure_) {
            return;
        }
        finalizePartsAndSections();
        if (failure_) {
            return;
        }
        expandInstances();
        if (failure_) {
            return;
        }
        if (!definition_.shellElements.empty()) {
            auto geometry = preprocessShellGeometry(
                definition_.nodes,
                definition_.shellElements,
                definition_.shellSections);
            if (!geometry.hasValue()) {
                const auto& status = geometry.status();
                failure_ = MappingFailure{
                    status.failureCategory().value_or(FailureCategory::model),
                    status.diagnostics().front()};
                return;
            }
            definition_.shellNodeInitialFrames =
                std::move(geometry.value().nodalFrames);
        }
        expandAssemblySets();
        if (failure_) {
            return;
        }
        finalizeStep();
    }

    void finalizeMaterials() {
        for (const auto& material : materials_) {
            if (!material.hasElastic) {
                inputFailure(
                    "unresolved-reference", material.location,
                    "MATERIAL", material.name,
                    "A material must own exactly one ELASTIC row.");
                return;
            }
            if (modelElementFamily_ == ElementFamily::shell) {
                if (!(material.youngsModulus > 0.0) ||
                    !(material.poissonRatio > -1.0) ||
                    !(material.poissonRatio < 0.5)) {
                    modelFailure(
                        "invalid-shell-material", material.elasticLocation,
                        "ELASTIC", material.name,
                        "Shell isotropic elasticity requires E>0 and -1<nu<0.5.");
                    return;
                }
                definition_.materials.push_back({
                    material.name,
                    material.youngsModulus,
                    material.poissonRatio,
                    material.location});
                continue;
            }
            const double denominator = 2.0 * (1.0 + material.poissonRatio);
            const double shearModulus = material.youngsModulus / denominator;
            // The approved ledger deliberately validates derived G, not an
            // independent upper bound on Poisson's ratio.
            if (!(material.youngsModulus > 0.0) ||
                !std::isfinite(shearModulus) || !(shearModulus > 0.0)) {
                modelFailure(
                    "invalid-beam-property", material.elasticLocation,
                    "ELASTIC", material.name,
                    "E and the derived G=E/(2*(1+nu)) must be positive.");
                return;
            }
            definition_.materials.push_back({
                material.name,
                material.youngsModulus,
                material.poissonRatio,
                material.location});
        }
    }

    void finalizePartsAndSections() {
        partSectionAssignments_.resize(parts_.size());
        partShellSectionAssignments_.resize(parts_.size());
        const bool shellModel = modelElementFamily_ == ElementFamily::shell;
        for (std::size_t partIndex = 0U; partIndex < parts_.size(); ++partIndex) {
            const RawPart& part = parts_[partIndex];
            if (part.nodes.empty() || part.elements.empty() ||
                (!shellModel && part.sections.empty())) {
                inputFailure(
                    "invalid-model-cardinality", part.location,
                    "PART", part.name,
                    "A part requires nodes, elements, and its approved section form.");
                return;
            }

            PartDefinition partDefinition{};
            partDefinition.name = part.name;
            partDefinition.location = part.location;
            for (const auto& node : part.nodes) {
                partDefinition.nodeSourceLabels.push_back(node.label);
            }
            for (const auto& element : part.elements) {
                partDefinition.elementSourceLabels.push_back(element.label);
                for (const auto nodeLabel : element.nodeLabels) {
                    if (findNode(part, nodeLabel) == nullptr) {
                        inputFailure(
                            shellModel
                                ? "invalid-shell-connectivity"
                                : "unresolved-reference",
                            element.location, "ELEMENT", element.labelText,
                            "Element connectivity must resolve within its part.");
                        return;
                    }
                }
            }
            for (const auto& set : part.nodeSets) {
                partDefinition.nodeSetNames.push_back(set.name);
                for (const auto label : set.members) {
                    if (findNode(part, label) == nullptr) {
                        inputFailure(
                            "unresolved-reference", set.location,
                            "NSET", set.name,
                            "Every node-set member must resolve within its part.");
                        return;
                    }
                }
            }
            for (const auto& set : part.elementSets) {
                partDefinition.elementSetNames.push_back(set.name);
                for (const auto label : set.members) {
                    if (findElement(part, label) == nullptr) {
                        inputFailure(
                            "unresolved-reference", set.location,
                            "ELSET", set.name,
                            "Every element-set member must resolve within its part.");
                        return;
                    }
                }
            }
            definition_.parts.push_back(std::move(partDefinition));

            if (shellModel) {
                auto& assignments = partShellSectionAssignments_[partIndex];
                for (const auto& section : part.shellSections) {
                    const auto* elementSet = findSet(
                        part.elementSets, section.elementSetName);
                    const auto materialIndex = findMaterialIndex(section.materialName);
                    if (elementSet == nullptr || !materialIndex) {
                        inputFailure(
                            "unresolved-shell-section", section.location,
                            "SHELL SECTION", section.elementSetName,
                            "Shell section ELSET and MATERIAL references must resolve.");
                        return;
                    }
                    const EntityIndex sectionIndex =
                        static_cast<EntityIndex>(definition_.shellSections.size());
                    definition_.shellSections.push_back({
                        section.elementSetName,
                        section.thickness,
                        *materialIndex,
                        section.location});
                    for (const auto elementLabel : elementSet->members) {
                        if (!assignments.emplace(
                                elementLabel,
                                std::make_pair(
                                    sectionIndex, *materialIndex)).second) {
                            inputFailure(
                                "invalid-shell-section-assignment",
                                section.location, "SHELL SECTION",
                                std::to_string(elementLabel),
                                "A shell element cannot receive multiple section assignments.");
                            return;
                        }
                    }
                }
                for (const auto& element : part.elements) {
                    if (assignments.find(element.label) == assignments.end()) {
                        inputFailure(
                            "invalid-shell-section-assignment", element.location,
                            "ELEMENT", element.labelText,
                            "Every shell element requires exactly one resolved section assignment.");
                        return;
                    }
                }
                continue;
            }

            auto& assignments = partSectionAssignments_[partIndex];
            for (const auto& section : part.sections) {
                const auto* elementSet = findSet(
                    part.elementSets, section.elementSetName);
                const auto materialIndex = findMaterialIndex(section.materialName);
                if (elementSet == nullptr || !materialIndex) {
                    inputFailure(
                        "unresolved-reference", section.location,
                        "BEAM GENERAL SECTION", section.elementSetName,
                        "Section ELSET and MATERIAL references must resolve.");
                    return;
                }
                if (section.properties[2] != 0.0) {
                    modelFailure(
                        "unsupported-coupled-section", section.location,
                        "BEAM GENERAL SECTION", section.elementSetName,
                        "V0 requires exact I12=0.");
                    return;
                }
                if (!(section.properties[0] > 0.0) ||
                    !(section.properties[1] > 0.0) ||
                    !(section.properties[3] > 0.0) ||
                    !(section.properties[4] > 0.0)) {
                    modelFailure(
                        "invalid-beam-property", section.location,
                        "BEAM GENERAL SECTION", section.elementSetName,
                        "A, I11, I22, and J must be positive.");
                    return;
                }
                const EntityIndex sectionIndex =
                    static_cast<EntityIndex>(definition_.sections.size());
                definition_.sections.push_back({
                    section.elementSetName,
                    section.properties[0],
                    section.properties[1],
                    section.properties[2],
                    section.properties[3],
                    section.properties[4],
                    section.firstAxis,
                    section.sectionPoints,
                    section.location});
                for (const auto elementLabel : elementSet->members) {
                    if (!assignments.emplace(
                            elementLabel,
                            std::make_pair(sectionIndex, *materialIndex)).second) {
                        inputFailure(
                            "duplicate-entity", section.location,
                            "BEAM GENERAL SECTION",
                            std::to_string(elementLabel),
                            "An element cannot receive multiple section assignments.");
                        return;
                    }
                }
            }
            for (const auto& element : part.elements) {
                if (assignments.find(element.label) == assignments.end()) {
                    inputFailure(
                        "unresolved-reference", element.location,
                        "ELEMENT", element.labelText,
                        "Every B33 element requires a resolved section assignment.");
                    return;
                }
            }
        }
    }

    std::size_t partIndex(const RawPart& part) const {
        return static_cast<std::size_t>(&part - parts_.data());
    }

    bool validateGeometry(
        const RawElement& raw,
        const Node& first,
        const Node& second,
        const GeneralBeamSection& section) {
        const auto norm = [](const std::array<double, 3>& vector) {
            return std::hypot(vector[0], vector[1], vector[2]);
        };
        const auto maximumAbsolute = [](const std::array<double, 3>& vector) {
            return std::max({
                std::abs(vector[0]),
                std::abs(vector[1]),
                std::abs(vector[2])});
        };

        // Compare both approved inequalities after a common scaling. This
        // preserves the exact ratios while avoiding overflow in x*x and in
        // subtraction between large finite coordinates.
        const double globalCoordinateScale = std::max({
            1.0,
            maximumAbsolute(first.coordinates),
            maximumAbsolute(second.coordinates)});
        std::array<double, 3> firstScaled{};
        std::array<double, 3> secondScaled{};
        std::array<double, 3> deltaScaled{};
        for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) {
            firstScaled[coordinate] =
                first.coordinates[coordinate] / globalCoordinateScale;
            secondScaled[coordinate] =
                second.coordinates[coordinate] / globalCoordinateScale;
            deltaScaled[coordinate] =
                secondScaled[coordinate] - firstScaled[coordinate];
        }
        const double lengthRatio = norm(deltaScaled);
        const double coordinateNormRatio = std::max({
            1.0 / globalCoordinateScale,
            norm(firstScaled),
            norm(secondScaled)});
        if (!(lengthRatio > 1.0e-12 * coordinateNormRatio)) {
            return modelFailure(
                "invalid-beam-length", raw.location, "ELEMENT",
                raw.labelText,
                "Beam length fails the approved scale-aware threshold.");
        }
        std::array<double, 3> tangent{
            deltaScaled[0] / lengthRatio,
            deltaScaled[1] / lengthRatio,
            deltaScaled[2] / lengthRatio};

        const double globalGuideScale =
            std::max(1.0, maximumAbsolute(section.firstAxis));
        std::array<double, 3> guideScaled{};
        for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) {
            guideScaled[coordinate] =
                section.firstAxis[coordinate] / globalGuideScale;
        }
        const double projection =
            guideScaled[0] * tangent[0] +
            guideScaled[1] * tangent[1] +
            guideScaled[2] * tangent[2];
        std::array<double, 3> perpendicular{
            guideScaled[0] - projection * tangent[0],
            guideScaled[1] - projection * tangent[1],
            guideScaled[2] - projection * tangent[2]};
        const double guideNormRatio = std::max(
            1.0 / globalGuideScale,
            norm(guideScaled));
        if (!(norm(perpendicular) > 1.0e-12 * guideNormRatio)) {
            return modelFailure(
                "invalid-beam-guide-vector", section.location,
                "BEAM GENERAL SECTION", raw.labelText,
                "The first section axis cannot be zero or tangent-parallel.");
        }
        return true;
    }

    void expandInstances() {
        for (const auto& rawInstance : instances_) {
            const RawPart* part = findPart(rawInstance.partName);
            if (part == nullptr) {
                inputFailure(
                    "unresolved-reference", rawInstance.location,
                    "INSTANCE", rawInstance.name,
                    "INSTANCE PART must resolve case-insensitively.");
                return;
            }
            InstanceDefinition instance{
                rawInstance.name, part->name, {}, {}, rawInstance.location};
            std::map<std::int64_t, EntityIndex> nodeIndices;
            std::map<std::int64_t, EntityIndex> elementIndices;

            // Instance order, followed by part-local declaration order, is the
            // sole source of stable expanded internal IDs.
            for (const auto& rawNode : part->nodes) {
                if (definition_.nodes.size() >
                    std::numeric_limits<EntityIndex>::max()) {
                    inputFailure(
                        "entity-index-overflow", rawNode.location,
                        "NODE", rawNode.labelText,
                        "Expanded node count exceeds EntityIndex capacity.");
                    return;
                }
                const EntityIndex index =
                    static_cast<EntityIndex>(definition_.nodes.size());
                definition_.nodes.push_back({
                    {rawInstance.name, rawNode.label, rawNode.labelText},
                    rawNode.coordinates,
                    rawNode.location});
                nodeIndices.emplace(rawNode.label, index);
                instance.nodeMappings.push_back({rawNode.label, index});
            }

            for (const auto& rawElement : part->elements) {
                EntityIndex index = 0U;
                if (rawElement.type == RawElement::Type::b33) {
                    const auto first = nodeIndices.find(rawElement.nodeLabels[0]);
                    const auto second = nodeIndices.find(rawElement.nodeLabels[1]);
                    const auto& assignments =
                        partSectionAssignments_[partIndex(*part)];
                    const auto assignment = assignments.find(rawElement.label);
                    if (first == nodeIndices.end() || second == nodeIndices.end() ||
                        assignment == assignments.end()) {
                        inputFailure(
                            "unresolved-reference", rawElement.location,
                            "ELEMENT", rawElement.labelText,
                            "Expanded connectivity and section assignment must resolve.");
                        return;
                    }
                    if (definition_.elements.size() >
                        std::numeric_limits<EntityIndex>::max()) {
                        inputFailure(
                            "entity-index-overflow", rawElement.location,
                            "ELEMENT", rawElement.labelText,
                            "Expanded element count exceeds EntityIndex capacity.");
                        return;
                    }
                    index = static_cast<EntityIndex>(definition_.elements.size());
                    const auto& section =
                        definition_.sections[assignment->second.first];
                    if (!validateGeometry(
                            rawElement,
                            definition_.nodes[first->second],
                            definition_.nodes[second->second],
                            section)) {
                        return;
                    }
                    definition_.elements.push_back({
                        {rawInstance.name, rawElement.label, rawElement.labelText},
                        {first->second, second->second},
                        assignment->second.second,
                        assignment->second.first,
                        rawElement.location});
                } else {
                    std::array<EntityIndex, 4> connectedNodes{};
                    for (std::size_t node = 0U;
                         node < connectedNodes.size();
                         ++node) {
                        const auto found =
                            nodeIndices.find(rawElement.nodeLabels[node]);
                        if (found == nodeIndices.end()) {
                            inputFailure(
                                "invalid-shell-connectivity", rawElement.location,
                                "ELEMENT", rawElement.labelText,
                                "Expanded shell connectivity must resolve.");
                            return;
                        }
                        connectedNodes[node] = found->second;
                    }
                    const auto& assignments =
                        partShellSectionAssignments_[partIndex(*part)];
                    const auto assignment = assignments.find(rawElement.label);
                    if (assignment == assignments.end()) {
                        inputFailure(
                            "invalid-shell-section-assignment",
                            rawElement.location, "ELEMENT", rawElement.labelText,
                            "Expanded shell section assignment must resolve exactly once.");
                        return;
                    }
                    if (definition_.shellElements.size() >
                        std::numeric_limits<EntityIndex>::max()) {
                        inputFailure(
                            "entity-index-overflow", rawElement.location,
                            "ELEMENT", rawElement.labelText,
                            "Expanded shell element count exceeds EntityIndex capacity.");
                        return;
                    }
                    index =
                        static_cast<EntityIndex>(definition_.shellElements.size());
                    definition_.shellElements.push_back({
                        {rawInstance.name, rawElement.label, rawElement.labelText},
                        rawElement.type == RawElement::Type::s4
                            ? ShellSourceElementType::s4
                            : ShellSourceElementType::s4r,
                        connectedNodes,
                        assignment->second.second,
                        assignment->second.first,
                        rawElement.location});
                }
                elementIndices.emplace(rawElement.label, index);
                instance.elementMappings.push_back({rawElement.label, index});
            }

            for (const auto& rawSet : part->nodeSets) {
                NodeSet set{rawSet.name, rawInstance.name, {}, rawSet.location};
                for (const auto member : rawSet.members) {
                    const auto found = nodeIndices.find(member);
                    if (found == nodeIndices.end()) {
                        inputFailure(
                            "unresolved-reference", rawSet.location,
                            "NSET", rawSet.name,
                            "Part node-set expansion failed.");
                        return;
                    }
                    set.nodeIndices.push_back(found->second);
                }
                definition_.nodeSets.push_back(std::move(set));
            }
            for (const auto& rawSet : part->elementSets) {
                ElementSet set{
                    rawSet.name, rawInstance.name, {}, rawSet.location};
                for (const auto member : rawSet.members) {
                    const auto found = elementIndices.find(member);
                    if (found == elementIndices.end()) {
                        inputFailure(
                            "unresolved-reference", rawSet.location,
                            "ELSET", rawSet.name,
                            "Part element-set expansion failed.");
                        return;
                    }
                    set.elementIndices.push_back(found->second);
                }
                definition_.elementSets.push_back(std::move(set));
            }
            definition_.instances.push_back(std::move(instance));
        }
    }

    void expandAssemblySets() {
        for (const auto& rawSet : assemblySets_) {
            const RawInstance* rawInstance = findInstance(rawSet.instanceName);
            if (rawInstance == nullptr) {
                inputFailure(
                    "unresolved-reference", rawSet.location,
                    rawSet.isNodeSet ? "NSET" : "ELSET", rawSet.name,
                    "Assembly set INSTANCE must resolve.");
                return;
            }
            const auto definitionInstance = std::find_if(
                definition_.instances.begin(), definition_.instances.end(),
                [&rawInstance](const InstanceDefinition& instance) {
                    return equalName(instance.name, rawInstance->name);
                });
            if (definitionInstance == definition_.instances.end()) {
                inputFailure(
                    "unresolved-reference", rawSet.location,
                    rawSet.isNodeSet ? "NSET" : "ELSET", rawSet.name,
                    "Assembly set instance expansion is unavailable.");
                return;
            }
            if (rawSet.isNodeSet) {
                definition_.nodeSets.erase(
                    std::remove_if(
                        definition_.nodeSets.begin(),
                        definition_.nodeSets.end(),
                        [&rawSet](const NodeSet& set) {
                            return equalName(set.name, rawSet.name);
                        }),
                    definition_.nodeSets.end());
                NodeSet set{
                    rawSet.name, definitionInstance->name, {}, rawSet.location};
                for (const auto member : rawSet.members) {
                    const auto mapping = std::find_if(
                        definitionInstance->nodeMappings.begin(),
                        definitionInstance->nodeMappings.end(),
                        [member](const SourceIndexMapping& value) {
                            return value.sourceLabel == member;
                        });
                    if (mapping == definitionInstance->nodeMappings.end()) {
                        inputFailure(
                            "unresolved-reference", rawSet.location,
                            "NSET", rawSet.name,
                            "Assembly node-set member must resolve in its instance.");
                        return;
                    }
                    set.nodeIndices.push_back(mapping->internalIndex);
                }
                definition_.nodeSets.push_back(std::move(set));
            } else {
                definition_.elementSets.erase(
                    std::remove_if(
                        definition_.elementSets.begin(),
                        definition_.elementSets.end(),
                        [&rawSet](const ElementSet& set) {
                            return equalName(set.name, rawSet.name);
                        }),
                    definition_.elementSets.end());
                ElementSet set{
                    rawSet.name, definitionInstance->name, {}, rawSet.location};
                for (const auto member : rawSet.members) {
                    const auto mapping = std::find_if(
                        definitionInstance->elementMappings.begin(),
                        definitionInstance->elementMappings.end(),
                        [member](const SourceIndexMapping& value) {
                            return value.sourceLabel == member;
                        });
                    if (mapping == definitionInstance->elementMappings.end()) {
                        inputFailure(
                            "unresolved-reference", rawSet.location,
                            "ELSET", rawSet.name,
                            "Assembly element-set member must resolve in its instance.");
                        return;
                    }
                    set.elementIndices.push_back(mapping->internalIndex);
                }
                definition_.elementSets.push_back(std::move(set));
            }
        }
    }

    std::optional<std::vector<EntityIndex>> resolveNodeTarget(
        const std::string& target,
        const SourceLocation& location,
        const std::string& keyword) {
        std::vector<const NodeSet*> matchingSets;
        for (const auto& set : definition_.nodeSets) {
            if (equalName(set.name, target)) {
                matchingSets.push_back(&set);
            }
        }

        std::vector<EntityIndex> matchingNodes;
        std::int64_t label = 0;
        if (tryPositiveInteger(target, label)) {
            for (std::size_t index = 0U;
                 index < definition_.nodes.size();
                 ++index) {
                if (definition_.nodes[index].sourceId.sourceLabel == label) {
                    matchingNodes.push_back(static_cast<EntityIndex>(index));
                }
            }
        }

        // A token is resolved only after both approved interpretations have
        // been considered; declaration order never gives a node set priority
        // over an equally valid direct source-label target.
        if (matchingSets.size() > 1U) {
            inputFailure(
                "unresolved-reference", location, keyword, target,
                "The node-set target is ambiguous across identity instances.");
            return std::nullopt;
        }
        if (matchingNodes.size() > 1U) {
            inputFailure(
                "unresolved-reference", location, keyword, target,
                "The direct node-label target is ambiguous across identity instances.");
            return std::nullopt;
        }
        if (matchingSets.size() == 1U && matchingNodes.size() == 1U) {
            inputFailure(
                "unresolved-reference", location, keyword, target,
                "The target is ambiguous between a node-set name and a direct node label.");
            return std::nullopt;
        }
        if (matchingSets.size() == 1U) {
            return matchingSets.front()->nodeIndices;
        }
        if (matchingNodes.size() == 1U) {
            return matchingNodes;
        }
        inputFailure(
            "unresolved-reference", location, keyword, target,
            "The boundary or load target must resolve to one node or node set.");
        return std::nullopt;
    }

    void finalizeStep() {
        std::vector<BoundaryCondition> boundaries = modelBoundaries_;
        boundaries.insert(
            boundaries.end(), step_.boundaries.begin(), step_.boundaries.end());

        std::map<std::pair<EntityIndex, int>, double> prescribedValues;
        for (const auto& boundary : boundaries) {
            auto target = resolveNodeTarget(
                boundary.target, boundary.location, "BOUNDARY");
            if (!target) {
                return;
            }
            for (const auto node : *target) {
                for (int dof = boundary.firstDof; dof <= boundary.lastDof; ++dof) {
                    const auto key = std::make_pair(node, dof);
                    const auto existing = prescribedValues.find(key);
                    if (existing != prescribedValues.end() &&
                        existing->second != boundary.value) {
                        inputFailure(
                            "conflicting-boundary-condition",
                            boundary.location,
                            "BOUNDARY",
                            boundary.target,
                            "Expanded boundary rows prescribe different values to one node/DOF.");
                        return;
                    }
                    prescribedValues[key] = boundary.value;
                }
            }
        }
        for (const auto& load : step_.loads) {
            if (!resolveNodeTarget(load.target, load.location, "CLOAD")) {
                return;
            }
        }

        // Static time-control values are provenance only: V0 creates exactly
        // the canonical final frame 0 and performs no increment loop here.
        definition_.steps.push_back({
            "Step-1",
            std::move(boundaries),
            step_.loads,
            step_.staticValues[0],
            step_.staticValues[1],
            step_.staticValues[2],
            step_.staticValues[3],
            step_.location});
    }

    const ParsedInput& input_;
    ModelDefinition definition_{};
    std::optional<MappingFailure> failure_;

    std::vector<RawPart> parts_;
    std::vector<RawInstance> instances_;
    std::vector<RawAssemblySet> assemblySets_;
    std::vector<RawMaterial> materials_;
    std::vector<BoundaryCondition> modelBoundaries_;
    RawStep step_{};
    std::vector<std::map<
        std::int64_t,
        std::pair<EntityIndex, EntityIndex>>> partSectionAssignments_;
    std::vector<std::map<
        std::int64_t,
        std::pair<EntityIndex, EntityIndex>>> partShellSectionAssignments_;

    std::optional<std::size_t> currentPart_;
    std::optional<std::size_t> pendingSection_;
    std::optional<std::size_t> materialEligible_;
    std::optional<ElementFamily> modelElementFamily_;
    bool headingSeen_{false};
    bool partElementsSeen_{false};
    bool partSetsSeen_{false};
    bool partSectionsSeen_{false};
    bool beamSectionContextActive_{false};
    bool assemblySeen_{false};
    bool assemblySetSeen_{false};
    bool modelBoundarySeen_{false};
    bool inAssembly_{false};
    bool inInstance_{false};
    bool stepSeen_{false};
    bool inStep_{false};
    bool stepLoadSeen_{false};
    bool stepNoOpSeen_{false};
    bool activeOutput_{false};
};

} // namespace

Result<Domain> AbaqusDomainMapper::map(const ParsedInput& input) const {
    return MappingContext{input}.run();
}

} // namespace fesa
