#include <fesa/io/abaqus/semantic_mapper.hpp>

#include <fesa/io/abaqus/active_input.hpp>
#include <fesa/io/abaqus/set_resolver.hpp>

#include <algorithm>
#include <array>
#include <charconv>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <map>
#include <optional>
#include <set>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace fesa {
namespace {

using LabelMap = std::map<std::int64_t, NodeId>;
using ElementLabelMap = std::map<std::int64_t, ElementId>;
using RawSetMap = std::map<std::string, std::vector<std::int64_t>, std::less<>>;

const std::string* parameter(
    const DeckRecord& record,
    const std::string_view name) {
    const auto found = record.parameters.find(name);
    return found == record.parameters.end() ? nullptr : &found->second;
}

SourceLocation data_source(
    const DeckRecord& record,
    const std::size_t row) {
    return row < record.data_sources.size()
               ? record.data_sources[row]
               : record.source;
}

bool parse_number(const std::string_view text, double& value) {
    const auto parsed = std::from_chars(
        text.data(),
        text.data() + text.size(),
        value,
        std::chars_format::general);
    return parsed.ec == std::errc{} &&
           parsed.ptr == text.data() + text.size() && std::isfinite(value);
}

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

class DeckMapper final {
public:
    explicit DeckMapper(const ParsedDeck& deck) : deck_{deck} {}

    [[nodiscard]] DomainBuildResult map() {
        ActiveInputResult selected = select_active_input(deck_);
        if (!selected.input.has_value()) {
            diagnostics_ = std::move(selected.diagnostics);
            return failure();
        }
        active_records_ = selected.input->part_records;
        flat_ = selected.input->flat;
        part_name_ = std::move(selected.input->part_name);
        instance_name_ = std::move(selected.input->instance_name);

        collect_materials();
        collect_nodes(active_records_, node_ids_, true);
        SetResolutionResult resolved_sets = resolve_sets(deck_);
        if (!resolved_sets.diagnostics.empty()) {
            diagnostics_.insert(
                diagnostics_.end(),
                std::make_move_iterator(resolved_sets.diagnostics.begin()),
                std::make_move_iterator(resolved_sets.diagnostics.end()));
        }
        validate_inactive_parts(resolved_sets.sets);
        if (resolved_sets.diagnostics.empty()) {
            collect_resolved_sets(resolved_sets.sets);
        }
        collect_sections(active_records_, active_element_sets_, true);
        collect_elements(
            active_records_, node_ids_, element_ids_, true);
        emit_sets();
        collect_step();

        if (!diagnostics_.empty()) {
            return failure();
        }
        return std::move(builder_).build();
    }

private:
    struct SectionAssignment final {
        MaterialId material;
        SectionId section;
    };

    struct MaterialProperties final {
        MaterialId id;
        double young;
        double poisson;
    };

    struct MaterialDefinition final {
        std::string name;
        SourceLocation source;
        const DeckRecord* elastic = nullptr;
        bool duplicate = false;
    };

    [[nodiscard]] DomainBuildResult failure() {
        return {std::nullopt, std::move(diagnostics_)};
    }

    void add_error(
        std::string code,
        std::string message,
        const SourceLocation& source) {
        diagnostics_.push_back({
            DiagnosticStage::semantic,
            Severity::error,
            std::move(code),
            std::move(message),
            source,
        });
    }

    const std::string* require_parameter(
        const DeckRecord& record,
        const std::string_view name) {
        const std::string* value = parameter(record, name);
        if (value == nullptr || value->empty()) {
            add_error(
                "abaqus.semantic.missing_parameter",
                "*" + record.keyword + " requires parameter " +
                    std::string{name} + ".",
                record.source);
            return nullptr;
        }
        return value;
    }

    std::optional<std::int64_t> parse_label(
        const std::string_view text,
        const std::string_view purpose,
        const SourceLocation& source) {
        std::int64_t value = 0;
        const auto parsed =
            std::from_chars(text.data(), text.data() + text.size(), value);
        if (parsed.ec != std::errc{} ||
            parsed.ptr != text.data() + text.size() || value <= 0) {
            add_error(
                "abaqus.semantic.invalid_label",
                "Invalid " + std::string{purpose} + " label '" +
                    std::string{text} + "'.",
                source);
            return std::nullopt;
        }
        return value;
    }

