//
//	License type: BSD 3-Clause License
//	License copy: https://github.com/Telecominfraproject/wlan-cloud-ucentralgw/blob/master/LICENSE
//
//	Created by Stephane Bourque on 2021-03-04.
//	Arilia Wireless Inc.
//

#include "RESTAPI_venue_handler.h"

#include "RESTAPI/RESTAPI_db_helpers.h"
#include "RESTObjects/RESTAPI_ProvObjects.h"
#include "StorageService.h"
#include "Tasks/VenueConfigUpdater.h"
#include "Tasks/VenueRebooter.h"
#include "Tasks/VenueUpgrade.h"
#include "framework/CIDR.h"
#include "framework/MicroServiceFuncs.h"

#include "Kafka_ProvUpdater.h"

namespace OpenWifi {

	static Types::UUIDvec_t GetDevices(const ProvObjects::Venue &V, bool GetChildren) {
		Types::UUIDvec_t R;
		std::copy(V.devices.begin(), V.devices.end(), std::back_inserter(R));
		if (GetChildren) {
			for (const auto &i : V.children) {
				ProvObjects::Venue V2;
				if (StorageService()->VenueDB().GetRecord("id", i, V2)) {
					std::copy(V2.devices.begin(), V2.devices.end(), std::back_inserter(R));
					auto LowerDevs = GetDevices(V2, GetChildren);
					std::copy(LowerDevs.begin(), LowerDevs.end(), std::back_inserter(R));
				}
			}
		}

		std::sort(R.begin(), R.end());
		auto Last = std::unique(R.begin(), R.end());
		R.erase(Last, R.end());

		std::vector<std::string> SerialNumbers;

		for (const auto &device : R) {
			ProvObjects::InventoryTag IT;
			if (StorageService()->InventoryDB().GetRecord("id", device, IT)) {
				SerialNumbers.push_back(IT.serialNumber);
			}
		}

		return SerialNumbers;
	}

	void RESTAPI_venue_handler::DoGet() {
		std::string UUID = GetBinding("uuid", "");
		ProvObjects::Venue Existing;
		if (UUID.empty() || !DB_.GetRecord("id", UUID, Existing)) {
			return NotFound();
		}

		if (GetBoolParameter("getDevices")) {
			ProvObjects::VenueDeviceList VDL;
			VDL.id = Existing.info.id;
			VDL.name = Existing.info.name;
			VDL.description = Existing.info.description;
			auto GetChildren = GetBoolParameter("getChildren");
			VDL.devices = GetDevices(Existing, GetChildren);
			Poco::JSON::Object Answer;
			VDL.to_json(Answer);
			return ReturnObject(Answer);
		}

		Poco::JSON::Object Answer;
		if (QB_.AdditionalInfo)
			AddExtendedInfo(Existing, Answer);

		Existing.to_json(Answer);
		ReturnObject(Answer);
	}

	void RESTAPI_venue_handler::DoDelete() {
		std::string UUID = GetBinding("uuid", "");
		ProvObjects::Venue Existing;
		if (UUID.empty() || !DB_.GetRecord("id", UUID, Existing)) {
			return NotFound();
		}

		if (!Existing.children.empty() || !Existing.devices.empty()) {
			return BadRequest(RESTAPI::Errors::StillInUse);
		}

		if (!Existing.contacts.empty()) {
			for (const auto &contact_uuid : Existing.contacts)
				StorageService()->ContactDB().DeleteInUse(
					"id", contact_uuid, StorageService()->VenueDB().Prefix(), UUID);
		}
		if (!Existing.location.empty())
			StorageService()->LocationDB().DeleteInUse("id", Existing.location,
													   StorageService()->VenueDB().Prefix(), UUID);
		if (!Existing.managementPolicy.empty())
			StorageService()->PolicyDB().DeleteInUse("id", Existing.managementPolicy,
													 StorageService()->VenueDB().Prefix(), UUID);
		if (!Existing.deviceConfiguration.empty()) {
			for (auto &configuration_uuid : Existing.deviceConfiguration)
				StorageService()->ConfigurationDB().DeleteInUse(
					"id", configuration_uuid, StorageService()->VenueDB().Prefix(), UUID);
		}
		if (!Existing.parent.empty())
			StorageService()->VenueDB().DeleteChild("id", Existing.parent, UUID);
		if (!Existing.entity.empty())
			StorageService()->EntityDB().DeleteVenue("id", Existing.entity, UUID);
		DB_.DeleteRecord("id", UUID);

		UpdateKafkaProvisioningObject(ProvisioningOperation::removal, Existing);

		return OK();
	}

