//---------------------------------------------------------------------------
//
// SCSI Target Emulator PiSCSI
// for Raspberry Pi
//
// Copyright (C) 2021-2023 Uwe Seimet
//
//---------------------------------------------------------------------------

#include "shared/piscsi_util.h"
#include "shared/protobuf_util.h"
#include "shared/piscsi_exceptions.h"
#include "devices/disk.h"
#include "localizer.h"
#include "command_context.h"
#include "piscsi_executor.h"
#include <spdlog/spdlog.h>
#include <sstream>

using namespace spdlog;
using namespace protobuf_util;
using namespace piscsi_util;

bool PiscsiExecutor::ProcessDeviceCmd(const CommandContext& context, const PbDeviceDefinition& pb_device, bool dryRun)
{
	spdlog::info((dryRun ? "Validating: " : "Executing: ") + PrintCommand(context.GetCommand(), pb_device));

	const int id = pb_device.id();
	const int lun = pb_device.unit();

	if (!ValidateIdAndLun(context, id, lun)) {
		return false;
	}

	const PbOperation operation = context.GetCommand().operation();

	// For all commands except ATTACH the device and LUN must exist
	if (operation != ATTACH && !VerifyExistingIdAndLun(context, id, lun)) {
		return false;
	}

	auto device = controller_manager.GetDeviceForIdAndLun(id, lun);

	// Protect against non-existing device
	if (!device) {
		spdlog::error("ProcessDeviceCmd: No device found for id={}, lun={}", id, lun);
		return false;
	}

	if (!ValidateOperationAgainstDevice(context, *device, operation)) {
		return false;
	}

	switch (operation) {
		case START:
			return Start(*device, dryRun);

		case STOP:
			return Stop(*device, dryRun);

		case ATTACH:
			return Attach(context, pb_device, dryRun);

		case DETACH:
			return Detach(context, *device, dryRun);

		case INSERT:
			return Insert(context, pb_device, device, dryRun);

		case EJECT:
			return Eject(*device, dryRun);

		case PROTECT:
			return Protect(*device, dryRun);

		case UNPROTECT:
			return Unprotect(*device, dryRun);
			break;

		case CHECK_AUTHENTICATION:
		case NO_OPERATION:
			// Do nothing, just log
			spdlog::trace("Received " + PbOperation_Name(operation) + " command");
			break;

		default:
			return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION, to_string(operation));
	}

	return true;
}

bool PiscsiExecutor::ProcessCmd(const CommandContext& context)
{
	const PbCommand& command = context.GetCommand();

	// Handle commands that are not device-specific
	switch (command.operation()) {
		case DETACH_ALL:
			DetachAll();
			return context.ReturnSuccessStatus();

		case RESERVE_IDS: {
			if (const string error = SetReservedIds(GetParam(command, "ids")); !error.empty()) {
				return context.ReturnErrorStatus(error);
			}

			return context.ReturnSuccessStatus();
		}

		default:
			// This is a device-specific command handled below
			break;
	}

	// Remember the list of reserved files during the dry run
	const auto& reserved_files = StorageDevice::GetReservedFiles();
	const bool isDryRunError = ranges::find_if_not(command.devices(), [&] (const auto& device)
			{ return ProcessDeviceCmd(context, device, true); }) != command.devices().end();
	StorageDevice::SetReservedFiles(reserved_files);

	if (isDryRunError) {
		return false;
	}

	if (const string error = EnsureLun0(command); !error.empty()) {
		return context.ReturnErrorStatus(error);
	}

	if (ranges::find_if_not(command.devices(), [&] (const auto& device)
			{ return ProcessDeviceCmd(context, device, false); } ) != command.devices().end()) {
		return false;
	}

            // ATTACH and DETACH are special cases because they return the current device list
    return command.operation() == ATTACH || command.operation() == DETACH ? true : context.ReturnSuccessStatus();
}

bool PiscsiExecutor::Start(PrimaryDevice& device, bool dryRun) const
{
	if (!dryRun) {
		spdlog::info("Start requested for " + device.GetIdentifier());

		if (!device.Start()) {
			spdlog::warn("Starting " + device.GetIdentifier() + " failed");
		}
	}

	return true;
}

bool PiscsiExecutor::Stop(PrimaryDevice& device, bool dryRun) const
{
	if (!dryRun) {
		spdlog::info("Stop requested for " + device.GetIdentifier());

		device.Stop();
	}

	return true;
}

