#include <fesa/io/hdf5/writer.hpp>

#include <fesa/core/version.hpp>
#include <fesa/fem/beam_frame.hpp>
#include <fesa/model/domain_builder.hpp>

#include <hdf5.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <optional>
#include <ranges>
#include <span>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

namespace fesa {
namespace {

constexpr std::string_view schema_version = "2.0.0";
constexpr std::string_view unit_policy =
    "consistent_input_units_no_conversion";
constexpr std::string_view solver_backend = "mkl_pardiso";
constexpr std::string_view solver_matrix_storage = "symmetric_upper_csr";
constexpr std::string_view solver_matrix_type =
    "symmetric_positive_definite";
constexpr std::string_view solver_constraint_method =
    "essential_dof_elimination";
constexpr std::string_view solver_assembly = "deterministic_serial";
constexpr std::string_view fingerprint_prefix = "fnv1a64:";

[[nodiscard]] bool nearly_equal(
    const double left,
    const double right) noexcept {
    const double scale = std::max({1.0, std::abs(left), std::abs(right)});
    return std::abs(left - right) <=
           256.0 * std::numeric_limits<double>::epsilon() * scale;
}

[[nodiscard]] bool same_frame(
    const BeamFrame& left,
    const BeamFrame& right) noexcept {
    return nearly_equal(left.ex.x, right.ex.x) &&
           nearly_equal(left.ex.y, right.ex.y) &&
           nearly_equal(left.ex.z, right.ex.z) &&
           nearly_equal(left.ey.x, right.ey.x) &&
           nearly_equal(left.ey.y, right.ey.y) &&
           nearly_equal(left.ey.z, right.ey.z) &&
           nearly_equal(left.ez.x, right.ez.x) &&
           nearly_equal(left.ez.y, right.ez.y) &&
           nearly_equal(left.ez.z, right.ez.z);
}

[[nodiscard]] bool valid_input_identity(
    const Hdf5InputIdentity& identity) {
    if (
        identity.source.empty() ||
        identity.fingerprint.size() != fingerprint_prefix.size() + 16U ||
        !identity.fingerprint.starts_with(fingerprint_prefix)) {
        return false;
    }
    return std::ranges::all_of(
        identity.fingerprint.substr(fingerprint_prefix.size()),
        [](const char value) {
            return (value >= '0' && value <= '9') ||
                   (value >= 'a' && value <= 'f');
        });
}

class Hdf5Error final : public std::runtime_error {
public:
    Hdf5Error(std::string code, std::string message)
        : std::runtime_error{std::move(message)}, code_{std::move(code)} {}

    [[nodiscard]] const std::string& code() const noexcept {
        return code_;
    }

private:
    std::string code_;
};

template <class Function>
auto call_hdf5(Function&& function) {
    using Return = std::invoke_result_t<Function>;
    Return result{};
    H5E_BEGIN_TRY {
        result = std::forward<Function>(function)();
    }
    H5E_END_TRY;
    return result;
}

struct Hdf5Context final {
    std::string close_error;

    void record_close_result(
        const herr_t result,
        const std::string_view resource) {
        if (result < 0 && close_error.empty()) {
            close_error = "Failed to release HDF5 " +
                          std::string{resource} + ".";
        }
    }
};

using CloseFunction = herr_t (*)(hid_t);

class Hdf5Handle final {
public:
    Hdf5Handle() = default;

    Hdf5Handle(
        Hdf5Context& context,
        const hid_t id,
        CloseFunction close,
        std::string resource)
        : context_{&context},
          id_{id},
          close_{close},
          resource_{std::move(resource)} {}

    ~Hdf5Handle() {
        reset();
    }

    Hdf5Handle(const Hdf5Handle&) = delete;
    Hdf5Handle& operator=(const Hdf5Handle&) = delete;

    Hdf5Handle(Hdf5Handle&& other) noexcept
        : context_{std::exchange(other.context_, nullptr)},
          id_{std::exchange(other.id_, H5I_INVALID_HID)},
          close_{std::exchange(other.close_, nullptr)},
          resource_{std::move(other.resource_)} {}

    Hdf5Handle& operator=(Hdf5Handle&& other) noexcept {
        if (this != &other) {
            reset();
            context_ = std::exchange(other.context_, nullptr);
            id_ = std::exchange(other.id_, H5I_INVALID_HID);
            close_ = std::exchange(other.close_, nullptr);
            resource_ = std::move(other.resource_);
        }
        return *this;
    }

    [[nodiscard]] hid_t get() const noexcept {
        return id_;
    }

    void reset() noexcept {
        if (id_ >= 0 && close_ != nullptr && context_ != nullptr) {
            const herr_t result = call_hdf5([this] { return close_(id_); });
            context_->record_close_result(result, resource_);
        }
        id_ = H5I_INVALID_HID;
        close_ = nullptr;
        context_ = nullptr;
    }

private:
    Hdf5Context* context_{nullptr};
    hid_t id_{H5I_INVALID_HID};
    CloseFunction close_{nullptr};
    std::string resource_;
};

class Hdf5AllocatedString final {
public:
    Hdf5AllocatedString(Hdf5Context& context, char* value)
        : context_{context}, value_{value} {}

    ~Hdf5AllocatedString() {
        if (value_ != nullptr) {
            context_.record_close_result(
                call_hdf5([this] { return H5free_memory(value_); }),
                "variable-length string");
        }
    }

    Hdf5AllocatedString(const Hdf5AllocatedString&) = delete;
    Hdf5AllocatedString& operator=(const Hdf5AllocatedString&) = delete;

private:
    Hdf5Context& context_;
    char* value_;
};

class Hdf5VlenBuffer final {
public:
    Hdf5VlenBuffer(
        Hdf5Context& context,
        const hid_t type,
        const hid_t space,
        void* data)
        : context_{context}, type_{type}, space_{space}, data_{data} {}

    ~Hdf5VlenBuffer() {
        context_.record_close_result(
            call_hdf5([this] {
                return H5Treclaim(type_, space_, H5P_DEFAULT, data_);
            }),
            "variable-length dataset buffer");
    }

