#include "formula_profile.h"

#include <algorithm>
#include <cerrno>
#include <chrono>
#include <cctype>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <limits>
#include <numeric>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>

namespace lymphosat {
namespace {

ClauseLengthBucket bucket_for_len(std::uint64_t len) {
    if (len == 0) return kLen0;
    if (len == 1) return kLen1;
    if (len == 2) return kLen2;
    if (len == 3) return kLen3;
    if (len == 4) return kLen4;
    if (len == 5) return kLen5;
    if (len == 6) return kLen6;
    if (len == 7) return kLen7;
    if (len == 8) return kLen8;
    if (len <= 16) return kLen9To16;
    if (len <= 32) return kLen17To32;
    if (len <= 64) return kLen33To64;
    return kLen65Plus;
}

double ratio(std::uint64_t n, std::uint64_t d) {
    return d == 0 ? 0.0 : static_cast<double>(n) / static_cast<double>(d);
}

std::uint64_t abs_lit(int lit) {
    return lit < 0 ? static_cast<std::uint64_t>(-static_cast<long long>(lit))
                   : static_cast<std::uint64_t>(lit);
}

std::uint64_t binary_key(std::uint64_t a, std::uint64_t b) {
    if (a > b) std::swap(a, b);
    return (a << 32U) ^ b;
}

std::string json_escape(std::string_view s) {
    std::ostringstream out;
    for (char ch : s) {
        switch (ch) {
        case '\\': out << "\\\\"; break;
        case '"': out << "\\\""; break;
        case '\n': out << "\\n"; break;
        case '\r': out << "\\r"; break;
        case '\t': out << "\\t"; break;
        default:
            if (static_cast<unsigned char>(ch) < 0x20) {
                out << "\\u" << std::hex << std::setw(4) << std::setfill('0')
                    << static_cast<int>(static_cast<unsigned char>(ch));
            } else {
                out << ch;
            }
        }
    }
    return out.str();
}

void ensure_var(std::vector<std::uint32_t>& pos,
                std::vector<std::uint32_t>& neg,
                std::vector<std::uint32_t>& binary_degree,
                std::uint64_t var) {
    if (var >= pos.size()) {
        const std::size_t next = static_cast<std::size_t>(var + 1);
        pos.resize(next);
        neg.resize(next);
        binary_degree.resize(next);
    }
}

void parse_clause_shape(const std::vector<int>& clause,
                        bool* tautological,
                        bool* repeated_literal) {
    *tautological = false;
    *repeated_literal = false;
    if (clause.size() > 64) {
        return;
    }

    std::vector<int> lits = clause;
    std::sort(lits.begin(), lits.end());
    for (std::size_t i = 1; i < lits.size(); ++i) {
        if (lits[i] == lits[i - 1]) {
            *repeated_literal = true;
            break;
        }
    }

    std::vector<std::pair<std::uint64_t, int>> by_abs;
    by_abs.reserve(clause.size());
    for (int lit : clause) {
        by_abs.push_back({abs_lit(lit), lit > 0 ? 1 : -1});
    }
    std::sort(by_abs.begin(), by_abs.end());
    for (std::size_t i = 1; i < by_abs.size(); ++i) {
        if (by_abs[i].first == by_abs[i - 1].first &&
            by_abs[i].second != by_abs[i - 1].second) {
            *tautological = true;
            return;
        }
    }
}

std::uint64_t env_u64(const char* name, std::uint64_t fallback) {
    const char* raw = std::getenv(name);
    if (raw == nullptr || *raw == '\0') {
        return fallback;
    }
    char* end = nullptr;
    errno = 0;
    const unsigned long long value = std::strtoull(raw, &end, 10);
    if (errno != 0 || end == raw || *end != '\0') {
        return fallback;
    }
    return static_cast<std::uint64_t>(value);
}

std::size_t skip_ws(const std::string& json, std::size_t pos) {
    while (pos < json.size() && std::isspace(static_cast<unsigned char>(json[pos]))) {
        ++pos;
    }
    return pos;
}

std::size_t skip_json_string(const std::string& json, std::size_t pos) {
    if (pos >= json.size() || json[pos] != '"') return pos;
    ++pos;
    while (pos < json.size()) {
        if (json[pos] == '\\') {
            pos += 2;
            continue;
        }
        if (json[pos] == '"') {
            return pos + 1;
        }
        ++pos;
    }
    return json.size();
}

std::size_t skip_json_value(const std::string& json, std::size_t pos) {
    pos = skip_ws(json, pos);
    if (pos >= json.size()) return pos;
    if (json[pos] == '"') return skip_json_string(json, pos);

    if (json[pos] == '{' || json[pos] == '[') {
        std::vector<char> closers;
        for (std::size_t cur = pos; cur < json.size(); ++cur) {
            if (json[cur] == '"') {
                cur = skip_json_string(json, cur);
                if (cur == 0) return json.size();
                --cur;
                continue;
            }
            if (json[cur] == '{') {
                closers.push_back('}');
            } else if (json[cur] == '[') {
                closers.push_back(']');
            } else if (!closers.empty() && json[cur] == closers.back()) {
                closers.pop_back();
                if (closers.empty()) return cur + 1;
            }
        }
        return json.size();
    }

    std::size_t cur = pos;
    while (cur < json.size() && json[cur] != ',' && json[cur] != '}' && json[cur] != ']') {
        ++cur;
    }
    return cur;
}

using JsonSpan = std::pair<std::size_t, std::size_t>;

std::optional<JsonSpan> top_level_object_span(const std::string& json) {
    const std::size_t open = skip_ws(json, 0);
    if (open >= json.size() || json[open] != '{') return std::nullopt;
    const std::size_t close = skip_json_value(json, open);
    if (close <= open || close > json.size()) return std::nullopt;
    return JsonSpan{open, close};
}

std::optional<JsonSpan> find_member_value(
    const std::string& json,
    std::size_t object_open,
    std::size_t object_close,
    std::string_view key) {
    if (object_open >= object_close || json[object_open] != '{') return std::nullopt;

    std::size_t cur = object_open + 1;
    while (cur < object_close) {
        cur = skip_ws(json, cur);
        if (cur >= object_close || json[cur] == '}') return std::nullopt;
        if (json[cur] == ',') {
            ++cur;
            continue;
        }
        if (json[cur] != '"') return std::nullopt;

        const std::size_t key_start = cur + 1;
        const std::size_t key_end_after_quote = skip_json_string(json, cur);
        if (key_end_after_quote <= key_start || key_end_after_quote > object_close) {
            return std::nullopt;
        }
        const std::size_t key_end = key_end_after_quote - 1;
        const bool key_matches =
            key_end >= key_start &&
            key.size() == key_end - key_start &&
            json.compare(key_start, key.size(), key.data(), key.size()) == 0;

        cur = skip_ws(json, key_end_after_quote);
        if (cur >= object_close || json[cur] != ':') return std::nullopt;
        cur = skip_ws(json, cur + 1);
        const std::size_t value_start = cur;
        const std::size_t value_end = skip_json_value(json, value_start);
        if (value_end < value_start || value_end > object_close) return std::nullopt;
        if (key_matches) {
            return JsonSpan{value_start, value_end};
        }
        cur = value_end;
    }
    return std::nullopt;
}

std::optional<JsonSpan> find_top_level_value(const std::string& json, std::string_view key) {
    const auto top = top_level_object_span(json);
    if (!top) return std::nullopt;
    return find_member_value(json, top->first, top->second, key);
}

std::optional<double> parse_number_value(const std::string& json, JsonSpan span) {
    const std::size_t begin_pos = skip_ws(json, span.first);
    if (begin_pos >= span.second) return std::nullopt;
    const char* begin = json.c_str() + begin_pos;
    char* end = nullptr;
    errno = 0;
    const double value = std::strtod(begin, &end);
    if (errno != 0 || end == begin || end > json.c_str() + span.second) {
        return std::nullopt;
    }
    return value;
}

std::optional<double> find_number(const std::string& json, std::string_view key) {
    if (auto span = find_top_level_value(json, key)) {
        if (auto value = parse_number_value(json, *span)) return value;
    }
    return std::nullopt;
}

std::optional<bool> parse_bool_value(const std::string& json, JsonSpan span) {
    const std::size_t cur = skip_ws(json, span.first);
    if (cur >= span.second) return std::nullopt;
    if (json.compare(cur, 4, "true") == 0) return true;
    if (json.compare(cur, 5, "false") == 0) return false;
    return std::nullopt;
}

std::optional<bool> find_bool(const std::string& json, std::string_view key) {
    if (auto span = find_top_level_value(json, key)) {
        if (auto value = parse_bool_value(json, *span)) return value;
    }
    return std::nullopt;
}

std::optional<std::string> parse_string_value(const std::string& json, JsonSpan span) {
    std::size_t cur = skip_ws(json, span.first);
    if (cur >= span.second || json[cur] != '"') return std::nullopt;
    ++cur;

    std::string out;
    while (cur < span.second) {
        const char ch = json[cur++];
        if (ch == '"') return out;
        if (ch != '\\') {
            out.push_back(ch);
            continue;
        }
        if (cur >= span.second) return std::nullopt;
        const char escaped = json[cur++];
        switch (escaped) {
        case '"': out.push_back('"'); break;
        case '\\': out.push_back('\\'); break;
        case '/': out.push_back('/'); break;
        case 'b': out.push_back('\b'); break;
        case 'f': out.push_back('\f'); break;
        case 'n': out.push_back('\n'); break;
        case 'r': out.push_back('\r'); break;
        case 't': out.push_back('\t'); break;
        case 'u':
            // Profile strings are diagnostic/path text; keep unsupported
            // unicode escapes lossless enough for display instead of failing.
            out += "\\u";
            for (int i = 0; i < 4 && cur < span.second; ++i) {
                out.push_back(json[cur++]);
            }
            break;
        default:
            out.push_back(escaped);
            break;
        }
    }
    return std::nullopt;
}

template <std::size_t N>
void parse_number_array(const std::string& json, std::string_view key,
                        std::array<double, N>& out) {
    const auto span = find_top_level_value(json, key);
    if (!span) return;
    const std::size_t open = skip_ws(json, span->first);
    if (open >= span->second || json[open] != '[') return;
    const char* cur = json.c_str() + open + 1;
    const char* limit = json.c_str() + span->second;
    for (std::size_t i = 0; i < N; ++i) {
        while (cur < limit && (std::isspace(static_cast<unsigned char>(*cur)) || *cur == ',')) {
            ++cur;
        }
        if (cur >= limit || *cur == ']') return;
        char* end = nullptr;
        out[i] = std::strtod(cur, &end);
        if (end == cur || end > limit) return;
        cur = end;
    }
}

template <std::size_t N>
void parse_uint_array(const std::string& json, std::string_view key,
                      std::array<std::uint64_t, N>& out) {
    std::array<double, N> temp{};
    parse_number_array(json, key, temp);
    for (std::size_t i = 0; i < N; ++i) {
        out[i] = temp[i] < 0.0 ? 0 : static_cast<std::uint64_t>(temp[i]);
    }
}

void set_from_json(std::uint64_t& field, const std::string& json, std::string_view key) {
    if (auto v = find_number(json, key)) {
        field = *v < 0.0 ? 0 : static_cast<std::uint64_t>(*v);
    }
}

void set_from_json(double& field, const std::string& json, std::string_view key) {
    if (auto v = find_number(json, key)) {
        field = *v;
    }
}

void set_from_json(bool& field, const std::string& json, std::string_view key) {
    if (auto v = find_bool(json, key)) {
        field = *v;
    }
}

void set_from_json(std::string& field, const std::string& json, std::string_view key) {
    if (auto span = find_top_level_value(json, key)) {
        if (auto v = parse_string_value(json, *span)) {
            field = *v;
        }
    }
}

}  // namespace

FormulaProfile profile_formula(const std::string& path, const ProfileOptions& requested_options) {
    ProfileOptions options = requested_options;
    options.max_clauses = env_u64("LYMPHOSAT_PROFILE_MAX_CLAUSES", options.max_clauses);
    options.max_binary_pairs = env_u64("LYMPHOSAT_PROFILE_MAX_BINARY_PAIRS", options.max_binary_pairs);

    FormulaProfile p;
    p.source_path = path;
    p.profile_clauses_limit = options.max_clauses;

    const auto start = std::chrono::steady_clock::now();

    std::ifstream in(path);
    if (!in) {
        p.parse_error = true;
        p.parse_error_message = "could not open CNF";
        return p;
    }

    std::vector<std::uint32_t> pos_occ(1), neg_occ(1), binary_degree(1);
    std::vector<int> clause;
    std::array<std::uint64_t, 65> exact_len_counts{};
    std::array<std::uint64_t, 65> positive_block_len_counts{};
    std::vector<std::vector<std::vector<std::uint64_t>>> positive_block_samples(65);
    std::unordered_set<std::uint64_t> binary_pairs;
    std::unordered_set<std::uint64_t> negative_binary_pairs;
    std::unordered_set<std::uint64_t> positive_units;
    std::unordered_set<std::uint64_t> negative_units;

    double span_sum = 0.0;
    std::uint64_t span_count = 0;
    std::uint64_t small_span_count = 0;

    auto finish_clause = [&](const std::vector<int>& c) {
        if (p.clauses_seen >= options.max_clauses) {
            p.truncated = true;
            return;
        }

        const std::uint64_t len = c.size();
        p.clauses_seen += 1;
        p.num_literals += len;
        p.max_clause_len = std::max(p.max_clause_len, len);
        p.min_clause_len = p.min_clause_len == 0 ? len : std::min(p.min_clause_len, len);
        p.clause_len_counts[bucket_for_len(len)] += 1;
        if (len < exact_len_counts.size()) {
            exact_len_counts[len] += 1;
        }
        if (len == 0) {
            p.empty_clause_present = true;
            return;
        }

        std::uint64_t positives = 0;
        std::uint64_t negatives = 0;
        std::uint64_t min_var = std::numeric_limits<std::uint64_t>::max();
        std::uint64_t max_var = 0;
        std::vector<std::uint64_t> positive_clause_vars;
        if (len <= 64) {
            positive_clause_vars.reserve(static_cast<std::size_t>(len));
        }

        for (int lit : c) {
            const std::uint64_t var = abs_lit(lit);
            p.max_var_seen = std::max(p.max_var_seen, var);
            ensure_var(pos_occ, neg_occ, binary_degree, var);
            min_var = std::min(min_var, var);
            max_var = std::max(max_var, var);
            if (lit > 0) {
                positives += 1;
                p.positive_literals += 1;
                pos_occ[var] += 1;
                if (len <= 64) positive_clause_vars.push_back(var);
            } else {
                negatives += 1;
                p.negative_literals += 1;
                neg_occ[var] += 1;
            }
        }

        if (max_var >= min_var) {
            const std::uint64_t span = max_var - min_var;
            span_sum += static_cast<double>(span);
            span_count += 1;
            const std::uint64_t small_threshold = std::max<std::uint64_t>(16, len * 4);
            if (span <= small_threshold) {
                small_span_count += 1;
            }
        }

        if (positives == len) {
            p.all_positive_clauses += 1;
            if (len >= 2 && len < positive_block_len_counts.size()) {
                positive_block_len_counts[len] += 1;
                auto& samples = positive_block_samples[len];
                if (samples.size() < options.max_positive_block_samples_per_len) {
                    std::sort(positive_clause_vars.begin(), positive_clause_vars.end());
                    positive_clause_vars.erase(
                        std::unique(positive_clause_vars.begin(), positive_clause_vars.end()),
                        positive_clause_vars.end());
                    if (positive_clause_vars.size() == len) {
                        samples.push_back(std::move(positive_clause_vars));
                    }
                }
            }
        } else if (negatives == len) {
            p.all_negative_clauses += 1;
        } else {
            p.mixed_clauses += 1;
        }

        if (positives <= 1) p.horn_clauses += 1;
        if (negatives <= 1) p.dual_horn_clauses += 1;
        if (positives == 1) p.definite_horn_clauses += 1;

        if (len == 1) {
            p.unit_clauses += 1;
            const std::uint64_t var = abs_lit(c[0]);
            if (c[0] > 0) {
                positive_units.insert(var);
                if (negative_units.find(var) != negative_units.end()) {
                    p.conflicting_units_present = true;
                }
            } else {
                negative_units.insert(var);
                if (positive_units.find(var) != positive_units.end()) {
                    p.conflicting_units_present = true;
                }
            }
        } else if (len == 2) {
            p.binary_clauses += 1;
            const std::uint64_t a = abs_lit(c[0]);
            const std::uint64_t b = abs_lit(c[1]);
            ensure_var(pos_occ, neg_occ, binary_degree, a);
            ensure_var(pos_occ, neg_occ, binary_degree, b);
            binary_degree[a] += 1;
            binary_degree[b] += 1;
            if (binary_pairs.size() < options.max_binary_pairs) {
                binary_pairs.insert(binary_key(a, b));
            } else {
                p.binary_pair_sketch_truncated = true;
            }
            if (positives == 0) {
                p.negative_binary_clauses += 1;
                if (negative_binary_pairs.size() < options.max_binary_pairs) {
                    negative_binary_pairs.insert(binary_key(a, b));
                }
            } else if (negatives == 0) {
                p.positive_binary_clauses += 1;
            } else {
                p.mixed_binary_clauses += 1;
            }
        } else if (len == 3) {
            p.ternary_clauses += 1;
        }

        bool tautological = false;
        bool repeated = false;
        parse_clause_shape(c, &tautological, &repeated);
        if (tautological) p.tautological_clauses += 1;
        if (repeated) p.repeated_literal_clauses += 1;
    };

    std::string line;
    while (std::getline(in, line)) {
        if (p.truncated) break;
        const char* cur = line.c_str();
        while (*cur != '\0' && std::isspace(static_cast<unsigned char>(*cur))) ++cur;
        if (*cur == '\0' || *cur == 'c') {
            continue;
        }
        if (*cur == 'p') {
            std::istringstream header(line);
            std::string p_word;
            std::string cnf_word;
            header >> p_word >> cnf_word >> p.num_vars >> p.header_clauses;
            if (cnf_word != "cnf") {
                p.parse_error = true;
                p.parse_error_message = "unsupported DIMACS header";
                break;
            }
            pos_occ.resize(static_cast<std::size_t>(p.num_vars + 1));
            neg_occ.resize(static_cast<std::size_t>(p.num_vars + 1));
            binary_degree.resize(static_cast<std::size_t>(p.num_vars + 1));
            continue;
        }

        std::istringstream literals(line);
        long long lit = 0;
        while (literals >> lit) {
            if (lit == 0) {
                finish_clause(clause);
                clause.clear();
                if (p.truncated) break;
            } else if (lit >= std::numeric_limits<int>::min() &&
                       lit <= std::numeric_limits<int>::max()) {
                clause.push_back(static_cast<int>(lit));
            } else {
                p.parse_error = true;
                p.parse_error_message = "literal outside supported int range";
                break;
            }
        }
    }
    if (!clause.empty() && !p.truncated && !p.parse_error) {
        finish_clause(clause);
    }

    p.distinct_clause_len_count = std::count_if(
        exact_len_counts.begin(), exact_len_counts.end(),
        [](std::uint64_t count) { return count != 0; });

    std::uint64_t dominant_count = 0;
    for (std::size_t len = 0; len < exact_len_counts.size(); ++len) {
        if (exact_len_counts[len] > dominant_count) {
            dominant_count = exact_len_counts[len];
            p.dominant_clause_len = len;
        }
    }

    p.avg_clause_len = ratio(p.num_literals, p.clauses_seen);
    p.clauses_per_var = ratio(p.clauses_seen, p.num_vars);
    p.literals_per_var = ratio(p.num_literals, p.num_vars);
    p.positive_literal_fraction = ratio(p.positive_literals, p.num_literals);
    p.negative_literal_fraction = ratio(p.negative_literals, p.num_literals);
    p.mean_positive_literals_per_clause = ratio(p.positive_literals, p.clauses_seen);
    p.mean_negative_literals_per_clause = ratio(p.negative_literals, p.clauses_seen);
    p.dominant_clause_len_fraction = ratio(dominant_count, p.clauses_seen);

    for (std::size_t i = 0; i < kClauseLengthBucketCount; ++i) {
        p.clause_len_fracs[i] = ratio(p.clause_len_counts[i], p.clauses_seen);
    }

    p.all_positive_clause_fraction = ratio(p.all_positive_clauses, p.clauses_seen);
    p.all_negative_clause_fraction = ratio(p.all_negative_clauses, p.clauses_seen);
    p.mixed_clause_fraction = ratio(p.mixed_clauses, p.clauses_seen);
    p.horn_clause_fraction = ratio(p.horn_clauses, p.clauses_seen);
    p.dual_horn_clause_fraction = ratio(p.dual_horn_clauses, p.clauses_seen);
    p.definite_horn_clause_fraction = ratio(p.definite_horn_clauses, p.clauses_seen);
    p.unit_clause_fraction = ratio(p.unit_clauses, p.clauses_seen);
    p.binary_clause_fraction = ratio(p.binary_clauses, p.clauses_seen);
    p.ternary_clause_fraction = ratio(p.ternary_clauses, p.clauses_seen);
    p.negative_binary_fraction = ratio(p.negative_binary_clauses, p.binary_clauses);
    p.positive_binary_fraction = ratio(p.positive_binary_clauses, p.binary_clauses);
    p.mixed_binary_fraction = ratio(p.mixed_binary_clauses, p.binary_clauses);
    p.unique_binary_pairs_sampled = binary_pairs.size();
    p.binary_duplicate_estimate = p.binary_clauses == 0 ? 0.0 :
        1.0 - ratio(p.unique_binary_pairs_sampled, p.binary_clauses);
    p.tautological_clause_fraction = ratio(p.tautological_clauses, p.clauses_seen);
    p.repeated_literal_clause_fraction = ratio(p.repeated_literal_clauses, p.clauses_seen);

    std::uint64_t occurrence_sum = 0;
    std::uint64_t top1 = 0;
    std::vector<std::uint64_t> occurrences;
    occurrences.reserve(pos_occ.size());
    double polarity_sum = 0.0;

    const std::size_t var_limit = std::max(pos_occ.size(), neg_occ.size());
    for (std::size_t var = 1; var < var_limit; ++var) {
        const std::uint64_t pos = var < pos_occ.size() ? pos_occ[var] : 0;
        const std::uint64_t neg = var < neg_occ.size() ? neg_occ[var] : 0;
        const std::uint64_t total = pos + neg;
        if (total == 0) continue;
        p.used_var_count += 1;
        occurrence_sum += total;
        top1 = std::max(top1, total);
        occurrences.push_back(total);
        if (total == 1) p.singleton_vars += 1;
        if (pos == 0 || neg == 0) p.pure_vars += 1;
        if (pos > 0 && neg == 0) p.positive_only_vars += 1;
        if (neg > 0 && pos == 0) p.negative_only_vars += 1;
        polarity_sum += static_cast<double>(
            std::llabs(static_cast<long long>(pos) - static_cast<long long>(neg))) /
            static_cast<double>(total);
        if (var < binary_degree.size() && binary_degree[var] > 0) {
            p.binary_vars_touched += 1;
            p.binary_graph_degree_max = std::max<std::uint64_t>(
                p.binary_graph_degree_max, binary_degree[var]);
        }
    }

    p.unused_var_count = p.num_vars > p.used_var_count ? p.num_vars - p.used_var_count : 0;
    p.used_var_fraction = ratio(p.used_var_count, p.num_vars);
    p.occurrence_mean = ratio(occurrence_sum, p.used_var_count);
    p.occurrence_max = top1;
    p.singleton_var_fraction = ratio(p.singleton_vars, p.used_var_count);
    p.pure_var_fraction = ratio(p.pure_vars, p.used_var_count);
    p.positive_only_var_fraction = ratio(p.positive_only_vars, p.used_var_count);
    p.negative_only_var_fraction = ratio(p.negative_only_vars, p.used_var_count);
    p.polarity_balance_mean = p.used_var_count == 0 ? 0.0 :
        polarity_sum / static_cast<double>(p.used_var_count);
    p.binary_vars_touched_fraction = ratio(p.binary_vars_touched, p.used_var_count);
    p.binary_graph_degree_mean = p.binary_vars_touched == 0 ? 0.0 :
        (2.0 * static_cast<double>(p.binary_clauses)) /
        static_cast<double>(p.binary_vars_touched);

    double variance = 0.0;
    for (std::uint64_t total : occurrences) {
        const double delta = static_cast<double>(total) - p.occurrence_mean;
        variance += delta * delta;
    }
    p.occurrence_stddev = occurrences.empty() ? 0.0 :
        std::sqrt(variance / static_cast<double>(occurrences.size()));

    std::sort(occurrences.begin(), occurrences.end(), std::greater<std::uint64_t>());
    const std::size_t top5_count = std::max<std::size_t>(
        1, static_cast<std::size_t>(std::ceil(occurrences.size() * 0.05)));
    const std::uint64_t top5_sum = std::accumulate(
        occurrences.begin(),
        occurrences.begin() + std::min(top5_count, occurrences.size()),
        std::uint64_t{0});
    p.top1_occurrence_mass = ratio(top1, occurrence_sum);
    p.top5_occurrence_mass = ratio(top5_sum, occurrence_sum);

    p.clause_span_mean = span_count == 0 ? 0.0 : span_sum / static_cast<double>(span_count);
    p.normalized_clause_span_mean = p.num_vars == 0 ? 0.0 :
        p.clause_span_mean / static_cast<double>(p.num_vars);
    p.small_span_clause_fraction = ratio(small_span_count, span_count);

    std::uint64_t best_block_count = 0;
    for (std::size_t len = 2; len < positive_block_len_counts.size(); ++len) {
        if (positive_block_len_counts[len] > best_block_count) {
            best_block_count = positive_block_len_counts[len];
            p.positive_block_candidate_len = len;
        }
    }
    p.positive_block_candidate_count = best_block_count;
    p.positive_block_clause_fraction = ratio(best_block_count, p.clauses_seen);

    if (p.positive_block_candidate_len >= 2 &&
        p.positive_block_candidate_len < positive_block_samples.size()) {
        std::uint64_t checked_pairs = 0;
        std::uint64_t found_pairs = 0;
        for (const auto& block : positive_block_samples[p.positive_block_candidate_len]) {
            for (std::size_t i = 0; i < block.size(); ++i) {
                for (std::size_t j = i + 1; j < block.size(); ++j) {
                    checked_pairs += 1;
                    if (negative_binary_pairs.find(binary_key(block[i], block[j])) !=
                        negative_binary_pairs.end()) {
                        found_pairs += 1;
                    }
                }
            }
        }
        p.exactly_one_intra_pair_coverage = ratio(found_pairs, checked_pairs);
    }

    const auto end = std::chrono::steady_clock::now();
    p.parse_seconds = std::chrono::duration<double>(end - start).count();
    return p;
}

std::vector<FeatureValue> profile_features(const FormulaProfile& p) {
    std::vector<FeatureValue> out = {
        {"num_vars", static_cast<double>(p.num_vars)},
        {"header_clauses", static_cast<double>(p.header_clauses)},
        {"clauses_seen", static_cast<double>(p.clauses_seen)},
        {"num_literals", static_cast<double>(p.num_literals)},
        {"max_var_seen", static_cast<double>(p.max_var_seen)},
        {"used_var_count", static_cast<double>(p.used_var_count)},
        {"unused_var_count", static_cast<double>(p.unused_var_count)},
        {"max_clause_len", static_cast<double>(p.max_clause_len)},
        {"min_clause_len", static_cast<double>(p.min_clause_len)},
        {"avg_clause_len", p.avg_clause_len},
        {"clauses_per_var", p.clauses_per_var},
        {"literals_per_var", p.literals_per_var},
        {"used_var_fraction", p.used_var_fraction},
        {"distinct_clause_len_count", static_cast<double>(p.distinct_clause_len_count)},
        {"dominant_clause_len", static_cast<double>(p.dominant_clause_len)},
        {"dominant_clause_len_fraction", p.dominant_clause_len_fraction},
        {"positive_literals", static_cast<double>(p.positive_literals)},
        {"negative_literals", static_cast<double>(p.negative_literals)},
        {"positive_literal_fraction", p.positive_literal_fraction},
        {"negative_literal_fraction", p.negative_literal_fraction},
        {"mean_positive_literals_per_clause", p.mean_positive_literals_per_clause},
        {"mean_negative_literals_per_clause", p.mean_negative_literals_per_clause},
        {"all_positive_clauses", static_cast<double>(p.all_positive_clauses)},
        {"all_negative_clauses", static_cast<double>(p.all_negative_clauses)},
        {"mixed_clauses", static_cast<double>(p.mixed_clauses)},
        {"all_positive_clause_fraction", p.all_positive_clause_fraction},
        {"all_negative_clause_fraction", p.all_negative_clause_fraction},
        {"mixed_clause_fraction", p.mixed_clause_fraction},
        {"horn_clause_fraction", p.horn_clause_fraction},
        {"dual_horn_clause_fraction", p.dual_horn_clause_fraction},
        {"definite_horn_clause_fraction", p.definite_horn_clause_fraction},
        {"unit_clauses", static_cast<double>(p.unit_clauses)},
        {"binary_clauses", static_cast<double>(p.binary_clauses)},
        {"ternary_clauses", static_cast<double>(p.ternary_clauses)},
        {"unit_clause_fraction", p.unit_clause_fraction},
        {"binary_clause_fraction", p.binary_clause_fraction},
        {"ternary_clause_fraction", p.ternary_clause_fraction},
        {"negative_binary_fraction", p.negative_binary_fraction},
        {"positive_binary_fraction", p.positive_binary_fraction},
        {"mixed_binary_fraction", p.mixed_binary_fraction},
        {"unique_binary_pairs_sampled", static_cast<double>(p.unique_binary_pairs_sampled)},
        {"binary_duplicate_estimate", p.binary_duplicate_estimate},
        {"binary_vars_touched", static_cast<double>(p.binary_vars_touched)},
        {"binary_vars_touched_fraction", p.binary_vars_touched_fraction},
        {"binary_graph_degree_mean", p.binary_graph_degree_mean},
        {"binary_graph_degree_max", static_cast<double>(p.binary_graph_degree_max)},
        {"tautological_clause_fraction", p.tautological_clause_fraction},
        {"repeated_literal_clause_fraction", p.repeated_literal_clause_fraction},
        {"occurrence_mean", p.occurrence_mean},
        {"occurrence_stddev", p.occurrence_stddev},
        {"occurrence_max", static_cast<double>(p.occurrence_max)},
        {"singleton_var_fraction", p.singleton_var_fraction},
        {"pure_var_fraction", p.pure_var_fraction},
        {"positive_only_var_fraction", p.positive_only_var_fraction},
        {"negative_only_var_fraction", p.negative_only_var_fraction},
        {"polarity_balance_mean", p.polarity_balance_mean},
        {"top1_occurrence_mass", p.top1_occurrence_mass},
        {"top5_occurrence_mass", p.top5_occurrence_mass},
        {"clause_span_mean", p.clause_span_mean},
        {"normalized_clause_span_mean", p.normalized_clause_span_mean},
        {"small_span_clause_fraction", p.small_span_clause_fraction},
        {"positive_block_candidate_len", static_cast<double>(p.positive_block_candidate_len)},
        {"positive_block_candidate_count", static_cast<double>(p.positive_block_candidate_count)},
        {"positive_block_clause_fraction", p.positive_block_clause_fraction},
        {"exactly_one_intra_pair_coverage", p.exactly_one_intra_pair_coverage},
        {"truncated", p.truncated ? 1.0 : 0.0},
        {"parse_error", p.parse_error ? 1.0 : 0.0},
        {"empty_clause_present", p.empty_clause_present ? 1.0 : 0.0},
        {"conflicting_units_present", p.conflicting_units_present ? 1.0 : 0.0},
    };
    for (std::size_t i = 0; i < kClauseLengthBucketCount; ++i) {
        static std::array<std::string, kClauseLengthBucketCount> count_names = [] {
            std::array<std::string, kClauseLengthBucketCount> names{};
            for (std::size_t j = 0; j < kClauseLengthBucketCount; ++j) {
                names[j] = std::string(kClauseLengthBucketNames[j]) + "_count";
            }
            return names;
        }();
        static std::array<std::string, kClauseLengthBucketCount> frac_names = [] {
            std::array<std::string, kClauseLengthBucketCount> names{};
            for (std::size_t j = 0; j < kClauseLengthBucketCount; ++j) {
                names[j] = std::string(kClauseLengthBucketNames[j]) + "_frac";
            }
            return names;
        }();
        out.push_back({count_names[i], static_cast<double>(p.clause_len_counts[i])});
        out.push_back({frac_names[i], p.clause_len_fracs[i]});
    }
    return out;
}

double profile_feature(const FormulaProfile& profile, std::string_view name) {
    for (const auto& feature : profile_features(profile)) {
        if (feature.name == name) {
            return feature.value;
        }
    }
    return 0.0;
}

std::string profile_to_json(const FormulaProfile& p) {
    std::ostringstream out;
    out << std::setprecision(17);
    out << "{\n";
    out << "  \"schema_version\": \"" << json_escape(p.schema_version) << "\",\n";
    out << "  \"profiler_version\": \"" << json_escape(p.profiler_version) << "\",\n";
    out << "  \"source_path\": \"" << json_escape(p.source_path) << "\",\n";
    out << "  \"parse_error_message\": \"" << json_escape(p.parse_error_message) << "\",\n";
    out << "  \"truncated\": " << (p.truncated ? "true" : "false") << ",\n";
    out << "  \"parse_error\": " << (p.parse_error ? "true" : "false") << ",\n";
    out << "  \"empty_clause_present\": " << (p.empty_clause_present ? "true" : "false") << ",\n";
    out << "  \"conflicting_units_present\": " << (p.conflicting_units_present ? "true" : "false") << ",\n";
    out << "  \"binary_pair_sketch_truncated\": "
        << (p.binary_pair_sketch_truncated ? "true" : "false") << ",\n";
    out << "  \"profile_clauses_limit\": " << p.profile_clauses_limit << ",\n";
    out << "  \"parse_seconds\": " << p.parse_seconds << ",\n";

    auto write_u64 = [&](std::string_view name, std::uint64_t value) {
        out << "  \"" << name << "\": " << value << ",\n";
    };
    auto write_double = [&](std::string_view name, double value) {
        out << "  \"" << name << "\": " << value << ",\n";
    };

    write_u64("num_vars", p.num_vars);
    write_u64("header_clauses", p.header_clauses);
    write_u64("clauses_seen", p.clauses_seen);
    write_u64("num_literals", p.num_literals);
    write_u64("max_var_seen", p.max_var_seen);
    write_u64("used_var_count", p.used_var_count);
    write_u64("unused_var_count", p.unused_var_count);
    write_u64("max_clause_len", p.max_clause_len);
    write_u64("min_clause_len", p.min_clause_len);
    write_double("avg_clause_len", p.avg_clause_len);
    write_double("clauses_per_var", p.clauses_per_var);
    write_double("literals_per_var", p.literals_per_var);
    write_double("used_var_fraction", p.used_var_fraction);
    write_u64("distinct_clause_len_count", p.distinct_clause_len_count);
    write_u64("dominant_clause_len", p.dominant_clause_len);
    write_double("dominant_clause_len_fraction", p.dominant_clause_len_fraction);
    write_u64("positive_literals", p.positive_literals);
    write_u64("negative_literals", p.negative_literals);
    write_double("positive_literal_fraction", p.positive_literal_fraction);
    write_double("negative_literal_fraction", p.negative_literal_fraction);
    write_double("mean_positive_literals_per_clause", p.mean_positive_literals_per_clause);
    write_double("mean_negative_literals_per_clause", p.mean_negative_literals_per_clause);
    write_u64("all_positive_clauses", p.all_positive_clauses);
    write_u64("all_negative_clauses", p.all_negative_clauses);
    write_u64("mixed_clauses", p.mixed_clauses);
    write_double("all_positive_clause_fraction", p.all_positive_clause_fraction);
    write_double("all_negative_clause_fraction", p.all_negative_clause_fraction);
    write_double("mixed_clause_fraction", p.mixed_clause_fraction);
    write_u64("horn_clauses", p.horn_clauses);
    write_u64("dual_horn_clauses", p.dual_horn_clauses);
    write_u64("definite_horn_clauses", p.definite_horn_clauses);
    write_double("horn_clause_fraction", p.horn_clause_fraction);
    write_double("dual_horn_clause_fraction", p.dual_horn_clause_fraction);
    write_double("definite_horn_clause_fraction", p.definite_horn_clause_fraction);
    write_u64("unit_clauses", p.unit_clauses);
    write_u64("binary_clauses", p.binary_clauses);
    write_u64("ternary_clauses", p.ternary_clauses);
    write_double("unit_clause_fraction", p.unit_clause_fraction);
    write_double("binary_clause_fraction", p.binary_clause_fraction);
    write_double("ternary_clause_fraction", p.ternary_clause_fraction);
    write_u64("negative_binary_clauses", p.negative_binary_clauses);
    write_u64("positive_binary_clauses", p.positive_binary_clauses);
    write_u64("mixed_binary_clauses", p.mixed_binary_clauses);
    write_double("negative_binary_fraction", p.negative_binary_fraction);
    write_double("positive_binary_fraction", p.positive_binary_fraction);
    write_double("mixed_binary_fraction", p.mixed_binary_fraction);
    write_u64("unique_binary_pairs_sampled", p.unique_binary_pairs_sampled);
    write_double("binary_duplicate_estimate", p.binary_duplicate_estimate);
    write_u64("binary_vars_touched", p.binary_vars_touched);
    write_double("binary_vars_touched_fraction", p.binary_vars_touched_fraction);
    write_double("binary_graph_degree_mean", p.binary_graph_degree_mean);
    write_u64("binary_graph_degree_max", p.binary_graph_degree_max);
    write_u64("tautological_clauses", p.tautological_clauses);
    write_u64("repeated_literal_clauses", p.repeated_literal_clauses);
    write_double("tautological_clause_fraction", p.tautological_clause_fraction);
    write_double("repeated_literal_clause_fraction", p.repeated_literal_clause_fraction);
    write_double("occurrence_mean", p.occurrence_mean);
    write_double("occurrence_stddev", p.occurrence_stddev);
    write_u64("occurrence_max", p.occurrence_max);
    write_u64("singleton_vars", p.singleton_vars);
    write_u64("pure_vars", p.pure_vars);
    write_u64("positive_only_vars", p.positive_only_vars);
    write_u64("negative_only_vars", p.negative_only_vars);
    write_double("singleton_var_fraction", p.singleton_var_fraction);
    write_double("pure_var_fraction", p.pure_var_fraction);
    write_double("positive_only_var_fraction", p.positive_only_var_fraction);
    write_double("negative_only_var_fraction", p.negative_only_var_fraction);
    write_double("polarity_balance_mean", p.polarity_balance_mean);
    write_double("top1_occurrence_mass", p.top1_occurrence_mass);
    write_double("top5_occurrence_mass", p.top5_occurrence_mass);
    write_double("clause_span_mean", p.clause_span_mean);
    write_double("normalized_clause_span_mean", p.normalized_clause_span_mean);
    write_double("small_span_clause_fraction", p.small_span_clause_fraction);
    write_u64("positive_block_candidate_len", p.positive_block_candidate_len);
    write_u64("positive_block_candidate_count", p.positive_block_candidate_count);
    write_double("positive_block_clause_fraction", p.positive_block_clause_fraction);
    write_double("exactly_one_intra_pair_coverage", p.exactly_one_intra_pair_coverage);

    auto write_array_u64 = [&](std::string_view name, const auto& values, bool trailing_comma) {
        out << "  \"" << name << "\": [";
        for (std::size_t i = 0; i < values.size(); ++i) {
            if (i) out << ", ";
            out << values[i];
        }
        out << "]" << (trailing_comma ? ",\n" : "\n");
    };
    auto write_array_double = [&](std::string_view name, const auto& values, bool trailing_comma) {
        out << "  \"" << name << "\": [";
        for (std::size_t i = 0; i < values.size(); ++i) {
            if (i) out << ", ";
            out << values[i];
        }
        out << "]" << (trailing_comma ? ",\n" : "\n");
    };
    write_array_u64("clause_len_counts", p.clause_len_counts, true);
    write_array_double("clause_len_fracs", p.clause_len_fracs, false);

    out << "}\n";
    return out.str();
}

FormulaProfile profile_from_json(const std::string& json) {
    FormulaProfile p;
    set_from_json(p.schema_version, json, "schema_version");
    set_from_json(p.profiler_version, json, "profiler_version");
    set_from_json(p.source_path, json, "source_path");
    set_from_json(p.parse_error_message, json, "parse_error_message");
    set_from_json(p.profile_clauses_limit, json, "profile_clauses_limit");
    set_from_json(p.parse_seconds, json, "parse_seconds");
    set_from_json(p.truncated, json, "truncated");
    set_from_json(p.parse_error, json, "parse_error");
    set_from_json(p.empty_clause_present, json, "empty_clause_present");
    set_from_json(p.conflicting_units_present, json, "conflicting_units_present");
    set_from_json(p.binary_pair_sketch_truncated, json, "binary_pair_sketch_truncated");

    set_from_json(p.num_vars, json, "num_vars");
    set_from_json(p.header_clauses, json, "header_clauses");
    set_from_json(p.clauses_seen, json, "clauses_seen");
    set_from_json(p.num_literals, json, "num_literals");
    set_from_json(p.max_var_seen, json, "max_var_seen");
    set_from_json(p.used_var_count, json, "used_var_count");
    set_from_json(p.unused_var_count, json, "unused_var_count");
    set_from_json(p.max_clause_len, json, "max_clause_len");
    set_from_json(p.min_clause_len, json, "min_clause_len");
    set_from_json(p.avg_clause_len, json, "avg_clause_len");
    set_from_json(p.clauses_per_var, json, "clauses_per_var");
    set_from_json(p.literals_per_var, json, "literals_per_var");
    set_from_json(p.used_var_fraction, json, "used_var_fraction");
    set_from_json(p.distinct_clause_len_count, json, "distinct_clause_len_count");
    set_from_json(p.dominant_clause_len, json, "dominant_clause_len");
    set_from_json(p.dominant_clause_len_fraction, json, "dominant_clause_len_fraction");
    set_from_json(p.positive_literals, json, "positive_literals");
    set_from_json(p.negative_literals, json, "negative_literals");
    set_from_json(p.positive_literal_fraction, json, "positive_literal_fraction");
    set_from_json(p.negative_literal_fraction, json, "negative_literal_fraction");
    set_from_json(p.mean_positive_literals_per_clause, json, "mean_positive_literals_per_clause");
    set_from_json(p.mean_negative_literals_per_clause, json, "mean_negative_literals_per_clause");
    set_from_json(p.all_positive_clauses, json, "all_positive_clauses");
    set_from_json(p.all_negative_clauses, json, "all_negative_clauses");
    set_from_json(p.mixed_clauses, json, "mixed_clauses");
    set_from_json(p.all_positive_clause_fraction, json, "all_positive_clause_fraction");
    set_from_json(p.all_negative_clause_fraction, json, "all_negative_clause_fraction");
    set_from_json(p.mixed_clause_fraction, json, "mixed_clause_fraction");
    set_from_json(p.horn_clauses, json, "horn_clauses");
    set_from_json(p.dual_horn_clauses, json, "dual_horn_clauses");
    set_from_json(p.definite_horn_clauses, json, "definite_horn_clauses");
    set_from_json(p.horn_clause_fraction, json, "horn_clause_fraction");
    set_from_json(p.dual_horn_clause_fraction, json, "dual_horn_clause_fraction");
    set_from_json(p.definite_horn_clause_fraction, json, "definite_horn_clause_fraction");
    set_from_json(p.unit_clauses, json, "unit_clauses");
    set_from_json(p.binary_clauses, json, "binary_clauses");
    set_from_json(p.ternary_clauses, json, "ternary_clauses");
    set_from_json(p.unit_clause_fraction, json, "unit_clause_fraction");
    set_from_json(p.binary_clause_fraction, json, "binary_clause_fraction");
    set_from_json(p.ternary_clause_fraction, json, "ternary_clause_fraction");
    set_from_json(p.negative_binary_clauses, json, "negative_binary_clauses");
    set_from_json(p.positive_binary_clauses, json, "positive_binary_clauses");
    set_from_json(p.mixed_binary_clauses, json, "mixed_binary_clauses");
    set_from_json(p.negative_binary_fraction, json, "negative_binary_fraction");
    set_from_json(p.positive_binary_fraction, json, "positive_binary_fraction");
    set_from_json(p.mixed_binary_fraction, json, "mixed_binary_fraction");
    set_from_json(p.unique_binary_pairs_sampled, json, "unique_binary_pairs_sampled");
    set_from_json(p.binary_duplicate_estimate, json, "binary_duplicate_estimate");
    set_from_json(p.binary_vars_touched, json, "binary_vars_touched");
    set_from_json(p.binary_vars_touched_fraction, json, "binary_vars_touched_fraction");
    set_from_json(p.binary_graph_degree_mean, json, "binary_graph_degree_mean");
    set_from_json(p.binary_graph_degree_max, json, "binary_graph_degree_max");
    set_from_json(p.tautological_clauses, json, "tautological_clauses");
    set_from_json(p.repeated_literal_clauses, json, "repeated_literal_clauses");
    set_from_json(p.tautological_clause_fraction, json, "tautological_clause_fraction");
    set_from_json(p.repeated_literal_clause_fraction, json, "repeated_literal_clause_fraction");
    set_from_json(p.occurrence_mean, json, "occurrence_mean");
    set_from_json(p.occurrence_stddev, json, "occurrence_stddev");
    set_from_json(p.occurrence_max, json, "occurrence_max");
    set_from_json(p.singleton_vars, json, "singleton_vars");
    set_from_json(p.pure_vars, json, "pure_vars");
    set_from_json(p.positive_only_vars, json, "positive_only_vars");
    set_from_json(p.negative_only_vars, json, "negative_only_vars");
    set_from_json(p.singleton_var_fraction, json, "singleton_var_fraction");
    set_from_json(p.pure_var_fraction, json, "pure_var_fraction");
    set_from_json(p.positive_only_var_fraction, json, "positive_only_var_fraction");
    set_from_json(p.negative_only_var_fraction, json, "negative_only_var_fraction");
    set_from_json(p.polarity_balance_mean, json, "polarity_balance_mean");
    set_from_json(p.top1_occurrence_mass, json, "top1_occurrence_mass");
    set_from_json(p.top5_occurrence_mass, json, "top5_occurrence_mass");
    set_from_json(p.clause_span_mean, json, "clause_span_mean");
    set_from_json(p.normalized_clause_span_mean, json, "normalized_clause_span_mean");
    set_from_json(p.small_span_clause_fraction, json, "small_span_clause_fraction");
    set_from_json(p.positive_block_candidate_len, json, "positive_block_candidate_len");
    set_from_json(p.positive_block_candidate_count, json, "positive_block_candidate_count");
    set_from_json(p.positive_block_clause_fraction, json, "positive_block_clause_fraction");
    set_from_json(p.exactly_one_intra_pair_coverage, json, "exactly_one_intra_pair_coverage");

    parse_uint_array(json, "clause_len_counts", p.clause_len_counts);
    parse_number_array(json, "clause_len_fracs", p.clause_len_fracs);
    return p;
}

}  // namespace lymphosat