bool PiscsiExecutor::Eject(PrimaryDevice& device, bool dryRun) const
{
	if (!dryRun) {
		spdlog::info("Eject requested for " + device.GetIdentifier());

		if (!device.Eject(true)) {
			spdlog::warn("Ejecting " + device.GetIdentifier() + " failed");
		}
	}

	return true;
}

bool PiscsiExecutor::Protect(PrimaryDevice& device, bool dryRun) const
{
	if (!dryRun) {
		spdlog::info("Write protection requested for " + device.GetIdentifier());

		device.SetProtected(true);
	}

	return true;
}

bool PiscsiExecutor::Unprotect(PrimaryDevice& device, bool dryRun) const
{
	if (!dryRun) {
		spdlog::info("Write unprotection requested for " + device.GetIdentifier());

		device.SetProtected(false);
	}

	return true;
}

bool PiscsiExecutor::Attach(const CommandContext& context, const PbDeviceDefinition& pb_device, bool dryRun)
{
	const int id = pb_device.id();
	const int lun = pb_device.unit();
	const PbDeviceType type = pb_device.type();

	if (lun >= ControllerManager::GetScsiLunMax()) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_LUN, to_string(lun),
				to_string(ControllerManager::GetScsiLunMax()));
	}

	if (controller_manager.HasDeviceForIdAndLun(id, lun)) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_DUPLICATE_ID, to_string(id), to_string(lun));
	}

	if (reserved_ids.contains(id)) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_RESERVED_ID, to_string(id));
	}

	const string filename = GetParam(pb_device, "file");

	auto device = CreateDevice(context, type, lun, filename);
	if (device == nullptr) {
		return false;
	}

	// If no filename was provided the medium is considered not inserted
	device->SetRemoved(device->SupportsFile() ? filename.empty() : false);

	if (!SetProductData(context, pb_device, *device)) {
		return false;
	}

	if (!SetSectorSize(context, device, pb_device.block_size())) {
		return false;
	}

	const auto storage_device = dynamic_pointer_cast<StorageDevice>(device);
	if (device->SupportsFile()) {
		// Only with removable media drives, CD and MO the medium (=file) may be inserted later
		if (!device->IsRemovable() && filename.empty()) {
			return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_FILENAME, PbDeviceType_Name(type));
		}

		if (!ValidateImageFile(context, *storage_device, filename)) {
			return false;
		}
	}

	// Only non read-only devices support protect/unprotect
	// This operation must not be executed before Open() because Open() overrides some settings.
	if (device->IsProtectable() && !device->IsReadOnly()) {
		device->SetProtected(pb_device.protected_());
	}

	// Stop the dry run here, before actually attaching
	if (dryRun) {
		return true;
	}

	param_map params = { pb_device.params().begin(), pb_device.params().end() };
	if (!device->SupportsFile()) {
		// Clients like scsictl might have sent both "file" and "interfaces"
		params.erase("file");
	}

	if (!device->Init(params)) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_INITIALIZATION, device->GetIdentifier());
	}

	if (!controller_manager.AttachToController(bus, id, device)) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_SCSI_CONTROLLER);
	}

	if (storage_device != nullptr && !storage_device->IsRemoved()) {
		storage_device->ReserveFile();
	}

	string msg = "Attached ";
	if (device->IsReadOnly()) {
		msg += "read-only ";
	}
	else if (device->IsProtectable() && device->IsProtected()) {
		msg += "protected ";
	}
	msg += device->GetIdentifier();
	spdlog::info(msg);

	return true;
}

bool PiscsiExecutor::Insert(const CommandContext& context, const PbDeviceDefinition& pb_device,
		const shared_ptr<PrimaryDevice>& device, bool dryRun) const
{
	if (!device->SupportsFile()) {
		return false;
	}

	if (!device->IsRemoved()) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_EJECT_REQUIRED);
	}

	if (!pb_device.vendor().empty() || !pb_device.product().empty() || !pb_device.revision().empty()) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_DEVICE_NAME_UPDATE);
	}

	const string filename = GetParam(pb_device, "file");
	if (filename.empty()) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_FILENAME);
	}

	// Stop the dry run here, before modifying the device
	if (dryRun) {
		return true;
	}

	spdlog::info("Insert " + string(pb_device.protected_() ? "protected " : "") + "file '" + filename +
			"' requested into " + device->GetIdentifier());

	if (!SetSectorSize(context, device, pb_device.block_size())) {
		return false;
	}

	auto storage_device = dynamic_pointer_cast<StorageDevice>(device);
	if (!ValidateImageFile(context, *storage_device, filename)) {
		return false;
	}

	storage_device->SetProtected(pb_device.protected_());
	storage_device->ReserveFile();
	storage_device->SetMediumChanged(true);

	return true;
}