	void RESTAPI_venue_handler::DoPost() {
		std::string UUID = GetBinding("uuid", "");
		if (UUID.empty()) {
			return BadRequest(RESTAPI::Errors::MissingUUID);
		}

		const auto &RawObject = ParsedBody_;
		ProvObjects::Venue NewObject;
		if (!NewObject.from_json(RawObject)) {
			return BadRequest(RESTAPI::Errors::InvalidJSONDocument);
		}

		if ((RawObject->has("deviceRules") && !ValidDeviceRules(NewObject.deviceRules, *this))) {
			return;
		}

		if (!CreateObjectInfo(RawObject, UserInfo_.userinfo, NewObject.info)) {
			return BadRequest(RESTAPI::Errors::NameMustBeSet);
		}

		if (NewObject.parent.empty() && NewObject.entity.empty()) {
			return BadRequest(RESTAPI::Errors::ParentOrEntityMustBeSet);
		}

		if (!NewObject.parent.empty() && !NewObject.entity.empty()) {
			return BadRequest(RESTAPI::Errors::NotBoth);
		}

		if (!NewObject.parent.empty() && !DB_.Exists("id", NewObject.parent)) {
			return BadRequest(RESTAPI::Errors::VenueMustExist);
		}

		if (NewObject.entity == EntityDB::RootUUID()) {
			return BadRequest(RESTAPI::Errors::ValidNonRootUUID);
		}

		if (!NewObject.entity.empty() &&
			!StorageService()->EntityDB().Exists("id", NewObject.entity)) {
			return BadRequest(RESTAPI::Errors::EntityMustExist);
		}

        if(StorageService()->VenueDB().DoesVenueNameAlreadyExist(NewObject.info.name,NewObject.entity, NewObject.parent)) {
            return BadRequest(RESTAPI::Errors::VenuesNameAlreadyExists);
        }

		if (!NewObject.contacts.empty()) {
			for (const auto &i : NewObject.contacts) {
				if (!StorageService()->ContactDB().Exists("id", i)) {
					return BadRequest(RESTAPI::Errors::ContactMustExist);
				}
			}
		}

		if (!NewObject.location.empty() &&
			!StorageService()->LocationDB().Exists("id", NewObject.location)) {
			return BadRequest(RESTAPI::Errors::LocationMustExist);
		}

		if (!NewObject.managementPolicy.empty() &&
			!StorageService()->PolicyDB().Exists("id", NewObject.managementPolicy)) {
			return BadRequest(RESTAPI::Errors::UnknownManagementPolicyUUID);
		}

		if (!NewObject.deviceConfiguration.empty()) {
			for (auto &i : NewObject.deviceConfiguration) {
				if (!StorageService()->ConfigurationDB().Exists("id", i)) {
					return BadRequest(RESTAPI::Errors::UnknownManagementPolicyUUID);
				}
			}
		}

		if (!NewObject.sourceIP.empty() && !CIDR::ValidateIpRanges(NewObject.sourceIP)) {
			return BadRequest(RESTAPI::Errors::InvalidIPRanges);
		}

		NewObject.children.clear();

		RESTAPI::Errors::msg Error = RESTAPI::Errors::SUCCESS;
		std::vector<std::string> Errors;
		auto ObjectsCreated = CreateObjects(NewObject, *this, Errors);
		if (Error.err_num != 0) {
			return BadRequest(RESTAPI::Errors::InternalError);
		}

		if (DB_.CreateRecord(NewObject)) {
			MoveUsage(StorageService()->ContactDB(), DB_, {}, NewObject.contacts,
					  NewObject.info.id);
			MoveUsage(StorageService()->LocationDB(), DB_, "", NewObject.location,
					  NewObject.info.id);
			MoveUsage(StorageService()->PolicyDB(), DB_, "", NewObject.managementPolicy,
					  NewObject.info.id);
			ManageMembership(StorageService()->EntityDB(), &ProvObjects::Entity::venues, "",
							 NewObject.entity, NewObject.info.id);
			ManageMembership(StorageService()->VenueDB(), &ProvObjects::Venue::children, "",
							 NewObject.parent, NewObject.info.id);
			MoveUsage(StorageService()->ConfigurationDB(), DB_, {}, NewObject.deviceConfiguration,
					  NewObject.info.id);

			ProvObjects::Venue NewRecord;
			DB_.GetRecord("id", NewObject.info.id, NewRecord);
			Poco::JSON::Object Answer;
			NewRecord.to_json(Answer);
			return ReturnObject(Answer);
		}
		InternalError(RESTAPI::Errors::RecordNotCreated);
	}

