#include <iostream>
#include <fstream>
#include <sstream>
#include <variant>
#include <chrono>
#include <algorithm>
#include "database.hpp"
#include "network.hpp"
#include <memory>
#include <cstdlib> 
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <atomic>
#include <thread>
#include <stdio.h>
#include "commander.hpp"

constexpr auto record_sync_freq = std::chrono::microseconds(500000);
std::atomic<std::chrono::microseconds> last_cycle_ts;
std::atomic<std::chrono::microseconds> last_cycle_durr;
std::atomic<std::chrono::microseconds> max_cycle_durr;
std::atomic<std::chrono::microseconds> avg_cycle_durr;
std::atomic<bool> will_stop{false};

void sync_records_thread(const database& db)
{
	std::chrono::microseconds total{0};
	uint16_t count = 0;
	do{
		auto begin = std::chrono::high_resolution_clock::now();
		fdatasync(db.records.fd_v());
		auto durr = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::high_resolution_clock::now() - begin);
		total += durr;
		count++;
		auto avg = total/count;
		if(count>20)
		{
			total -= avg;
			count--;
		}
		avg_cycle_durr.store(avg);
		last_cycle_durr.store(durr);
		if(max_cycle_durr.load() < durr)
		{
			max_cycle_durr.store(durr);
		}
		if(durr<record_sync_freq)
		{
			std::this_thread::sleep_for(record_sync_freq - durr);
		}
	}while(!will_stop);
}

enum class cleanup_type : size_t {
	normal = sizeof(received_data),
	complete = sizeof(received_bulk_data)
};

template<cleanup_type mode>
void buffer_cleanup(received_bulk_data& recv_impl) {
	constexpr size_t total = ((size_t)mode)/(((size_t)mode % 8 == 0) ? 8 : 4);

	for(
		size_t i = 0; 
		i < total; 
		i++
	)
	{
		if constexpr ((size_t)mode % 8 == 0)
		{
			((uint64_t*)&recv_impl)[i] = 0;
		} else {
			((uint32_t*)&recv_impl)[i] = 0;
		}
	}
}


int main(
	[[maybe_unused]] int argc, 
	[[maybe_unused]] char** argv
)
{
	CMD::commander cmd_args(argc, argv);

	std::string database_str = "/data/";

	if(cmd_args.isFlagSet("-database"))
	{
		database_str = cmd_args.getFlagValue("-database");
	}

	std::string utest_str = "/tmp/";

	if(cmd_args.isFlagSet("-utest"))
	{
		utest_str = cmd_args.getFlagValue("-utest");
	}

	size_t database_size = 4096;

	if(cmd_args.isFlagSet("-page-count"))
	{
		try{
			database_size = std::stoll(cmd_args.getFlagValue("-page-count"));
		} catch (...) {
			std::cerr << "Invalid page-count value" << std::endl;
			return 1;
		}

	}

	size_t db_port = 20450;

	if(cmd_args.isFlagSet("-port"))
	{
		try{
			db_port = std::stoi(cmd_args.getFlagValue("-port"));
			if(db_port>std::numeric_limits<uint16_t>::max()) throw std::runtime_error("invalid_port");
		} catch (...) {
			std::cerr << "Invalid port value" << std::endl;
			return 1;
		}

	}

#ifdef UNITTEST
	size_t utest_size = 4096;

	if(cmd_args.isFlagSet("-utest-count"))
	{
		try{
			utest_size = std::stoll(cmd_args.getFlagValue("-utest-count"));
		} catch (...) {
			std::cerr << "Invalid utest-count value" << std::endl;
			return 1;
		}
	}

	{
		database db(database::create(utest_str, utest_size*2));

		db_page v;
		v.fill(1);

		record_identifier tar;

		auto begin = std::chrono::high_resolution_clock::now();

		for(size_t idx=0;idx<utest_size;idx++)
		{
			tar.y.internal = idx;
			db.write(tar, v);
		}

		auto durr = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - begin)/utest_size;

		std::cerr << "durr/elem = " << durr.count() << "ns/op" << std::endl;

		auto a = db.read(tar);

		if(a.second[0] != 1) {
			std::cerr << "read failed" << std::endl;
		}

		db.remove(tar);

		a = db.read(tar);
		if(a.second[0] == 1) {
			std::cerr << "remove failed" << std::endl;
		}
		
		{
			db.write(tar, v);
			a = db.read(tar);
			if(a.second[0] != 1) {
				std::cerr << "read failed" << std::endl;
			}
			db.remove(tar);
			a = db.read(tar);
			if(a.second[0] != 0) {
				std::cerr << "remove failed" << std::endl;
			}
			v.fill(2);
			db.write(tar, v);
		}

		database reop(database::open(utest_str));

		{
			a = reop.read(tar);

			if(a.second[0] != 2) {
				std::cerr << "reopen read failed" << std::endl;
			}

			v.fill(3);

			reop.write(tar, v);

			a = reop.read(tar);
			if(a.second[0] != 3) {
				std::cerr << "reopen write failed" << std::endl;
			}
		}
		reop.pages.clear();
		reop.metadata.clear();
		reop.records.clear();
		reop.delete_table.clear();

		db.pages.clear();
		db.metadata.clear();
		db.records.clear();
		db.delete_table.clear();
	}
