//
// Created by mark on 5/30/25.
//

#include <any>
#include <string_view>
#include <utility>
#include <boost/proto/transform/env.hpp>
#include <ReportsAndCharts//WChartJSLibrary.h>
#include <Wt/EscapeOStream.h>
#include <Wt/WAbstractItemModel.h>
#include <Wt/WApplication.h>
#include <Wt/WFlags.h>
#include <Wt/WPainter.h>
#include <Wt/WWidget.h>
#include <Wt/Chart/WAbstractChartModel.h>
#include <Wt/Chart/WStandardChartProxyModel.h>
#include <Wt/Json/Serializer.h>

constexpr std::string_view WChartJSLibrary_js = R"wtjs(
window.WChartJSLibrary = window.WChartJSLibrary || function(APP, el, chart_config_str) {
    const self = this;
    el.chartJSObj = this;
    this.chart = null;
    this.widget = el;

    this.canvas = document.createElement('canvas');
    this.canvas.style.position = 'absolute';
    this.canvas.style.top = '0';
    this.canvas.style.left = '0';
    this.canvas.style.width = '100%';
    this.canvas.style.height = '100%';
    el.appendChild(this.canvas);

    this.handleResize = function() {
        if (self.chart && self.canvas && self.canvas.parentNode) {
            self.chart.resize();
        }
    };

    this.addNonModelData = function(label_str, newData_str) {
        const label = JSON.parse(label_str);
        const newData = JSON.parse(newData_str);
        self.chart.data.labels.push(label);
        self.chart.data.datasets.forEach((dataset) => {
            dataset.data.push(newData);
        });
        self.chart.update();
    }

    this.resetSpecificDataSeries = function(seriesNumber) {
        var index = parseInt(seriesNumber, 10);
        if (self.chart && self.chart.data && self.chart.data.datasets && self.chart.data.datasets[index]) {
            self.chart.data.datasets[index].data = [];
            self.chart.update();
        } else {
            console.warn("ChartJS tried to reset dataset at index " + index + ", but it is undefined.");
        }
    }

    this.removeSpecificDataSeries = function(seriesNumber) {
        self.chart.data.datasets.splice(seriesNumber, 1);
        self.chart.update();
    }

    this.replaceSpecificDataSeries = function(seriesNumber, data_str) {
        const newData = JSON.parse(data_str);
        self.chart.data.datasets.splice(seriesNumber, 1);
        self.chart.data.datasets.push(newData);
        self.chart.update();
    }

    this.removeAllData = function() {
        self.chart.data.labels.pop();
        self.chart.data.datasets.forEach((dataset) => {
            dataset.data.pop();
        });
        self.chart.update();
    }

    this.addModelData = function(data_str) {
        const newData = JSON.parse(data_str);
        self.chart.data.datasets.push(newData);
        self.chart.update();
    }

    this.appendExistingDataSeries = function(seriesNumber, data_str) {
        self.chart.options.animations.x = false;
        const newData = JSON.parse(data_str);
        const data = newData.data;
        const targetDataset = self.chart.data.datasets[seriesNumber];
        targetDataset.data = targetDataset.data.concat(data);
        self.chart.data.labels = targetDataset.data.map((_, index) => index + 1);
        self.chart.update();
    }

    this.updateChartTitle = function(title_str) {
        self.chart.options.plugins.title.text = title_str;
        self.chart.update();
    }

    this.updateConfigAsNewObject = function(config_str) {
        const options = JSON.parse(config_str);
        self.chart.options = options;
        self.chart.update();
    }

    this.destroy = function() {
        if (self.chart) self.chart.destroy();
    }

    this.init = function(chart_config_str) {
        const chart_config = JSON.parse(chart_config_str);
        const type = chart_config.type;
        const data = chart_config.data || {};

        if (data.labels === "datacount" && data.datasets && data.datasets.length > 0) {
            data.labels = data.datasets[0].data.map((_, index) => index + 1);
        }

        const plugins = chart_config.plugins || {};
        const options = chart_config.options || {};

        options.onClick = function(event, activeElements, chart) {
            if (activeElements && activeElements.length > 0) {
                const firstPoint = activeElements[0];
                Wt.emit(self.widget, 'chartClicked', firstPoint.datasetIndex, firstPoint.index);
            }
        };

        if (options && options.scales && options.scales.x && options.scales.x.ticks && options.scales.x.ticks.callback === "toFixed") {
             options.scales.x.ticks.callback = function(value, index, values) { return value.toFixed(0); };
        }

        if (options && options.plugins && options.plugins.tooltip && options.plugins.tooltip.callback === "percentage") {
            options.plugins.tooltip.callbacks = {
                label: function(context) {
                    let label = context.dataset.label || '';
                    if (label) {
                        label += ': ';
                    }
                    if (context.parsed.y !== null) {
                        label += (context.parsed.y * 100).toFixed(1) + '%';
                    }
                    return label;
                }
            };
        }

        self.chart = new Chart(self.canvas, {
            type,
            data,
            plugins,
            options
        });
    };

    if (chart_config_str) this.init(chart_config_str);
};
)wtjs";

