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

#include <algorithm>
#include <array>
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <iomanip>
#include <initializer_list>
#include <iterator>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace fesa {
namespace {

enum class Scope { global, part, assembly, instance, step };

struct KeywordLine final {
    std::string keyword;
    std::map<std::string, std::string, std::less<>> parameters;
    std::set<std::string, std::less<>> flag_parameters;
    SourceLocation source;
};

const std::string* parameter(
    const KeywordLine& keyword,
    std::string_view name);

std::string_view trim(const std::string_view value) {
    constexpr std::string_view whitespace{" \t\f\v\r\n"};
    const std::size_t first = value.find_first_not_of(whitespace);
    if (first == std::string_view::npos) {
        return {};
    }
    const std::size_t last = value.find_last_not_of(whitespace);
    return value.substr(first, last - first + 1);
}

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;
}

std::string input_fingerprint(const std::string_view bytes) {
    std::uint64_t fingerprint = 14695981039346656037ULL;
    for (const char byte : bytes) {
        fingerprint ^=
            static_cast<std::uint8_t>(static_cast<unsigned char>(byte));
        fingerprint *= 1099511628211ULL;
    }

    std::ostringstream encoded;
    encoded << "fnv1a64:" << std::hex << std::setfill('0')
            << std::setw(16) << fingerprint;
    return encoded.str();
}

std::vector<std::string> split_fields(const std::string_view value) {
    std::vector<std::string> fields;
    std::size_t first = 0;
    while (true) {
        const std::size_t comma = value.find(',', first);
        const std::string_view field = comma == std::string_view::npos
                                           ? value.substr(first)
                                           : value.substr(first, comma - first);
        fields.emplace_back(trim(field));
        if (comma == std::string_view::npos) {
            break;
        }
        first = comma + 1;
    }
    return fields;
}

ParseDeckResult failure(
    const DiagnosticStage stage,
    std::string code,
    std::string message,
    std::optional<SourceLocation> source) {
    std::vector<Diagnostic> diagnostics;
    diagnostics.push_back({
        stage,
        Severity::error,
        std::move(code),
        std::move(message),
        std::move(source),
    });
    return {std::nullopt, std::move(diagnostics)};
}

ParseDeckResult syntax_failure(
    std::string code,
    std::string message,
    SourceLocation source) {
    return failure(
        DiagnosticStage::syntax,
        std::move(code),
        std::move(message),
        std::move(source));
}

bool is_supported_record(const std::string_view keyword) {
    constexpr std::array supported{
        std::string_view{"BEAM GENERAL SECTION"},
        std::string_view{"BOUNDARY"},
        std::string_view{"CLOAD"},
        std::string_view{"ELASTIC"},
        std::string_view{"ELEMENT"},
        std::string_view{"ELSET"},
        std::string_view{"HEADING"},
        std::string_view{"MATERIAL"},
        std::string_view{"NODE"},
        std::string_view{"NSET"},
        std::string_view{"OUTPUT"},
        std::string_view{"PREPRINT"},
        std::string_view{"RESTART"},
        std::string_view{"STATIC"},
        std::string_view{"TRANSVERSE SHEAR STIFFNESS"},
    };
    return std::ranges::find(supported, keyword) != supported.end();
}

bool is_known_keyword(const std::string_view keyword) {
    constexpr std::array scope_keywords{
        std::string_view{"ASSEMBLY"},
        std::string_view{"END ASSEMBLY"},
        std::string_view{"END INSTANCE"},
        std::string_view{"END PART"},
        std::string_view{"INSTANCE"},
        std::string_view{"PART"},
    };
    return is_supported_record(keyword) ||
           std::ranges::find(scope_keywords, keyword) != scope_keywords.end();
}