#endif


	if(cmd_args.isFlagSet("-create"))
	{
		try{
			database creat(database::create(database_str, database_size));
			std::cout << "Created" << std::endl;
			return 0;
		} catch (...) {
			std::cerr << "Creating of " << database_str << " of " << database_size << " pages failed" << std::endl;
			return 1;
		}
	}

	try{

		database run_db(database::open(database_str));
		auto soc = socket(AF_INET, SOCK_DGRAM, 0);
		struct sockaddr_in addr;
		addr.sin_family = AF_INET;
		addr.sin_port = htons(db_port);
		memset((void*)&addr.sin_addr, 0, sizeof(addr.sin_addr));
		bind(soc,(struct sockaddr*)&addr,sizeof(addr));
		std::thread syncher(sync_records_thread, run_db);
		received_bulk_data recv_impl;
		buffer_cleanup<cleanup_type::complete>(recv_impl);

		do{
			received_data& recv = *(received_data*)&recv_impl;
			sending_data reply;
			sockaddr_in client;
			socklen_t packet_sz;
			size_t reply_size;
			recvfrom(
				soc, 
				(void*)&recv_impl, 
				sizeof(received_bulk_data), 
				MSG_WAITFORONE, 
				(struct sockaddr*)&client,
				&packet_sz
			);

			if(packet_sz < sizeof(db_op)) continue;

			switch (static_cast<db_op>(recv.op))
			{
				case db_op::version:
					reply.rep_id = recv.rep_id;
					reply.identifier.record_head.split.x = 1;
					reply_size = sizeof(reply.rep_id) + sizeof(reply.identifier.record_head.split.x);
					buffer_cleanup<cleanup_type::normal>(recv_impl);
				break;
				case db_op::read:
					{
						reply.rep_id = recv.rep_id;
						auto req = run_db.read(recv.identifier);
						if(req.first.timestamp != 0)
						{
							reply.identifier = req.first;
							reply.page = req.second;
							reply_size = sizeof(reply);
						}
						else
						{
							reply_size = sizeof(reply.rep_id) + sizeof(reply.identifier);
						}
						buffer_cleanup<cleanup_type::normal>(recv_impl);
					}
				break;
				case db_op::write:
					{
						reply.rep_id = recv.rep_id;
						try{
							auto req = run_db.write(recv.identifier, recv.page);
							reply.identifier = req;
						} catch (...) {
							std::cerr << "cluster overfull"<< std::endl;
						}
						reply_size = sizeof(reply.rep_id) + sizeof(reply.identifier); 
						buffer_cleanup<cleanup_type::normal>(recv_impl);
					}
				break;
				// case db_op::sremove:
				case db_op::remove:
					{
						reply.rep_id = recv.rep_id;
						run_db.remove(recv.identifier);
						reply.identifier.record_head.split = recv.identifier;
						reply_size = sizeof(reply.rep_id) + sizeof(reply.identifier); 
						buffer_cleanup<cleanup_type::normal>(recv_impl);
					}
				break;
				case db_op::stats:
					{
						reply.rep_id = recv.rep_id;
						auto stats = (stats_data*)&reply.page;
						stats->total_pages = (*run_db.metadata).page_cnt;
						stats->total_records = (*run_db.metadata).record_cnt;
						stats->free_deleted = (*run_db.metadata).last_delete;
						stats->free = 
							(*run_db.metadata).last_delete 
							+ (*run_db.metadata).page_cnt 
							- (*run_db.metadata).last_page;
						uint64_t cow_full = 0;
						uint64_t usable_records = 0;
						for(auto& elem : run_db.records)
						{
							if (elem.second.timestamp == std::numeric_limits<uint64_t>::max())
							{
								++cow_full;
								++usable_records;
							} else if(elem.second.timestamp == 0) {
								++usable_records;
							}
						}
						stats->cow_full = cow_full;
						stats->free_records = usable_records;
						stats->sync_rate_µs = record_sync_freq.count();
						stats->sync_duration_µs = last_cycle_durr.load().count();
						stats->max_sync_duration_µs = max_cycle_durr.load().count();
						stats->avg_sync_duration_µs = avg_cycle_durr.load().count();
						reply_size = sizeof(reply);
						buffer_cleanup<cleanup_type::normal>(recv_impl);
					}
				break;
				case db_op::sallocate:
					{
						reply.rep_id = recv.rep_id;
						reply.identifier = run_db.try_allocate(recv.identifier);
						reply_size = sizeof(reply.rep_id) + sizeof(reply.identifier);
						buffer_cleanup<cleanup_type::normal>(recv_impl);
					}
				break;
				case db_op::sread:
					{
						reply.rep_id = recv.rep_id;
						auto req = run_db.stepped_read(recv.identifier);
						if(req.first.timestamp != 0)
						{
							reply.identifier = req.first;
							reply.page = req.second;
							reply_size = sizeof(reply);
						}
						else
						{
							reply_size = sizeof(reply.rep_id) + sizeof(reply.identifier);
						}
						buffer_cleanup<cleanup_type::normal>(recv_impl);
					}
				break;
				case db_op::swrite:
					{
						reply.rep_id = recv.rep_id;
						try{
							auto req = run_db.stepped_write(recv.identifier, recv.page);
							reply.identifier = req;
						} catch (...) {
							std::cerr << "cluster overfull"<< std::endl;
						}
						reply_size = sizeof(reply.rep_id) + sizeof(reply.identifier); 
						buffer_cleanup<cleanup_type::normal>(recv_impl);
					}
				break;
				case db_op::confirm:
					{
						reply.rep_id = recv.rep_id;
						run_db.confirm(recv.identifier, *(bitops::regulated<uint64_t>*)&recv.page);
						reply_size = sizeof(reply.rep_id);
						buffer_cleanup<cleanup_type::normal>(recv_impl);
					}
				break;
				default:
					std::cerr << "bad_request " << (uint32_t)static_cast<db_op>(recv.op)  << std::endl;
					continue;
				break;
			}
			sendto(
				soc,
				(void*)&reply,
				reply_size,
				0,
				(struct sockaddr*)&client,
				(socklen_t)sizeof(client)
			);
			//std::cerr << "reply to " << (uint32_t)static_cast<db_op>(recv.op)  << std::endl;
		}while(true);
	} catch (...) {
		std::cerr << "GoJDB crashed !" << std::endl;
		return 1;
	}

}