WChartJSLibrary::WChartJSLibrary(const ChartJSType chartType) : chartType_(chartType),
                                                                resized_(false),
																jsDefined_(false),
																chartClicked_(this, "chartClicked")
{
	wApp->enableUpdates(true);
	WWebWidget::setPositionScheme(Wt::PositionScheme::Relative);

	setWidth(Wt::WLength(100, Wt::LengthUnit::Percentage));
	// Must hard code the height or ChartJS resizing function will go into an infinite
	// resizing loop with CSS Flexbox
	setHeight(Wt::WLength(300, Wt::LengthUnit::Pixel));
	chartTypeName_ = getChartJSType(chartType);
}

std::string WChartJSLibrary::getChartJSType(ChartJSType type) {
	switch (chartType_) {
	case ChartJSType::BAR: return "bar";
	case ChartJSType::BUBBLE: return "bubble";
	case ChartJSType::DOUGHNUT: return "doughnut";
	case ChartJSType::PIE: return "pie";
	case ChartJSType::LINE: return "line";
	case ChartJSType::POLAR: return "polarArea";
	case ChartJSType::RADAR: return "radar";
	case ChartJSType::SCATTER: return "scatter";
	}

	return "line";
}

void WChartJSLibrary::updateChartTitleOnly(const Wt::WString& title) {
	set(titleText_, title);
	if (fullyRendered_) {
		Wt::WStringStream jsStream; // Local stream
		{ // Scope block for EscapeOStream
			Wt::EscapeOStream es(jsStream);
			es << "var o=" << jsRef() << ";if(o && o.chartJSObj){o.chartJSObj.updateChartTitle('";
			es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
			es << titleText_.toUTF8();
			es.popEscape();
			es << "');};";
		} // Forces flush

		doJavaScript(jsStream.str());
	}
}

void WChartJSLibrary::setModel(const std::shared_ptr<Wt::Chart::WAbstractChartModel>& model) {
	if (model_ != nullptr) {
		/* disconnect slots from previous model */
		for (auto& modelConnection : modelConnections_)
			modelConnection.disconnect();
		modelConnections_.clear();
	}
	model_ = model;
	modelConnections_.push_back(model_->changed().connect
		(this, &WChartJSLibrary::modelChanged));

	modelChanged();
}

void WChartJSLibrary::setModel(const std::shared_ptr<Wt::WAbstractItemModel>& model) {
	standardModel_ = model;
	setModel(std::shared_ptr<Wt::Chart::WAbstractChartModel>(
		std::make_shared<Wt::Chart::WStandardChartProxyModel>(model)));
}

std::shared_ptr<Wt::WAbstractItemModel> WChartJSLibrary::itemModel() const {
	const auto* proxy
			= dynamic_cast<Wt::Chart::WStandardChartProxyModel*>(model_.get());
	if (proxy)
		return proxy->sourceModel();
	else
		return nullptr;
}

void WChartJSLibrary::modelChanged() {
	// not sure what should be here. Will figure it out later.
	// modelColumnsLoaded_.clear();
}