    std::optional<std::int64_t> parse_label(
        const std::string_view text,
        const DeckRecord& record,
        const std::string_view purpose) {
        return parse_label(text, purpose, record.source);
    }

    std::optional<std::uint8_t> parse_dof(
        const std::string_view text,
        const SourceLocation& source) {
        std::int64_t value = 0;
        const auto parsed =
            std::from_chars(text.data(), text.data() + text.size(), value);
        if (parsed.ec != std::errc{} ||
            parsed.ptr != text.data() + text.size() || value < 1 ||
            value > 6) {
            add_error(
                "abaqus.semantic.invalid_dof",
                "Phase 1 supports only degrees of freedom 1 through 6.",
                source);
            return std::nullopt;
        }
        return static_cast<std::uint8_t>(value);
    }

    std::optional<double> parse_real(
        const std::string_view text,
        const SourceLocation& source,
        const std::string_view purpose) {
        double value = 0.0;
        const auto parsed = std::from_chars(
            text.data(),
            text.data() + text.size(),
            value,
            std::chars_format::general);
        if (parsed.ec != std::errc{} ||
            parsed.ptr != text.data() + text.size() || !std::isfinite(value)) {
            add_error(
                "abaqus.semantic.invalid_number",
                "Invalid " + std::string{purpose} + " value '" +
                    std::string{text} + "'.",
                source);
            return std::nullopt;
        }
        return value;
    }

    void collect_materials() {
        std::vector<MaterialDefinition> definitions;
        std::map<std::string, std::size_t, std::less<>> first_definition;
        std::optional<std::size_t> current_definition;

        for (const DeckRecord& record : deck_.global_records) {
            if (record.keyword == "MATERIAL") {
                const std::string* name = require_parameter(record, "NAME");
                if (name == nullptr) {
                    current_definition.reset();
                    continue;
                }

                const bool duplicate = first_definition.contains(*name);
                if (duplicate) {
                    add_error(
                        "abaqus.semantic.duplicate_material",
                        "Material '" + *name +
                            "' is defined more than once.",
                        record.source);
                }
                definitions.push_back({
                    *name,
                    record.source,
                    nullptr,
                    duplicate,
                });
                current_definition = definitions.size() - 1U;
                first_definition.try_emplace(*name, *current_definition);
                continue;
            }
            if (record.keyword != "ELASTIC") {
                continue;
            }
            if (!current_definition.has_value()) {
                add_error(
                    "abaqus.semantic.elastic_without_material",
                    "*ELASTIC requires a preceding *MATERIAL.",
                    record.source);
                continue;
            }

            MaterialDefinition& definition =
                definitions[*current_definition];
            if (definition.elastic != nullptr) {
                add_error(
                    "abaqus.semantic.invalid_elastic_data",
                    "A material accepts exactly one *ELASTIC record.",
                    record.source);
                continue;
            }
            definition.elastic = &record;
        }

        for (const MaterialDefinition& definition : definitions) {
            if (definition.duplicate) {
                continue;
            }
            if (definition.elastic == nullptr) {
                add_error(
                    "abaqus.semantic.missing_elastic",
                    "Material '" + definition.name +
                        "' requires one *ELASTIC record.",
                    definition.source);
                continue;
            }

            const DeckRecord& elastic = *definition.elastic;
            const SourceLocation source =
                elastic.data.empty() ? elastic.source
                                     : data_source(elastic, 0U);
            if (elastic.data.size() != 1U ||
                elastic.data[0].size() != 2U ||
                elastic.data[0][0].empty() ||
                elastic.data[0][1].empty()) {
                add_error(
                    "abaqus.semantic.invalid_elastic_data",
                    "*ELASTIC requires exactly one E, nu data row.",
                    source);
                continue;
            }

            double young = 0.0;
            double poisson = 0.0;
            if (!parse_number(elastic.data[0][0], young) ||
                !parse_number(elastic.data[0][1], poisson) ||
                young <= 0.0 || poisson <= -1.0 || poisson >= 0.5) {
                add_error(
                    "abaqus.semantic.invalid_elastic_data",
                    "*ELASTIC requires finite E > 0 and -1 < nu < 0.5.",
                    source);
                continue;
            }

            const MaterialId id{next_material_id_++};
            materials_.emplace(
                definition.name,
                MaterialProperties{id, young, poisson});
            builder_.add_material(
                {id, definition.name, young, poisson});
        }
    }