	/*
	 *      Get all the device types for the devices in the venue
	 *      For each unique device type, get all the firmwares that apply for it
	 *      create the intersection of all resulting firmwares
	 *      create a list ordered in 3 types
	 *      - Releases
	 *      - RC of releases
	 *      - Dev
	 *
	 *      We obtain the revision by stripping and trimming the revision field previous to the "/"
	 *      The sets should be built on the revision and the URI of the images, all per devicetype
	 */

	enum class RevisionTypes { release, release_candidate, development };

	RevisionTypes RevisionIdentify(const std::string &r) {
		auto it = r.find_first_of('/');
		if (it == std::string::npos) {
			return RevisionTypes::development;
		}
		++it;
		if (r.find("TIP-devel-", it) != std::string::npos) {
			return RevisionTypes::development;
		}
		if (r.find("-rc", it) != std::string::npos) {
			return RevisionTypes::release_candidate;
		}
		if (r.find("TIP-v", it) != std::string::npos) {
			return RevisionTypes::release;
		}
		return RevisionTypes::development;
	}

	std::uint64_t RevisionDate(const std::string &R, const std::vector<FMSObjects::Firmware> &F) {

		for (const auto &f : F) {
			if (f.revision == R)
				return f.imageDate;
		}
		return 0;
	}

	void RESTAPI_venue_handler::DoPut() {
		std::string UUID = GetBinding("uuid", "");

		ProvObjects::Venue Existing;
		if (UUID.empty() || !DB_.GetRecord("id", UUID, Existing)) {
			return NotFound();
		}

		auto testUpdateOnly = GetBoolParameter("testUpdateOnly");
		if (testUpdateOnly) {
			ProvObjects::SerialNumberList SNL;
            StorageService()->InventoryDB().GetDevicesForVenue(UUID, SNL.serialNumbers);
			Poco::JSON::Object Answer;
			SNL.to_json(Answer);
			return ReturnObject(Answer);
		}

		if (GetBoolParameter("updateAllDevices")) {
			ProvObjects::SerialNumberList SNL;
            StorageService()->InventoryDB().GetDevicesForVenue(UUID, SNL.serialNumbers);

			Poco::JSON::Object Answer;
			auto JobId = MicroServiceCreateUUID();
			Types::StringVec Parameters{UUID};
			auto NewJob = new VenueConfigUpdater(JobId, "VenueConfigurationUpdater", Parameters, 0,
												 UserInfo_.userinfo, Logger());
			JobController()->AddJob(dynamic_cast<Job *>(NewJob));
			SNL.to_json(Answer);
			Answer.set("jobId", JobId);
			return ReturnObject(Answer);
		}

		if (GetBoolParameter("upgradeAllDevices")) {
			if (GetBoolParameter("revisionsAvailable")) {
				std::set<std::string> DeviceTypes;
                std::vector<ProvObjects::InventoryTag> ExistingDevices;
                StorageService()->InventoryDB().GetDevicesForVenue(UUID, ExistingDevices);
				for (const auto &device : ExistingDevices) {
                    DeviceTypes.insert(device.deviceType);
				}

				//  Get all the revisions for all the device types
				using FirmwareList = std::vector<FMSObjects::Firmware>;
				using FirmwareRevisions = std::set<std::string>;

				std::map<std::string, FirmwareList> AllFMs;
				FirmwareRevisions AllRevisions;
				bool first_pass = true;
				for (const auto &device_type : DeviceTypes) {
					FirmwareList list;
					if (SDK::FMS::Firmware::GetDeviceTypeFirmwares(device_type, list)) {
						AllFMs[device_type] = list;
						FirmwareRevisions DeviceRevisions;
						if (first_pass) {
							for (const auto &revision : list) {
								AllRevisions.insert(revision.revision);
							}
						} else {
							FirmwareRevisions DeviceTypeRevisions;
							for (const auto &revision : list) {
								DeviceTypeRevisions.insert(revision.revision);
							}
							FirmwareRevisions NewRevisions;
							std::set_intersection(
								AllRevisions.begin(), AllRevisions.end(),
								DeviceTypeRevisions.begin(), DeviceTypeRevisions.end(),
								std::inserter(NewRevisions, NewRevisions.begin()));
							AllRevisions = NewRevisions;
						}
					}
					first_pass = false;
				}

				//  OK we start the solution map with Our first device, and then we
				Poco::JSON::Array Releases, ReleaseCandidates, DevelReleases;
				for (const auto &revision : AllRevisions) {
					auto Date = RevisionDate(revision, AllFMs.begin()->second);
					switch (RevisionIdentify(revision)) {
					case RevisionTypes::release_candidate: {
						Poco::JSON::Object E;
						E.set("revision", revision);
						E.set("date", Date);
						ReleaseCandidates.add(E);
					} break;
					case RevisionTypes::release: {
						Poco::JSON::Object E;
						E.set("revision", revision);
						E.set("date", Date);
						Releases.add(E);
					} break;
					case RevisionTypes::development:
					default: {
						Poco::JSON::Object E;
						E.set("revision", revision);
						E.set("date", Date);
						DevelReleases.add(E);
					}
					}
				}
				Poco::JSON::Object Answer;
				Answer.set("releases", Releases);
				Answer.set("releasesCandidates", ReleaseCandidates);
				Answer.set("developmentReleases", DevelReleases);
				return ReturnObject(Answer);
			}

			auto Revision = GetParameter("revision", "");
			if (Revision.empty()) {
				return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters);
			}

            ProvObjects::SerialNumberList SNL;
            StorageService()->InventoryDB().GetDevicesForVenue(UUID, SNL.serialNumbers);

			Poco::JSON::Object Answer;
			auto JobId = MicroServiceCreateUUID();
			Types::StringVec Parameters{UUID, Revision};
			auto NewJob = new VenueUpgrade(JobId, "VenueFirmwareUpgrade", Parameters, 0,
										   UserInfo_.userinfo, Logger());
			JobController()->AddJob(dynamic_cast<Job *>(NewJob));
			SNL.to_json(Answer);
			Answer.set("jobId", JobId);
			return ReturnObject(Answer);
		}