void WChartJSLibrary::updateModel() {
	if (!modelColumnsLoaded_.empty()) {
		// add test for multiple series. Right now, assume only one
		Wt::Json::Object dataSeries;
		Wt::Json::Array series;
		int modelCount = static_cast<int>(modelColumnsLoaded_.size());
		for (unsigned i = 0; i < modelCount; ++i) {
			for (int j = 0; j < model_->rowCount(); ++j) {
				series.push_back(model_->data(j, modelColumnsLoaded_[i].modelColumnNumber));
			}

			if (modelColumnsLoaded_[i].countForLabels) {
				countForLabels_ = true;
				dataSeries["labels"] = Wt::Json::Value("datacount");
			} else {
				countForLabels_ = false;
				dataLabels_.clear();
				int numRows = standardModel_->rowCount();
				for (int j = 0; j < numRows; ++j) {
					auto headerData = std::any_cast<std::string>(
						standardModel_->headerData(j, Wt::Orientation::Vertical, Wt::ItemDataRole::Display));
					dataLabels_.push_back(Wt::Json::Value(headerData));
				}
				dataSeries["labels"] = Wt::Json::Value(dataLabels_);
			}

			dataSeries["data"] = series;
			std::string output = Wt::Json::serialize(dataSeries);

				Wt::WStringStream jsStream; // Local stream
				{ // Scope block for EscapeOStream
					Wt::EscapeOStream es(jsStream);
					es << "var o=" << jsRef() << ";if(o && o.chartJSObj){o.chartJSObj.replaceSpecificDataSeries('" << i << "','";
					es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
					es << output;
					es.popEscape();
					es << "');};";
				} // Forces flush

				doJavaScript(jsStream.str());

			// if there are multiple series, this javascript function will delete this particular series
			// and replace it with the newly created series by pushing it onto the end of datasets. To
			// remain in sync, the same needs to be done with the modelColumnsLoaded_.
			ModelColumnInfo modelColumnInfo = modelColumnsLoaded_[i];
			modelColumnsLoaded_.erase(modelColumnsLoaded_.begin() + i);
			modelColumnsLoaded_.push_back(modelColumnInfo);

		}
	}
}

std::pair<int, int> WChartJSLibrary::getModelCoordinates(int datasetIndex, int dataIndex) const {
	// 1. Bounds checking for safety against rogue JS events
	if (datasetIndex < 0 || datasetIndex >= static_cast<int>(modelColumnsLoaded_.size())) {
		return {-1, -1};
	}

	// 2. The dataIndex from Chart.js maps 1:1 to the rows you iterated over in addSeries
	int modelRow = dataIndex;

	// 3. Look up the exact C++ model column from your tracking struct
	int modelColumn = modelColumnsLoaded_[datasetIndex].modelColumnNumber;

	return {modelRow, modelColumn};
}