    void collect_nodes(
        const std::span<const DeckRecord> records,
        LabelMap& node_ids,
        const bool emit) {
        for (const DeckRecord& record : records) {
            if (record.keyword != "NODE") {
                continue;
            }
            if (record.data.empty()) {
                add_error(
                    "abaqus.semantic.invalid_node_data",
                    "*NODE requires at least one data row.",
                    record.source);
                continue;
            }
            for (std::size_t row_index = 0; row_index < record.data.size();
                 ++row_index) {
                const auto& row = record.data[row_index];
                const SourceLocation source = data_source(record, row_index);
                if (row.size() != 4U || std::ranges::any_of(
                                             row,
                                             [](const std::string& field) {
                                                 return field.empty();
                                             })) {
                    add_error(
                        "abaqus.semantic.invalid_node_data",
                        "*NODE requires a label and three coordinates.",
                        source);
                    continue;
                }

                const auto label =
                    parse_label(row[0], "node", source);
                const auto x = parse_real(row[1], source, "node coordinate");
                const auto y = parse_real(row[2], source, "node coordinate");
                const auto z = parse_real(row[3], source, "node coordinate");
                if (!label.has_value() || !x.has_value() || !y.has_value() ||
                    !z.has_value()) {
                    continue;
                }

                if (node_ids.contains(*label)) {
                    add_error(
                        "abaqus.semantic.duplicate_node_label",
                        "Node label " + std::to_string(*label) +
                            " is defined more than once in its mesh scope.",
                        source);
                    continue;
                }

                const NodeId id = emit
                                      ? NodeId{next_node_id_++}
                                      : NodeId{static_cast<std::int64_t>(
                                            node_ids.size())};
                node_ids.emplace(*label, id);
                if (emit) {
                    builder_.add_node({
                        id,
                        EntityOrigin{part_name_, instance_name_, *label},
                        Vec3{*x, *y, *z},
                    });
                }
            }
        }
    }

    void validate_inactive_mesh(const ParsedPart& part) {
        LabelMap node_ids;
        ElementLabelMap element_ids;
        collect_nodes(part.records, node_ids, false);
        collect_elements(part.records, node_ids, element_ids, false);
    }

    struct SectionData final {
        std::array<double, 5> properties{};
        std::array<double, 3> orientation{};
        std::optional<std::array<double, 3>> shear_values;
        bool valid = true;
    };

    SectionData validate_section_data(
        const DeckRecord& record,
        const DeckRecord* shear) {
        SectionData result;
        if (record.data.size() != 2U ||
            record.data[0].size() != result.properties.size() ||
            record.data[1].size() != result.orientation.size()) {
            SourceLocation source = record.source;
            if (!record.data.empty()) {
                source = record.data[0].size() != result.properties.size()
                             ? data_source(record, 0U)
                             : data_source(
                                   record,
                                   record.data.size() > 2U ? 2U : 1U);
            }
            add_error(
                "abaqus.semantic.invalid_section_data",
                "*BEAM GENERAL SECTION requires exactly A, Iy, Iyz, Iz, J "
                "and one three-component orientation row.",
                source);
            result.valid = false;
        } else {
            bool valid_properties = true;
            for (std::size_t field = 0; field < result.properties.size();
                 ++field) {
                valid_properties =
                    parse_number(
                        record.data[0][field], result.properties[field]) &&
                    valid_properties;
            }
            valid_properties =
                valid_properties && result.properties[0] > 0.0 &&
                result.properties[1] > 0.0 && result.properties[2] == 0.0 &&
                result.properties[3] > 0.0 && result.properties[4] > 0.0;
            if (!valid_properties) {
                add_error(
                    "abaqus.semantic.invalid_section_data",
                    "General Beam properties require finite positive A, Iy, "
                    "Iz, J and Iyz=0.",
                    data_source(record, 0U));
            }

            bool valid_orientation = true;
            for (std::size_t field = 0; field < result.orientation.size();
                 ++field) {
                valid_orientation =
                    parse_number(
                        record.data[1][field], result.orientation[field]) &&
                    valid_orientation;
            }
            if (!valid_orientation) {
                add_error(
                    "abaqus.semantic.invalid_section_data",
                    "Beam section orientation components must be finite.",
                    data_source(record, 1U));
            }
            result.valid = valid_properties && valid_orientation;
        }

        if (shear == nullptr) {
            return result;
        }
        const SourceLocation source =
            shear->data.empty() ? shear->source : data_source(*shear, 0U);
        std::array<double, 3> values{};
        bool valid_shear = shear->data.size() == 1U &&
                           shear->data[0].size() == values.size();
        if (valid_shear) {
            for (std::size_t field = 0; field < values.size(); ++field) {
                valid_shear =
                    parse_number(shear->data[0][field], values[field]) &&
                    valid_shear;
            }
        }
        if (!valid_shear || values[0] <= 0.0 || values[1] <= 0.0) {
            add_error(
                "abaqus.semantic.invalid_transverse_shear_data",
                "*TRANSVERSE SHEAR STIFFNESS requires finite positive K23, "
                "K13 and numeric SCF=0.",
                source);
            result.valid = false;
        } else if (values[2] != 0.0) {
            add_error(
                "abaqus.semantic.nonzero_scf",
                "Phase 1 supports only SCF=0.",
                source);
            result.valid = false;
        } else {
            result.shear_values = values;
        }
        return result;
    }

