#include <fesa/validation/comparison.hpp>

#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <iomanip>
#include <limits>
#include <map>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>

namespace fesa {
namespace {

using PositionKey = std::tuple<
    ReferenceQuantity,
    std::string,
    std::int64_t,
    std::optional<std::int64_t>>;

void add_failure(
    std::vector<Diagnostic>& failures,
    std::string code,
    std::string message) {
    failures.push_back({
        DiagnosticStage::validation,
        Severity::error,
        std::move(code),
        std::move(message),
        std::nullopt,
    });
}

std::string_view quantity_name(const ReferenceQuantity quantity) {
    switch (quantity) {
        case ReferenceQuantity::displacement:
            return "displacement";
        case ReferenceQuantity::reaction:
            return "reaction";
        case ReferenceQuantity::internal_force:
            return "internal_force";
        case ReferenceQuantity::centroid_stress:
            return "centroid_stress";
    }
    return "unknown";
}

std::size_t expected_component_count(
    const ReferenceQuantity quantity) {
    switch (quantity) {
        case ReferenceQuantity::displacement:
        case ReferenceQuantity::reaction:
        case ReferenceQuantity::internal_force:
            return 6;
        case ReferenceQuantity::centroid_stress:
            return 1;
    }
    return 0;
}

std::string component_name(
    const ReferenceQuantity quantity,
    const std::size_t index) {
    switch (quantity) {
        case ReferenceQuantity::displacement:
            return std::string{NodalFrame::displacement_components[index]};
        case ReferenceQuantity::reaction:
            return std::string{NodalFrame::reaction_components[index]};
        case ReferenceQuantity::internal_force:
            return std::string{
                BeamElementFrame::section_force_components[index]};
        case ReferenceQuantity::centroid_stress:
            return std::string{BeamElementFrame::axial_stress_component};
    }
    return "unknown";
}

std::string number_text(const double value) {
    std::ostringstream stream;
    stream << std::setprecision(std::numeric_limits<double>::max_digits10)
           << value;
    return stream.str();
}

std::string tolerance_text(const Tolerance tolerance) {
    return " relative_tolerance=" + number_text(tolerance.relative) +
           " absolute_scale=" + number_text(tolerance.absolute_scale);
}

std::string position_text(
    const ReferenceQuantity quantity,
    const ResultPosition& position) {
    std::string text = "quantity=" + std::string{quantity_name(quantity)} +
                       " instance=" + position.instance_name +
                       " entity=" + std::to_string(position.entity_label);
    if (position.end_node_label.has_value()) {
        text += " end_node=" +
                std::to_string(*position.end_node_label);
    } else {
        text += " end_node=n/a";
    }
    return text;
}

std::string scalar_failure_text(
    const ComparisonSample& sample,
    const std::size_t component,
    const double normalized_error) {
    return position_text(sample.quantity, sample.position) +
           " component=" + component_name(sample.quantity, component) +
           " reference=" + number_text(sample.reference[component]) +
           " actual=" + number_text(sample.actual[component]) +
           " normalized_error=" + number_text(normalized_error) +
           tolerance_text(sample.tolerance);
}

std::string unevaluable_failure_text(
    const ReferenceQuantity quantity,
    const ResultPosition& position,
    const Tolerance tolerance,
    const std::string_view reason) {
    return position_text(quantity, position) +
           " component=n/a reference=n/a actual=n/a "
           "normalized_error=inf" + tolerance_text(tolerance) +
           " reason=" + std::string{reason};
}

bool origin_matches(
    const EntityOrigin& origin,
    const std::string& instance_name,
    const std::int64_t local_label) {
    return origin.instance_name == instance_name &&
           origin.local_label == local_label;
}

ComparisonSampleMatch matching_failure(
    const ReferenceQuantity quantity,
    const ResultPosition& position,
    const Tolerance tolerance,
    std::string code,
    const std::string_view reason) {
    std::vector<Diagnostic> failures;
    add_failure(
        failures,
        std::move(code),
        unevaluable_failure_text(
            quantity, position, tolerance, reason));
    return {std::nullopt, std::move(failures)};
}

std::vector<double> as_vector(const std::array<double, 6>& values) {
    return {values.begin(), values.end()};
}

}  // namespace

ComparisonSampleMatch make_comparison_sample(
    const ResultFrame& frame,
    const ReferenceQuantity quantity,
    const ResultPosition& position,
    const std::span<const double> reference,
    const Tolerance tolerance) {
    std::vector<double> actual;

    if (
        quantity == ReferenceQuantity::displacement ||
        quantity == ReferenceQuantity::reaction) {
        if (position.end_node_label.has_value()) {
            return matching_failure(
                quantity,
                position,
                tolerance,
                "validation.invalid_result_position",
                "nodal_position_has_end_node");
        }

        std::optional<std::size_t> matched_index;
        for (
            std::size_t index = 0;
            index < frame.nodal.origins.size();
            ++index) {
            if (origin_matches(
                    frame.nodal.origins[index],
                    position.instance_name,
                    position.entity_label)) {
                if (matched_index.has_value()) {
                    return matching_failure(
                        quantity,
                        position,
                        tolerance,
                        "validation.unknown_result_origin",
                        "ambiguous_result_origin");
                }
                matched_index = index;
            }
        }
        if (!matched_index.has_value()) {
            return matching_failure(
                quantity,
                position,
                tolerance,
                "validation.unknown_result_origin",
                "unknown_result_origin");
        }

        const auto& field =
            quantity == ReferenceQuantity::displacement
                ? frame.nodal.displacement
                : frame.nodal.reaction;
        if (*matched_index >= field.size()) {
            return matching_failure(
                quantity,
                position,
                tolerance,
                "validation.component_count_mismatch",
                "missing_actual_components");
        }
        actual = as_vector(field[*matched_index]);
    } else {
        if (!position.end_node_label.has_value()) {
            return matching_failure(
                quantity,
                position,
                tolerance,
                "validation.invalid_element_node_pair",
                "missing_end_node");
        }

        const BeamElementFrame* matched_beam = nullptr;
        for (const BeamElementFrame& beam : frame.element.beams) {
            if (origin_matches(
                    beam.origin,
                    position.instance_name,
                    position.entity_label)) {
                if (matched_beam != nullptr) {
                    return matching_failure(
                        quantity,
                        position,
                        tolerance,
                        "validation.unknown_result_origin",
                        "ambiguous_result_origin");
                }
                matched_beam = &beam;
            }
        }
        if (matched_beam == nullptr) {
            return matching_failure(
                quantity,
                position,
                tolerance,
                "validation.unknown_result_origin",
                "unknown_result_origin");
        }

        std::optional<NodeId> end_node;
        for (
            std::size_t index = 0;
            index < frame.nodal.origins.size() &&
            index < frame.nodal.node_ids.size();
            ++index) {
            if (origin_matches(
                    frame.nodal.origins[index],
                    position.instance_name,
                    *position.end_node_label)) {
                if (end_node.has_value()) {
                    return matching_failure(
                        quantity,
                        position,
                        tolerance,
                        "validation.unknown_result_origin",
                        "ambiguous_end_node_origin");
                }
                end_node = frame.nodal.node_ids[index];
            }
        }
        if (!end_node.has_value()) {
            return matching_failure(
                quantity,
                position,
                tolerance,
                "validation.unknown_result_origin",
                "unknown_end_node_origin");
        }

        const BeamSectionResult* matched_end = nullptr;
        for (const BeamSectionResult& end : matched_beam->end_results) {
            if (end.end_node == *end_node) {
                matched_end = &end;
                break;
            }
        }
        if (matched_end == nullptr) {
            return matching_failure(
                quantity,
                position,
                tolerance,
                "validation.invalid_element_node_pair",
                "node_is_not_element_end");
        }

        if (quantity == ReferenceQuantity::internal_force) {
            actual = as_vector(matched_end->section_force);
        } else {
            actual = {matched_end->centroid_sigma_xx};
        }
    }

    ComparisonSample matched{
        quantity,
        position,
        {reference.begin(), reference.end()},
        std::move(actual),
        tolerance,
    };
    return {std::move(matched), {}};
}

ComparisonReport compare_samples(
    const std::span<const ComparisonSample> samples) {
    ComparisonReport report{true, 0.0, {}};
    std::set<PositionKey> positions;

    for (const ComparisonSample& sample : samples) {
        const PositionKey key{
            sample.quantity,
            sample.position.instance_name,
            sample.position.entity_label,
            sample.position.end_node_label,
        };
        if (!positions.insert(key).second) {
            add_failure(
                report.failures,
                "validation.duplicate_result_position",
                unevaluable_failure_text(
                    sample.quantity,
                    sample.position,
                    sample.tolerance,
                    "duplicate_result_position"));
            report.maximum_normalized_error =
                std::numeric_limits<double>::infinity();
            continue;
        }

        const std::size_t expected =
            expected_component_count(sample.quantity);
        if (
            sample.reference.size() != expected ||
            sample.actual.size() != expected) {
            add_failure(
                report.failures,
                "validation.component_count_mismatch",
                unevaluable_failure_text(
                    sample.quantity,
                    sample.position,
                    sample.tolerance,
                    "component_count_mismatch") +
                    " expected=" + std::to_string(expected) +
                    " reference_count=" +
                    std::to_string(sample.reference.size()) +
                    " actual_count=" +
                    std::to_string(sample.actual.size()));
            report.maximum_normalized_error =
                std::numeric_limits<double>::infinity();
            continue;
        }

        if (
            !std::isfinite(sample.tolerance.relative) ||
            !std::isfinite(sample.tolerance.absolute_scale) ||
            sample.tolerance.relative < 0.0 ||
            sample.tolerance.absolute_scale < 0.0) {
            add_failure(
                report.failures,
                "validation.invalid_tolerance",
                unevaluable_failure_text(
                    sample.quantity,
                    sample.position,
                    sample.tolerance,
                    "invalid_tolerance"));
            report.maximum_normalized_error =
                std::numeric_limits<double>::infinity();
            continue;
        }

        for (std::size_t component = 0; component < expected; ++component) {
            const double reference = sample.reference[component];
            const double actual = sample.actual[component];
            if (!std::isfinite(reference) || !std::isfinite(actual)) {
                add_failure(
                    report.failures,
                    "validation.nonfinite_comparison_value",
                    scalar_failure_text(
                        sample,
                        component,
                        std::numeric_limits<double>::infinity()));
                report.maximum_normalized_error =
                    std::numeric_limits<double>::infinity();
                continue;
            }

            const double denominator =
                sample.tolerance.absolute_scale +
                sample.tolerance.relative * std::abs(reference);
            if (!(denominator > 0.0) || !std::isfinite(denominator)) {
                add_failure(
                    report.failures,
                    "validation.invalid_tolerance",
                    scalar_failure_text(
                        sample,
                        component,
                        std::numeric_limits<double>::infinity()));
                report.maximum_normalized_error =
                    std::numeric_limits<double>::infinity();
                continue;
            }

            const double normalized_error =
                std::abs(actual - reference) / denominator;
            report.maximum_normalized_error = std::max(
                report.maximum_normalized_error,
                normalized_error);
            if (!(normalized_error <= 1.0)) {
                add_failure(
                    report.failures,
                    "validation.tolerance_exceeded",
                    scalar_failure_text(
                        sample, component, normalized_error));
            }
        }
    }

    report.passed = report.failures.empty();
    return report;
}

CorrelationReport correlate_samples(
    const std::span<const ComparisonSample> samples) {
    struct Accumulator final {
        double squared_error{};
        double squared_reference{};
        double squared_absolute_scale{};
        std::size_t value_count{};
    };

    CorrelationReport report{true, {}, {}};
    if (samples.empty()) {
        add_failure(
            report.failures,
            "validation.empty_comparison",
            "No comparison samples were provided for correlation.");
        report.evaluable = false;
        return report;
    }

    std::set<PositionKey> positions;
    std::map<std::pair<ReferenceQuantity, std::size_t>, Accumulator>
        accumulators;

    for (const ComparisonSample& sample : samples) {
        const PositionKey key{
            sample.quantity,
            sample.position.instance_name,
            sample.position.entity_label,
            sample.position.end_node_label,
        };
        if (!positions.insert(key).second) {
            add_failure(
                report.failures,
                "validation.duplicate_result_position",
                unevaluable_failure_text(
                    sample.quantity,
                    sample.position,
                    sample.tolerance,
                    "duplicate_result_position"));
            continue;
        }

        const std::size_t expected =
            expected_component_count(sample.quantity);
        if (
            sample.reference.size() != expected ||
            sample.actual.size() != expected) {
            add_failure(
                report.failures,
                "validation.component_count_mismatch",
                unevaluable_failure_text(
                    sample.quantity,
                    sample.position,
                    sample.tolerance,
                    "component_count_mismatch") +
                    " expected=" + std::to_string(expected) +
                    " reference_count=" +
                    std::to_string(sample.reference.size()) +
                    " actual_count=" +
                    std::to_string(sample.actual.size()));
            continue;
        }

        if (
            !std::isfinite(sample.tolerance.relative) ||
            !std::isfinite(sample.tolerance.absolute_scale) ||
            sample.tolerance.relative < 0.0 ||
            sample.tolerance.absolute_scale < 0.0) {
            add_failure(
                report.failures,
                "validation.invalid_tolerance",
                unevaluable_failure_text(
                    sample.quantity,
                    sample.position,
                    sample.tolerance,
                    "invalid_tolerance"));
            continue;
        }

        for (std::size_t component = 0; component < expected; ++component) {
            const double reference = sample.reference[component];
            const double actual = sample.actual[component];
            if (!std::isfinite(reference) || !std::isfinite(actual)) {
                add_failure(
                    report.failures,
                    "validation.nonfinite_comparison_value",
                    scalar_failure_text(
                        sample,
                        component,
                        std::numeric_limits<double>::infinity()));
                continue;
            }

            const double error = actual - reference;
            Accumulator& accumulator =
                accumulators[{sample.quantity, component}];
            accumulator.squared_error += error * error;
            accumulator.squared_reference += reference * reference;
            accumulator.squared_absolute_scale +=
                sample.tolerance.absolute_scale *
                sample.tolerance.absolute_scale;
            ++accumulator.value_count;
        }
    }

    for (const auto& [key, accumulator] : accumulators) {
        const auto [quantity, component] = key;
        const double error_l2 = std::sqrt(accumulator.squared_error);
        const double root_mean_square_error =
            error_l2 /
            std::sqrt(static_cast<double>(accumulator.value_count));
        const double reference_l2 =
            std::sqrt(accumulator.squared_reference);
        const double absolute_scale_l2 =
            std::sqrt(accumulator.squared_absolute_scale);
        const double denominator =
            std::max(reference_l2, absolute_scale_l2);
        const double relative_l2_error = denominator > 0.0
                                             ? error_l2 / denominator
                                             : error_l2 == 0.0
                                                   ? 0.0
                                                   : std::numeric_limits<
                                                         double>::infinity();

        report.metrics.push_back({
            quantity,
            component,
            accumulator.value_count,
            root_mean_square_error,
            relative_l2_error,
        });
        if (
            !std::isfinite(root_mean_square_error) ||
            !std::isfinite(relative_l2_error)) {
            add_failure(
                report.failures,
                "validation.nonfinite_correlation_metric",
                "quantity=" + std::string{quantity_name(quantity)} +
                    " component=" + component_name(quantity, component) +
                    " rmse=" + number_text(root_mean_square_error) +
                    " relative_l2=" + number_text(relative_l2_error));
        }
    }

    report.evaluable = report.failures.empty();
    return report;
}

}  // namespace fesa