bool PiscsiExecutor::Detach(const CommandContext& context, PrimaryDevice& device, bool dryRun)
{
	auto controller = controller_manager.FindController(device.GetId());
	if (controller == nullptr) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH);
	}

	// LUN 0 can only be detached if there is no other LUN anymore
	if (!device.GetLun() && controller->GetLunCount() > 1) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_LUN0);
	}

	if (!dryRun) {
		// Remember the device identifier for the log message before the device data become invalid on removal
		const string identifier = device.GetIdentifier();

		if (!controller->RemoveDevice(device)) {
			return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH);
		}

		// If no LUN is left also delete the controller
		if (!controller->GetLunCount() && !controller_manager.DeleteController(*controller)) {
			return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH);
		}

		spdlog::info("Detached " + identifier);
	}

	return true;
}

void PiscsiExecutor::DetachAll()
{
	controller_manager.DeleteAllControllers();

	spdlog::info("Detached all devices");
}


string PiscsiExecutor::SetReservedIds(string_view ids)
{
	set<int> ids_to_reserve;
	stringstream ss(ids.data());
    string id;
    while (getline(ss, id, ',')) {
    	int res_id;
    	if (!GetAsUnsignedInt(id, res_id) || res_id > 7) {
    		return "Invalid ID " + id;
    	}

    	if (controller_manager.HasController(res_id)) {
    		return "ID " + id + " is currently in use";
    	}

    	ids_to_reserve.insert(res_id);
    }

    reserved_ids = { ids_to_reserve.begin(), ids_to_reserve.end() };

    if (!ids_to_reserve.empty()) {
    	spdlog::info("Reserved ID(s) set to " + Join(ids_to_reserve));
    }
    else {
    	spdlog::info("Cleared reserved ID(s)");
    }

	return "";
}

bool PiscsiExecutor::ValidateImageFile(const CommandContext& context, StorageDevice& storage_device,
		const string& filename) const
{
	if (filename.empty()) {
		return true;
	}

	if (!CheckForReservedFile(context, filename)) {
		return false;
	}

	storage_device.SetFilename(filename);

	if (!StorageDevice::FileExists(filename)) {
		// If the file does not exist search for it in the default image folder
		const string effective_filename = context.GetDefaultFolder() + "/" + filename;

		if (!CheckForReservedFile(context, effective_filename)) {
			return false;
		}

		storage_device.SetFilename(effective_filename);
	}

	try {
		storage_device.Open();
	}
	catch(const io_exception&) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_FILE_OPEN, storage_device.GetFilename());
	}

	return true;
}

bool PiscsiExecutor::CheckForReservedFile(const CommandContext& context, const string& filename)
{
	if (const auto [id, lun] = StorageDevice::GetIdsForReservedFile(filename); id != -1) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_IMAGE_IN_USE, filename,
				to_string(id) + ":" + to_string(lun));
	}

	return true;
}

string PiscsiExecutor::PrintCommand(const PbCommand& command, const PbDeviceDefinition& pb_device) const
{
	const map<string, string, less<>> params = { command.params().begin(), command.params().end() };

	ostringstream s;
	s << "operation=" << PbOperation_Name(command.operation());

	if (!params.empty()) {
		s << ", command params=";
		bool isFirst = true;
		for (const auto& [key, value] : params) {
			if (!isFirst) {
				s << ", ";
			}
			isFirst = false;
			string v = key != "token" ? value : "???";
			s << "'" << key << "=" << v << "'";
		}
	}

	s << ", device=" << pb_device.id() << ":" << pb_device.unit() << ", type=" << PbDeviceType_Name(pb_device.type());

	if (pb_device.params_size()) {
		s << ", device params=";
		bool isFirst = true;
		for (const auto& [key, value]: pb_device.params()) {
			if (!isFirst) {
				s << ":";
			}
			isFirst = false;
			s << "'" << key << "=" << value << "'";
		}
	}

	s << ", vendor='" << pb_device.vendor() << "', product='" << pb_device.product()
		<< "', revision='" << pb_device.revision() << "', block size=" << pb_device.block_size();

	return s.str();
}