    void validate_inactive_parts(
        const std::vector<ResolvedSet>& resolved_sets) {
        if (flat_) {
            return;
        }
        for (const ParsedPart& part : deck_.parts) {
            if (part.name == part_name_) {
                continue;
            }
            validate_inactive_mesh(part);
            RawSetMap element_sets;
            for (const ResolvedSet& set : resolved_sets) {
                if (set.scope == ResolvedSetScope::part &&
                    set.scope_name == part.name &&
                    set.kind == ResolvedSetKind::element) {
                    element_sets[set.set_name] = set.sorted_unique_labels;
                }
            }
            collect_sections(part.records, element_sets, false);
        }
    }

    void collect_resolved_sets(const std::vector<ResolvedSet>& sets) {
        for (const ResolvedSet& set : sets) {
            const bool mesh_scope =
                (flat_ && set.scope == ResolvedSetScope::global) ||
                (!flat_ && set.scope == ResolvedSetScope::part &&
                 set.scope_name == part_name_);
            const bool assembly_scope =
                !flat_ && set.scope == ResolvedSetScope::assembly &&
                deck_.assembly.has_value() &&
                set.scope_name == deck_.assembly->name;
            if (!mesh_scope && !assembly_scope) {
                continue;
            }

            RawSetMap& target = set.kind == ResolvedSetKind::node
                                    ? raw_node_sets_
                                    : raw_element_sets_;
            std::vector<std::int64_t>& labels = target[set.set_name];
            if (assembly_scope) {
                labels = set.sorted_unique_labels;
                if (set.kind == ResolvedSetKind::node) {
                    raw_assembly_node_sets_[set.set_name] =
                        set.sorted_unique_labels;
                }
            } else {
                labels.insert(
                    labels.end(),
                    set.sorted_unique_labels.begin(),
                    set.sorted_unique_labels.end());
                std::ranges::sort(labels);
                labels.erase(
                    std::ranges::unique(labels).begin(), labels.end());
            }

            if (mesh_scope && set.kind == ResolvedSetKind::element) {
                active_element_sets_[set.set_name] =
                    set.sorted_unique_labels;
            }
        }
    }

