// join_tables — Pure multi-key hash join between two tables.
// Promoted from cpp/apps/primitives_gallery/playground/tables/ (issue 0081-E).
// No ImGui, no I/O.

#include "core/join_tables.h"

#include <string>
#include <unordered_map>
#include <vector>

namespace data_table {

namespace {

// Find column index by name in a header list. Returns -1 if not found.
static int find_col_idx(const std::vector<std::string>& hdrs, const std::string& name) {
    for (size_t i = 0; i < hdrs.size(); ++i)
        if (hdrs[i] == name) return (int)i;
    return -1;
}

// Build a composite key string from multiple columns in a row.
// Each key component is separated by \x1f (unit separator, unlikely in data).
// Missing or null columns produce an empty component followed by |\x1f to
// distinguish "missing column" from "empty value in a present column".
static std::string make_key(const char* const* cells, int row, int cols,
                             const std::vector<int>& key_cols) {
    std::string k;
    for (int c : key_cols) {
        if (c < 0 || c >= cols) { k += "\x1f|"; continue; }
        const char* s = cells[row * cols + c];
        k += (s ? s : "");
        k += "\x1f";
    }
    return k;
}

} // anon

StageOutput join_tables(const char* const* left_cells, int left_rows, int left_cols,
                        const std::vector<std::string>& left_headers,
                        const std::vector<ColumnType>&  left_types,
                        const TableInput& right,
                        const Join& jn)
{
    StageOutput out;

    // Resolve key column indices in left and right.
    std::vector<int> lk_idx, rk_idx;
    for (const auto& p : jn.on) {
        lk_idx.push_back(find_col_idx(left_headers, p.first));
        rk_idx.push_back(find_col_idx(right.headers, p.second));
    }

    // Determine which right columns to include in output.
    std::vector<int> right_fields;
    if (jn.fields.empty()) {
        for (int i = 0; i < right.cols; ++i) right_fields.push_back(i);
    } else {
        for (const auto& f : jn.fields) {
            int i = find_col_idx(right.headers, f);
            if (i >= 0) right_fields.push_back(i);
        }
    }

    // Build output headers + types: all left columns, then right fields (aliased).
    out.cols = left_cols + (int)right_fields.size();
    out.headers.reserve(out.cols);
    out.types.reserve(out.cols);
    for (int c = 0; c < left_cols; ++c) {
        out.headers.push_back(c < (int)left_headers.size() ? left_headers[c] : "");
        out.types.push_back(c < (int)left_types.size() ? left_types[c] : ColumnType::Auto);
    }
    for (int rc : right_fields) {
        std::string name = (rc < (int)right.headers.size()) ? right.headers[rc] : "";
        std::string prefixed = jn.alias.empty() ? name : (jn.alias + "." + name);
        out.headers.push_back(std::move(prefixed));
        out.types.push_back(rc < (int)right.types.size() ? right.types[rc] : ColumnType::Auto);
    }

    // Hash right rows by composite key.
    std::unordered_map<std::string, std::vector<int>> right_idx;
    right_idx.reserve(right.rows);
    for (int r = 0; r < right.rows; ++r) {
        right_idx[make_key(right.cells, r, right.cols, rk_idx)].push_back(r);
    }

    // Track which right rows matched (needed for right/full outer).
    std::vector<bool> right_matched(right.rows, false);

    // Pre-size backing storage to avoid reallocation invalidating pointers.
    out.cell_backing.reserve((size_t)(left_rows + right.rows) * (size_t)out.cols);

    // Helpers to append rows into cell_backing.
    auto append_left_row = [&](int lr) {
        for (int c = 0; c < left_cols; ++c) {
            const char* s = left_cells[lr * left_cols + c];
            out.cell_backing.emplace_back(s ? s : "");
        }
    };
    auto append_left_empty = [&]() {
        for (int c = 0; c < left_cols; ++c) out.cell_backing.emplace_back("");
    };
    auto append_right_row = [&](int rr) {
        for (int rc : right_fields) {
            const char* s = (right.cells && rr >= 0 && rr < right.rows && rc >= 0 && rc < right.cols)
                            ? right.cells[rr * right.cols + rc] : nullptr;
            out.cell_backing.emplace_back(s ? s : "");
        }
    };
    auto append_right_empty = [&]() {
        for (int rc : right_fields) { (void)rc; out.cell_backing.emplace_back(""); }
    };

    // Strategy flags.
    bool keep_unmatched_left  = (jn.strategy == JoinStrategy::Left  || jn.strategy == JoinStrategy::Full);
    bool keep_unmatched_right = (jn.strategy == JoinStrategy::Right || jn.strategy == JoinStrategy::Full);
    // For Right join we still iterate left to find matches; unmatched left rows are dropped.
    bool iterate_left = (jn.strategy != JoinStrategy::Right)
                        ? true
                        : true; // always iterate left to mark right_matched

    int row_count = 0;

    if (iterate_left) {
        for (int lr = 0; lr < left_rows; ++lr) {
            std::string k = make_key(left_cells, lr, left_cols, lk_idx);
            auto it = right_idx.find(k);
            if (it == right_idx.end() || it->second.empty()) {
                // No match on right side.
                if (keep_unmatched_left) {
                    append_left_row(lr);
                    append_right_empty();
                    ++row_count;
                }
                // For Right/Inner: skip this left row.
                continue;
            }
            // Matched: produce one output row per matching right row (Cartesian for duplicates).
            for (int rr : it->second) {
                // For Right join: emit only if we keep left+right matches.
                // Inner/Left/Full all emit matched rows.
                if (jn.strategy == JoinStrategy::Right) {
                    // Right join: left side appears for matched rows, left cells come first.
                    append_left_row(lr);
                } else {
                    append_left_row(lr);
                }
                append_right_row(rr);
                right_matched[rr] = true;
                ++row_count;
            }
        }
    }

    // Append unmatched right rows (Right join / Full outer).
    if (keep_unmatched_right) {
        for (int rr = 0; rr < right.rows; ++rr) {
            if (right_matched[rr]) continue;
            append_left_empty();
            append_right_row(rr);
            ++row_count;
        }
    }

    out.rows = row_count;

    // Build pointer list after all backing strings are stable.
    out.cells.reserve(out.cell_backing.size());
    for (auto& s : out.cell_backing) out.cells.push_back(s.c_str());
    return out;
}

} // namespace data_table