		if (GetBoolParameter("rebootAllDevices")) {
			ProvObjects::SerialNumberList SNL;
            StorageService()->InventoryDB().GetDevicesForVenue(UUID, SNL.serialNumbers);

			Poco::JSON::Object Answer;
			auto JobId = MicroServiceCreateUUID();
			Types::StringVec Parameters{UUID};
			;
			auto NewJob = new VenueRebooter(JobId, "VenueRebooter", Parameters, 0,
											UserInfo_.userinfo, Logger());
			JobController()->AddJob(dynamic_cast<Job *>(NewJob));
			SNL.to_json(Answer);
			Answer.set("jobId", JobId);
			return ReturnObject(Answer);
		}

		const auto &RawObject = ParsedBody_;
		ProvObjects::Venue NewObject;
		if (!NewObject.from_json(RawObject)) {
			return BadRequest(RESTAPI::Errors::InvalidJSONDocument);
		}

		if ((RawObject->has("deviceRules") && !ValidDeviceRules(NewObject.deviceRules, *this))) {
			return;
		}

		if (!UpdateObjectInfo(RawObject, UserInfo_.userinfo, Existing.info)) {
			return BadRequest(RESTAPI::Errors::NameMustBeSet);
		}

		if (RawObject->has("deviceRules"))
			Existing.deviceRules = NewObject.deviceRules;

		if (RawObject->has("sourceIP")) {
			if (!NewObject.sourceIP.empty() && !CIDR::ValidateIpRanges(NewObject.sourceIP)) {
				return BadRequest(RESTAPI::Errors::InvalidIPRanges);
			}
			Existing.sourceIP = NewObject.sourceIP;
		}

		std::string MoveFromEntity, MoveToEntity;
		if (AssignIfPresent(RawObject, "entity", MoveToEntity)) {
			if (MoveToEntity.empty() || !StorageService()->EntityDB().Exists("id", MoveToEntity)) {
				return BadRequest(RESTAPI::Errors::EntityMustExist);
			}
			MoveFromEntity = Existing.entity;
			Existing.entity = MoveToEntity;
		}