    void collect_sections(
        const std::span<const DeckRecord> records,
        const RawSetMap& element_sets,
        const bool emit) {
        std::set<std::string, std::less<>> assigned_set_names;
        std::set<std::int64_t> assigned_elements;
        for (std::size_t index = 0; index < records.size(); ++index) {
            const DeckRecord& record = records[index];
            if (record.keyword == "TRANSVERSE SHEAR STIFFNESS") {
                add_error(
                    "abaqus.semantic.orphan_transverse_shear",
                    "*TRANSVERSE SHEAR STIFFNESS must immediately follow "
                    "*BEAM GENERAL SECTION.",
                    record.source);
                continue;
            }
            if (record.keyword != "BEAM GENERAL SECTION") {
                continue;
            }

            const DeckRecord* shear = nullptr;
            if (index + 1U < records.size() &&
                records[index + 1U].keyword ==
                    "TRANSVERSE SHEAR STIFFNESS") {
                shear = &records[index + 1U];
                ++index;
            }

            bool valid = true;
            const std::string* section_type =
                require_parameter(record, "SECTION");
            const std::string* element_set =
                require_parameter(record, "ELSET");
            const std::string* material_name =
                require_parameter(record, "MATERIAL");
            if (section_type == nullptr || element_set == nullptr ||
                material_name == nullptr) {
                valid = false;
            } else if (uppercase_ascii(*section_type) != "GENERAL") {
                add_error(
                    "abaqus.semantic.unsupported_section",
                    "Phase 1 supports only SECTION=GENERAL.",
                    record.source);
                valid = false;
            }

            if (element_set != nullptr &&
                !assigned_set_names.insert(*element_set).second) {
                add_error(
                    "abaqus.semantic.duplicate_section_assignment",
                    "Element set '" + *element_set +
                        "' has more than one section assignment.",
                    record.source);
                valid = false;
            }

            auto material = materials_.end();
            if (material_name != nullptr) {
                material = materials_.find(*material_name);
                if (material == materials_.end()) {
                    add_error(
                        "abaqus.semantic.missing_material",
                        "Beam section references missing Material '" +
                            *material_name + "'.",
                        record.source);
                    valid = false;
                }
            }

            auto resolved_set = element_sets.end();
            if (element_set != nullptr) {
                resolved_set = element_sets.find(*element_set);
                if (resolved_set == element_sets.end()) {
                    add_error(
                        "abaqus.semantic.missing_element_set",
                        "Beam section references missing element set '" +
                            *element_set + "'.",
                        record.source);
                    valid = false;
                }
            }

            SectionData section_data = validate_section_data(record, shear);
            valid = valid && section_data.valid;

            double shear_area_y = 0.0;
            double shear_area_z = 0.0;
            ShearPropertySource shear_source =
                ShearPropertySource::phase1_default;
            if (shear == nullptr) {
                shear_area_y = 5.0 * section_data.properties[0] / 6.0;
                shear_area_z = shear_area_y;
            } else if (section_data.shear_values.has_value() &&
                       material != materials_.end()) {
                const SourceLocation shear_source_location =
                    shear->data.empty() ? shear->source
                                        : data_source(*shear, 0U);
                const double shear_modulus =
                    material->second.young /
                    (2.0 * (1.0 + material->second.poisson));
                shear_area_y = (*section_data.shear_values)[0] / shear_modulus;
                shear_area_z = (*section_data.shear_values)[1] / shear_modulus;
                if (!std::isfinite(shear_area_y) ||
                    !std::isfinite(shear_area_z) || shear_area_y <= 0.0 ||
                    shear_area_z <= 0.0) {
                    add_error(
                        "abaqus.semantic.invalid_transverse_shear_data",
                        "Transverse stiffness produces an invalid effective "
                        "shear area.",
                        shear_source_location);
                    valid = false;
                }
                shear_source = ShearPropertySource::input;
            }

            if (!valid || element_set == nullptr ||
                material == materials_.end() ||
                resolved_set == element_sets.end()) {
                continue;
            }

            bool duplicate_member = false;
            for (const std::int64_t label : resolved_set->second) {
                if (assigned_elements.contains(label)) {
                    duplicate_member = true;
                    break;
                }
            }
            if (duplicate_member) {
                add_error(
                    "abaqus.semantic.duplicate_section_assignment",
                    "An element has more than one section assignment.",
                    record.source);
                continue;
            }
            for (const std::int64_t label : resolved_set->second) {
                assigned_elements.insert(label);
            }
            if (!emit) {
                continue;
            }

            const SectionId section_id{next_section_id_++};
            const SectionAssignment assignment{
                material->second.id,
                section_id,
            };
            for (const std::int64_t label : resolved_set->second) {
                element_assignments_.emplace(label, assignment);
            }

            builder_.add_section({
                section_id,
                *element_set,
                section_data.properties[0],
                section_data.properties[1],
                section_data.properties[3],
                section_data.properties[4],
                shear_area_y,
                shear_area_z,
                shear_source,
                Vec3{
                    section_data.orientation[0],
                    section_data.orientation[1],
                    section_data.orientation[2]},
                {},
            });
        }
    }

