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

#include <algorithm>
#include <array>
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace fesa {
namespace {

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

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

bool is_mesh_record(const DeckRecord& record) {
    return record.keyword == "NODE" || record.keyword == "ELEMENT";
}

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

    [[nodiscard]] DomainBuildResult map() {
        if (!select_active_scope()) {
            return failure();
        }

        collect_materials();
        collect_nodes();
        collect_raw_sets(*active_records_, false);
        if (active_assembly_ != nullptr) {
            collect_raw_sets(active_assembly_->records, true);
        }
        collect_sections();
        collect_elements();
        emit_sets();
        collect_step();

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

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

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

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

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

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

    std::optional<std::uint8_t> parse_dof(
        const std::string_view text,
        const DeckRecord& record) {
        const std::optional<std::int64_t> value =
            parse_label(text, record, "degree-of-freedom");
        if (!value.has_value()) {
            return std::nullopt;
        }
        if (*value > 6) {
            add_error(
                "abaqus.semantic.invalid_dof",
                "Phase 1 supports only degrees of freedom 1 through 6.",
                record.source);
            return std::nullopt;
        }
        return static_cast<std::uint8_t>(*value);
    }

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

    bool select_active_scope() {
        const bool has_hierarchical_input =
            !deck_.parts.empty() || deck_.assembly.has_value();
        if (!has_hierarchical_input) {
            active_records_ = &deck_.global_records;
            return true;
        }

        if (std::ranges::any_of(deck_.global_records, is_mesh_record)) {
            add_error(
                "abaqus.semantic.mixed_mesh_organization",
                "Flat mesh records cannot be mixed with Part/Assembly input.",
                std::ranges::find_if(
                    deck_.global_records, is_mesh_record)->source);
            return false;
        }
        if (!deck_.assembly.has_value()) {
            const SourceLocation source =
                deck_.parts.empty() ? SourceLocation{} : deck_.parts[0].source;
            add_error(
                "abaqus.semantic.assembly_count",
                "Hierarchical Phase 1 input requires exactly one Assembly.",
                source);
            return false;
        }

        active_assembly_ = &*deck_.assembly;
        if (active_assembly_->instances.size() != 1U) {
            const SourceLocation& source =
                active_assembly_->instances.size() > 1U
                    ? active_assembly_->instances[1].source
                    : active_assembly_->source;
            add_error(
                "abaqus.semantic.instance_count",
                "Phase 1 requires exactly one Instance.",
                source);
            return false;
        }

        const ParsedInstance& instance = active_assembly_->instances.front();
        if (!instance.transform_data.empty()) {
            add_error(
                "abaqus.semantic.instance_transform",
                "Instance translation and rotation data are unsupported.",
                instance.source);
            return false;
        }

        const auto part = std::ranges::find(
            deck_.parts, instance.part_name, &ParsedPart::name);
        if (part == deck_.parts.end()) {
            add_error(
                "abaqus.semantic.missing_part",
                "Instance '" + instance.name + "' references missing Part '" +
                    instance.part_name + "'.",
                instance.source);
            return false;
        }

        active_records_ = &part->records;
        part_name_ = part->name;
        instance_name_ = instance.name;
        return true;
    }

    void collect_materials() {
        const std::string* current_name = nullptr;
        for (const DeckRecord& record : deck_.global_records) {
            if (record.keyword == "MATERIAL") {
                current_name = require_parameter(record, "NAME");
                continue;
            }
            if (record.keyword != "ELASTIC") {
                continue;
            }
            if (current_name == nullptr) {
                add_error(
                    "abaqus.semantic.elastic_without_material",
                    "*ELASTIC requires a preceding *MATERIAL.",
                    record.source);
                continue;
            }
            if (record.data.empty() || record.data[0].size() < 2U) {
                add_error(
                    "abaqus.semantic.invalid_elastic_data",
                    "*ELASTIC requires Young's modulus and Poisson ratio.",
                    record.source);
                continue;
            }

            const auto young =
                parse_real(record.data[0][0], record, "Young's modulus");
            const auto poisson =
                parse_real(record.data[0][1], record, "Poisson ratio");
            if (!young.has_value() || !poisson.has_value()) {
                continue;
            }

            const MaterialId id{next_material_id_++};
            if (!material_ids_.emplace(*current_name, id).second) {
                add_error(
                    "abaqus.semantic.duplicate_material",
                    "Material '" + *current_name + "' is defined more than once.",
                    record.source);
                continue;
            }
            builder_.add_material({id, *current_name, *young, *poisson});
        }
    }

    void collect_nodes() {
        for (const DeckRecord& record : *active_records_) {
            if (record.keyword != "NODE") {
                continue;
            }
            for (const auto& row : record.data) {
                if (row.size() < 4U) {
                    add_error(
                        "abaqus.semantic.invalid_node_data",
                        "*NODE requires a label and three coordinates.",
                        record.source);
                    continue;
                }

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

                const NodeId id{next_node_id_++};
                node_ids_.emplace(*label, id);
                builder_.add_node({
                    id,
                    EntityOrigin{part_name_, instance_name_, *label},
                    Vec3{*x, *y, *z},
                });
            }
        }
    }

    void collect_raw_sets(
        const std::vector<DeckRecord>& records,
        const bool assembly_scope) {
        for (const DeckRecord& record : records) {
            const bool is_node_set = record.keyword == "NSET";
            const bool is_element_set = record.keyword == "ELSET";
            if (!is_node_set && !is_element_set) {
                continue;
            }

            const std::string* name =
                require_parameter(record, is_node_set ? "NSET" : "ELSET");
            if (name == nullptr) {
                continue;
            }
            if (assembly_scope) {
                const std::string* instance = parameter(record, "INSTANCE");
                if (instance == nullptr || *instance != instance_name_) {
                    add_error(
                        "abaqus.semantic.wrong_instance",
                        "Assembly set '" + *name +
                            "' must reference the active Instance.",
                        record.source);
                    continue;
                }
            }

            std::vector<std::int64_t> labels;
            for (const auto& row : record.data) {
                for (const std::string& field : row) {
                    if (field.empty()) {
                        continue;
                    }
                    const auto label =
                        parse_label(field, record, "set member");
                    if (label.has_value()) {
                        labels.push_back(*label);
                    }
                }
            }
            RawSetMap& sets = is_node_set ? raw_node_sets_ : raw_element_sets_;
            sets[*name] = std::move(labels);
        }
    }

    void collect_sections() {
        for (const DeckRecord& record : *active_records_) {
            if (record.keyword != "BEAM GENERAL SECTION") {
                continue;
            }

            const std::string* element_set =
                require_parameter(record, "ELSET");
            const std::string* material_name =
                require_parameter(record, "MATERIAL");
            if (element_set == nullptr || material_name == nullptr) {
                continue;
            }
            const auto material = material_ids_.find(*material_name);
            if (material == material_ids_.end()) {
                add_error(
                    "abaqus.semantic.missing_material",
                    "Beam section references missing Material '" +
                        *material_name + "'.",
                    record.source);
                continue;
            }
            if (record.data.size() < 2U ||
                record.data[0].size() < 5U ||
                record.data[1].size() < 3U) {
                add_error(
                    "abaqus.semantic.invalid_section_data",
                    "*BEAM GENERAL SECTION requires general properties and "
                    "an orientation vector.",
                    record.source);
                continue;
            }

            const auto area = parse_real(record.data[0][0], record, "area");
            const auto iy = parse_real(record.data[0][1], record, "Iy");
            const auto iz = parse_real(record.data[0][3], record, "Iz");
            const auto torsion =
                parse_real(record.data[0][4], record, "torsion J");
            const auto ox =
                parse_real(record.data[1][0], record, "orientation");
            const auto oy =
                parse_real(record.data[1][1], record, "orientation");
            const auto oz =
                parse_real(record.data[1][2], record, "orientation");
            if (!area.has_value() || !iy.has_value() || !iz.has_value() ||
                !torsion.has_value() || !ox.has_value() || !oy.has_value() ||
                !oz.has_value()) {
                continue;
            }

            const SectionId section_id{next_section_id_++};
            if (!section_assignments_
                     .emplace(
                         *element_set,
                         SectionAssignment{material->second, section_id})
                     .second) {
                add_error(
                    "abaqus.semantic.duplicate_section_assignment",
                    "Element set '" + *element_set +
                        "' has more than one section assignment.",
                    record.source);
                continue;
            }
            builder_.add_section({
                section_id,
                *element_set,
                *area,
                *iy,
                *iz,
                *torsion,
                5.0 * *area / 6.0,
                5.0 * *area / 6.0,
                ShearPropertySource::phase1_default,
                Vec3{*ox, *oy, *oz},
                {},
            });
        }
    }

    void collect_elements() {
        for (const DeckRecord& record : *active_records_) {
            if (record.keyword != "ELEMENT") {
                continue;
            }

            const std::string* type = require_parameter(record, "TYPE");
            const std::string* element_set =
                require_parameter(record, "ELSET");
            if (type == nullptr || element_set == nullptr) {
                continue;
            }
            if (*type != "B31") {
                add_error(
                    "abaqus.semantic.unsupported_element",
                    "Phase 1 supports only B31 elements.",
                    record.source);
                continue;
            }
            const auto assignment = section_assignments_.find(*element_set);
            if (assignment == section_assignments_.end()) {
                add_error(
                    "abaqus.semantic.missing_section",
                    "Element set '" + *element_set +
                        "' has no Beam section assignment.",
                    record.source);
                continue;
            }

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

                const auto first = node_ids_.find(*first_label);
                const auto second = node_ids_.find(*second_label);
                if (first == node_ids_.end() || second == node_ids_.end()) {
                    add_error(
                        "abaqus.semantic.missing_node",
                        "B31 element references a missing node label.",
                        record.source);
                    continue;
                }

                const ElementId id{next_element_id_++};
                element_ids_.emplace(*label, id);
                builder_.add_beam_element({
                    id,
                    EntityOrigin{part_name_, instance_name_, *label},
                    {first->second, second->second},
                    assignment->second.material,
                    assignment->second.section,
                });
            }
        }
    }

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

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

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

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

    void collect_boundary(
        const DeckRecord& record,
        StepDefinition& step) {
        for (const auto& row : record.data) {
            if (row.size() < 2U) {
                add_error(
                    "abaqus.semantic.invalid_boundary_data",
                    "*BOUNDARY requires a target and degree of freedom.",
                    record.source);
                continue;
            }
            const auto first_dof = parse_dof(row[1], record);
            const auto last_dof = row.size() >= 3U && !row[2].empty()
                                      ? parse_dof(row[2], record)
                                      : first_dof;
            const auto value = row.size() >= 4U && !row[3].empty()
                                   ? parse_real(
                                         row[3], record, "prescribed value")
                                   : std::optional<double>{0.0};
            if (!first_dof.has_value() || !last_dof.has_value() ||
                !value.has_value()) {
                continue;
            }
            if (*last_dof < *first_dof) {
                add_error(
                    "abaqus.semantic.invalid_dof_range",
                    "*BOUNDARY degree-of-freedom range is reversed.",
                    record.source);
                continue;
            }

            const std::vector<NodeId> nodes =
                resolve_node_target(row[0], record);
            for (const NodeId node : nodes) {
                for (std::uint8_t dof = *first_dof; dof <= *last_dof; ++dof) {
                    step.prescribed_dofs.push_back({node, dof, *value});
                }
            }
        }
    }

    void collect_loads(
        const DeckRecord& record,
        std::map<std::int64_t, std::array<double, 6>>& loads) {
        for (const auto& row : record.data) {
            if (row.size() < 3U) {
                add_error(
                    "abaqus.semantic.invalid_cload_data",
                    "*CLOAD requires a target, degree of freedom, and value.",
                    record.source);
                continue;
            }
            const auto dof = parse_dof(row[1], record);
            const auto value = parse_real(row[2], record, "concentrated load");
            if (!dof.has_value() || !value.has_value()) {
                continue;
            }

            const std::vector<NodeId> nodes =
                resolve_node_target(row[0], record);
            for (const NodeId node : nodes) {
                loads[node.value()][static_cast<std::size_t>(*dof - 1U)] +=
                    *value;
            }
        }
    }

    void collect_step() {
        StepDefinition step;
        std::map<std::int64_t, std::array<double, 6>> loads;
        bool has_step = false;

        for (const DeckRecord& record : deck_.global_records) {
            if (record.keyword == "STEP") {
                has_step = true;
                const std::string* name = parameter(record, "NAME");
                step.name = name == nullptr ? "Step-1" : *name;
            } else if (record.keyword == "BOUNDARY") {
                collect_boundary(record, step);
            } else if (record.keyword == "CLOAD") {
                collect_loads(record, loads);
            }
        }
        if (!has_step) {
            const SourceLocation source =
                deck_.global_records.empty()
                    ? SourceLocation{}
                    : deck_.global_records.front().source;
            add_error(
                "abaqus.semantic.missing_step",
                "Phase 1 input requires one *STEP.",
                source);
            return;
        }

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

    const ParsedDeck& deck_;
    DomainBuilder builder_;
    std::vector<Diagnostic> diagnostics_;
    const std::vector<DeckRecord>* active_records_ = nullptr;
    const ParsedAssembly* active_assembly_ = nullptr;
    std::string part_name_;
    std::string instance_name_;
    LabelMap node_ids_;
    ElementLabelMap element_ids_;
    std::map<std::string, MaterialId, std::less<>> material_ids_;
    std::map<std::string, SectionAssignment, std::less<>>
        section_assignments_;
    RawSetMap raw_node_sets_;
    RawSetMap raw_element_sets_;
    std::map<std::string, std::vector<NodeId>, std::less<>> node_sets_;
    std::int64_t next_node_id_ = 0;
    std::int64_t next_element_id_ = 0;
    std::int64_t next_material_id_ = 0;
    std::int64_t next_section_id_ = 0;
};

}  // namespace

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

}  // namespace fesa