// addSeries needs a modelColumn and a fully populated Wt::Json::Object called ChartJSDataSeries
void WChartJSLibrary::addSeries(int modelColumn, bool countForLabels, Wt::Json::Object dataSeriesOptions,
                                bool dependantIndependantSeries) {
	// if series_[modelColumn] exists, throw an error otherwise, set value.
	int labelCol = dependantIndependantSeries ? 1 : modelColumn;
	//concatenate the data series title and the actual data and add it to the series_ std::map
	Wt::Json::Object dataSeries = std::move(dataSeriesOptions);
	Wt::Json::Array series;
	// If the Chart type is bubble, the data should be constructed as [{x: 0, y: 0, r: 0}]
	if ((chartType_ != ChartJSType::BUBBLE) && !dependantIndependantSeries) {
		for (int i = 0; i < model_->rowCount(); ++i) {
			series.push_back(model_->data(i, modelColumn));
		}
	}

	if (dependantIndependantSeries) {
		for (int i = 0; i < model_->rowCount(); ++i) {
			series.push_back(model_->data(i, 1));
		}
	}

	// set the labels for contiguous data points when there are no row headers set
	countForLabels_ = countForLabels;
	// and the data is numeric
	dataSeries["data"] = series;

	auto headerData = Wt::asString(standardModel_->headerData(labelCol, Wt::Orientation::Horizontal, Wt::ItemDataRole::Display)).toUTF8();
	dataSeries["label"] = Wt::Json::Value(headerData);

	if (countForLabels_) {
		dataSeries["label"] = model_->headerData(modelColumn);
	} else {
		dataLabels_.clear();
		int numRows = standardModel_->rowCount();
		for (int i = 0; i < numRows; ++i) {
			// Extract the raw, unformatted data from the Edit role
			auto editData = standardModel_->data(i, 0, Wt::ItemDataRole::Edit);

			// Safely unpack the std::any container based on its true C++ type
			if (editData.type() == typeid(double)) {
				dataLabels_.push_back(Wt::Json::Value(std::any_cast<double>(editData)));
			} else if (editData.type() == typeid(int)) {
				dataLabels_.push_back(Wt::Json::Value(std::any_cast<int>(editData)));
			} else if (editData.type() == typeid(long long)) {
				dataLabels_.push_back(Wt::Json::Value(static_cast<double>(std::any_cast<long long>(editData))));
			} else {
				// Fallback for categorical strings and dates
				auto labelStr = Wt::asString(editData).toUTF8();
				dataLabels_.push_back(Wt::Json::Value(labelStr));
			}
		}
	}

	Wt::Json::Object additionalSeries;
	if (fullyRendered_) additionalSeries = Wt::Json::Value(dataSeries);
	ModelColumnInfo colInfo;
	colInfo.countForLabels = countForLabels_;
	colInfo.dependantIndependantSeries = dependantIndependantSeries;
	colInfo.modelColumnNumber = modelColumn;
	colInfo.lastDataCount = model_->rowCount();
	modelColumnsLoaded_.push_back(colInfo);
	series_.push_back(dataSeries);
	assert(modelColumnsLoaded_.size() == series_.size());
	// If the chart is already rendered, we need to add the net change to the
	// chart. Otherwise, the prior data will get picked up on the full render.

	if (fullyRendered_) {
		std::string output = Wt::Json::serialize(additionalSeries);
		Wt::WStringStream jsStream; // Local stream

		{ // Scope block for EscapeOStream
			Wt::EscapeOStream es(jsStream);
			es << "var o=" << jsRef() << ";if(o && o.chartJSObj){o.chartJSObj.addModelData('";
			es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
			es << output;
			es.popEscape();
			es << "');};";
		} // Forces flush

		doJavaScript(jsStream.str()); // Execute directly instead of relying on render()
	}
	scheduleRender();
}

void WChartJSLibrary::addRawSeries(const Wt::Json::Object& rawSeries) {
	series_.push_back(rawSeries);
	if (fullyRendered_) {
		std::string output = Wt::Json::serialize(rawSeries);
		Wt::WStringStream jsStream;
		{
			Wt::EscapeOStream es(jsStream);
			es << "var o=" << jsRef() << ";if(o && o.chartJSObj){o.chartJSObj.addModelData('";
			es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
			es << output;
			es.popEscape();
			es << "');};";
		}
		doJavaScript(jsStream.str());
	}
	scheduleRender();
}