    void collect_elements(
        const std::span<const DeckRecord> records,
        const LabelMap& node_ids,
        ElementLabelMap& element_ids,
        const bool emit) {
        for (const DeckRecord& record : records) {
            if (record.keyword != "ELEMENT") {
                continue;
            }

            const std::string* type = require_parameter(record, "TYPE");
            if (type == nullptr) {
                continue;
            }
            if (uppercase_ascii(*type) != "B31") {
                add_error(
                    "abaqus.semantic.unsupported_element",
                    "Phase 1 supports only B31 elements.",
                    record.source);
                continue;
            }
            if (record.data.empty()) {
                add_error(
                    "abaqus.semantic.invalid_element_data",
                    "*ELEMENT requires at least one data row.",
                    record.source);
                continue;
            }

            for (std::size_t row_index = 0; row_index < record.data.size();
                 ++row_index) {
                const auto& row = record.data[row_index];
                const SourceLocation source = data_source(record, row_index);
                if (row.size() != 3U || std::ranges::any_of(
                                             row,
                                             [](const std::string& field) {
                                                 return field.empty();
                                             })) {
                    add_error(
                        "abaqus.semantic.invalid_element_data",
                        "B31 element data requires a label and two nodes.",
                        source);
                    continue;
                }
                const auto label =
                    parse_label(row[0], "element", source);
                const auto first_label =
                    parse_label(row[1], "node", source);
                const auto second_label =
                    parse_label(row[2], "node", source);
                if (!label.has_value() || !first_label.has_value() ||
                    !second_label.has_value()) {
                    continue;
                }

                if (element_ids.contains(*label)) {
                    add_error(
                        "abaqus.semantic.duplicate_element_label",
                        "Element label " + std::to_string(*label) +
                            " is defined more than once in its mesh scope.",
                        source);
                    continue;
                }

                auto assignment = element_assignments_.end();
                if (emit) {
                    assignment = element_assignments_.find(*label);
                    if (assignment == element_assignments_.end()) {
                        add_error(
                            "abaqus.semantic.missing_section",
                            "B31 element " + std::to_string(*label) +
                                " has no Beam section assignment.",
                            source);
                        continue;
                    }
                }
                const auto first = node_ids.find(*first_label);
                const auto second = node_ids.find(*second_label);
                if (first == node_ids.end() || second == node_ids.end()) {
                    add_error(
                        "abaqus.semantic.missing_node",
                        "B31 element references a missing node label.",
                        source);
                    continue;
                }

                const ElementId id = emit
                                         ? ElementId{next_element_id_++}
                                         : ElementId{static_cast<std::int64_t>(
                                               element_ids.size())};
                element_ids.emplace(*label, id);
                if (!emit) {
                    continue;
                }
                builder_.add_beam_element({
                    id,
                    EntityOrigin{part_name_, instance_name_, *label},
                    {first->second, second->second},
                    assignment->second.material,
                    assignment->second.section,
                });
            }
        }
    }

    template <class Id, class Lookup>
    std::vector<Id> resolve_set(
        const std::vector<std::int64_t>& labels,
        const Lookup& lookup,
        const std::string& name) {
        std::vector<Id> members;
        for (const std::int64_t label : labels) {
            const auto found = lookup.find(label);
            if (found == lookup.end()) {
                diagnostics_.push_back({
                    DiagnosticStage::semantic,
                    Severity::error,
                    "abaqus.semantic.missing_set_member",
                    "Set '" + name + "' references a missing entity label " +
                        std::to_string(label) + ".",
                    std::nullopt,
                });
                continue;
            }
            members.push_back(found->second);
        }
        std::ranges::sort(members);
        members.erase(std::ranges::unique(members).begin(), members.end());
        return members;
    }

    void emit_sets() {
        for (const auto& [name, labels] : raw_node_sets_) {
            std::vector<NodeId> members =
                resolve_set<NodeId>(labels, node_ids_, name);
            node_sets_[name] = members;
            builder_.add_node_set({name, std::move(members)});
        }
        for (const auto& [name, labels] : raw_assembly_node_sets_) {
            assembly_node_sets_[name] =
                resolve_set<NodeId>(labels, node_ids_, name);
        }
        for (const auto& [name, labels] : raw_element_sets_) {
            std::vector<ElementId> members =
                resolve_set<ElementId>(labels, element_ids_, name);
            builder_.add_element_set({name, std::move(members)});
        }
    }