    Hdf5VlenBuffer(const Hdf5VlenBuffer&) = delete;
    Hdf5VlenBuffer& operator=(const Hdf5VlenBuffer&) = delete;

private:
    Hdf5Context& context_;
    hid_t type_;
    hid_t space_;
    void* data_;
};

[[noreturn]] void fail(std::string code, std::string message) {
    throw Hdf5Error{std::move(code), std::move(message)};
}

void check_status(
    const herr_t result,
    const std::string_view operation,
    const std::string_view code) {
    if (result < 0) {
        fail(
            std::string{code},
            "HDF5 operation failed: " + std::string{operation} + ".");
    }
}

Hdf5Handle checked_handle(
    Hdf5Context& context,
    const hid_t id,
    CloseFunction close,
    std::string resource,
    const std::string_view code) {
    if (id < 0) {
        fail(
            std::string{code},
            "Failed to open or create HDF5 " + resource + ".");
    }
    return {context, id, close, std::move(resource)};
}

Hdf5Handle create_group(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name) {
    const std::string owned_name{name};
    return checked_handle(
        context,
        call_hdf5([&] {
            return H5Gcreate2(
                parent,
                owned_name.c_str(),
                H5P_DEFAULT,
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Gclose,
        "group '" + owned_name + "'",
        "hdf5.write_failed");
}

Hdf5Handle open_group(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name) {
    const std::string owned_name{name};
    return checked_handle(
        context,
        call_hdf5([&] {
            return H5Gopen2(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Gclose,
        "group '" + owned_name + "'",
        "hdf5.read_failed");
}

Hdf5Handle create_space(
    Hdf5Context& context,
    const std::span<const hsize_t> dimensions,
    const std::string_view code) {
    return checked_handle(
        context,
        call_hdf5([&] {
            return H5Screate_simple(
                static_cast<int>(dimensions.size()),
                dimensions.data(),
                nullptr);
        }),
        &H5Sclose,
        "dataspace",
        code);
}

Hdf5Handle create_scalar_space(
    Hdf5Context& context,
    const std::string_view code) {
    return checked_handle(
        context,
        call_hdf5([] { return H5Screate(H5S_SCALAR); }),
        &H5Sclose,
        "scalar dataspace",
        code);
}

Hdf5Handle create_utf8_type(
    Hdf5Context& context,
    const std::string_view code) {
    auto type = checked_handle(
        context,
        call_hdf5([] { return H5Tcopy(H5T_C_S1); }),
        &H5Tclose,
        "UTF-8 string datatype",
        code);
    check_status(
        call_hdf5([&] { return H5Tset_size(type.get(), H5T_VARIABLE); }),
        "set variable string size",
        code);
    check_status(
        call_hdf5([&] { return H5Tset_cset(type.get(), H5T_CSET_UTF8); }),
        "set UTF-8 character set",
        code);
    return type;
}

std::string path_utf8(const std::filesystem::path& path) {
    const std::u8string value = path.u8string();
    return {reinterpret_cast<const char*>(value.data()), value.size()};
}

Diagnostic error_diagnostic(std::string code, std::string message) {
    return {
        DiagnosticStage::results,
        Severity::error,
        std::move(code),
        std::move(message),
        std::nullopt,
    };
}

std::optional<std::int64_t> find_unknown_result_node(
    const ResultDatabase& database,
    const std::unordered_set<std::int64_t>& model_node_ids) {
    for (const ResultStep& step : database.steps) {
        for (const ResultFrame& frame : step.frames) {
            for (const NodeId node_id : frame.nodal.node_ids) {
                if (!model_node_ids.contains(node_id.value())) {
                    return node_id.value();
                }
            }
        }
    }
    return std::nullopt;
}

std::optional<Diagnostic> validate_result_contract(
    const Domain& domain,
    const ResultDatabase& database) {
    if (
        database.steps.size() != 1U ||
        database.steps[0].name != domain.step().name) {
        return error_diagnostic(
            "hdf5.analysis_result_mismatch",
            "HDF5 schema 2.0.0 requires one result step matching the "
            "Domain step.");
    }
    if (database.steps[0].frames.size() != 1U) {
        return error_diagnostic(
            "hdf5.incomplete_result_frame",
            "HDF5 schema 2.0.0 requires exactly one result frame.");
    }

    std::unordered_set<std::int64_t> model_node_ids;
    std::unordered_map<std::int64_t, const Node*> model_nodes;
    for (const Node& node : domain.nodes()) {
        model_node_ids.insert(node.id.value());
        model_nodes.emplace(node.id.value(), &node);
    }
    if (const auto unknown =
            find_unknown_result_node(database, model_node_ids)) {
        return error_diagnostic(
            "hdf5.result_node_not_in_model",
            "Result node " + std::to_string(*unknown) +
                " is not present in the serialized model.");
    }

    std::unordered_map<std::int64_t, const BeamElement*> model_elements;
    for (const BeamElement& element : domain.beam_elements()) {
        model_elements.emplace(element.id.value(), &element);
    }
    for (const ResultFrame& frame : database.steps[0].frames) {
        for (std::size_t index = 0;
             index < frame.nodal.node_ids.size(); ++index) {
            const auto found =
                model_nodes.find(frame.nodal.node_ids[index].value());
            if (
                found == model_nodes.end() ||
                frame.nodal.origins[index] != found->second->origin) {
                return error_diagnostic(
                    "hdf5.result_provenance_mismatch",
                    "Nodal result provenance does not match the model.");
            }
        }
        for (const BeamElementFrame& beam : frame.element.beams) {
            const auto found = model_elements.find(beam.element.value());
            if (found == model_elements.end()) {
                return error_diagnostic(
                    "hdf5.result_element_not_in_model",
                    "Beam result references an element not present in "
                    "the serialized model.");
            }
            const BeamElement& element = *found->second;
            const BeamSection& section = domain.section(element.section);
            const BeamFrameResult expected_frame = make_beam_frame(
                domain.node(element.nodes[0]).position,
                domain.node(element.nodes[1]).position,
                section.orientation);
            if (
                beam.origin != element.origin ||
                beam.end_results[0].end_node != element.nodes[0] ||
                beam.end_results[1].end_node != element.nodes[1] ||
                beam.end_results[0].sigma_xx.size() !=
                    section.recovery_points.size() ||
                !expected_frame.frame.has_value() ||
                !same_frame(beam.local_frame, *expected_frame.frame)) {
                return error_diagnostic(
                    "hdf5.result_element_mismatch",
                    "Beam result provenance, connectivity, local frame, "
                    "or recovery points do not match the model.");
            }
        }
        // Result-database validation guarantees unique IDs. The unknown-ID
        // checks above plus equal counts therefore imply exact coverage.
        if (
            frame.nodal.node_ids.size() != model_nodes.size() ||
            frame.element.beams.size() != model_elements.size()) {
            return error_diagnostic(
                "hdf5.incomplete_result_frame",
                "Every result frame must contain exactly one nodal row "
                "per model node and one Beam row per model element.");
        }
    }
    return std::nullopt;
}

Domain rebuild_domain(
    const Hdf5ModelSnapshot& model,
    const Hdf5AnalysisSnapshot& analysis) {
    DomainBuilder builder;
    std::vector<std::optional<NodeId>> node_ids(model.nodes.size());
    for (const Hdf5NodeSnapshot& node : model.nodes) {
        if (
            node.dense_index >= node_ids.size() ||
            node_ids[node.dense_index].has_value()) {
            fail(
                "hdf5.invalid_model_data",
                "Serialized node dense indices are invalid.");
        }
        node_ids[node.dense_index] = node.id;
        builder.add_node({node.id, node.origin, node.coordinates});
    }
    for (const IsotropicElastic& material : model.materials) {
        builder.add_material(material);
    }
    for (const Hdf5SectionSnapshot& section : model.sections) {
        builder.add_section({
            section.id,
            section.name,
            section.area,
            section.iy,
            section.iz,
            section.torsion_j,
            section.shear_area_y,
            section.shear_area_z,
            section.shear_source,
            section.orientation,
            section.recovery_points,
        });
    }
    for (const Hdf5ElementSnapshot& element : model.elements) {
        if (
            element.connectivity[0] >= node_ids.size() ||
            element.connectivity[1] >= node_ids.size() ||
            !node_ids[element.connectivity[0]].has_value() ||
            !node_ids[element.connectivity[1]].has_value()) {
            fail(
                "hdf5.invalid_model_data",
                "Serialized element connectivity is invalid.");
        }
        builder.add_beam_element({
            element.id,
            element.origin,
            {
                *node_ids[element.connectivity[0]],
                *node_ids[element.connectivity[1]],
            },
            element.material,
            element.section,
        });
    }
    for (const NodeSet& set : model.node_sets) {
        builder.add_node_set(set);
    }
    for (const ElementSet& set : model.element_sets) {
        builder.add_element_set(set);
    }
    builder.set_step(analysis.step);

    DomainBuildResult built = std::move(builder).build();
    if (!built.domain.has_value()) {
        std::string message = "Serialized model violates Domain rules.";
        if (!built.diagnostics.empty()) {
            message += " " + built.diagnostics.front().code + ": " +
                       built.diagnostics.front().message;
        }
        fail("hdf5.invalid_model_data", std::move(message));
    }
    return std::move(*built.domain);
}

void write_string_attribute(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const std::string_view value) {
    auto type = create_utf8_type(context, "hdf5.write_failed");
    auto space = create_scalar_space(context, "hdf5.write_failed");
    const std::string owned_name{name};
    auto attribute = checked_handle(
        context,
        call_hdf5([&] {
            return H5Acreate2(
                parent,
                owned_name.c_str(),
                type.get(),
                space.get(),
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Aclose,
        "attribute '" + owned_name + "'",
        "hdf5.write_failed");
    const std::string owned_value{value};
    const char* pointer = owned_value.c_str();
    check_status(
        call_hdf5([&] {
            return H5Awrite(attribute.get(), type.get(), &pointer);
        }),
        "write string attribute '" + owned_name + "'",
        "hdf5.write_failed");
}

void write_double_attribute(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const double value) {
    auto space = create_scalar_space(context, "hdf5.write_failed");
    const std::string owned_name{name};
    auto attribute = checked_handle(
        context,
        call_hdf5([&] {
            return H5Acreate2(
                parent,
                owned_name.c_str(),
                H5T_IEEE_F64LE,
                space.get(),
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Aclose,
        "attribute '" + owned_name + "'",
        "hdf5.write_failed");
    check_status(
        call_hdf5([&] {
            return H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, &value);
        }),
        "write floating-point attribute '" + owned_name + "'",
        "hdf5.write_failed");
}

template <class Value>
void write_numeric_1d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const hid_t file_type,
    const hid_t memory_type,
    const std::span<const Value> values) {
    const std::array<hsize_t, 1> dimensions{
        static_cast<hsize_t>(values.size())};
    auto space = create_space(context, dimensions, "hdf5.write_failed");
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dcreate2(
                parent,
                owned_name.c_str(),
                file_type,
                space.get(),
                H5P_DEFAULT,
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.write_failed");
    if (!values.empty()) {
        check_status(
            call_hdf5([&] {
                return H5Dwrite(
                    dataset.get(),
                    memory_type,
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    values.data());
            }),
            "write dataset '" + owned_name + "'",
            "hdf5.write_failed");
    }
}

template <class Value>
void write_numeric_2d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const hid_t file_type,
    const hid_t memory_type,
    const std::size_t row_count,
    const std::size_t column_count,
    const std::span<const Value> values,
    const std::string_view coordinate_system = {},
    const std::string_view components = {},
    const std::string_view quantity = {}) {
    if (row_count * column_count != values.size()) {
        fail(
            "hdf5.write_failed",
            "Internal HDF5 dataset shape does not match its values.");
    }
    const std::array<hsize_t, 2> dimensions{
        static_cast<hsize_t>(row_count),
        static_cast<hsize_t>(column_count),
    };
    auto space = create_space(context, dimensions, "hdf5.write_failed");
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dcreate2(
                parent,
                owned_name.c_str(),
                file_type,
                space.get(),
                H5P_DEFAULT,
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.write_failed");
    if (!values.empty()) {
        check_status(
            call_hdf5([&] {
                return H5Dwrite(
                    dataset.get(),
                    memory_type,
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    values.data());
            }),
            "write dataset '" + owned_name + "'",
            "hdf5.write_failed");
    }
    if (!coordinate_system.empty()) {
        write_string_attribute(
            context, dataset.get(), "coordinate_system", coordinate_system);
    }
    if (!components.empty()) {
        write_string_attribute(
            context, dataset.get(), "components", components);
    }
    if (!quantity.empty()) {
        write_string_attribute(context, dataset.get(), "quantity", quantity);
    }
}

template <class Value>
void write_numeric_3d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const hid_t file_type,
    const hid_t memory_type,
    const std::size_t first_count,
    const std::size_t second_count,
    const std::size_t third_count,
    const std::span<const Value> values,
    const std::string_view coordinate_system = {},
    const std::string_view components = {}) {
    if (first_count * second_count * third_count != values.size()) {
        fail(
            "hdf5.write_failed",
            "Internal HDF5 dataset shape does not match its values.");
    }
    const std::array<hsize_t, 3> dimensions{
        static_cast<hsize_t>(first_count),
        static_cast<hsize_t>(second_count),
        static_cast<hsize_t>(third_count),
    };
    auto space = create_space(context, dimensions, "hdf5.write_failed");
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dcreate2(
                parent,
                owned_name.c_str(),
                file_type,
                space.get(),
                H5P_DEFAULT,
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.write_failed");
    if (!values.empty()) {
        check_status(
            call_hdf5([&] {
                return H5Dwrite(
                    dataset.get(),
                    memory_type,
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    values.data());
            }),
            "write dataset '" + owned_name + "'",
            "hdf5.write_failed");
    }
    if (!coordinate_system.empty()) {
        write_string_attribute(
            context, dataset.get(), "coordinate_system", coordinate_system);
    }
    if (!components.empty()) {
        write_string_attribute(
            context, dataset.get(), "components", components);
    }
}

void write_strings_1d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const std::span<const std::string> values) {
    auto type = create_utf8_type(context, "hdf5.write_failed");
    const std::array<hsize_t, 1> dimensions{
        static_cast<hsize_t>(values.size())};
    auto space = create_space(context, dimensions, "hdf5.write_failed");
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dcreate2(
                parent,
                owned_name.c_str(),
                type.get(),
                space.get(),
                H5P_DEFAULT,
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.write_failed");
    if (!values.empty()) {
        std::vector<const char*> pointers;
        pointers.reserve(values.size());
        for (const std::string& value : values) {
            pointers.push_back(value.c_str());
        }
        check_status(
            call_hdf5([&] {
                return H5Dwrite(
                    dataset.get(),
                    type.get(),
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    pointers.data());
            }),
            "write string dataset '" + owned_name + "'",
            "hdf5.write_failed");
    }
}

void write_model(
    Hdf5Context& context,
    const hid_t file,
    const Domain& domain) {
    auto model_group = create_group(context, file, "model");

    std::unordered_map<std::int64_t, std::uint64_t> node_dense_indices;
    std::vector<std::uint64_t> node_dense;
    std::vector<std::int64_t> node_ids;
    std::vector<std::string> part_names;
    std::vector<std::string> instance_names;
    std::vector<std::int64_t> local_labels;
    std::vector<double> coordinates;
    node_dense.reserve(domain.nodes().size());
    node_ids.reserve(domain.nodes().size());
    part_names.reserve(domain.nodes().size());
    instance_names.reserve(domain.nodes().size());
    local_labels.reserve(domain.nodes().size());
    coordinates.reserve(domain.nodes().size() * 3U);
    for (std::size_t index = 0; index < domain.nodes().size(); ++index) {
        const Node& node = domain.nodes()[index];
        const auto dense_index = static_cast<std::uint64_t>(index);
        if (!node_dense_indices.emplace(node.id.value(), dense_index).second) {
            fail("hdf5.write_failed", "Domain contains duplicate node IDs.");
        }
        node_dense.push_back(dense_index);
        node_ids.push_back(node.id.value());
        part_names.push_back(node.origin.part_name);
        instance_names.push_back(node.origin.instance_name);
        local_labels.push_back(node.origin.local_label);
        coordinates.push_back(node.position.x);
        coordinates.push_back(node.position.y);
        coordinates.push_back(node.position.z);
    }

    {
        auto nodes_group = create_group(context, model_group.get(), "nodes");
        write_numeric_1d(
            context,
            nodes_group.get(),
            "dense_index",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64,
            std::span<const std::uint64_t>{node_dense});
        write_numeric_1d(
            context,
            nodes_group.get(),
            "internal_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{node_ids});
        write_strings_1d(context, nodes_group.get(), "part_name", part_names);
        write_strings_1d(
            context, nodes_group.get(), "instance_name", instance_names);
        write_numeric_1d(
            context,
            nodes_group.get(),
            "local_label",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{local_labels});
        write_numeric_2d(
            context,
            nodes_group.get(),
            "coordinates",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            domain.nodes().size(),
            3U,
            std::span<const double>{coordinates},
            "global",
            "X,Y,Z");
    }

    std::vector<std::uint64_t> element_dense;
    std::vector<std::int64_t> element_ids;
    std::vector<std::string> element_part_names;
    std::vector<std::string> element_instance_names;
    std::vector<std::int64_t> element_local_labels;
    std::vector<std::uint64_t> connectivity;
    std::vector<std::int64_t> element_material_ids;
    std::vector<std::int64_t> element_section_ids;
    element_dense.reserve(domain.beam_elements().size());
    element_ids.reserve(domain.beam_elements().size());
    element_part_names.reserve(domain.beam_elements().size());
    element_instance_names.reserve(domain.beam_elements().size());
    element_local_labels.reserve(domain.beam_elements().size());
    connectivity.reserve(domain.beam_elements().size() * 2U);
    element_material_ids.reserve(domain.beam_elements().size());
    element_section_ids.reserve(domain.beam_elements().size());
    for (std::size_t index = 0; index < domain.beam_elements().size(); ++index) {
        const BeamElement& element = domain.beam_elements()[index];
        const auto first = node_dense_indices.find(element.nodes[0].value());
        const auto second = node_dense_indices.find(element.nodes[1].value());
        if (first == node_dense_indices.end() ||
            second == node_dense_indices.end()) {
            fail(
                "hdf5.write_failed",
                "Beam connectivity references a missing node ID.");
        }
        element_dense.push_back(static_cast<std::uint64_t>(index));
        element_ids.push_back(element.id.value());
        element_part_names.push_back(element.origin.part_name);
        element_instance_names.push_back(element.origin.instance_name);
        element_local_labels.push_back(element.origin.local_label);
        connectivity.push_back(first->second);
        connectivity.push_back(second->second);
        element_material_ids.push_back(element.material.value());
        element_section_ids.push_back(element.section.value());
    }

    {
        auto elements_group =
            create_group(context, model_group.get(), "elements");
        write_numeric_1d(
            context,
            elements_group.get(),
            "dense_index",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64,
            std::span<const std::uint64_t>{element_dense});
        write_numeric_1d(
            context,
            elements_group.get(),
            "internal_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{element_ids});
        write_strings_1d(
            context,
            elements_group.get(),
            "part_name",
            element_part_names);
        write_strings_1d(
            context,
            elements_group.get(),
            "instance_name",
            element_instance_names);
        write_numeric_1d(
            context,
            elements_group.get(),
            "local_label",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{element_local_labels});
        write_numeric_2d(
            context,
            elements_group.get(),
            "connectivity",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64,
            domain.beam_elements().size(),
            2U,
            std::span<const std::uint64_t>{connectivity});
        write_numeric_1d(
            context,
            elements_group.get(),
            "material_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{element_material_ids});
        write_numeric_1d(
            context,
            elements_group.get(),
            "section_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{element_section_ids});
    }

    {
        auto materials_group =
            create_group(context, model_group.get(), "materials");
        std::vector<std::int64_t> ids;
        std::vector<std::string> names;
        std::vector<double> young;
        std::vector<double> poisson;
        ids.reserve(domain.materials().size());
        names.reserve(domain.materials().size());
        young.reserve(domain.materials().size());
        poisson.reserve(domain.materials().size());
        for (const IsotropicElastic& material : domain.materials()) {
            ids.push_back(material.id.value());
            names.push_back(material.name);
            young.push_back(material.young);
            poisson.push_back(material.poisson);
        }
        write_numeric_1d(
            context,
            materials_group.get(),
            "internal_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{ids});
        write_strings_1d(context, materials_group.get(), "name", names);
        write_numeric_1d(
            context,
            materials_group.get(),
            "young_modulus",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            std::span<const double>{young});
        write_numeric_1d(
            context,
            materials_group.get(),
            "poisson_ratio",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            std::span<const double>{poisson});
    }

    std::vector<std::int64_t> section_ids;
    std::vector<std::string> section_names;
    std::vector<double> areas;
    std::vector<double> moment_y;
    std::vector<double> moment_z;
    std::vector<double> torsion_constants;
    std::vector<double> shear_area_y;
    std::vector<double> shear_area_z;
    std::vector<std::uint8_t> shear_sources;
    std::vector<double> orientations;
    std::vector<std::uint64_t> recovery_offsets{0U};
    std::vector<double> recovery_points;
    section_ids.reserve(domain.sections().size());
    section_names.reserve(domain.sections().size());
    areas.reserve(domain.sections().size());
    moment_y.reserve(domain.sections().size());
    moment_z.reserve(domain.sections().size());
    torsion_constants.reserve(domain.sections().size());
    shear_area_y.reserve(domain.sections().size());
    shear_area_z.reserve(domain.sections().size());
    shear_sources.reserve(domain.sections().size());
    orientations.reserve(domain.sections().size() * 3U);
    recovery_offsets.reserve(domain.sections().size() + 1U);
    for (const BeamSection& section : domain.sections()) {
        section_ids.push_back(section.id.value());
        section_names.push_back(section.name);
        areas.push_back(section.area);
        moment_y.push_back(section.iy);
        moment_z.push_back(section.iz);
        torsion_constants.push_back(section.torsion_j);
        shear_area_y.push_back(section.shear_area_y);
        shear_area_z.push_back(section.shear_area_z);
        shear_sources.push_back(
            section.shear_source == ShearPropertySource::input ? 0U : 1U);
        orientations.push_back(section.orientation.x);
        orientations.push_back(section.orientation.y);
        orientations.push_back(section.orientation.z);
        for (const auto& point : section.recovery_points) {
            recovery_points.push_back(point[0]);
            recovery_points.push_back(point[1]);
        }
        recovery_offsets.push_back(
            static_cast<std::uint64_t>(recovery_points.size() / 2U));
    }

    {
        auto sections_group =
            create_group(context, model_group.get(), "sections");
        write_numeric_1d(
            context,
            sections_group.get(),
            "internal_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{section_ids});
        write_strings_1d(
            context, sections_group.get(), "name", section_names);
        write_numeric_1d(
            context,
            sections_group.get(),
            "area",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            std::span<const double>{areas});
        write_numeric_1d(
            context,
            sections_group.get(),
            "moment_y",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            std::span<const double>{moment_y});
        write_numeric_1d(
            context,
            sections_group.get(),
            "moment_z",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            std::span<const double>{moment_z});
        write_numeric_1d(
            context,
            sections_group.get(),
            "torsion_constant",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            std::span<const double>{torsion_constants});
        write_numeric_1d(
            context,
            sections_group.get(),
            "shear_area_y",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            std::span<const double>{shear_area_y});
        write_numeric_1d(
            context,
            sections_group.get(),
            "shear_area_z",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            std::span<const double>{shear_area_z});
        write_numeric_1d(
            context,
            sections_group.get(),
            "shear_source",
            H5T_STD_U8LE,
            H5T_NATIVE_UINT8,
            std::span<const std::uint8_t>{shear_sources});
        write_numeric_2d(
            context,
            sections_group.get(),
            "orientation",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            domain.sections().size(),
            3U,
            std::span<const double>{orientations},
            "global",
            "X,Y,Z");
        write_numeric_1d(
            context,
            sections_group.get(),
            "recovery_point_offsets",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64,
            std::span<const std::uint64_t>{recovery_offsets});
        write_numeric_2d(
            context,
            sections_group.get(),
            "recovery_points",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            recovery_points.size() / 2U,
            2U,
            std::span<const double>{recovery_points},
            "element_local",
            "y,z");
    }

    auto sets_group = create_group(context, model_group.get(), "sets");
    {
        auto node_sets_group = create_group(context, sets_group.get(), "node");
        std::vector<std::string> names;
        std::vector<std::uint64_t> offsets{0U};
        std::vector<std::int64_t> members;
        names.reserve(domain.node_sets().size());
        offsets.reserve(domain.node_sets().size() + 1U);
        for (const NodeSet& set : domain.node_sets()) {
            names.push_back(set.name);
            for (const NodeId member : set.members) {
                members.push_back(member.value());
            }
            offsets.push_back(static_cast<std::uint64_t>(members.size()));
        }
        write_strings_1d(context, node_sets_group.get(), "names", names);
        write_numeric_1d(
            context,
            node_sets_group.get(),
            "member_offsets",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64,
            std::span<const std::uint64_t>{offsets});
        write_numeric_1d(
            context,
            node_sets_group.get(),
            "members",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{members});
    }
    {
        auto element_sets_group =
            create_group(context, sets_group.get(), "element");
        std::vector<std::string> names;
        std::vector<std::uint64_t> offsets{0U};
        std::vector<std::int64_t> members;
        names.reserve(domain.element_sets().size());
        offsets.reserve(domain.element_sets().size() + 1U);
        for (const ElementSet& set : domain.element_sets()) {
            names.push_back(set.name);
            for (const ElementId member : set.members) {
                members.push_back(member.value());
            }
            offsets.push_back(static_cast<std::uint64_t>(members.size()));
        }
        write_strings_1d(context, element_sets_group.get(), "names", names);
        write_numeric_1d(
            context,
            element_sets_group.get(),
            "member_offsets",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64,
            std::span<const std::uint64_t>{offsets});
        write_numeric_1d(
            context,
            element_sets_group.get(),
            "members",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{members});
    }
}

void write_analysis(
    Hdf5Context& context,
    const hid_t file,
    const Domain& domain) {
    auto analysis_group = create_group(context, file, "analysis");
    auto steps_group = create_group(context, analysis_group.get(), "steps");
    auto step_group = create_group(context, steps_group.get(), "0");
    write_string_attribute(
        context, step_group.get(), "name", domain.step().name);

    auto boundary_group =
        create_group(context, step_group.get(), "boundary_conditions");
    std::vector<std::int64_t> boundary_nodes;
    std::vector<std::uint8_t> boundary_dofs;
    std::vector<double> boundary_values;
    boundary_nodes.reserve(domain.step().prescribed_dofs.size());
    boundary_dofs.reserve(domain.step().prescribed_dofs.size());
    boundary_values.reserve(domain.step().prescribed_dofs.size());
    for (const PrescribedDof& prescribed : domain.step().prescribed_dofs) {
        boundary_nodes.push_back(prescribed.node.value());
        boundary_dofs.push_back(prescribed.dof);
        boundary_values.push_back(prescribed.value);
    }
    write_numeric_1d(
        context,
        boundary_group.get(),
        "node_ids",
        H5T_STD_I64LE,
        H5T_NATIVE_INT64,
        std::span<const std::int64_t>{boundary_nodes});
    write_numeric_1d(
        context,
        boundary_group.get(),
        "dofs",
        H5T_STD_U8LE,
        H5T_NATIVE_UINT8,
        std::span<const std::uint8_t>{boundary_dofs});
    write_numeric_1d(
        context,
        boundary_group.get(),
        "values",
        H5T_IEEE_F64LE,
        H5T_NATIVE_DOUBLE,
        std::span<const double>{boundary_values});

    auto loads_group =
        create_group(context, step_group.get(), "nodal_loads");
    std::vector<std::int64_t> load_nodes;
    std::vector<double> load_values;
    load_nodes.reserve(domain.step().nodal_loads.size());
    load_values.reserve(domain.step().nodal_loads.size() * 6U);
    for (const NodalLoad& load : domain.step().nodal_loads) {
        load_nodes.push_back(load.node.value());
        load_values.insert(
            load_values.end(), load.values.begin(), load.values.end());
    }
    write_numeric_1d(
        context,
        loads_group.get(),
        "node_ids",
        H5T_STD_I64LE,
        H5T_NATIVE_INT64,
        std::span<const std::int64_t>{load_nodes});
    write_numeric_2d(
        context,
        loads_group.get(),
        "values",
        H5T_IEEE_F64LE,
        H5T_NATIVE_DOUBLE,
        domain.step().nodal_loads.size(),
        6U,
        std::span<const double>{load_values},
        "global",
        "Fx,Fy,Fz,Mx,My,Mz");

    auto solver_group =
        create_group(context, analysis_group.get(), "solver_settings");
    write_string_attribute(
        context, solver_group.get(), "backend", solver_backend);
    write_string_attribute(
        context,
        solver_group.get(),
        "matrix_storage",
        solver_matrix_storage);
    write_string_attribute(
        context,
        solver_group.get(),
        "matrix_type",
        solver_matrix_type);
    write_string_attribute(
        context,
        solver_group.get(),
        "constraint_method",
        solver_constraint_method);
    write_string_attribute(
        context, solver_group.get(), "assembly", solver_assembly);
}

void write_results(
    Hdf5Context& context,
    const hid_t file,
    const ResultDatabase& database) {
    auto results_group = create_group(context, file, "results");
    auto steps_group = create_group(context, results_group.get(), "steps");
    for (std::size_t step_index = 0;
         step_index < database.steps.size();
         ++step_index) {
        const ResultStep& step = database.steps[step_index];
        auto step_group = create_group(
            context, steps_group.get(), std::to_string(step_index));
        write_string_attribute(context, step_group.get(), "name", step.name);
        auto frames_group =
            create_group(context, step_group.get(), "frames");
        for (std::size_t frame_index = 0;
             frame_index < step.frames.size();
             ++frame_index) {
            const ResultFrame& frame = step.frames[frame_index];
            auto frame_group = create_group(
                context, frames_group.get(), std::to_string(frame_index));
            write_double_attribute(
                context, frame_group.get(), "step_time", frame.step_time);
            auto nodal_group =
                create_group(context, frame_group.get(), "nodal");

            std::vector<std::int64_t> node_ids;
            std::vector<double> displacement;
            std::vector<double> reaction;
            node_ids.reserve(frame.nodal.node_ids.size());
            displacement.reserve(frame.nodal.displacement.size() * 6U);
            reaction.reserve(frame.nodal.reaction.size() * 6U);
            for (const NodeId node_id : frame.nodal.node_ids) {
                node_ids.push_back(node_id.value());
            }
            for (const auto& values : frame.nodal.displacement) {
                displacement.insert(
                    displacement.end(), values.begin(), values.end());
            }
            for (const auto& values : frame.nodal.reaction) {
                reaction.insert(reaction.end(), values.begin(), values.end());
            }
            write_numeric_1d(
                context,
                nodal_group.get(),
                "node_ids",
                H5T_STD_I64LE,
                H5T_NATIVE_INT64,
                std::span<const std::int64_t>{node_ids});
            write_numeric_2d(
                context,
                nodal_group.get(),
                "displacement",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                frame.nodal.node_ids.size(),
                6U,
                std::span<const double>{displacement},
                "global",
                "Ux,Uy,Uz,Rx,Ry,Rz");
            write_numeric_2d(
                context,
                nodal_group.get(),
                "reaction",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                frame.nodal.node_ids.size(),
                6U,
                std::span<const double>{reaction},
                "global",
                "RFx,RFy,RFz,RMx,RMy,RMz");

            auto element_group =
                create_group(context, frame_group.get(), "element");
            auto beam_group =
                create_group(context, element_group.get(), "beam");
            std::vector<std::int64_t> element_ids;
            std::vector<std::string> part_names;
            std::vector<std::string> instance_names;
            std::vector<std::int64_t> local_labels;
            std::vector<double> local_frames;
            std::vector<std::int64_t> end_node_ids;
            std::vector<double> xis;
            std::vector<double> section_strains;
            std::vector<double> section_forces;
            std::vector<double> centroid_stresses;
            std::vector<std::uint64_t> recovery_offsets{0U};
            std::vector<double> recovery_stresses;
            element_ids.reserve(frame.element.beams.size());
            part_names.reserve(frame.element.beams.size());
            instance_names.reserve(frame.element.beams.size());
            local_labels.reserve(frame.element.beams.size());
            local_frames.reserve(frame.element.beams.size() * 9U);
            end_node_ids.reserve(frame.element.beams.size() * 2U);
            xis.reserve(frame.element.beams.size() * 2U);
            section_strains.reserve(frame.element.beams.size() * 12U);
            section_forces.reserve(frame.element.beams.size() * 12U);
            centroid_stresses.reserve(frame.element.beams.size() * 2U);
            recovery_offsets.reserve(frame.element.beams.size() + 1U);
            for (const BeamElementFrame& beam : frame.element.beams) {
                element_ids.push_back(beam.element.value());
                part_names.push_back(beam.origin.part_name);
                instance_names.push_back(beam.origin.instance_name);
                local_labels.push_back(beam.origin.local_label);
                for (const Vec3 axis : {
                         beam.local_frame.ex,
                         beam.local_frame.ey,
                         beam.local_frame.ez}) {
                    local_frames.push_back(axis.x);
                    local_frames.push_back(axis.y);
                    local_frames.push_back(axis.z);
                }
                for (const BeamSectionResult& end : beam.end_results) {
                    end_node_ids.push_back(end.end_node.value());
                    xis.push_back(end.xi);
                    section_strains.insert(
                        section_strains.end(),
                        end.section_strain.begin(),
                        end.section_strain.end());
                    section_forces.insert(
                        section_forces.end(),
                        end.section_force.begin(),
                        end.section_force.end());
                    centroid_stresses.push_back(end.centroid_sigma_xx);
                }
                for (std::size_t point = 0;
                     point < beam.end_results[0].sigma_xx.size();
                     ++point) {
                    recovery_stresses.push_back(
                        beam.end_results[0].sigma_xx[point]);
                    recovery_stresses.push_back(
                        beam.end_results[1].sigma_xx[point]);
                }
                recovery_offsets.push_back(static_cast<std::uint64_t>(
                    recovery_stresses.size() / 2U));
            }
            write_numeric_1d(
                context,
                beam_group.get(),
                "element_ids",
                H5T_STD_I64LE,
                H5T_NATIVE_INT64,
                std::span<const std::int64_t>{element_ids});
            write_strings_1d(
                context, beam_group.get(), "part_name", part_names);
            write_strings_1d(
                context,
                beam_group.get(),
                "instance_name",
                instance_names);
            write_numeric_1d(
                context,
                beam_group.get(),
                "local_label",
                H5T_STD_I64LE,
                H5T_NATIVE_INT64,
                std::span<const std::int64_t>{local_labels});
            write_numeric_3d(
                context,
                beam_group.get(),
                "local_frame",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                frame.element.beams.size(),
                3U,
                3U,
                std::span<const double>{local_frames},
                "global",
                "ex,ey,ez");
            write_numeric_2d(
                context,
                beam_group.get(),
                "end_node_ids",
                H5T_STD_I64LE,
                H5T_NATIVE_INT64,
                frame.element.beams.size(),
                2U,
                std::span<const std::int64_t>{end_node_ids});
            write_numeric_2d(
                context,
                beam_group.get(),
                "xi",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                frame.element.beams.size(),
                2U,
                std::span<const double>{xis},
                {},
                "end_minus,end_plus");
            write_numeric_3d(
                context,
                beam_group.get(),
                "section_strain",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                frame.element.beams.size(),
                2U,
                6U,
                std::span<const double>{section_strains},
                "element_local",
                "epsilon,gamma_y,gamma_z,kappa_x,kappa_y,kappa_z");
            write_numeric_3d(
                context,
                beam_group.get(),
                "section_force",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                frame.element.beams.size(),
                2U,
                6U,
                std::span<const double>{section_forces},
                "element_local",
                "N,Vy,Vz,T,My,Mz");
            write_numeric_2d(
                context,
                beam_group.get(),
                "centroid_sigma_xx",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                frame.element.beams.size(),
                2U,
                std::span<const double>{centroid_stresses},
                "element_local",
                "end_minus,end_plus",
                "sigma_xx");
            write_numeric_1d(
                context,
                beam_group.get(),
                "recovery_point_offsets",
                H5T_STD_U64LE,
                H5T_NATIVE_UINT64,
                std::span<const std::uint64_t>{recovery_offsets});
            write_numeric_2d(
                context,
                beam_group.get(),
                "recovery_point_sigma_xx",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                recovery_stresses.size() / 2U,
                2U,
                std::span<const double>{recovery_stresses},
                "element_local",
                "end_minus,end_plus",
                "sigma_xx");

            auto diagnostics_group =
                create_group(context, frame_group.get(), "diagnostics");
            std::vector<std::uint8_t> stages;
            std::vector<std::uint8_t> severities;
            std::vector<std::string> codes;
            std::vector<std::string> messages;
            std::vector<std::uint8_t> has_source;
            std::vector<std::string> source_files;
            std::vector<std::uint64_t> source_lines;
            std::vector<std::uint64_t> source_columns;
            stages.reserve(frame.diagnostics.size());
            severities.reserve(frame.diagnostics.size());
            codes.reserve(frame.diagnostics.size());
            messages.reserve(frame.diagnostics.size());
            has_source.reserve(frame.diagnostics.size());
            source_files.reserve(frame.diagnostics.size());
            source_lines.reserve(frame.diagnostics.size());
            source_columns.reserve(frame.diagnostics.size());
            for (const Diagnostic& diagnostic : frame.diagnostics) {
                stages.push_back(
                    static_cast<std::uint8_t>(diagnostic.stage));
                severities.push_back(
                    static_cast<std::uint8_t>(diagnostic.severity));
                codes.push_back(diagnostic.code);
                messages.push_back(diagnostic.message);
                has_source.push_back(diagnostic.source.has_value() ? 1U : 0U);
                source_files.push_back(
                    diagnostic.source.has_value()
                        ? path_utf8(diagnostic.source->file)
                        : std::string{});
                source_lines.push_back(
                    diagnostic.source.has_value()
                        ? static_cast<std::uint64_t>(diagnostic.source->line)
                        : 0U);
                source_columns.push_back(
                    diagnostic.source.has_value()
                        ? static_cast<std::uint64_t>(
                              diagnostic.source->column)
                        : 0U);
            }
            write_numeric_1d(
                context,
                diagnostics_group.get(),
                "stage",
                H5T_STD_U8LE,
                H5T_NATIVE_UINT8,
                std::span<const std::uint8_t>{stages});
            write_numeric_1d(
                context,
                diagnostics_group.get(),
                "severity",
                H5T_STD_U8LE,
                H5T_NATIVE_UINT8,
                std::span<const std::uint8_t>{severities});
            write_strings_1d(
                context, diagnostics_group.get(), "code", codes);
            write_strings_1d(
                context, diagnostics_group.get(), "message", messages);
            write_numeric_1d(
                context,
                diagnostics_group.get(),
                "has_source",
                H5T_STD_U8LE,
                H5T_NATIVE_UINT8,
                std::span<const std::uint8_t>{has_source});
            write_strings_1d(
                context,
                diagnostics_group.get(),
                "source_file",
                source_files);
            write_numeric_1d(
                context,
                diagnostics_group.get(),
                "source_line",
                H5T_STD_U64LE,
                H5T_NATIVE_UINT64,
                std::span<const std::uint64_t>{source_lines});
            write_numeric_1d(
                context,
                diagnostics_group.get(),
                "source_column",
                H5T_STD_U64LE,
                H5T_NATIVE_UINT64,
                std::span<const std::uint64_t>{source_columns});
        }
    }
}

std::string read_string_attribute(
    Hdf5Context& context,
    hid_t parent,
    std::string_view name);

void require_field_metadata(
    Hdf5Context& context,
    hid_t dataset,
    std::string_view coordinate_system,
    std::string_view components,
    std::string_view quantity = {});

void require_numeric_type(
    Hdf5Context& context,
    const hid_t dataset,
    const hid_t expected,
    const std::string_view name) {
    auto type = checked_handle(
        context,
        call_hdf5([&] { return H5Dget_type(dataset); }),
        &H5Tclose,
        "datatype for dataset '" + std::string{name} + "'",
        "hdf5.read_failed");
    const htri_t equal =
        call_hdf5([&] { return H5Tequal(type.get(), expected); });
    if (equal <= 0) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + std::string{name} +
                "' has an unexpected datatype.");
    }
}

std::size_t checked_size(
    const hsize_t value,
    const std::string_view description) {
    if (value > static_cast<hsize_t>(std::numeric_limits<std::size_t>::max())) {
        fail(
            "hdf5.read_failed",
            std::string{description} + " exceeds addressable memory.");
    }
    return static_cast<std::size_t>(value);
}

template <class Value>
std::vector<Value> read_numeric_1d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const hid_t file_type,
    const hid_t memory_type) {
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dopen2(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.read_failed");
    require_numeric_type(context, dataset.get(), file_type, owned_name);
    auto space = checked_handle(
        context,
        call_hdf5([&] { return H5Dget_space(dataset.get()); }),
        &H5Sclose,
        "dataspace for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    const int rank = call_hdf5(
        [&] { return H5Sget_simple_extent_ndims(space.get()); });
    if (rank != 1) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + owned_name + "' must have rank 1.");
    }
    std::array<hsize_t, 1> dimensions{};
    check_status(
        call_hdf5([&] {
            return H5Sget_simple_extent_dims(
                space.get(), dimensions.data(), nullptr);
        }),
        "read shape for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    std::vector<Value> values(
        checked_size(dimensions[0], "Dataset row count"));
    if (!values.empty()) {
        check_status(
            call_hdf5([&] {
                return H5Dread(
                    dataset.get(),
                    memory_type,
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    values.data());
            }),
            "read dataset '" + owned_name + "'",
            "hdf5.read_failed");
    }
    return values;
}

template <class Value>
std::vector<Value> read_numeric_2d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const hid_t file_type,
    const hid_t memory_type,
    const std::size_t expected_columns,
    const std::string_view coordinate_system = {},
    const std::string_view components = {},
    const std::string_view quantity = {}) {
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dopen2(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.read_failed");
    require_numeric_type(context, dataset.get(), file_type, owned_name);
    auto space = checked_handle(
        context,
        call_hdf5([&] { return H5Dget_space(dataset.get()); }),
        &H5Sclose,
        "dataspace for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    const int rank = call_hdf5(
        [&] { return H5Sget_simple_extent_ndims(space.get()); });
    if (rank != 2) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + owned_name + "' must have rank 2.");
    }
    std::array<hsize_t, 2> dimensions{};
    check_status(
        call_hdf5([&] {
            return H5Sget_simple_extent_dims(
                space.get(), dimensions.data(), nullptr);
        }),
        "read shape for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    if (dimensions[1] != static_cast<hsize_t>(expected_columns)) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + owned_name +
                "' has an unexpected column count.");
    }
    const std::size_t row_count =
        checked_size(dimensions[0], "Dataset row count");
    if (row_count >
        std::numeric_limits<std::size_t>::max() / expected_columns) {
        fail("hdf5.read_failed", "Dataset element count overflows.");
    }
    std::vector<Value> values(row_count * expected_columns);
    if (!values.empty()) {
        check_status(
            call_hdf5([&] {
                return H5Dread(
                    dataset.get(),
                    memory_type,
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    values.data());
            }),
            "read dataset '" + owned_name + "'",
            "hdf5.read_failed");
    }
    require_field_metadata(
        context,
        dataset.get(),
        coordinate_system,
        components,
        quantity);
    return values;
}

template <class Value>
std::vector<Value> read_numeric_3d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const hid_t file_type,
    const hid_t memory_type,
    const std::size_t expected_second,
    const std::size_t expected_third,
    const std::string_view coordinate_system = {},
    const std::string_view components = {}) {
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dopen2(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.read_failed");
    require_numeric_type(context, dataset.get(), file_type, owned_name);
    auto space = checked_handle(
        context,
        call_hdf5([&] { return H5Dget_space(dataset.get()); }),
        &H5Sclose,
        "dataspace for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    const int rank = call_hdf5(
        [&] { return H5Sget_simple_extent_ndims(space.get()); });
    if (rank != 3) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + owned_name + "' must have rank 3.");
    }
    std::array<hsize_t, 3> dimensions{};
    check_status(
        call_hdf5([&] {
            return H5Sget_simple_extent_dims(
                space.get(), dimensions.data(), nullptr);
        }),
        "read shape for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    if (
        dimensions[1] != static_cast<hsize_t>(expected_second) ||
        dimensions[2] != static_cast<hsize_t>(expected_third)) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + owned_name +
                "' has an unexpected trailing shape.");
    }
    const std::size_t first_count =
        checked_size(dimensions[0], "Dataset row count");
    if (
        expected_second != 0U && expected_third != 0U &&
        first_count > std::numeric_limits<std::size_t>::max() /
                          expected_second / expected_third) {
        fail("hdf5.read_failed", "Dataset element count overflows.");
    }
    std::vector<Value> values(
        first_count * expected_second * expected_third);
    if (!values.empty()) {
        check_status(
            call_hdf5([&] {
                return H5Dread(
                    dataset.get(),
                    memory_type,
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    values.data());
            }),
            "read dataset '" + owned_name + "'",
            "hdf5.read_failed");
    }
    require_field_metadata(
        context, dataset.get(), coordinate_system, components);
    return values;
}

void require_utf8_string_type(
    const hid_t type,
    const std::string_view description) {
    const H5T_class_t type_class =
        call_hdf5([&] { return H5Tget_class(type); });
    const htri_t is_variable =
        call_hdf5([&] { return H5Tis_variable_str(type); });
    const H5T_cset_t character_set =
        call_hdf5([&] { return H5Tget_cset(type); });
    if (
        type_class != H5T_STRING || is_variable <= 0 ||
        character_set != H5T_CSET_UTF8) {
        fail(
            "hdf5.read_failed",
            std::string{description} +
                " must be a variable-length UTF-8 string.");
    }
}

std::vector<std::string> read_strings_1d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name) {
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dopen2(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.read_failed");
    auto type = checked_handle(
        context,
        call_hdf5([&] { return H5Dget_type(dataset.get()); }),
        &H5Tclose,
        "datatype for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    require_utf8_string_type(type.get(), "Dataset '" + owned_name + "'");
    auto space = checked_handle(
        context,
        call_hdf5([&] { return H5Dget_space(dataset.get()); }),
        &H5Sclose,
        "dataspace for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    const int rank = call_hdf5(
        [&] { return H5Sget_simple_extent_ndims(space.get()); });
    if (rank != 1) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + owned_name + "' must have rank 1.");
    }
    std::array<hsize_t, 1> dimensions{};
    check_status(
        call_hdf5([&] {
            return H5Sget_simple_extent_dims(
                space.get(), dimensions.data(), nullptr);
        }),
        "read shape for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    const std::size_t count =
        checked_size(dimensions[0], "String dataset row count");
    std::vector<char*> raw_values(count, nullptr);
    if (raw_values.empty()) {
        return {};
    }
    check_status(
        call_hdf5([&] {
            return H5Dread(
                dataset.get(),
                type.get(),
                H5S_ALL,
                H5S_ALL,
                H5P_DEFAULT,
                raw_values.data());
        }),
        "read string dataset '" + owned_name + "'",
        "hdf5.read_failed");
    Hdf5VlenBuffer reclaim{
        context, type.get(), space.get(), raw_values.data()};
    std::vector<std::string> values;
    values.reserve(count);
    for (const char* value : raw_values) {
        if (value == nullptr) {
            fail(
                "hdf5.read_failed",
                "String dataset '" + owned_name +
                    "' contains a null value.");
        }
        values.emplace_back(value);
    }
    return values;
}

std::string read_string_attribute(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name) {
    const std::string owned_name{name};
    auto attribute = checked_handle(
        context,
        call_hdf5([&] {
            return H5Aopen(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Aclose,
        "attribute '" + owned_name + "'",
        "hdf5.read_failed");
    auto type = checked_handle(
        context,
        call_hdf5([&] { return H5Aget_type(attribute.get()); }),
        &H5Tclose,
        "datatype for attribute '" + owned_name + "'",
        "hdf5.read_failed");
    require_utf8_string_type(type.get(), "Attribute '" + owned_name + "'");
    auto space = checked_handle(
        context,
        call_hdf5([&] { return H5Aget_space(attribute.get()); }),
        &H5Sclose,
        "dataspace for attribute '" + owned_name + "'",
        "hdf5.read_failed");
    const H5S_class_t space_class =
        call_hdf5([&] { return H5Sget_simple_extent_type(space.get()); });
    if (space_class != H5S_SCALAR) {
        fail(
            "hdf5.read_failed",
            "Attribute '" + owned_name + "' must be scalar.");
    }
    char* raw_value = nullptr;
    check_status(
        call_hdf5([&] {
            return H5Aread(attribute.get(), type.get(), &raw_value);
        }),
        "read string attribute '" + owned_name + "'",
        "hdf5.read_failed");
    Hdf5AllocatedString allocated{context, raw_value};
    if (raw_value == nullptr) {
        fail(
            "hdf5.read_failed",
            "Attribute '" + owned_name + "' contains a null string.");
    }
    return std::string{raw_value};
}

void require_field_metadata(
    Hdf5Context& context,
    const hid_t dataset,
    const std::string_view coordinate_system,
    const std::string_view components,
    const std::string_view quantity) {
    const auto require = [&](
                             const std::string_view name,
                             const std::string_view expected) {
        if (expected.empty()) {
            return;
        }
        const std::string actual =
            read_string_attribute(context, dataset, name);
        if (actual != expected) {
            fail(
                "hdf5.read_failed",
                "Field attribute '" + std::string{name} +
                    "' has an unexpected value.");
        }
    };
    require("coordinate_system", coordinate_system);
    require("components", components);
    require("quantity", quantity);
}

double read_double_attribute(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name) {
    const std::string owned_name{name};
    auto attribute = checked_handle(
        context,
        call_hdf5([&] {
            return H5Aopen(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Aclose,
        "attribute '" + owned_name + "'",
        "hdf5.read_failed");
    auto type = checked_handle(
        context,
        call_hdf5([&] { return H5Aget_type(attribute.get()); }),
        &H5Tclose,
        "datatype for attribute '" + owned_name + "'",
        "hdf5.read_failed");
    const htri_t equal =
        call_hdf5([&] { return H5Tequal(type.get(), H5T_IEEE_F64LE); });
    if (equal <= 0) {
        fail(
            "hdf5.read_failed",
            "Attribute '" + owned_name +
                "' has an unexpected datatype.");
    }
    auto space = checked_handle(
        context,
        call_hdf5([&] { return H5Aget_space(attribute.get()); }),
        &H5Sclose,
        "dataspace for attribute '" + owned_name + "'",
        "hdf5.read_failed");
    const H5S_class_t space_class =
        call_hdf5([&] { return H5Sget_simple_extent_type(space.get()); });
    if (space_class != H5S_SCALAR) {
        fail(
            "hdf5.read_failed",
            "Attribute '" + owned_name + "' must be scalar.");
    }
    double value{};
    check_status(
        call_hdf5([&] {
            return H5Aread(attribute.get(), H5T_NATIVE_DOUBLE, &value);
        }),
        "read floating-point attribute '" + owned_name + "'",
        "hdf5.read_failed");
    return value;
}

std::size_t group_link_count(
    const hid_t group,
    const std::string_view description) {
    H5G_info_t information{};
    check_status(
        call_hdf5([&] { return H5Gget_info(group, &information); }),
        "inspect " + std::string{description},
        "hdf5.read_failed");
    return checked_size(information.nlinks, "Group link count");
}

void require_offsets(
    const std::span<const std::uint64_t> offsets,
    const std::size_t row_count,
    const std::size_t flattened_count,
    const std::string_view description) {
    if (
        offsets.size() != row_count + 1U || offsets.front() != 0U ||
        offsets.back() != static_cast<std::uint64_t>(flattened_count) ||
        !std::ranges::is_sorted(offsets)) {
        fail(
            "hdf5.read_failed",
            std::string{description} + " offsets are invalid.");
    }
}

Hdf5ModelSnapshot read_model(
    Hdf5Context& context,
    const hid_t file) {
    auto model_group = open_group(context, file, "model");
    Hdf5ModelSnapshot model;

    {
        auto group = open_group(context, model_group.get(), "nodes");
        const auto dense = read_numeric_1d<std::uint64_t>(
            context, group.get(), "dense_index", H5T_STD_U64LE,
            H5T_NATIVE_UINT64);
        const auto ids = read_numeric_1d<std::int64_t>(
            context, group.get(), "internal_id", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto parts = read_strings_1d(context, group.get(), "part_name");
        const auto instances =
            read_strings_1d(context, group.get(), "instance_name");
        const auto labels = read_numeric_1d<std::int64_t>(
            context, group.get(), "local_label", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto coordinates = read_numeric_2d<double>(
            context, group.get(), "coordinates", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE, 3U, "global", "X,Y,Z");
        const std::size_t count = dense.size();
        if (
            ids.size() != count || parts.size() != count ||
            instances.size() != count || labels.size() != count ||
            coordinates.size() != count * 3U) {
            fail("hdf5.read_failed", "Node datasets have inconsistent rows.");
        }
        std::unordered_set<std::int64_t> unique_ids;
        model.nodes.reserve(count);
        for (std::size_t index = 0; index < count; ++index) {
            if (dense[index] != static_cast<std::uint64_t>(index)) {
                fail(
                    "hdf5.read_failed",
                    "Node dense_index must be contiguous and 0-based.");
            }
            if (!unique_ids.insert(ids[index]).second) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model contains duplicate node internal IDs.");
            }
            const Vec3 value{
                coordinates[index * 3U], coordinates[index * 3U + 1U],
                coordinates[index * 3U + 2U]};
            if (!is_finite(value)) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model contains nonfinite node coordinates.");
            }
            model.nodes.push_back({
                dense[index], NodeId{ids[index]},
                {parts[index], instances[index], labels[index]}, value});
        }
    }

    {
        auto group = open_group(context, model_group.get(), "materials");
        const auto ids = read_numeric_1d<std::int64_t>(
            context, group.get(), "internal_id", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto names = read_strings_1d(context, group.get(), "name");
        const auto young = read_numeric_1d<double>(
            context, group.get(), "young_modulus", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE);
        const auto poisson = read_numeric_1d<double>(
            context, group.get(), "poisson_ratio", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE);
        if (
            names.size() != ids.size() || young.size() != ids.size() ||
            poisson.size() != ids.size()) {
            fail(
                "hdf5.read_failed",
                "Material datasets have inconsistent rows.");
        }
        std::unordered_set<std::int64_t> unique_ids;
        model.materials.reserve(ids.size());
        for (std::size_t index = 0; index < ids.size(); ++index) {
            if (!unique_ids.insert(ids[index]).second) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model contains duplicate material IDs.");
            }
            if (
                !std::isfinite(young[index]) || young[index] <= 0.0 ||
                !std::isfinite(poisson[index]) || poisson[index] <= -1.0 ||
                poisson[index] >= 0.5) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model contains invalid material data.");
            }
            model.materials.push_back(
                {MaterialId{ids[index]}, names[index], young[index],
                 poisson[index]});
        }
    }

    {
        auto group = open_group(context, model_group.get(), "sections");
        const auto ids = read_numeric_1d<std::int64_t>(
            context, group.get(), "internal_id", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto names = read_strings_1d(context, group.get(), "name");
        const auto areas = read_numeric_1d<double>(
            context, group.get(), "area", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE);
        const auto iy = read_numeric_1d<double>(
            context, group.get(), "moment_y", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE);
        const auto iz = read_numeric_1d<double>(
            context, group.get(), "moment_z", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE);
        const auto torsion = read_numeric_1d<double>(
            context, group.get(), "torsion_constant", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE);
        const auto shear_y = read_numeric_1d<double>(
            context, group.get(), "shear_area_y", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE);
        const auto shear_z = read_numeric_1d<double>(
            context, group.get(), "shear_area_z", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE);
        const auto sources = read_numeric_1d<std::uint8_t>(
            context, group.get(), "shear_source", H5T_STD_U8LE,
            H5T_NATIVE_UINT8);
        const auto orientations = read_numeric_2d<double>(
            context, group.get(), "orientation", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE, 3U, "global", "X,Y,Z");
        const auto offsets = read_numeric_1d<std::uint64_t>(
            context, group.get(), "recovery_point_offsets", H5T_STD_U64LE,
            H5T_NATIVE_UINT64);
        const auto points = read_numeric_2d<double>(
            context, group.get(), "recovery_points", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE, 2U, "element_local", "y,z");
        const std::size_t count = ids.size();
        if (
            names.size() != count || areas.size() != count ||
            iy.size() != count || iz.size() != count ||
            torsion.size() != count || shear_y.size() != count ||
            shear_z.size() != count || sources.size() != count ||
            orientations.size() != count * 3U) {
            fail(
                "hdf5.read_failed",
                "Section datasets have inconsistent rows.");
        }
        require_offsets(offsets, count, points.size() / 2U, "Section");
        std::unordered_set<std::int64_t> unique_ids;
        model.sections.reserve(count);
        for (std::size_t index = 0; index < count; ++index) {
            if (!unique_ids.insert(ids[index]).second) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model contains duplicate section internal IDs.");
            }
            const std::array<double, 6> positive{
                areas[index], iy[index], iz[index], torsion[index],
                shear_y[index], shear_z[index]};
            if (!std::ranges::all_of(positive, [](const double value) {
                    return std::isfinite(value) && value > 0.0;
                })) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model requires finite positive section data.");
            }
            ShearPropertySource source;
            if (sources[index] == 0U) {
                source = ShearPropertySource::input;
            } else if (sources[index] == 1U) {
                source = ShearPropertySource::phase1_default;
            } else {
                fail(
                    "hdf5.read_failed",
                    "Section shear_source contains an unknown value.");
            }
            const Vec3 orientation{
                orientations[index * 3U], orientations[index * 3U + 1U],
                orientations[index * 3U + 2U]};
            if (!is_finite(orientation)) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model contains a nonfinite orientation.");
            }
            std::vector<std::array<double, 2>> recovery;
            for (std::uint64_t point = offsets[index];
                 point < offsets[index + 1U]; ++point) {
                const std::size_t value = static_cast<std::size_t>(point) * 2U;
                if (
                    !std::isfinite(points[value]) ||
                    !std::isfinite(points[value + 1U])) {
                    fail(
                        "hdf5.invalid_model_data",
                        "Serialized model contains a nonfinite recovery point.");
                }
                recovery.push_back({points[value], points[value + 1U]});
            }
            model.sections.push_back({
                SectionId{ids[index]}, names[index], areas[index], iy[index],
                iz[index], torsion[index], shear_y[index], shear_z[index],
                source, orientation, std::move(recovery)});
        }
    }

    {
        auto group = open_group(context, model_group.get(), "elements");
        const auto dense = read_numeric_1d<std::uint64_t>(
            context, group.get(), "dense_index", H5T_STD_U64LE,
            H5T_NATIVE_UINT64);
        const auto ids = read_numeric_1d<std::int64_t>(
            context, group.get(), "internal_id", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto parts = read_strings_1d(context, group.get(), "part_name");
        const auto instances =
            read_strings_1d(context, group.get(), "instance_name");
        const auto labels = read_numeric_1d<std::int64_t>(
            context, group.get(), "local_label", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto connectivity = read_numeric_2d<std::uint64_t>(
            context, group.get(), "connectivity", H5T_STD_U64LE,
            H5T_NATIVE_UINT64, 2U);
        const auto material_ids = read_numeric_1d<std::int64_t>(
            context, group.get(), "material_id", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto section_ids = read_numeric_1d<std::int64_t>(
            context, group.get(), "section_id", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const std::size_t count = dense.size();
        if (
            ids.size() != count || parts.size() != count ||
            instances.size() != count || labels.size() != count ||
            connectivity.size() != count * 2U ||
            material_ids.size() != count || section_ids.size() != count) {
            fail(
                "hdf5.read_failed",
                "Element datasets have inconsistent rows.");
        }
        std::unordered_set<std::int64_t> materials;
        for (const IsotropicElastic& material : model.materials) {
            materials.insert(material.id.value());
        }
        std::unordered_set<std::int64_t> sections;
        for (const Hdf5SectionSnapshot& section : model.sections) {
            sections.insert(section.id.value());
        }
        std::unordered_set<std::int64_t> unique_ids;
        model.elements.reserve(count);
        for (std::size_t index = 0; index < count; ++index) {
            const std::uint64_t first = connectivity[index * 2U];
            const std::uint64_t second = connectivity[index * 2U + 1U];
            if (dense[index] != static_cast<std::uint64_t>(index)) {
                fail(
                    "hdf5.read_failed",
                    "Element dense_index must be contiguous and 0-based.");
            }
            if (!unique_ids.insert(ids[index]).second) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model contains duplicate element internal IDs.");
            }
            if (
                first >= static_cast<std::uint64_t>(model.nodes.size()) ||
                second >= static_cast<std::uint64_t>(model.nodes.size()) ||
                first == second || !materials.contains(material_ids[index]) ||
                !sections.contains(section_ids[index])) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized element contains an invalid reference.");
            }
            model.elements.push_back({
                dense[index], ElementId{ids[index]},
                {parts[index], instances[index], labels[index]},
                {first, second}, MaterialId{material_ids[index]},
                SectionId{section_ids[index]}});
        }
    }

    auto sets_group = open_group(context, model_group.get(), "sets");
    {
        auto group = open_group(context, sets_group.get(), "node");
        const auto names = read_strings_1d(context, group.get(), "names");
        const auto offsets = read_numeric_1d<std::uint64_t>(
            context, group.get(), "member_offsets", H5T_STD_U64LE,
            H5T_NATIVE_UINT64);
        const auto members = read_numeric_1d<std::int64_t>(
            context, group.get(), "members", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        require_offsets(offsets, names.size(), members.size(), "Node set");
        std::unordered_set<std::int64_t> nodes;
        for (const Hdf5NodeSnapshot& node : model.nodes) {
            nodes.insert(node.id.value());
        }
        for (std::size_t index = 0; index < names.size(); ++index) {
            NodeSet set{names[index], {}};
            for (std::uint64_t member = offsets[index];
                 member < offsets[index + 1U]; ++member) {
                const std::int64_t id =
                    members[static_cast<std::size_t>(member)];
                if (!nodes.contains(id)) {
                    fail(
                        "hdf5.invalid_model_data",
                        "Node set references a missing node.");
                }
                set.members.emplace_back(id);
            }
            model.node_sets.push_back(std::move(set));
        }
    }
    {
        auto group = open_group(context, sets_group.get(), "element");
        const auto names = read_strings_1d(context, group.get(), "names");
        const auto offsets = read_numeric_1d<std::uint64_t>(
            context, group.get(), "member_offsets", H5T_STD_U64LE,
            H5T_NATIVE_UINT64);
        const auto members = read_numeric_1d<std::int64_t>(
            context, group.get(), "members", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        require_offsets(offsets, names.size(), members.size(), "Element set");
        std::unordered_set<std::int64_t> elements;
        for (const Hdf5ElementSnapshot& element : model.elements) {
            elements.insert(element.id.value());
        }
        for (std::size_t index = 0; index < names.size(); ++index) {
            ElementSet set{names[index], {}};
            for (std::uint64_t member = offsets[index];
                 member < offsets[index + 1U]; ++member) {
                const std::int64_t id =
                    members[static_cast<std::size_t>(member)];
                if (!elements.contains(id)) {
                    fail(
                        "hdf5.invalid_model_data",
                        "Element set references a missing element.");
                }
                set.members.emplace_back(id);
            }
            model.element_sets.push_back(std::move(set));
        }
    }

    return model;
}

Hdf5AnalysisSnapshot read_analysis(
    Hdf5Context& context,
    const hid_t file,
    const Hdf5ModelSnapshot& model) {
    auto analysis_group = open_group(context, file, "analysis");
    auto steps_group = open_group(context, analysis_group.get(), "steps");
    if (group_link_count(steps_group.get(), "analysis steps") != 1U) {
        fail(
            "hdf5.read_failed",
            "Phase 1 HDF5 requires exactly one analysis step.");
    }
    auto step_group = open_group(context, steps_group.get(), "0");
    StepDefinition step;
    step.name = read_string_attribute(context, step_group.get(), "name");

    std::unordered_set<std::int64_t> node_ids;
    for (const Hdf5NodeSnapshot& node : model.nodes) {
        node_ids.insert(node.id.value());
    }
    {
        auto group =
            open_group(context, step_group.get(), "boundary_conditions");
        const auto nodes = read_numeric_1d<std::int64_t>(
            context, group.get(), "node_ids", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto dofs = read_numeric_1d<std::uint8_t>(
            context, group.get(), "dofs", H5T_STD_U8LE,
            H5T_NATIVE_UINT8);
        const auto values = read_numeric_1d<double>(
            context, group.get(), "values", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE);
        if (dofs.size() != nodes.size() || values.size() != nodes.size()) {
            fail(
                "hdf5.read_failed",
                "Boundary-condition datasets have inconsistent rows.");
        }
        step.prescribed_dofs.reserve(nodes.size());
        for (std::size_t index = 0; index < nodes.size(); ++index) {
            if (
                !node_ids.contains(nodes[index]) || dofs[index] < 1U ||
                dofs[index] > 6U || !std::isfinite(values[index])) {
                fail(
                    "hdf5.invalid_analysis_data",
                    "Serialized boundary condition is invalid.");
            }
            step.prescribed_dofs.push_back(
                {NodeId{nodes[index]}, dofs[index], values[index]});
        }
    }
    {
        auto group = open_group(context, step_group.get(), "nodal_loads");
        const auto nodes = read_numeric_1d<std::int64_t>(
            context, group.get(), "node_ids", H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto values = read_numeric_2d<double>(
            context, group.get(), "values", H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE, 6U, "global", "Fx,Fy,Fz,Mx,My,Mz");
        if (values.size() != nodes.size() * 6U) {
            fail(
                "hdf5.read_failed",
                "Nodal-load datasets have inconsistent rows.");
        }
        step.nodal_loads.reserve(nodes.size());
        for (std::size_t index = 0; index < nodes.size(); ++index) {
            if (!node_ids.contains(nodes[index])) {
                fail(
                    "hdf5.invalid_analysis_data",
                    "Serialized nodal load references a missing node.");
            }
            std::array<double, 6> load{};
            std::copy_n(
                values.begin() +
                    static_cast<std::ptrdiff_t>(index * 6U),
                6U,
                load.begin());
            if (!std::ranges::all_of(load, [](const double value) {
                    return std::isfinite(value);
                })) {
                fail(
                    "hdf5.invalid_analysis_data",
                    "Serialized nodal load contains a nonfinite value.");
            }
            step.nodal_loads.push_back({NodeId{nodes[index]}, load});
        }
    }

    auto solver_group =
        open_group(context, analysis_group.get(), "solver_settings");
    Hdf5SolverSettingsSnapshot solver{
        read_string_attribute(context, solver_group.get(), "backend"),
        read_string_attribute(
            context, solver_group.get(), "matrix_storage"),
        read_string_attribute(context, solver_group.get(), "matrix_type"),
        read_string_attribute(
            context, solver_group.get(), "constraint_method"),
        read_string_attribute(context, solver_group.get(), "assembly"),
    };
    if (
        solver.backend != solver_backend ||
        solver.matrix_storage != solver_matrix_storage ||
        solver.matrix_type != solver_matrix_type ||
        solver.constraint_method != solver_constraint_method ||
        solver.assembly != solver_assembly) {
        fail(
            "hdf5.invalid_analysis_data",
            "Serialized solver settings are not the Phase 1 settings.");
    }
    return {std::move(step), std::move(solver)};
}

ResultDatabase read_database(
    Hdf5Context& context,
    const hid_t file,
    const std::string& version,
    const Hdf5ModelSnapshot& model) {
    std::unordered_map<std::int64_t, EntityOrigin> node_origins;
    for (const Hdf5NodeSnapshot& node : model.nodes) {
        node_origins.emplace(node.id.value(), node.origin);
    }
    std::unordered_map<std::int64_t, const Hdf5ElementSnapshot*> elements;
    for (const Hdf5ElementSnapshot& element : model.elements) {
        elements.emplace(element.id.value(), &element);
    }
    std::unordered_map<std::int64_t, const Hdf5SectionSnapshot*> sections;
    for (const Hdf5SectionSnapshot& section : model.sections) {
        sections.emplace(section.id.value(), &section);
    }

    ResultDatabase database{version, {}};
    auto results_group = open_group(context, file, "results");
    auto steps_group = open_group(context, results_group.get(), "steps");
    const std::size_t step_count =
        group_link_count(steps_group.get(), "results/steps group");
    database.steps.reserve(step_count);
    for (std::size_t step_index = 0; step_index < step_count; ++step_index) {
        auto step_group = open_group(
            context, steps_group.get(), std::to_string(step_index));
        ResultStep step;
        step.name = read_string_attribute(context, step_group.get(), "name");
        auto frames_group =
            open_group(context, step_group.get(), "frames");
        const std::size_t frame_count =
            group_link_count(frames_group.get(), "frames group");
        step.frames.reserve(frame_count);
        for (std::size_t frame_index = 0;
             frame_index < frame_count;
             ++frame_index) {
            auto frame_group = open_group(
                context, frames_group.get(), std::to_string(frame_index));
            ResultFrame frame;
            frame.step_time = read_double_attribute(
                context, frame_group.get(), "step_time");
            auto nodal_group =
                open_group(context, frame_group.get(), "nodal");
            const auto node_ids = read_numeric_1d<std::int64_t>(
                context,
                nodal_group.get(),
                "node_ids",
                H5T_STD_I64LE,
                H5T_NATIVE_INT64);
            const auto displacement = read_numeric_2d<double>(
                context,
                nodal_group.get(),
                "displacement",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                6U,
                "global",
                "Ux,Uy,Uz,Rx,Ry,Rz");
            const auto reaction = read_numeric_2d<double>(
                context,
                nodal_group.get(),
                "reaction",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                6U,
                "global",
                "RFx,RFy,RFz,RMx,RMy,RMz");
            if (
                displacement.size() != node_ids.size() * 6U ||
                reaction.size() != node_ids.size() * 6U) {
                fail(
                    "hdf5.read_failed",
                    "Nodal result datasets have inconsistent row counts.");
            }
            frame.nodal.node_ids.reserve(node_ids.size());
            frame.nodal.origins.reserve(node_ids.size());
            frame.nodal.displacement.reserve(node_ids.size());
            frame.nodal.reaction.reserve(node_ids.size());
            for (std::size_t index = 0; index < node_ids.size(); ++index) {
                frame.nodal.node_ids.emplace_back(node_ids[index]);
                const auto origin = node_origins.find(node_ids[index]);
                frame.nodal.origins.push_back(
                    origin == node_origins.end()
                        ? EntityOrigin{}
                        : origin->second);
                std::array<double, 6> displacement_values{};
                std::array<double, 6> reaction_values{};
                std::copy_n(
                    displacement.begin() +
                        static_cast<std::ptrdiff_t>(index * 6U),
                    6U,
                    displacement_values.begin());
                std::copy_n(
                    reaction.begin() +
                        static_cast<std::ptrdiff_t>(index * 6U),
                    6U,
                    reaction_values.begin());
                frame.nodal.displacement.push_back(displacement_values);
                frame.nodal.reaction.push_back(reaction_values);
            }

            auto element_group =
                open_group(context, frame_group.get(), "element");
            auto beam_group =
                open_group(context, element_group.get(), "beam");
            const auto element_ids = read_numeric_1d<std::int64_t>(
                context, beam_group.get(), "element_ids", H5T_STD_I64LE,
                H5T_NATIVE_INT64);
            const auto part_names =
                read_strings_1d(context, beam_group.get(), "part_name");
            const auto instance_names =
                read_strings_1d(context, beam_group.get(), "instance_name");
            const auto local_labels = read_numeric_1d<std::int64_t>(
                context, beam_group.get(), "local_label", H5T_STD_I64LE,
                H5T_NATIVE_INT64);
            const auto local_frames = read_numeric_3d<double>(
                context, beam_group.get(), "local_frame", H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE, 3U, 3U, "global", "ex,ey,ez");
            const auto end_nodes = read_numeric_2d<std::int64_t>(
                context, beam_group.get(), "end_node_ids", H5T_STD_I64LE,
                H5T_NATIVE_INT64, 2U);
            const auto xis = read_numeric_2d<double>(
                context, beam_group.get(), "xi", H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE, 2U, {}, "end_minus,end_plus");
            const auto strains = read_numeric_3d<double>(
                context, beam_group.get(), "section_strain",
                H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, 2U, 6U,
                "element_local",
                "epsilon,gamma_y,gamma_z,kappa_x,kappa_y,kappa_z");
            const auto forces = read_numeric_3d<double>(
                context, beam_group.get(), "section_force",
                H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, 2U, 6U,
                "element_local", "N,Vy,Vz,T,My,Mz");
            const auto centroid = read_numeric_2d<double>(
                context, beam_group.get(), "centroid_sigma_xx",
                H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, 2U, "element_local",
                "end_minus,end_plus", "sigma_xx");
            const auto offsets = read_numeric_1d<std::uint64_t>(
                context, beam_group.get(), "recovery_point_offsets",
                H5T_STD_U64LE, H5T_NATIVE_UINT64);
            const auto recovery = read_numeric_2d<double>(
                context, beam_group.get(), "recovery_point_sigma_xx",
                H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, 2U, "element_local",
                "end_minus,end_plus", "sigma_xx");
            const std::size_t beam_count = element_ids.size();
            if (
                part_names.size() != beam_count ||
                instance_names.size() != beam_count ||
                local_labels.size() != beam_count ||
                local_frames.size() != beam_count * 9U ||
                end_nodes.size() != beam_count * 2U ||
                xis.size() != beam_count * 2U ||
                strains.size() != beam_count * 12U ||
                forces.size() != beam_count * 12U ||
                centroid.size() != beam_count * 2U) {
                fail(
                    "hdf5.read_failed",
                    "Beam result datasets have inconsistent rows.");
            }
            require_offsets(
                offsets, beam_count, recovery.size() / 2U, "Beam result");
            frame.element.beams.reserve(beam_count);
            for (std::size_t index = 0; index < beam_count; ++index) {
                const auto found = elements.find(element_ids[index]);
                if (found == elements.end()) {
                    fail(
                        "hdf5.invalid_result_data",
                        "Beam result references a missing element.");
                }
                const Hdf5ElementSnapshot& element = *found->second;
                const EntityOrigin origin{
                    part_names[index], instance_names[index],
                    local_labels[index]};
                if (origin != element.origin) {
                    fail(
                        "hdf5.invalid_result_data",
                        "Beam result provenance does not match the model.");
                }
                const NodeId first_model =
                    model.nodes[static_cast<std::size_t>(
                        element.connectivity[0])]
                        .id;
                const NodeId second_model =
                    model.nodes[static_cast<std::size_t>(
                        element.connectivity[1])]
                        .id;
                if (
                    end_nodes[index * 2U] != first_model.value() ||
                    end_nodes[index * 2U + 1U] != second_model.value()) {
                    fail(
                        "hdf5.invalid_result_data",
                        "Beam result connectivity does not match the model.");
                }
                const auto section = sections.find(element.section.value());
                const std::size_t recovery_count = static_cast<std::size_t>(
                    offsets[index + 1U] - offsets[index]);
                if (
                    section == sections.end() ||
                    recovery_count !=
                        section->second->recovery_points.size()) {
                    fail(
                        "hdf5.invalid_result_data",
                        "Beam recovery-point count does not match its section.");
                }
                const std::size_t frame_base = index * 9U;
                const BeamFrame local_frame{
                    {local_frames[frame_base], local_frames[frame_base + 1U],
                     local_frames[frame_base + 2U]},
                    {local_frames[frame_base + 3U],
                     local_frames[frame_base + 4U],
                     local_frames[frame_base + 5U]},
                    {local_frames[frame_base + 6U],
                     local_frames[frame_base + 7U],
                     local_frames[frame_base + 8U]}};
                std::array<BeamSectionResult, 2> ends{
                    BeamSectionResult{
                        xis[index * 2U], NodeId{end_nodes[index * 2U]},
                        {}, {}, centroid[index * 2U], {}},
                    BeamSectionResult{
                        xis[index * 2U + 1U],
                        NodeId{end_nodes[index * 2U + 1U]}, {}, {},
                        centroid[index * 2U + 1U], {}}};
                for (std::size_t end = 0; end < 2U; ++end) {
                    const std::size_t base = (index * 2U + end) * 6U;
                    std::copy_n(
                        strains.begin() + static_cast<std::ptrdiff_t>(base),
                        6U, ends[end].section_strain.begin());
                    std::copy_n(
                        forces.begin() + static_cast<std::ptrdiff_t>(base),
                        6U, ends[end].section_force.begin());
                }
                for (std::uint64_t point = offsets[index];
                     point < offsets[index + 1U]; ++point) {
                    const std::size_t base =
                        static_cast<std::size_t>(point) * 2U;
                    ends[0].sigma_xx.push_back(recovery[base]);
                    ends[1].sigma_xx.push_back(recovery[base + 1U]);
                }
                frame.element.beams.push_back({
                    ElementId{element_ids[index]}, origin, local_frame,
                    std::move(ends)});
            }

            auto diagnostics_group =
                open_group(context, frame_group.get(), "diagnostics");
            const auto stages = read_numeric_1d<std::uint8_t>(
                context, diagnostics_group.get(), "stage", H5T_STD_U8LE,
                H5T_NATIVE_UINT8);
            const auto severities = read_numeric_1d<std::uint8_t>(
                context, diagnostics_group.get(), "severity", H5T_STD_U8LE,
                H5T_NATIVE_UINT8);
            const auto codes =
                read_strings_1d(context, diagnostics_group.get(), "code");
            const auto messages =
                read_strings_1d(context, diagnostics_group.get(), "message");
            const auto has_source = read_numeric_1d<std::uint8_t>(
                context, diagnostics_group.get(), "has_source",
                H5T_STD_U8LE, H5T_NATIVE_UINT8);
            const auto source_files = read_strings_1d(
                context, diagnostics_group.get(), "source_file");
            const auto source_lines = read_numeric_1d<std::uint64_t>(
                context, diagnostics_group.get(), "source_line",
                H5T_STD_U64LE, H5T_NATIVE_UINT64);
            const auto source_columns = read_numeric_1d<std::uint64_t>(
                context, diagnostics_group.get(), "source_column",
                H5T_STD_U64LE, H5T_NATIVE_UINT64);
            const std::size_t diagnostic_count = stages.size();
            if (
                severities.size() != diagnostic_count ||
                codes.size() != diagnostic_count ||
                messages.size() != diagnostic_count ||
                has_source.size() != diagnostic_count ||
                source_files.size() != diagnostic_count ||
                source_lines.size() != diagnostic_count ||
                source_columns.size() != diagnostic_count) {
                fail(
                    "hdf5.read_failed",
                    "Diagnostic datasets have inconsistent rows.");
            }
            frame.diagnostics.reserve(diagnostic_count);
            for (std::size_t index = 0;
                 index < diagnostic_count; ++index) {
                if (
                    stages[index] >
                        static_cast<std::uint8_t>(
                            DiagnosticStage::validation) ||
                    severities[index] >
                        static_cast<std::uint8_t>(Severity::error) ||
                    has_source[index] > 1U) {
                    fail(
                        "hdf5.invalid_result_data",
                        "Serialized diagnostic enum value is invalid.");
                }
                std::optional<SourceLocation> source;
                if (has_source[index] == 1U) {
                    source = SourceLocation{
                        std::filesystem::path{source_files[index]},
                        checked_size(source_lines[index], "Source line"),
                        checked_size(
                            source_columns[index], "Source column")};
                } else if (
                    !source_files[index].empty() ||
                    source_lines[index] != 0U ||
                    source_columns[index] != 0U) {
                    fail(
                        "hdf5.invalid_result_data",
                        "Diagnostic without source contains source data.");
                }
                frame.diagnostics.push_back({
                    static_cast<DiagnosticStage>(stages[index]),
                    static_cast<Severity>(severities[index]), codes[index],
                    messages[index], std::move(source)});
            }
            step.frames.push_back(std::move(frame));
        }
        database.steps.push_back(std::move(step));
    }
    return database;
}

}  // namespace

std::vector<Diagnostic> write_hdf5(
    const std::filesystem::path& path,
    const Domain& domain,
    const ResultDatabase& database,
    const Hdf5InputIdentity& input_identity) {
    const Status validation = validate_result_database(database);
    if (!validation.succeeded) {
        return validation.diagnostics;
    }
    if (database.schema_version != schema_version) {
        return {error_diagnostic(
            "hdf5.unsupported_schema",
            "HDF5 writer supports schema version 2.0.0 only.")};
    }
    if (!valid_input_identity(input_identity)) {
        return {error_diagnostic(
            "hdf5.invalid_input_identity",
            "HDF5 schema 2.0.0 requires an input source and a lowercase "
            "FNV-1a 64-bit fingerprint.")};
    }
    if (const auto contract = validate_result_contract(domain, database)) {
        return {*contract};
    }

    Hdf5Context context;
    try {
        const std::string encoded_path = path_utf8(path);
        auto file = checked_handle(
            context,
            call_hdf5([&] {
                return H5Fcreate(
                    encoded_path.c_str(),
                    H5F_ACC_TRUNC,
                    H5P_DEFAULT,
                    H5P_DEFAULT);
            }),
            &H5Fclose,
            "file '" + encoded_path + "'",
            "hdf5.create_failed");
        write_string_attribute(
            context, file.get(), "schema_version", schema_version);
        write_string_attribute(
            context, file.get(), "fesa_version", version());
        write_string_attribute(
            context, file.get(), "unit_policy", unit_policy);
        write_string_attribute(
            context, file.get(), "input_source", input_identity.source);
        write_string_attribute(
            context,
            file.get(),
            "input_fingerprint",
            input_identity.fingerprint);
        write_model(context, file.get(), domain);
        write_analysis(context, file.get(), domain);
        write_results(context, file.get(), database);
        check_status(
            call_hdf5([&] { return H5Fflush(file.get(), H5F_SCOPE_GLOBAL); }),
            "flush output file",
            "hdf5.write_failed");
        file.reset();
        if (!context.close_error.empty()) {
            fail("hdf5.close_failed", context.close_error);
        }
        return {};
    } catch (const Hdf5Error& error) {
        return {error_diagnostic(error.code(), error.what())};
    } catch (const std::exception& error) {
        return {error_diagnostic("hdf5.write_failed", error.what())};
    }
}

Hdf5ReadResult read_hdf5_results(const std::filesystem::path& path) {
    Hdf5Context context;
    try {
        const std::string encoded_path = path_utf8(path);
        auto file = checked_handle(
            context,
            call_hdf5([&] {
                return H5Fopen(
                    encoded_path.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
            }),
            &H5Fclose,
            "file '" + encoded_path + "'",
            "hdf5.open_failed");
        const std::string version =
            read_string_attribute(context, file.get(), "schema_version");
        if (version != schema_version) {
            fail(
                "hdf5.unsupported_schema",
                "HDF5 reader supports schema version 2.0.0 only.");
        }
        Hdf5MetadataSnapshot metadata{
            version,
            read_string_attribute(context, file.get(), "fesa_version"),
            read_string_attribute(context, file.get(), "unit_policy"),
            read_string_attribute(context, file.get(), "input_source"),
            read_string_attribute(
                context, file.get(), "input_fingerprint"),
        };
        if (metadata.unit_policy != unit_policy) {
            fail(
                "hdf5.read_failed",
                "HDF5 unit policy is not supported.");
        }
        if (!valid_input_identity({
                metadata.input_source,
                metadata.input_fingerprint,
            })) {
            fail(
                "hdf5.invalid_input_identity",
                "Serialized input identity is invalid.");
        }
        Hdf5ModelSnapshot model = read_model(context, file.get());
        Hdf5AnalysisSnapshot analysis =
            read_analysis(context, file.get(), model);
        const Domain domain = rebuild_domain(model, analysis);
        ResultDatabase database =
            read_database(context, file.get(), version, model);
        const Status validation = validate_result_database(database);
        if (!validation.succeeded) {
            file.reset();
            return {std::nullopt, validation.diagnostics, std::nullopt};
        }
        if (const auto contract =
                validate_result_contract(domain, database)) {
            fail(contract->code, contract->message);
        }
        file.reset();
        if (!context.close_error.empty()) {
            fail("hdf5.close_failed", context.close_error);
        }
        return {
            std::move(database),
            {},
            std::move(model),
            std::move(metadata),
            std::move(analysis),
        };
    } catch (const Hdf5Error& error) {
        return {
            std::nullopt,
            {error_diagnostic(error.code(), error.what())},
            std::nullopt,
        };
    } catch (const std::exception& error) {
        return {
            std::nullopt,
            {error_diagnostic("hdf5.read_failed", error.what())},
            std::nullopt,
        };
    }
}

}  // namespace fesa