void WChartJSLibrary::updateExistingSeries() {
	if (!modelColumnsLoaded_.empty()) {
		// add test for multiple series. Right now, assume only one
		Wt::Json::Object dataSeries;
		Wt::Json::Array series;
		int modelCount = static_cast<int>(modelColumnsLoaded_.size());
		for (int i = 0; i < modelCount; ++i) {
			int netChangeRows = (int)model_->rowCount() - modelColumnsLoaded_[i].lastDataCount;
			if (netChangeRows < 0) {
				modelColumnsLoaded_[i].lastDataCount = 0;
				resetSeries(modelColumnsLoaded_[i].modelColumnNumber);
				netChangeRows = (int)model_->rowCount();
			}
			if (netChangeRows == 0) continue;
			int recordsAdded = 0;
			for (int j = modelColumnsLoaded_[i].lastDataCount; j < model_->rowCount(); j++) {
				series.push_back(model_->data(j, modelColumnsLoaded_[i].modelColumnNumber));
				recordsAdded++;
			}
			modelColumnsLoaded_[i].lastDataCount += recordsAdded;

			if (modelColumnsLoaded_[i].countForLabels) {
				countForLabels_ = true;
				dataSeries["labels"] = Wt::Json::Value("datacount");
			} else {
				countForLabels_ = false;
				dataLabels_.clear();
				int numRows = standardModel_->rowCount();
				for (int i = 0; i < numRows; ++i) {
					auto labelStr = Wt::asString(standardModel_->data(i, 0, Wt::ItemDataRole::Display)).toUTF8();
					dataLabels_.push_back(Wt::Json::Value(labelStr));
				}
				dataSeries["labels"] = Wt::Json::Value(dataLabels_);
			}

			dataSeries["data"] = series;
			std::string output = Wt::Json::serialize(dataSeries);

			Wt::WStringStream jsStream; // Local stream
			{ // Scope block for EscapeOStream
				Wt::EscapeOStream es(jsStream);
				es << "var o=" << jsRef() << ";if(o && o.chartJSObj){o.chartJSObj.appendExistingDataSeries(" << i << ",'";
				es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
				es << output;
				es.popEscape();
				es << "');};";
			} // Forces flush

			doJavaScript(jsStream.str());
		}
	}
}

void WChartJSLibrary::removeSeries(int modelColumn) {
	Wt::WStringStream jsStream; // Local stream
	{
		Wt::EscapeOStream es(jsStream);
		if (!modelColumnsLoaded_.empty()) {

			// CRITICAL: Iterate backwards so erasing items don't corrupt the loop index!
			for (int j = modelColumnsLoaded_.size() - 1; j >= 0; --j) {
				if (modelColumnsLoaded_[j].modelColumnNumber == modelColumn) {

					// 1. Tell JS to completely destroy the dataset
					es << "var o=" << jsRef() << ";if(o && o.chartJSObj){"
						  "if(o.chartJSObj.chart.data.datasets[" << j << "]) {"
						  "  o.chartJSObj.chart.data.datasets.splice(" << j << ", 1);"
						  "  o.chartJSObj.chart.update();"
						  "}"
						  "};";

					// 2. Remove it from the C++ tracker so it stops stacking up infinitely
					series_.erase(series_.begin() + j);
					modelColumnsLoaded_.erase(modelColumnsLoaded_.begin() + j);
				}
			}
		}
	}
	doJavaScript(jsStream.str());
}

void WChartJSLibrary::removeLastRawSeries() {
	if (series_.empty()) return;

	// Grab the index of the last dataset (which will be our residuals)
	int lastIndex = series_.size() - 1;

	// Remove from the C++ tracker so it doesn't infinitely stack on redraws
	series_.erase(series_.begin() + lastIndex);

	// Call your existing JavaScript function to splice it out of ChartJS
	Wt::WStringStream jsStream;
	{
		Wt::EscapeOStream es(jsStream);
		es << "var o=" << jsRef() << ";if(o && o.chartJSObj){"
		   << "o.chartJSObj.removeSpecificDataSeries(" << lastIndex << ");"
		   << "};";
	}
	doJavaScript(jsStream.str());
}


void WChartJSLibrary::resetSeries(int modelColumn) {
	Wt::WStringStream jsStream;
	{
		Wt::EscapeOStream es(jsStream);
		if (!modelColumnsLoaded_.empty()) {
			for (int j = 0; j < modelColumnsLoaded_.size(); ++j) {
				if (modelColumnsLoaded_[j].modelColumnNumber == modelColumn) {
					// FIX: Removed the single quotes around the j variable
					es << "var o=" << jsRef() << ";if(o && o.chartJSObj){"
						  "o.chartJSObj.resetSpecificDataSeries(" << j << ");"
						  "};";
				}
			}
		}
	}
	doJavaScript(jsStream.str());
}

void WChartJSLibrary::setXaxisLabels(const std::vector<std::string>& labels) {
	dataLabels_.clear();
	for (const auto& label : labels) {
		dataLabels_.push_back(Wt::Json::Value(label));
	}
	countForLabels_ = false;
}

void WChartJSLibrary::setOptions(const Wt::Json::Object& chartOptions) {
	chartOptions_ = chartOptions;
}