    std::vector<NodeId> resolve_node_target(
        const std::string& target,
        const SourceLocation& source) {
        std::int64_t label = 0;
        const auto parsed =
            std::from_chars(target.data(), target.data() + target.size(), label);
        if (flat_ && parsed.ec == std::errc{} &&
            parsed.ptr == target.data() + target.size()) {
            const auto found = node_ids_.find(label);
            if (found != node_ids_.end()) {
                return {found->second};
            }
        } else if (parsed.ec != std::errc{} ||
                   parsed.ptr != target.data() + target.size()) {
            const auto& target_sets = flat_ ? node_sets_ : assembly_node_sets_;
            const auto found = target_sets.find(target);
            if (found != target_sets.end()) {
                return found->second;
            }
        }

        add_error(
            "abaqus.semantic.missing_node_target",
            "Node target '" + target + "' does not resolve.",
            source);
        return {};
    }

    void collect_boundary(
        const DeckRecord& record,
        std::map<std::pair<std::int64_t, std::uint8_t>, double>&
            prescribed_values) {
        for (std::size_t row_index = 0; row_index < record.data.size();
             ++row_index) {
            const auto& row = record.data[row_index];
            const SourceLocation source = data_source(record, row_index);
            if (row.size() < 2U || row.size() > 4U || row[0].empty() ||
                row[1].empty()) {
                add_error(
                    "abaqus.semantic.invalid_boundary_data",
                    "*BOUNDARY requires target, first DOF, optional last DOF, "
                    "and optional value.",
                    source);
                continue;
            }
            const auto first_dof = parse_dof(row[1], source);
            const auto last_dof = row.size() >= 3U && !row[2].empty()
                                      ? parse_dof(row[2], source)
                                      : first_dof;
            double value = 0.0;
            if (row.size() >= 4U && !row[3].empty() &&
                !parse_number(row[3], value)) {
                add_error(
                    "abaqus.semantic.invalid_boundary_data",
                    "*BOUNDARY prescribed value must be finite.",
                    source);
                continue;
            }
            if (!first_dof.has_value() || !last_dof.has_value()) {
                continue;
            }
            if (*last_dof < *first_dof) {
                add_error(
                    "abaqus.semantic.invalid_dof_range",
                    "*BOUNDARY degree-of-freedom range is reversed.",
                    source);
                continue;
            }

            const std::vector<NodeId> nodes =
                resolve_node_target(row[0], source);
            for (const NodeId node : nodes) {
                for (std::uint8_t dof = *first_dof; dof <= *last_dof; ++dof) {
                    const auto key = std::pair{node.value(), dof};
                    const auto [existing, inserted] =
                        prescribed_values.emplace(key, value);
                    if (!inserted && existing->second != value) {
                        add_error(
                            "abaqus.semantic.conflicting_boundary",
                            "A node DOF has conflicting prescribed values.",
                            source);
                    }
                }
            }
        }
    }

    void collect_loads(
        const DeckRecord& record,
        std::map<std::int64_t, std::array<double, 6>>& loads) {
        for (std::size_t row_index = 0; row_index < record.data.size();
             ++row_index) {
            const auto& row = record.data[row_index];
            const SourceLocation source = data_source(record, row_index);
            if (row.size() != 3U || row[0].empty() || row[1].empty() ||
                row[2].empty()) {
                add_error(
                    "abaqus.semantic.invalid_cload_data",
                    "*CLOAD requires exactly target, DOF, and magnitude.",
                    source);
                continue;
            }
            const auto dof = parse_dof(row[1], source);
            double value = 0.0;
            if (!parse_number(row[2], value)) {
                add_error(
                    "abaqus.semantic.invalid_cload_data",
                    "*CLOAD magnitude must be finite.",
                    source);
                continue;
            }
            if (!dof.has_value()) {
                continue;
            }

            const std::vector<NodeId> nodes =
                resolve_node_target(row[0], source);
            for (const NodeId node : nodes) {
                double& component =
                    loads[node.value()][static_cast<std::size_t>(*dof - 1U)];
                const double sum = component + value;
                if (!std::isfinite(sum)) {
                    add_error(
                        "abaqus.semantic.invalid_cload_data",
                        "Accumulated *CLOAD magnitude must remain finite.",
                        source);
                    continue;
                }
                component = sum;
            }
        }
    }

