#include "calibration_master.hpp"

#include <cstdint>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <vector>

namespace radar::preprocessing {
namespace {

[[nodiscard]] auto combo_to_string(const ipc::ComboKey& combo) -> std::string {
    return "input=" + std::to_string(combo.input_pos) + " output=" + std::to_string(combo.output_pos);
}

[[nodiscard]] auto read_binary_file(const std::string& path, const std::string& bundle_label)
    -> std::vector<std::uint8_t> {
    std::ifstream stream(path, std::ios::binary);
    if (!stream.is_open()) {
        throw std::runtime_error("Failed to open " + bundle_label + " bundle: " + path);
    }

    return std::vector<std::uint8_t>(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
}

void validate_trace_layout(const ipc::SweepTraceBlock& trace, const std::string& trace_label) {
    if (trace.frequency_hz.size() != trace.s21.size()) {
        throw std::runtime_error(
            trace_label + " frequency/complex vector size mismatch for combo " + combo_to_string(trace.combo)
        );
    }
}

}  // namespace

CalibrationMaster::CalibrationMaster(std::unique_ptr<CalibratorInterface> calibrator)
    : calibrator_impl_(std::move(calibrator)) {
    if (!calibrator_impl_) {
        throw std::runtime_error("CalibrationMaster requires a non-null calibrator");
    }
}

void CalibrationMaster::load_bundle(const std::string& path) {
    if (path.empty()) {
        throw std::runtime_error("Calibration bundle path must not be empty");
    }

    const auto bytes = read_binary_file(path, "calibration");
    if (bytes.empty()) {
        throw std::runtime_error("Calibration bundle is empty: " + path);
    }

    const auto collection = ipc::deserialize_raw_collection(bytes);
    standards_by_combo_.clear();
    standards_by_combo_.reserve(collection.traces.size());
    for (const auto& trace : collection.traces) {
        validate_trace_layout(trace, "Calibration standard");
        standards_by_combo_.insert_or_assign(trace.combo, trace);
    }

    if (standards_by_combo_.empty()) {
        throw std::runtime_error("Calibration bundle does not contain traces: " + path);
    }
}

void CalibrationMaster::validate_combos(const std::vector<ipc::ComboKey>& combos) const {
    for (const auto& combo : combos) {
        if (!standards_by_combo_.contains(combo)) {
            throw std::runtime_error("Calibration data is missing for combo " + combo_to_string(combo));
        }
    }
}

auto CalibrationMaster::apply(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock {
    validate_trace_layout(measured_trace, "Measured trace");

    const auto found = standards_by_combo_.find(measured_trace.combo);
    if (found == standards_by_combo_.end()) {
        throw std::runtime_error(
            "Calibration standard is missing for measured combo " + combo_to_string(measured_trace.combo)
        );
    }

    const auto& standard = found->second;
    validate_trace_layout(standard, "Calibration standard");
    if (measured_trace.s21.size() != standard.s21.size()) {
        throw std::runtime_error(
            "Calibration standard point count mismatch for combo " + combo_to_string(measured_trace.combo)
        );
    }

    ipc::SweepTraceBlock output{};
    output.combo = measured_trace.combo;
    output.frequency_hz = measured_trace.frequency_hz;
    output.s21 = calibrator_impl_->apply(measured_trace.s21, standard.s21);

    return output;
}

}  // namespace radar::preprocessing
