#include <fesa/validation/reference_csv.hpp>

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

namespace fesa {
namespace {

constexpr std::string_view instance_column{"Part Instance Name"};
constexpr std::string_view node_column{"Node Label"};
constexpr std::string_view element_column{"Element Label"};

using PositionKey =
    std::tuple<std::string, std::int64_t, std::optional<std::int64_t>>;
using ColumnIndices =
    std::map<std::string, std::size_t, std::less<>>;

std::size_t column_index(
    const ColumnIndices& column_indices,
    const std::string_view name) {
    return column_indices.find(name)->second;
}

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

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

void remove_trailing_empty_fields(std::vector<std::string>& fields) {
    while (!fields.empty() && fields.back().empty()) {
        fields.pop_back();
    }
}

ReferenceCsvReadResult failure(
    const DiagnosticStage stage,
    std::string code,
    std::string message,
    const std::filesystem::path& path,
    const std::size_t line) {
    std::vector<Diagnostic> diagnostics;
    diagnostics.push_back({
        stage,
        Severity::error,
        std::move(code),
        std::move(message),
        SourceLocation{path, line, line == 0U ? 0U : 1U},
    });
    return {{}, std::move(diagnostics)};
}

ReferenceCsvReadResult validation_failure(
    std::string code,
    std::string message,
    const std::filesystem::path& path,
    const std::size_t line) {
    return failure(
        DiagnosticStage::validation,
        std::move(code),
        std::move(message),
        path,
        line);
}

bool is_valid_utf8(const std::string_view text) {
    std::size_t index = 0U;
    while (index < text.size()) {
        const auto first = static_cast<unsigned char>(text[index]);
        if (first <= 0x7FU) {
            ++index;
            continue;
        }

        std::size_t continuation_count = 0U;
        std::uint32_t code_point = 0U;
        std::uint32_t minimum = 0U;
        if (first >= 0xC2U && first <= 0xDFU) {
            continuation_count = 1U;
            code_point = first & 0x1FU;
            minimum = 0x80U;
        } else if (first >= 0xE0U && first <= 0xEFU) {
            continuation_count = 2U;
            code_point = first & 0x0FU;
            minimum = 0x800U;
        } else if (first >= 0xF0U && first <= 0xF4U) {
            continuation_count = 3U;
            code_point = first & 0x07U;
            minimum = 0x10000U;
        } else {
            return false;
        }
        if (index + continuation_count >= text.size()) {
            return false;
        }
        for (std::size_t offset = 1U; offset <= continuation_count; ++offset) {
            const auto continuation =
                static_cast<unsigned char>(text[index + offset]);
            if ((continuation & 0xC0U) != 0x80U) {
                return false;
            }
            code_point = (code_point << 6U) | (continuation & 0x3FU);
        }
        if (code_point < minimum || code_point > 0x10FFFFU ||
            (code_point >= 0xD800U && code_point <= 0xDFFFU)) {
            return false;
        }
        index += continuation_count + 1U;
    }
    return true;
}

std::size_t line_at_offset(
    const std::string_view text,
    const std::size_t offset) {
    return 1U + static_cast<std::size_t>(std::ranges::count(
                    text.substr(0U, offset), '\n'));
}

std::vector<std::string_view> required_columns(
    const ReferenceQuantity quantity) {
    switch (quantity) {
        case ReferenceQuantity::displacement:
            return {
                node_column,
                "U-U1",
                "U-U2",
                "U-U3",
                "UR-UR1",
                "UR-UR2",
                "UR-UR3",
            };
        case ReferenceQuantity::reaction:
            return {
                node_column,
                "RF-RF1",
                "RF-RF2",
                "RF-RF3",
                "RM-RM1",
                "RM-RM2",
                "RM-RM3",
            };
        case ReferenceQuantity::internal_force:
            return {
                element_column,
                node_column,
                "SF-SF1",
                "SF-SF2",
                "SF-SF3",
                "SM-SM1",
                "SM-SM2",
                "SM-SM3",
            };
        case ReferenceQuantity::centroid_stress:
            return {element_column, node_column, "Sxx"};
    }
    return {};
}

std::vector<std::string_view> value_columns(
    const ReferenceQuantity quantity) {
    switch (quantity) {
        case ReferenceQuantity::displacement:
            return {"U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"};
        case ReferenceQuantity::reaction:
            return {"RF-RF1", "RF-RF2", "RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3"};
        case ReferenceQuantity::internal_force:
            return {"SF-SF1", "SF-SF3", "SF-SF2", "SM-SM3", "SM-SM1", "SM-SM2"};
        case ReferenceQuantity::centroid_stress:
            return {"Sxx"};
    }
    return {};
}

bool parse_positive_label(
    std::string_view text,
    std::int64_t& value) {
    if (text.starts_with('+')) {
        text.remove_prefix(1U);
    }
    const auto parsed = std::from_chars(
        text.data(), text.data() + text.size(), value);
    return !text.empty() && parsed.ec == std::errc{} &&
           parsed.ptr == text.data() + text.size() && value > 0;
}

bool parse_finite_double(std::string_view text, double& value) {
    if (text.starts_with('+')) {
        text.remove_prefix(1U);
    }
    const auto parsed = std::from_chars(
        text.data(),
        text.data() + text.size(),
        value,
        std::chars_format::general);
    return !text.empty() && parsed.ec == std::errc{} &&
           parsed.ptr == text.data() + text.size() &&
           std::isfinite(value);
}

}  // namespace

ReferenceCsvReadResult read_reference_csv(
    const ReferenceQuantity quantity,
    const std::filesystem::path& path,
    const std::string_view single_instance_name) {
    std::ifstream file{path, std::ios::binary};
    if (!file) {
        return failure(
            DiagnosticStage::io,
            "validation.reference_csv_open_failed",
            "Unable to open reference CSV file.",
            path,
            0U);
    }

    std::string bytes{
        std::istreambuf_iterator<char>{file},
        std::istreambuf_iterator<char>{},
    };
    if (file.bad()) {
        return failure(
            DiagnosticStage::io,
            "validation.reference_csv_read_failed",
            "Failed while reading reference CSV file.",
            path,
            0U);
    }

    constexpr std::string_view bom{"\xEF\xBB\xBF"};
    if (bytes.starts_with(bom)) {
        bytes.erase(0U, bom.size());
    }
    const std::size_t misplaced_bom = bytes.find(bom);
    if (misplaced_bom != std::string::npos || !is_valid_utf8(bytes)) {
        return validation_failure(
            "validation.reference_csv_invalid_encoding",
            "Reference CSV must be UTF-8 with an optional BOM only at the file start.",
            path,
            misplaced_bom == std::string::npos
                ? 1U
                : line_at_offset(bytes, misplaced_bom));
    }

    std::istringstream input{bytes};
    std::string line;
    if (!std::getline(input, line)) {
        return validation_failure(
            "validation.reference_csv_missing_header",
            "Reference CSV is missing its header row.",
            path,
            1U);
    }

    std::vector<std::string> headers = split_fields(line);
    remove_trailing_empty_fields(headers);
    ColumnIndices column_indices;
    for (std::size_t index = 0U; index < headers.size(); ++index) {
        if (headers[index].empty() ||
            !column_indices.emplace(headers[index], index).second) {
            return validation_failure(
                "validation.reference_csv_duplicate_column",
                "Reference CSV contains an empty or duplicate column name.",
                path,
                1U);
        }
    }

    const std::vector<std::string_view> required = required_columns(quantity);
    for (const std::string_view name : required) {
        if (!column_indices.contains(name)) {
            return validation_failure(
                "validation.reference_csv_missing_column",
                "Reference CSV is missing required column '" +
                    std::string{name} + "'.",
                path,
                1U);
        }
    }
    const bool has_instance = column_indices.contains(instance_column);
    if (!has_instance && single_instance_name.empty()) {
        return validation_failure(
            "validation.reference_csv_missing_column",
            "Reference CSV omits 'Part Instance Name' without a single-Instance name.",
            path,
            1U);
    }

    std::set<std::string, std::less<>> allowed_columns;
    allowed_columns.emplace(instance_column);
    for (const std::string_view name : required) {
        allowed_columns.emplace(name);
    }
    for (const std::string& header : headers) {
        if (!allowed_columns.contains(header)) {
            return validation_failure(
                "validation.reference_csv_unsupported_column",
                "Reference CSV contains unsupported column '" + header + "'.",
                path,
                1U);
        }
    }

    const std::vector<std::string_view> components = value_columns(quantity);
    std::vector<ReferenceRow> rows;
    std::set<PositionKey> positions;
    std::size_t line_number = 1U;
    while (std::getline(input, line)) {
        ++line_number;
        if (trim(line).empty()) {
            continue;
        }
        std::vector<std::string> fields = split_fields(line);
        remove_trailing_empty_fields(fields);
        if (fields.size() != headers.size()) {
            return validation_failure(
                "validation.reference_csv_invalid_row",
                "Reference CSV row field count does not match the header.",
                path,
                line_number);
        }

        std::string instance_name = has_instance
                                        ? fields[column_index(
                                              column_indices,
                                              instance_column)]
                                        : std::string{single_instance_name};
        if (instance_name.empty()) {
            return validation_failure(
                "validation.reference_csv_invalid_row",
                "Reference CSV row has an empty Instance name.",
                path,
                line_number);
        }

        std::int64_t entity_label = 0;
        const std::string_view entity_column =
            quantity == ReferenceQuantity::displacement ||
                    quantity == ReferenceQuantity::reaction
                ? node_column
                : element_column;
        if (!parse_positive_label(
                fields[column_index(column_indices, entity_column)],
                entity_label)) {
            return validation_failure(
                "validation.reference_csv_invalid_number",
                "Reference CSV entity label must be a positive integer.",
                path,
                line_number);
        }

        std::optional<std::int64_t> end_node_label;
        if (quantity == ReferenceQuantity::internal_force ||
            quantity == ReferenceQuantity::centroid_stress) {
            std::int64_t parsed_end_node = 0;
            if (!parse_positive_label(
                    fields[column_index(column_indices, node_column)],
                    parsed_end_node)) {
                return validation_failure(
                    "validation.reference_csv_invalid_number",
                    "Reference CSV end-node label must be a positive integer.",
                    path,
                    line_number);
            }
            end_node_label = parsed_end_node;
        }

        std::vector<double> values;
        values.reserve(components.size());
        for (const std::string_view component : components) {
            double value = 0.0;
            if (!parse_finite_double(
                    fields[column_index(column_indices, component)],
                    value)) {
                return validation_failure(
                    "validation.reference_csv_invalid_number",
                    "Reference CSV component '" + std::string{component} +
                        "' must be a finite number.",
                    path,
                    line_number);
            }
            values.push_back(value);
        }

        PositionKey key{instance_name, entity_label, end_node_label};
        if (!positions.insert(key).second) {
            return validation_failure(
                "validation.reference_csv_duplicate_row",
                "Reference CSV contains a duplicate result position.",
                path,
                line_number);
        }
        rows.push_back({
            quantity,
            {std::move(instance_name), entity_label, end_node_label},
            std::move(values),
        });
    }
    if (input.bad()) {
        return failure(
            DiagnosticStage::io,
            "validation.reference_csv_read_failed",
            "Failed while reading reference CSV file.",
            path,
            line_number);
    }
    if (rows.empty()) {
        return validation_failure(
            "validation.reference_csv_missing_rows",
            "Reference CSV contains no result rows.",
            path,
            line_number);
    }
    return {std::move(rows), {}};
}

}  // namespace fesa