string PiscsiExecutor::EnsureLun0(const PbCommand& command) const
{
	// Mapping of available LUNs (bit vector) to devices
	unordered_map<int32_t, int32_t> luns;

	// Collect LUN bit vectors of new devices
	for (const auto& device : command.devices()) {
		luns[device.id()] |= 1 << device.unit();
	}

	// Collect LUN bit vectors of existing devices
	for (const auto& device : controller_manager.GetAllDevices()) {
		luns[device->GetId()] |= 1 << device->GetLun();
	}

	const auto& it = ranges::find_if_not(luns, [] (const auto& l) { return l.second & 0x01; } );
	return it == luns.end() ? "" : "LUN 0 is missing for device ID " + to_string((*it).first);
}

bool PiscsiExecutor::VerifyExistingIdAndLun(const CommandContext& context, int id, int lun) const
{
	if (!controller_manager.HasController(id)) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_NON_EXISTING_DEVICE, to_string(id));
	}

	if (!controller_manager.HasDeviceForIdAndLun(id, lun)) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_NON_EXISTING_UNIT, to_string(id), to_string(lun));
	}

	return true;
}

shared_ptr<PrimaryDevice> PiscsiExecutor::CreateDevice(const CommandContext& context, const PbDeviceType type,
		int lun, const string& filename) const
{
	auto device = device_factory.CreateDevice(type, lun, filename);
	if (device == nullptr) {
		if (type == UNDEFINED) {
			context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, filename);
		}
		else {
			context.ReturnLocalizedError(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, PbDeviceType_Name(type));
		}
	}

	return device;
}

bool PiscsiExecutor::SetSectorSize(const CommandContext& context, shared_ptr<PrimaryDevice> device, int size) const
{
	if (size) {
		const auto disk = dynamic_pointer_cast<Disk>(device);
		if (disk != nullptr && disk->IsSectorSizeConfigurable()) {
			if (!disk->SetConfiguredSectorSize(size)) {
				return context.ReturnLocalizedError(LocalizationKey::ERROR_BLOCK_SIZE, to_string(size));
			}
		}
		else {
			return context.ReturnLocalizedError(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE,
					device->GetTypeString());
		}
	}

	return true;
}

bool PiscsiExecutor::ValidateOperationAgainstDevice(const CommandContext& context, const PrimaryDevice& device,
		PbOperation operation)
{
	if ((operation == START || operation == STOP) && !device.IsStoppable()) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, PbOperation_Name(operation),
				device.GetTypeString());
	}

	if ((operation == INSERT || operation == EJECT) && !device.IsRemovable()) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, PbOperation_Name(operation),
				device.GetTypeString());
	}

	if ((operation == PROTECT || operation == UNPROTECT) && !device.IsProtectable()) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, PbOperation_Name(operation),
				device.GetTypeString());
	}

	if ((operation == PROTECT || operation == UNPROTECT) && !device.IsReady()) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_READY, PbOperation_Name(operation),
				device.GetTypeString());
	}

	return true;
}

bool PiscsiExecutor::ValidateIdAndLun(const CommandContext& context, int id, int lun)
{
	if (id < 0) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_DEVICE_ID);
	}
	if (id >= ControllerManager::GetScsiIdMax()) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_ID, to_string(id),
				to_string(ControllerManager::GetScsiIdMax() - 1));
	}
	if (lun < 0 || lun >= ControllerManager::GetScsiLunMax()) {
		return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_LUN, to_string(lun),
				to_string(ControllerManager::GetScsiLunMax() - 1));
	}

	return true;
}

bool PiscsiExecutor::SetProductData(const CommandContext& context, const PbDeviceDefinition& pb_device,
		PrimaryDevice& device)
{
	try {
		if (!pb_device.vendor().empty()) {
			device.SetVendor(pb_device.vendor());
		}
		if (!pb_device.product().empty()) {
			device.SetProduct(pb_device.product());
		}
		if (!pb_device.revision().empty()) {
			device.SetRevision(pb_device.revision());
		}
	}
	catch(const invalid_argument& e) {
		return context.ReturnErrorStatus(e.what());
	}

	return true;
}