ParseDeckResult invalid_record_scope(const KeywordLine& keyword) {
    std::string code = "abaqus.syntax.invalid_step_scope";
    if (keyword.keyword == "NODE") {
        code = "abaqus.syntax.invalid_node_scope";
    } else if (keyword.keyword == "ELEMENT") {
        code = "abaqus.syntax.invalid_element_scope";
    } else if (keyword.keyword == "NSET" || keyword.keyword == "ELSET") {
        code = "abaqus.syntax.invalid_set_scope";
    } else if (keyword.keyword == "MATERIAL" ||
               keyword.keyword == "ELASTIC") {
        code = "abaqus.syntax.invalid_material_scope";
    } else if (keyword.keyword == "BEAM GENERAL SECTION") {
        code = "abaqus.syntax.invalid_section_scope";
    } else if (keyword.keyword == "TRANSVERSE SHEAR STIFFNESS") {
        code = "abaqus.syntax.invalid_transverse_shear_scope";
    } else if (keyword.keyword == "BOUNDARY") {
        code = "abaqus.syntax.invalid_boundary_scope";
    } else if (keyword.keyword == "CLOAD") {
        code = "abaqus.syntax.invalid_cload_scope";
    } else if (keyword.keyword == "HEADING") {
        code = "abaqus.syntax.invalid_heading_scope";
    } else if (keyword.keyword == "PREPRINT") {
        code = "abaqus.syntax.invalid_preprint_scope";
    } else if (keyword.keyword == "RESTART") {
        code = "abaqus.syntax.invalid_restart_scope";
    } else if (keyword.keyword == "OUTPUT") {
        code = "abaqus.syntax.invalid_output_scope";
    } else if (keyword.keyword == "PART") {
        code = "abaqus.syntax.invalid_part_scope";
    } else if (keyword.keyword == "ASSEMBLY") {
        code = "abaqus.syntax.invalid_assembly_scope";
    } else if (keyword.keyword == "INSTANCE") {
        code = "abaqus.syntax.invalid_instance_scope";
    }
    return syntax_failure(
        std::move(code),
        "*" + keyword.keyword + " is invalid in the current input scope.",
        keyword.source);
}

bool is_allowed_parameter(
    const std::string_view name,
    const std::initializer_list<std::string_view> allowed) {
    return std::ranges::find(allowed, name) != allowed.end();
}

std::optional<ParseDeckResult> validate_parameter_names(
    const KeywordLine& keyword,
    const std::initializer_list<std::string_view> allowed) {
    for (const auto& [name, value] : keyword.parameters) {
        static_cast<void>(value);
        if (!is_allowed_parameter(name, allowed)) {
            return syntax_failure(
                "abaqus.syntax.unsupported_parameter",
                "Unsupported parameter '" + name + "' on *" +
                    keyword.keyword + ".",
                keyword.source);
        }
    }
    return std::nullopt;
}

std::optional<ParseDeckResult> unsupported_parameter_value(
    const KeywordLine& keyword,
    std::string_view name);

std::optional<ParseDeckResult> validate_parameter_schema(
    const KeywordLine& keyword,
    const std::initializer_list<std::string_view> valued,
    const std::initializer_list<std::string_view> flags = {}) {
    for (const auto& [name, value] : keyword.parameters) {
        if (is_allowed_parameter(name, valued)) {
            if (keyword.flag_parameters.contains(name) || value.empty()) {
                return syntax_failure(
                    "abaqus.syntax.invalid_parameter",
                    "Parameter '" + name + "' on *" + keyword.keyword +
                        " requires a nonempty value.",
                    keyword.source);
            }
        } else if (is_allowed_parameter(name, flags)) {
            if (!keyword.flag_parameters.contains(name)) {
                return unsupported_parameter_value(keyword, name);
            }
        } else {
            return syntax_failure(
                "abaqus.syntax.unsupported_parameter",
                "Unsupported parameter '" + name + "' on *" +
                    keyword.keyword + ".",
                keyword.source);
        }
    }
    return std::nullopt;
}

std::optional<ParseDeckResult> unsupported_parameter_value(
    const KeywordLine& keyword,
    const std::string_view name) {
    return syntax_failure(
        "abaqus.syntax.unsupported_parameter",
        "Unsupported value for parameter '" + std::string{name} +
            "' on *" + keyword.keyword + ".",
        keyword.source);
}