		std::string MoveToVenue, MoveFromVenue;
		if (AssignIfPresent(RawObject, "venue", MoveToVenue)) {
			if (MoveToVenue.empty() || !StorageService()->VenueDB().Exists("id", MoveToVenue)) {
				return BadRequest(RESTAPI::Errors::VenueMustExist);
			}
			MoveFromVenue = Existing.parent;
			Existing.parent = MoveToVenue;
		}

		std::string MoveFromLocation, MoveToLocation;
		if (AssignIfPresent(RawObject, "location", MoveToLocation)) {
			if (MoveToLocation.empty() ||
				!StorageService()->LocationDB().Exists("id", MoveToLocation)) {
				return BadRequest(RESTAPI::Errors::LocationMustExist);
			}
			MoveFromLocation = Existing.location;
			Existing.location = MoveToLocation;
		}

		Types::UUIDvec_t MoveFromContacts, MoveToContacts;
		if (AssignIfPresent(RawObject, "contacts", MoveToContacts)) {
			for (const auto &contact : NewObject.contacts) {
				if (!StorageService()->ContactDB().Exists("id", contact)) {
					return BadRequest(RESTAPI::Errors::ContactMustExist);
				}
			}
			MoveFromContacts = Existing.contacts;
			Existing.contacts = MoveToContacts;
		}

		std::string MoveFromPolicy, MoveToPolicy;
		if (AssignIfPresent(RawObject, "managementPolicy", MoveToPolicy)) {
			if (MoveToPolicy.empty() || !StorageService()->PolicyDB().Exists("id", MoveToPolicy)) {
				return BadRequest(RESTAPI::Errors::UnknownManagementPolicyUUID);
			}
			MoveFromPolicy = Existing.managementPolicy;
			Existing.managementPolicy = MoveToPolicy;
		}

		Types::UUIDvec_t MoveToConfigurations, MoveFromConfigurations;
		if (RawObject->has("deviceConfiguration")) {
			MoveToConfigurations = NewObject.deviceConfiguration;
			for (auto &configuration : MoveToConfigurations) {
				if (!StorageService()->ConfigurationDB().Exists("id", configuration)) {
					return BadRequest(RESTAPI::Errors::ConfigurationMustExist);
				}
			}
			MoveToConfigurations = NewObject.deviceConfiguration;
			MoveFromConfigurations = Existing.deviceConfiguration;
			Existing.deviceConfiguration = MoveToConfigurations;
		}

		std::string ErrorText;
		NewObject.parent = Existing.parent;
		NewObject.entity = Existing.entity;

		std::vector<std::string> Errors;
		auto ObjectsCreated = CreateObjects(NewObject, *this, Errors);
		if (!Errors.empty()) {
			return BadRequest(RESTAPI::Errors::ConfigBlockInvalid);
		}

		if (!ObjectsCreated.empty()) {
			if (!ObjectsCreated.empty()) {
				auto it = ObjectsCreated.find("location");
				if (it != ObjectsCreated.end()) {
					MoveFromLocation = "";
					MoveToLocation = it->second;
					Existing.location = MoveToLocation;
				}
			}
		}

		if (RawObject->has("boards")) {
			Existing.boards = NewObject.boards;
		}

		if (StorageService()->VenueDB().UpdateRecord("id", UUID, Existing)) {
			UpdateKafkaProvisioningObject(ProvisioningOperation::modification, Existing);
			MoveUsage(StorageService()->ContactDB(), DB_, MoveFromContacts, MoveToContacts,
					  Existing.info.id);
			MoveUsage(StorageService()->LocationDB(), DB_, MoveFromLocation, MoveToLocation,
					  Existing.info.id);
			MoveUsage(StorageService()->PolicyDB(), DB_, MoveFromPolicy, MoveToPolicy,
					  Existing.info.id);
			ManageMembership(StorageService()->EntityDB(), &ProvObjects::Entity::venues,
							 MoveFromEntity, MoveToEntity, Existing.info.id);
			ManageMembership(StorageService()->VenueDB(), &ProvObjects::Venue::children,
							 MoveFromVenue, MoveToVenue, Existing.info.id);
			MoveUsage(StorageService()->ConfigurationDB(), DB_, MoveFromConfigurations,
					  MoveToConfigurations, Existing.info.id);

			ProvObjects::Venue AddedRecord;
			DB_.GetRecord("id", UUID, AddedRecord);
			Poco::JSON::Object Answer;
			AddedRecord.to_json(Answer);
			return ReturnObject(Answer);
		}
		InternalError(RESTAPI::Errors::RecordNotUpdated);
	}
} // namespace OpenWifi