void WChartJSLibrary::defineJavaScript() {
	Wt::WApplication* app = Wt::WApplication::instance();

	// 1. Ensure external libraries load
	app->require("https://cdn.jsdelivr.net/npm/chart.js");
	app->require("https://cdn.jsdelivr.net/npm/perfect-scrollbar@1.5.6/dist/perfect-scrollbar.min.js");
	app->require("https://cdn.jsdelivr.net/npm/smooth-scrollbar@8.8.4/dist/smooth-scrollbar.min.js");
	app->require("https://cdn.jsdelivr.net/npm/hammerjs@2.0.8");
	app->require("https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.2.0/dist/chartjs-plugin-zoom.min.js");

	std::string output = Wt::Json::serialize(chartjsConfigObj);
	Wt::WStringStream jsStream;

	// 2. Output the class definition securely into the stream
	jsStream << std::string(WChartJSLibrary_js) << "\n";

	// 3. Instantiate the chart using the newly defined class
	{
		Wt::EscapeOStream es(jsStream);
		es << "var el = " << jsRef() << ";"
		   << "if (el) {"
		   << "  el.chartJSObj = new window.WChartJSLibrary(Wt, el, '";
		es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
		es << output;
		es.popEscape();
		es << "');"
		   << "}";
	}

	// 4. Force Wt to evaluate everything synchronously in the widget queue
	doJavaScript(jsStream.str());

	jsDefined_ = true;
	scheduleRender();
}

void WChartJSLibrary::render(Wt::WFlags<Wt::RenderFlag> flags) {
	if (flags.test(Wt::RenderFlag::Full) || !jsDefined_) {
		defineJavaScript();
		fullyRendered_ = true;
	}

	Wt::WContainerWidget::render(flags);
}

void WChartJSLibrary::layoutSizeChanged(int width, int height) {
	Wt::WContainerWidget::layoutSizeChanged(width, height);
	if (jsDefined_ && fullyRendered_) {
		// C++20 formatting for string concatenation
		doJavaScript(std::format("var o={}; if(o && o.chartJSObj) o.chartJSObj.handleResize();", jsRef()));
	}
}

WChartJSLibrary::~WChartJSLibrary() {
	if (jsDefined_) {
		// Clean up Chart.js to prevent memory leaks in the browser
		Wt::WWebWidget::doJavaScript(std::format("var o={}; if(o && o.chartJSObj) o.chartJSObj.destroy();", jsRef()));
	}
}

void WChartJSLibrary::buildAndShowChart() {
	chartjsConfigObj["type"] = Wt::Json::Value(chartTypeName_);

	if (countForLabels_) {
		chartData_["labels"] = Wt::Json::Value("datacount");
		countForLabels_ = false;
	} else {
		chartData_["labels"] = Wt::Json::Value(dataLabels_);
	}

	chartData_["datasets"] = Wt::Json::Value(series_);
	chartjsConfigObj["data"] = Wt::Json::Value(chartData_);

	if (!chartOptions_.empty()) {
		chartjsConfigObj["options"] = Wt::Json::Value(chartOptions_);
	} else {
		// FIX: Use an empty Json::Object instead of the string "{},"
		// which breaks JSON parsing in JS
		chartjsConfigObj["options"] = Wt::Json::Object{};
	}

	if (!plugins_.empty()) {
		chartjsConfigObj["plugins"] = Wt::Json::Value(plugins_);
	}

	// NEW: If the chart is already on screen, forcefully sync the whole thing
	// to pick up any new Date boundaries from the Redraw button.
	if (fullyRendered_) {
		std::string output = Wt::Json::serialize(chartjsConfigObj);
		Wt::WStringStream jsStream;
		{
			Wt::EscapeOStream es(jsStream);
			es << "var o=" << jsRef() << ";if(o && o.chartJSObj){"
			   << "o.chartJSObj.destroy();"
			   << "o.chartJSObj.init('";
			es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
			es << output;
			es.popEscape();
			es << "');};";
		}
		doJavaScript(jsStream.str());
	}

	scheduleRender();
}