std::optional<ParseDeckResult> validate_noop_parameters(
    const KeywordLine& keyword) {
    if (keyword.keyword == "HEADING") {
        return validate_parameter_schema(keyword, {});
    }
    if (keyword.keyword == "PREPRINT") {
        if (auto error = validate_parameter_schema(
                keyword, {"ECHO", "MODEL", "HISTORY", "CONTACT"})) {
            return error;
        }
        for (const auto& [name, value] : keyword.parameters) {
            const std::string normalized = uppercase_ascii(value);
            if (normalized != "YES" && normalized != "NO") {
                return unsupported_parameter_value(keyword, name);
            }
        }
        return std::nullopt;
    }
    if (keyword.keyword == "RESTART") {
        if (auto error = validate_parameter_schema(
                keyword, {"FREQUENCY"}, {"WRITE"})) {
            return error;
        }
        if (const std::string* write = parameter(keyword, "WRITE");
            write != nullptr && !write->empty()) {
            return unsupported_parameter_value(keyword, "WRITE");
        }
        if (const std::string* frequency = parameter(keyword, "FREQUENCY");
            frequency != nullptr) {
            std::int64_t value = 0;
            const auto parsed = std::from_chars(
                frequency->data(), frequency->data() + frequency->size(), value);
            if (frequency->empty() || parsed.ec != std::errc{} ||
                parsed.ptr != frequency->data() + frequency->size() ||
                value < 0) {
                return unsupported_parameter_value(keyword, "FREQUENCY");
            }
        }
        return std::nullopt;
    }
    if (keyword.keyword == "OUTPUT") {
        if (auto error = validate_parameter_schema(
                keyword, {"VARIABLE"}, {"FIELD", "HISTORY"})) {
            return error;
        }
        const bool field = keyword.parameters.contains("FIELD");
        const bool history = keyword.parameters.contains("HISTORY");
        if (field == history) {
            return unsupported_parameter_value(keyword, "FIELD/HISTORY");
        }
        const std::string* flag = parameter(
            keyword, field ? std::string_view{"FIELD"}
                           : std::string_view{"HISTORY"});
        if (flag == nullptr || !flag->empty()) {
            return unsupported_parameter_value(
                keyword, field ? "FIELD" : "HISTORY");
        }
        if (const std::string* variable = parameter(keyword, "VARIABLE");
            variable != nullptr && uppercase_ascii(*variable) != "PRESELECT") {
            return unsupported_parameter_value(keyword, "VARIABLE");
        }
        return std::nullopt;
    }
    return std::nullopt;
}

std::optional<KeywordLine> parse_keyword_line(
    const std::string_view line,
    const SourceLocation& source,
    ParseDeckResult& error) {
    const std::vector<std::string> fields = split_fields(line.substr(1));
    if (fields.empty() || fields[0].empty()) {
        error = syntax_failure(
            "abaqus.syntax.empty_keyword",
            "Abaqus keyword name is empty.",
            source);
        return std::nullopt;
    }

    KeywordLine parsed{
        uppercase_ascii(fields[0]),
        {},
        {},
        source,
    };
    for (std::size_t index = 1; index < fields.size(); ++index) {
        const std::string_view field = fields[index];
        if (field.empty()) {
            continue;
        }

        const std::size_t equals = field.find('=');
        const std::string_view key_text =
            trim(field.substr(0, equals));
        const std::string_view value_text =
            equals == std::string_view::npos
                ? std::string_view{}
                : trim(field.substr(equals + 1));
        if (key_text.empty()) {
            error = syntax_failure(
                "abaqus.syntax.invalid_parameter",
                "Abaqus keyword parameter name is empty.",
                source);
            return std::nullopt;
        }

        const std::string key = uppercase_ascii(std::string{key_text});
        if (!parsed.parameters.emplace(key, value_text).second) {
            error = syntax_failure(
                "abaqus.syntax.duplicate_parameter",
                "Abaqus keyword parameter '" + key +
                    "' is specified more than once.",
                source);
            return std::nullopt;
        }
        if (equals == std::string_view::npos) {
            parsed.flag_parameters.insert(key);
        }
    }
    return parsed;
}

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