    void validate_static(const DeckRecord& record) {
        if (record.data.empty()) {
            return;
        }

        const SourceLocation first_source = data_source(record, 0U);
        if (record.data[0].empty() || record.data[0].size() > 4U ||
            std::ranges::any_of(
                record.data[0],
                [](const std::string& field) { return field.empty(); })) {
            add_error(
                "abaqus.semantic.invalid_static_data",
                "*STATIC accepts one row of one through four positive values.",
                first_source);
            return;
        }

        for (const std::string& field : record.data[0]) {
            double value = 0.0;
            if (!parse_number(field, value) || value <= 0.0) {
                add_error(
                    "abaqus.semantic.invalid_static_data",
                    "*STATIC values must be finite and positive.",
                    first_source);
                return;
            }
        }
        if (record.data.size() > 1U) {
            add_error(
                "abaqus.semantic.invalid_static_data",
                "*STATIC accepts only one data row.",
                data_source(record, 1U));
        }
    }

    void collect_step() {
        StepDefinition step;
        std::map<std::pair<std::int64_t, std::uint8_t>, double>
            prescribed_values;
        std::map<std::int64_t, std::array<double, 6>> loads;
        const DeckRecord* first_step = nullptr;
        std::size_t step_count = 0U;
        std::size_t static_count = 0U;

        for (const DeckRecord& record : deck_.global_records) {
            if (record.keyword == "STEP") {
                ++step_count;
                if (first_step == nullptr) {
                    first_step = &record;
                    const std::string* name = parameter(record, "NAME");
                    step.name = name == nullptr ? "Step-1" : *name;
                } else {
                    add_error(
                        "abaqus.semantic.step_count",
                        "Phase 1 accepts exactly one *STEP.",
                        record.source);
                }

                if (const std::string* nlgeom = parameter(record, "NLGEOM");
                    nlgeom != nullptr && uppercase_ascii(*nlgeom) != "NO") {
                    add_error(
                        "abaqus.semantic.unsupported_step_option",
                        "Phase 1 supports only NLGEOM=NO.",
                        record.source);
                }
            } else if (record.keyword == "STATIC") {
                ++static_count;
                if (static_count > 1U) {
                    add_error(
                        "abaqus.semantic.invalid_static_data",
                        "A Phase 1 Step accepts exactly one *STATIC record.",
                        record.source);
                }
                validate_static(record);
            } else if (record.keyword == "BOUNDARY") {
                collect_boundary(record, prescribed_values);
            } else if (record.keyword == "CLOAD") {
                collect_loads(record, loads);
            }
        }
        if (step_count == 0U) {
            const SourceLocation source =
                deck_.global_records.empty()
                    ? SourceLocation{}
                    : deck_.global_records.front().source;
            add_error(
                "abaqus.semantic.step_count",
                "Phase 1 input requires exactly one *STEP.",
                source);
            return;
        }
        if (step_count == 1U && static_count == 0U) {
            add_error(
                "abaqus.semantic.missing_static",
                "The Phase 1 Step requires one *STATIC record.",
                first_step->source);
        }

        for (const auto& [key, value] : prescribed_values) {
            step.prescribed_dofs.push_back({NodeId{key.first}, key.second, value});
        }
        for (const auto& [node, values] : loads) {
            step.nodal_loads.push_back({NodeId{node}, values});
        }
        builder_.set_step(std::move(step));
    }

    const ParsedDeck& deck_;
    DomainBuilder builder_;
    std::vector<Diagnostic> diagnostics_;
    std::span<const DeckRecord> active_records_;
    bool flat_ = true;
    std::string part_name_;
    std::string instance_name_;
    LabelMap node_ids_;
    ElementLabelMap element_ids_;
    std::map<std::string, MaterialProperties, std::less<>> materials_;
    std::map<std::string, std::vector<std::int64_t>, std::less<>>
        active_element_sets_;
    std::map<std::int64_t, SectionAssignment> element_assignments_;
    RawSetMap raw_node_sets_;
    RawSetMap raw_assembly_node_sets_;
    RawSetMap raw_element_sets_;
    std::map<std::string, std::vector<NodeId>, std::less<>> node_sets_;
    std::map<std::string, std::vector<NodeId>, std::less<>>
        assembly_node_sets_;
    std::int64_t next_node_id_ = 0;
    std::int64_t next_element_id_ = 0;
    std::int64_t next_material_id_ = 0;
    std::int64_t next_section_id_ = 0;
};

}  // namespace

DomainBuildResult map_deck_to_domain(const ParsedDeck& deck) {
    return DeckMapper{deck}.map();
}

}  // namespace fesa