ParseDeckResult missing_parameter(
    const KeywordLine& keyword,
    const std::string_view parameter_name) {
    return syntax_failure(
        "abaqus.syntax.missing_parameter",
        "Abaqus *" + keyword.keyword + " requires parameter " +
            std::string{parameter_name} + ".",
        keyword.source);
}

}  // namespace

ParseDeckResult parse_deck(const std::filesystem::path& path) {
    std::ifstream file{path, std::ios::binary};
    if (!file) {
        return failure(
            DiagnosticStage::io,
            "abaqus.io.open_failed",
            "Unable to open Abaqus input file.",
            SourceLocation{path, 0U, 0U});
    }
    const std::string source_bytes{
        std::istreambuf_iterator<char>{file},
        std::istreambuf_iterator<char>{},
    };
    if (file.bad()) {
        return failure(
            DiagnosticStage::io,
            "abaqus.io.read_failed",
            "Failed while reading Abaqus input file.",
            SourceLocation{path, 0U, 0U});
    }
    std::istringstream input{source_bytes};

    ParsedDeck deck;
    Scope scope = Scope::global;
    std::optional<ParsedPart> current_part;
    std::optional<ParsedAssembly> current_assembly;
    std::optional<ParsedInstance> current_instance;
    std::optional<SourceLocation> current_step_source;
    bool completed_step = false;
    DeckRecord* current_record = nullptr;

    std::string line;
    std::size_t line_number = 0;
    while (std::getline(input, line)) {
        ++line_number;
        if (line_number == 1U && line.starts_with("\xEF\xBB\xBF")) {
            line.erase(0, 3);
        }

        const std::size_t first_nonspace =
            line.find_first_not_of(" \t\f\v\r");
        if (first_nonspace == std::string::npos) {
            continue;
        }
        const std::string_view content = trim(line);
        if (content.starts_with("**")) {
            continue;
        }

        if (!content.starts_with('*')) {
            const std::vector<std::string> fields = split_fields(content);
            if (scope == Scope::instance) {
                current_instance->transform_data.push_back(fields);
                current_instance->transform_sources.push_back(SourceLocation{
                    path,
                    line_number,
                    first_nonspace + 1U,
                });
            } else if (current_record != nullptr) {
                current_record->data.push_back(fields);
                current_record->data_sources.push_back(SourceLocation{
                    path,
                    line_number,
                    first_nonspace + 1U,
                });
            } else {
                return syntax_failure(
                    "abaqus.syntax.data_without_keyword",
                    "Abaqus data line has no preceding keyword record.",
                    SourceLocation{path, line_number, first_nonspace + 1U});
            }
            continue;
        }

        const SourceLocation source{
            path,
            line_number,
            first_nonspace + 1U,
        };
        ParseDeckResult keyword_error;
        std::optional<KeywordLine> parsed =
            parse_keyword_line(content, source, keyword_error);
        if (!parsed.has_value()) {
            return keyword_error;
        }
        KeywordLine& keyword = *parsed;
        current_record = nullptr;

        if (keyword.keyword == "STEP") {
            if (scope != Scope::global) {
                return syntax_failure(
                    "abaqus.syntax.invalid_step_scope",
                    "*STEP is only valid in global input scope.",
                    source);
            }
            if (auto error = validate_parameter_schema(
                    keyword, {"NAME", "NLGEOM"})) {
                return std::move(*error);
            }
            if (const std::string* name = parameter(keyword, "NAME");
                name != nullptr && name->empty()) {
                return syntax_failure(
                    "abaqus.syntax.invalid_parameter",
                    "*STEP parameter NAME requires a nonempty value.",
                    source);
            }
            if (const std::string* nlgeom = parameter(keyword, "NLGEOM");
                nlgeom != nullptr && nlgeom->empty()) {
                return syntax_failure(
                    "abaqus.syntax.invalid_parameter",
                    "*STEP parameter NLGEOM requires a value.",
                    source);
            }
            deck.global_records.push_back({
                std::move(keyword.keyword),
                std::move(keyword.parameters),
                {},
                source,
            });
            current_step_source = source;
            scope = Scope::step;
            continue;
        }

        if (keyword.keyword == "END STEP") {
            if (auto error = validate_parameter_names(keyword, {})) {
                return std::move(*error);
            }
            if (scope != Scope::step || !current_step_source.has_value()) {
                return syntax_failure(
                    "abaqus.syntax.unexpected_end_step",
                    "*END STEP does not match an open *STEP.",
                    source);
            }
            deck.global_records.push_back({
                std::move(keyword.keyword),
                std::move(keyword.parameters),
                {},
                source,
            });
            current_step_source.reset();
            completed_step = true;
            scope = Scope::global;
            continue;
        }

        if (scope == Scope::global && completed_step &&
            is_known_keyword(keyword.keyword)) {
            return invalid_record_scope(keyword);
        }

        if (keyword.keyword == "PART") {
            if (scope != Scope::global) {
                return syntax_failure(
                    "abaqus.syntax.invalid_part_scope",
                    "*PART is only valid in global input scope.",
                    source);
            }
            if (auto error = validate_parameter_schema(keyword, {"NAME"})) {
                return std::move(*error);
            }
            const std::string* name = parameter(keyword, "NAME");
            if (name == nullptr || name->empty()) {
                return missing_parameter(keyword, "NAME");
            }
            current_part = ParsedPart{*name, {}, source};
            scope = Scope::part;
            continue;
        }

        if (keyword.keyword == "END PART") {
            if (scope != Scope::part || !current_part.has_value()) {
                return syntax_failure(
                    "abaqus.syntax.unexpected_end_part",
                    "*END PART does not match an open *PART.",
                    source);
            }
            if (auto error = validate_parameter_schema(keyword, {})) {
                return std::move(*error);
            }
            deck.parts.push_back(std::move(*current_part));
            current_part.reset();
            scope = Scope::global;
            continue;
        }

        if (keyword.keyword == "ASSEMBLY") {
            if (scope != Scope::global) {
                return syntax_failure(
                    "abaqus.syntax.invalid_assembly_scope",
                    "*ASSEMBLY is only valid in global input scope.",
                    source);
            }
            if (auto error = validate_parameter_schema(keyword, {"NAME"})) {
                return std::move(*error);
            }
            if (deck.assembly.has_value() ||
                current_assembly.has_value()) {
                return syntax_failure(
                    "abaqus.syntax.multiple_assemblies",
                    "ParsedDeck can preserve only one *ASSEMBLY.",
                    source);
            }
            const std::string* name = parameter(keyword, "NAME");
            if (name == nullptr || name->empty()) {
                return missing_parameter(keyword, "NAME");
            }
            current_assembly = ParsedAssembly{*name, {}, {}, source};
            scope = Scope::assembly;
            continue;
        }

        if (keyword.keyword == "END ASSEMBLY") {
            if (scope != Scope::assembly ||
                !current_assembly.has_value()) {
                return syntax_failure(
                    "abaqus.syntax.unexpected_end_assembly",
                    "*END ASSEMBLY does not match an open *ASSEMBLY.",
                    source);
            }
            if (auto error = validate_parameter_schema(keyword, {})) {
                return std::move(*error);
            }
            deck.assembly = std::move(*current_assembly);
            current_assembly.reset();
            scope = Scope::global;
            continue;
        }

        if (keyword.keyword == "INSTANCE") {
            if (scope != Scope::assembly ||
                !current_assembly.has_value()) {
                return syntax_failure(
                    "abaqus.syntax.invalid_instance_scope",
                    "*INSTANCE is only valid in an open *ASSEMBLY.",
                    source);
            }
            if (auto error =
                    validate_parameter_schema(keyword, {"NAME", "PART"})) {
                return std::move(*error);
            }
            const std::string* name = parameter(keyword, "NAME");
            if (name == nullptr || name->empty()) {
                return missing_parameter(keyword, "NAME");
            }
            const std::string* part_name = parameter(keyword, "PART");
            if (part_name == nullptr || part_name->empty()) {
                return missing_parameter(keyword, "PART");
            }
            current_instance =
                ParsedInstance{*name, *part_name, {}, {}, source};
            scope = Scope::instance;
            continue;
        }

        if (keyword.keyword == "END INSTANCE") {
            if (scope != Scope::instance ||
                !current_instance.has_value() ||
                !current_assembly.has_value()) {
                return syntax_failure(
                    "abaqus.syntax.unexpected_end_instance",
                    "*END INSTANCE does not match an open *INSTANCE.",
                    source);
            }
            if (auto error = validate_parameter_schema(keyword, {})) {
                return std::move(*error);
            }
            current_assembly->instances.push_back(
                std::move(*current_instance));
            current_instance.reset();
            scope = Scope::assembly;
            continue;
        }

        if (keyword.keyword == "HEADING" ||
            keyword.keyword == "PREPRINT") {
            if (scope != Scope::global) {
                return syntax_failure(
                    keyword.keyword == "HEADING"
                        ? "abaqus.syntax.invalid_heading_scope"
                        : "abaqus.syntax.invalid_preprint_scope",
                    "*" + keyword.keyword +
                        " is only valid in global input scope.",
                    source);
            }
            if (auto error = validate_noop_parameters(keyword)) {
                return std::move(*error);
            }
            deck.global_records.push_back({
                std::move(keyword.keyword),
                std::move(keyword.parameters),
                {},
                source,
            });
            if (deck.global_records.back().keyword == "HEADING") {
                current_record = &deck.global_records.back();
            }
            continue;
        }

        if (keyword.keyword == "RESTART" || keyword.keyword == "OUTPUT") {
            if (scope != Scope::step) {
                return syntax_failure(
                    keyword.keyword == "RESTART"
                        ? "abaqus.syntax.invalid_restart_scope"
                        : "abaqus.syntax.invalid_output_scope",
                    "*" + keyword.keyword +
                        " is only valid inside *STEP.",
                    source);
            }
            if (auto error = validate_noop_parameters(keyword)) {
                return std::move(*error);
            }
            deck.global_records.push_back({
                std::move(keyword.keyword),
                std::move(keyword.parameters),
                {},
                source,
            });
            continue;
        }

        if (keyword.keyword == "STATIC") {
            if (scope != Scope::step) {
                return syntax_failure(
                    "abaqus.syntax.invalid_step_scope",
                    "*STATIC is only valid inside *STEP.",
                    source);
            }
            if (auto error = validate_parameter_names(keyword, {})) {
                return std::move(*error);
            }
        } else if (keyword.keyword == "CLOAD") {
            if (scope != Scope::step) {
                return syntax_failure(
                    "abaqus.syntax.invalid_cload_scope",
                    "*CLOAD is only valid inside *STEP.",
                    source);
            }
            if (auto error = validate_parameter_names(keyword, {})) {
                return std::move(*error);
            }
        } else if (keyword.keyword == "BOUNDARY") {
            if (scope != Scope::global && scope != Scope::step) {
                return syntax_failure(
                    "abaqus.syntax.invalid_boundary_scope",
                    "*BOUNDARY is valid only in global or Step scope.",
                    source);
            }
            if (auto error = validate_parameter_names(keyword, {})) {
                return std::move(*error);
            }
        }

        if (!is_supported_record(keyword.keyword)) {
            return syntax_failure(
                "abaqus.unsupported_keyword",
                "Unsupported Abaqus keyword *" + keyword.keyword + ".",
                source);
        }
        if (scope == Scope::step && keyword.keyword != "STATIC" &&
            keyword.keyword != "BOUNDARY" && keyword.keyword != "CLOAD") {
            return invalid_record_scope(keyword);
        }
        if (scope != Scope::instance) {
            const bool mesh_scope = scope == Scope::global || scope == Scope::part;
            const bool set_scope = mesh_scope || scope == Scope::assembly;
            if ((keyword.keyword == "NODE" || keyword.keyword == "ELEMENT" ||
                 keyword.keyword == "BEAM GENERAL SECTION" ||
                 keyword.keyword == "TRANSVERSE SHEAR STIFFNESS") &&
                !mesh_scope) {
                return invalid_record_scope(keyword);
            }
            if ((keyword.keyword == "NSET" || keyword.keyword == "ELSET") &&
                !set_scope) {
                return invalid_record_scope(keyword);
            }
            if ((keyword.keyword == "MATERIAL" ||
                 keyword.keyword == "ELASTIC") &&
                scope != Scope::global) {
                return invalid_record_scope(keyword);
            }
        }
        if (scope != Scope::instance) {
            std::optional<ParseDeckResult> parameter_error;
            if (keyword.keyword == "NODE" || keyword.keyword == "ELASTIC" ||
                keyword.keyword == "TRANSVERSE SHEAR STIFFNESS") {
                parameter_error = validate_parameter_schema(keyword, {});
            } else if (keyword.keyword == "ELEMENT") {
                parameter_error = validate_parameter_schema(
                    keyword, {"TYPE", "ELSET"});
            } else if (keyword.keyword == "NSET") {
                parameter_error = scope == Scope::assembly
                                      ? validate_parameter_schema(
                                            keyword,
                                            {"NSET", "INSTANCE"},
                                            {"GENERATE"})
                                      : validate_parameter_schema(
                                            keyword, {"NSET"}, {"GENERATE"});
            } else if (keyword.keyword == "ELSET") {
                parameter_error = scope == Scope::assembly
                                      ? validate_parameter_schema(
                                            keyword,
                                            {"ELSET", "INSTANCE"},
                                            {"GENERATE"})
                                      : validate_parameter_schema(
                                            keyword, {"ELSET"}, {"GENERATE"});
            } else if (keyword.keyword == "MATERIAL") {
                parameter_error = validate_parameter_schema(keyword, {"NAME"});
            } else if (keyword.keyword == "BEAM GENERAL SECTION") {
                parameter_error = validate_parameter_schema(
                    keyword, {"SECTION", "ELSET", "MATERIAL"});
            }
            if (parameter_error.has_value()) {
                return std::move(*parameter_error);
            }
        }

        DeckRecord next_record{
            std::move(keyword.keyword),
            std::move(keyword.parameters),
            {},
            source,
        };
        switch (scope) {
            case Scope::global:
                deck.global_records.push_back(std::move(next_record));
                if (deck.global_records.back().keyword != "MATERIAL") {
                    current_record = &deck.global_records.back();
                }
                break;
            case Scope::part:
                current_part->records.push_back(std::move(next_record));
                current_record = &current_part->records.back();
                break;
            case Scope::assembly:
                current_assembly->records.push_back(
                    std::move(next_record));
                current_record = &current_assembly->records.back();
                break;
            case Scope::instance:
                return syntax_failure(
                    "abaqus.syntax.instance_local_keyword",
                    "Keyword records inside *INSTANCE are unsupported.",
                    source);
            case Scope::step:
                deck.global_records.push_back(std::move(next_record));
                current_record = &deck.global_records.back();
                break;
        }
    }

    if (input.bad()) {
        return failure(
            DiagnosticStage::io,
            "abaqus.io.read_failed",
            "Failed while reading Abaqus input file.",
            SourceLocation{path, line_number, 0U});
    }

    if (current_instance.has_value()) {
        return syntax_failure(
            "abaqus.syntax.unclosed_instance",
            "Abaqus *INSTANCE is not closed by *END INSTANCE.",
            current_instance->source);
    }
    if (current_part.has_value()) {
        return syntax_failure(
            "abaqus.syntax.unclosed_part",
            "Abaqus *PART is not closed by *END PART.",
            current_part->source);
    }
    if (current_assembly.has_value()) {
        return syntax_failure(
            "abaqus.syntax.unclosed_assembly",
            "Abaqus *ASSEMBLY is not closed by *END ASSEMBLY.",
            current_assembly->source);
    }
    if (current_step_source.has_value()) {
        return syntax_failure(
            "abaqus.syntax.unclosed_step",
            "Abaqus *STEP is not closed by *END STEP.",
            *current_step_source);
    }

    return {
        std::move(deck),
        {},
        input_fingerprint(source_bytes),
    };
}

}  // namespace fesa
