#pragma once

#include "GateRelay.h"
#include "Common/FSecure/Sockets/Sockets.hpp"

namespace FSecure::C3::Core
{
	/// Virtualizes whole C3 Network with it's remote elements.
	struct Profiler : std::enable_shared_from_this<Profiler>
	{
		/// Build properties
		struct BuildProperties
		{
			bool m_IsX64 = false;												///< Is x64 build (true = x64, false = x86)
			bool m_IsBanned = false;											///< Is build banned
			json m_StartupCmd;													///< Build startup command
		};

		// An std::vector-based container used by Profiler to manage its sub-types.
		template<typename Element>
		struct Manager
		{
			/// Finds Element specified by ID.
			/// @param id ID of the Element to find.
			/// @return Element object if existed, otherwise null.
			Element* Find(typename Element::Id const& id)
			{
				auto it = FindElementIterator(id);
				return it == m_Elements.end() ? nullptr : &*it;
			}

			/// Adds a new Element.
			/// @param id ID of the Element to add.
			/// @param element Element to add.
			/// @return the newly created Element.
			/// @throw std::invalid_argument if specified ID is already in use.
			Element* Add(typename Element::Id id, Element const& element)
			{
				if (FindElementIterator(id) != m_Elements.end())
					throw std::invalid_argument{ OBF("Element with specified ID already exists.") };

				m_Elements.push_back(std::move(element));
				return &m_Elements.back();
			}

			/// Removes Element from container.
			/// @param id ID of the Element to remove.
			/// @throw std::invalid_argument on an attempt of removal of a non-existent Element.
			void Remove(typename Element::Id id)
			{
				if (auto it = FindElementIterator(id); it != m_Elements.end())
					m_Elements.erase(it);
				else
					throw std::invalid_argument{ OBF("Attempt of removing Element that doesn't exist.") };
			}

			/// Removes all elements matching the predicate
			/// @param predicate function that returns true for requested Element.
			template<typename Predicate>
			void RemoveIf(Predicate predicate)
			{
				m_Elements.erase(std::remove_if(begin(m_Elements), end(m_Elements), predicate), end(m_Elements));
			}


			/// Tries to remove Element from container.
			/// @param id ID of the Element to remove.
			bool TryRemove(typename Element::Id id)
			{
				if (auto it = FindElementIterator(id); it != m_Elements.end())
				{
					m_Elements.erase(it);
					return true;
				}
				else
					return false;
			}

			/// Dumps container contents to JSON format.
			/// @return Container description in JSON format.
			virtual json CreateProfileSnapshot() const
			{
				auto profile = json::array();
				for (auto& element : m_Elements)
					profile += element.CreateProfileSnapshot();

				return profile;
			}

			/// Remove all elements
			void Clear() noexcept
			{
				m_Elements.clear();
			}

			// TODO remove this method
			auto const& GetUnderlyingContainer() const noexcept
			{
				return m_Elements;
			}

			// TODO remove this method
			auto& GetUnderlyingContainer() noexcept
			{
				return m_Elements;
			}

		protected:
			/// Finds Element iterator specified by ID.
			/// @param id ID of the Element to find.
			/// @return Element iterator if existed, otherwise null.
			typename std::vector<Element>::iterator FindElementIterator(typename Element::Id const& id)
			{
				return std::find_if(m_Elements.begin(), m_Elements.end(), [&id](Element const& element) { return id == element.m_Id; });
			}

		private:
			std::vector<Element> m_Elements;																			///< Elements Container.
		};

		/// Basic class of a remote C3 Network elements.
		struct ProfileElement
		{
			/// Constuct a profile elment
			/// @param owner - Profile that owns this element
			ProfileElement(std::weak_ptr<Profiler> owner);

			/// Destructor
			virtual ~ProfileElement() = default;

			/// Dumps current Profile to JSON.
			/// @return Network Profile in JSON format.
			virtual json CreateProfileSnapshot() const;

			/// Performs C3 Command on itself.
			/// @param commandWithArguments whole Command in binary format.
			virtual void RunCommand(ByteView commandWithArguments) = 0;

			std::string m_ErrorState;																					///< A message like "BuildId collision" or empty if working correctly.
			std::weak_ptr<Profiler> m_Owner;																			///< Owner Profiler.
		};

		/// Virtual image of Device.
		struct Device : ProfileElement
		{
			using Id = DeviceId;																						///< ID typedef.

			/// Public ctor.
			/// @param id Device Identifier.
			/// @param typeHash Type of the Device.
			Device(std::weak_ptr<Profiler> owner, Id id, HashT typeHash);

			/// Destructor
			virtual ~Device() = default;

			/// Dumps current Profile to JSON.
			/// @return Network Profile in JSON format.
			json CreateProfileSnapshot() const override;

			/// Performs C3 Command on itself.
			/// @param commandWithArguments whole Command in binary format.
			void RunCommand(ByteView commandWithArguments) override;

			Id m_Id;																									///< Device Identifier.
			HashT m_TypeHash;																							///< Type name hash of the Device.
			json m_StartupArguments;																					///< Device's startup arguments
			std::pair<std::chrono::milliseconds, std::chrono::milliseconds> m_Jitter;									///< Current jitter pm device
		};

		/// Virtual image of Device.
		struct Channel : Device
		{
			using Id = DeviceId;																						///< ID typedef.

			/// Public ctor.
			/// @param id Device Identifier.
			/// @param typeHash Type of the Device.
			Channel(std::weak_ptr<Profiler> owner, Id id, HashT typeHash, bool isReturnChannel = false, bool isNegotiationChannel = false);

			/// Destructor
			virtual ~Channel() = default;

			/// Dumps current Profile to JSON.
			/// @return Network Profile in JSON format.
			json CreateProfileSnapshot() const override;

			bool m_IsReturnChannel;
			bool m_IsNegotiationChannel;
		};

		/// Forward declaration.
		struct Agent;

		/// Route is used by Relays to indicate that a particular Device (actually a Channel) is used to transport packets towards a specific Agent.
		struct Route : ProfileElement
		{
			/// Route constructor
			/// @param owner - owner of this element
			/// @param rid - route id
			/// @param outgoingDeviceId
			/// @param isNeighbour - if agent specified by rid is neighbouring the owner agent
			Route(std::weak_ptr<Profiler> owner, RouteId rid, Device::Id outgoingDeviceId, bool isNeighbour = false);

			/// Destructor
			virtual ~Route() = default;

			/// Dumps current Profile to JSON.
			/// @return Network Profile in JSON format.
			json CreateProfileSnapshot() const override;

			/// Performs C3 Command on itself.
			/// @param commandWithArguments whole Command in binary format.
			void RunCommand(ByteView commandWithArguments) override;

			Device::Id m_OutgoingDevice;

			using Id = RouteId;																							///< ID typedef.
			Id m_Id;																									///< Route Identifier.
			bool m_IsNeighbour = false;																					///< Is m_ReceivingAgent just after one Route hop?
		};

		/// Abstract class for all Relays.
		struct Relay : ProfileElement
		{
			/// A public ctor.
			/// @param agentId dynamic ID of the Relay.
			/// @param buildId Build identifier.
			/// @param encryptionKey asymmetric public key used to encrypt all outgoing transmission.
			/// @param isBanned flag indicating whether Agent should be added to the black-list.
			Relay(std::weak_ptr<Profiler> owner, AgentId agentId, BuildId buildId, int32_t lastSeen);

			/// Destructor
			virtual ~Relay() = default;

			using Id = AgentId;																							///< ID typedef.
			Id m_Id;																									///< Dynamic ID of the Relay.
			BuildId m_BuildId;																							///< Static ID of the Relay binary.

			/// Performs Command on a Device indicated by provided JSON format.
			/// @param jCommandElement JSON element to parse.
			/// @param commandWithArgs Command in binary format to run on Device.
			virtual void ParseAndRunCommand(json const& jCommandElement) noexcept(false);

			/// Performs C3 Command on itself.
			/// @param commandWithArguments whole Command in binary format.
			void RunCommand(ByteView commandWithArguments) override;

			/// Reprofile: AddNewChannel.
			/// @param did ID of the CHannel that was added.
			/// @param typeNameHash type name hash of a CHannel that was added.
			/// @param isReturnChannel if channel is a return channel.
			/// @param isNegotiationChannel if channel is a negotiation channel.
			virtual Channel* ReAddChannel(Device::Id did, HashT typeNameHash, bool isReturnChannel = false, bool isNegotiationChannel = false);

			/// Reprofile: AddNewPeripheral.
			/// @param did ID of the Peripheral that was added.
			/// @param typeNameHash type name hash of a Peripheral that was added.
			virtual Device* ReAddPeripheral(Device::Id did, HashT typeNameHash);

			/// Find a device that lies in direction of Agent
			/// @param aid - agent id to be reached
			/// @returns DeviceId through which agent aid can be reached
			DeviceId FindDirectionDevice(AgentId aid);

			/// Update new channel parameters based on negotiation channel parameters
			/// @param negotiationDid - negotiation channel device Id
			/// @param newDeviceId - newly created (negotiated) device
			/// @param newInputId - negotiated InputId
			/// @param newOutputId - negotiated OutputId
			void UpdateFromNegotiationChannel(DeviceId negotiationDid, DeviceId newDeviceId, std::string newInputId, std::string newOutputId);

			/// Add new route
			/// @param receivingRid - Destination receiving route id
			/// @param outgoingInterface - channel pointed towards receiving agent
			/// @param isNeighbour - true if agent specified in receivingRid is neigbour
			void ReAddRoute(RouteId receivingRid, DeviceId outgoingInterface, bool isNeighbour);

			/// Remove route
			/// @param rid - route to remove
			void ReRemoveRoute(RouteId rid);

			Manager<Route> m_Routes;																					///< Container for Routes.
			Manager<Channel> m_Channels;																				///< Container for Channels.
			Manager<Device> m_Peripherals;																				///< Container for Peripherals.

			DeviceId::UnderlyingIntegerType m_LastDeviceId = 0;															///< This value gets increased with every creation of an Device.

			int32_t m_LastSeen; // fixed size instead of 32 or 64 bits time_t
		};

		/// Virtual image of NodeRelay.
		struct Agent : Relay
		{
			/// A public ctor.
			/// @param agentId dynamic ID of the Relay.
			/// @param buildId Build identifier.
			/// @param encryptionKey asymmetric public key used to encrypt all outgoing transmission.
			/// @param isBanned flag indicating whether Agent should be added to the black-list.
			/// @param lastSeen timestamp when agent was last seen (responded)
			/// @param hostInfo agent's host information
			Agent(std::weak_ptr<Profiler> owner, AgentId agentId, BuildId buildId, FSecure::Crypto::PublicKey encryptionKey, bool isBanned, int32_t lastSeen, bool isX64, HostInfo hostInfo);

			/// Destructor
			virtual ~Agent() = default;

			/// Dumps current Profile to JSON.
			/// @return Network Profile in JSON format.
			json CreateProfileSnapshot() const override;

			/// Performs Command on a Device indicated by provided JSON format.
			/// @param jCommandElement JSON element to parse.
			/// @param commandWithArgs Command in binary format to run on Device.
			void ParseAndRunCommand(json const& jCommandElement) noexcept(false) override;

			/// Performs C3 Command on remote NodeRelay.
			/// @param commandWithArguments whole Command in json format.
			void RunCommand(ByteView commandWithArguments) override;

			/// Performs a create command (create new device)
			/// @param commandWithArguments whole Command in json format.
			void PerformCreateCommand(json const& jCommandElement);

			/// Add channel
			/// @param did - new channel DeviceId
			/// @param typeNameHash - new channel typename hash
			/// @param isReturnChannel - true if channel is a gateway return channel
			/// @param isNegotiationChannel - true if channel is a negotiation channel
			/// @returns a pointer to newly created channel
			Channel* ReAddChannel(Device::Id did, HashT typeNameHash, bool isReturnChannel = false, bool isNegotiationChannel = false) override;

			/// Add peripheral
			/// @param did - new peripheral DeviceId
			/// @param typeNameHash - new peripheral typename hash
			/// @returns a pointer to newly created peripheral
			Device* ReAddPeripheral(Device::Id did, HashT typeNameHash) override;

			/// Add scheduled device parameters
			/// @param deviceId - scheduled device Id
			/// @param command - scheduled device command
			void AddScheduledDevice(DeviceId deviceId, json command);

			/// Find gateway return channel
			/// @returns return channel or nullptr
			Channel* FindGrc();

			FSecure::Crypto::PublicKey m_EncryptionKey;																		///< Agent's public key.
			HostInfo m_HostInfo;																						///< Agent's Host information
			bool m_IsBanned;																							///< Is Agent black-listed?
			bool m_IsX64;

		private:
			std::unordered_map<DeviceId::UnderlyingIntegerType, json> m_ScheduledDevices;
		};

		/// Virtual image of Gateway.
		struct Gateway : Relay
		{
			/// A public ctor.
			/// @param gateway pointer to Gate Relay.
			Gateway(std::weak_ptr<Profiler> owner, std::string name, std::shared_ptr<GateRelay> gateway);

			/// Destructor
			virtual ~Gateway() = default;

			/// Reprofile: TurnOnConnector.
			/// @param typeNameHash type name hash of a Connector that was turned on.
			/// @param connector pointer to the Connector object.
			void ReTurnOnConnector(HashT typeNameHash, std::shared_ptr<ConnectorBridge> connector);

			/// Reprofile: AddAgent.
			/// @param agentId - new agent Id
			/// @param buildId - new agents' build Id
			/// @param encryptionKey - new agent's public encryption key
			/// @param isBanned - is agent banned
			/// @param lastSeen - when agent was last seen
			/// @param hostInfo - new agnet's host information
			Agent* ReAddAgent(AgentId agentId, BuildId buildId, FSecure::Crypto::PublicKey encryptionKey, bool isBanned, int32_t lastSeen, HostInfo hostInfo);

			/// Reprofile: Add remote agent (agent not neigbouring with gateway)
			/// @param agentId - new agent Id
			/// @param buildId - new agents' build Id
			/// @param encryptionKey - new agent's public encryption key
			/// @param ridOfConectionPlace
			/// @param childGrcHash - new agent's
			/// @param lastSeen - when agent was last seen
			/// @param hostInfo - new agnet's host information
			Agent* ReAddRemoteAgent(RouteId childRouteId, BuildId buildId, FSecure::Crypto::PublicKey encryptionKey, RouteId ridOfConectionPlace, HashT childGrcHash, int32_t lastSeen, HostInfo hostInfo);

			/// Find an agent directly connected to relay through given channel
			/// @param relay - relay whose neighbour to find
			/// @param did - device id through which the agent is connected
			/// @returns direct neighbour to relay
			/// @throws std::logic_error
			Agent* FindNeighborOnDevice(Relay& relay, DeviceId did);

			/// Add new known build
			/// @oaram bid - new build Id
			/// @param properties - build properties
			void AddAgentBuild(BuildId bid, BuildProperties properties);

			/// Get a list of all agents on the path from agent to gateway
			/// @param agent - starting point of the path
			/// @returns list of agents on path from agent to gateway
			/// @throws std::runtime_error if any agent on the path doesn't have a gateway return channel
			std::vector<Agent*> GetPathFromAgent(Agent* agent);

			/// Dumps current Profile to JSON.
			/// @return Network Profile in JSON format.
			json CreateProfileSnapshot() const override;

			/// Adds a default 'create' property
			/// @param interface - json definition of interface
			static void EnsureCreateExists(json& interface);

			/// Adds built-in command definitions (Close/TurnOff and UpdateDelay)
			/// @param interface - json definition of interface
			/// @param isDevice - whether interfce is a device (channel/peripheral) or not (connstors)
			static void AddBuildInCommands(json& interface, bool isDevice);

			/// Get JSON representing available Commands.
			/// @return Network's Capability in JSON format.
			json GetCapability();

			/// Performs C3 Command on itself.
			/// @param commandWithArguments whole Command in binary format.
			void RunCommand(ByteView commandWithArguments) override;

			/// Performs Command on a Device indicated by provided JSON format.
			/// @param jCommandElement JSON element to parse.
			/// @param commandWithArgs Command in binary format to run on Device.
			void ParseAndRunCommand(json const& jCommandElement) noexcept(false) override;

			std::string m_Name;																							///< Gateway's Name
			std::weak_ptr<GateRelay> m_Gateway;																			///< The "physical" Gateway.
			Manager<Agent> m_Agents;																					///< Table of Agents.
			std::map<BuildId, BuildProperties> m_AgentBuilds;															///< Known agent builds

			/// Virtual image of Connector.
			struct Connector : ProfileElement
			{
				using Id = HashT;																						///< ID typedef.

				/// Public ctor.
				/// @param id Connector identifier.
				/// @param connector the Connector object.
				Connector(std::weak_ptr<Profiler> owner, Id id, std::shared_ptr<ConnectorBridge> connector);

				/// Destructor
				virtual ~Connector() = default;

				/// Dumps Profile to JSON.
				/// @return Network Profile in JSON format.
				json CreateProfileSnapshot() const override;

				/// Performs C3 Command on itself.
				/// @param commandWithArguments whole Command in binary format.
				void RunCommand(ByteView commandWithArguments) override;

				Id m_Id;																								///< Hash of the name of the Connector.
				std::weak_ptr<ConnectorBridge> m_Connector;																///< Pointer to the Connector object.
				json m_StartupArguments;
			};

			/// Performs a create command (create new device)
			/// @param commandWithArguments whole Command in json format.
			void PerformCreateCommand(json const& jCommandElement);

			struct CreateCommand
			{
				uint16_t m_Id;
				uint32_t m_Hash;
				bool m_IsDevice;
				bool m_IsNegotiableChannel;
			};

			std::vector<CreateCommand> m_CreateCommands;
			Manager<Connector> m_Connectors;																			///< Container for Connectors.

			/// Reprofile - Turn off connector
			/// Removes connector from profiler
			/// @param connectorNameHash - connector typename hash
			void ReTurnOffConnector(HashT connectorNameHash);

			/// Reprofile - Delete channel
			/// Removes channel from profiler
			/// @param iidOfDeviceToDetach - channel's device id
			void ReDeleteChannel(DeviceId iidOfDeviceToDetach);

			/// Reprofile - Delete peripheral
			/// Removes peripheral from profiler
			/// @param iidOfDeviceToDetach - peripheral's device id
			void ReDeletePeripheral(DeviceId iidOfDeviceToDetach);

			/// Update last-seen timestamps on route to given agent
			/// @param agentId - the last agent on the route (origin on the message)
			/// @param timestamp - current timestamp
			void UpdateRouteTimestamps(AgentId agentId, int32_t timestamp);

			/// Find a (parent) agent directly connected through (child) agent's return channel
			/// @param agent - child agent
			/// @returns agent directly connected through gateway return channel
			Agent* FindGatewaySideAgent(Agent* agent);

			/// Check if connection to agent exists
			/// @param agentId - agent to check connection to
			/// @returns true if any route to agent exists
			bool ConnectionExist(AgentId agentId);

			/// Update negotiated channel parameters (arguments) if all the necessary information is available.
			/// @param connecitonPlace - route Id constructed from agent and device channel ID's to update
			void ConditionalUpdateChannelParameters(RouteId connectionPlace);

			/// Reset profile state - remove all elements
			void Reset();
		};

		/// Public ctor.
		Profiler(std::filesystem::path snapshotPath);

		/// @param gateway pointer to Gate Relay.
		void Initialize(std::string name, std::shared_ptr<GateRelay> gateway);

		/// Performs Actions parsed from provided packet.
		/// @param actionsPacket packet to parse.
		void HandleActionsPacket(ByteView actionsPacket);

		/// NewBuild message handler
		/// @param message - NewBuild message to process.
		/// @returns json representation of NewBuild response
		json HandleNewBuildMessage(json const& message);

		/// Translate command from JSON to binary representation
		/// @param command - command in JSON format
		/// @returns Binary representation of command [commandId][packed arguments]
		/// @throws nlohmann::basic_json::exception
		static ByteVector TranslateCommand(json const& command);

		/// Translate arguments from JSON to binary representation
		/// @param command - command in JSON format
		/// @returns Binary representation of arguments [packed arguments]
		/// @throws nlohmann::basic_json::exception
		static ByteVector TranslateArguments(json const& arguments);

		/// Translate startup command from JSON to binary representation
		/// @param command - command in JSON format
		/// @returns Binary representation of command [add device command arguments]
		/// @throws nlohmann::basic_json::exception
		ByteVector TranslateStartupCommand(json const& jcommand);

		/// Translate single argument from JSON to binary representation
		/// @param type - string decryption of argument
		/// @param value - json value to translate
		/// @returns Binary representation of argument [packed argument]
		/// @throws std::out_of_range if any integer value casting fails
		/// @throws std::invalid_argument if type is not any of [int8, int16, int32, int64, uint8, uint16, uint32, uint64, float, boolean, string, ip, binary]
		static ByteVector Translate(std::string const& type, json::value_type const& value);

		/// Current Profile snapshot with synchronized access.
		struct Profile
		{
			/// Public ctor.
			/// @param gateway which profile is locked
			Profile(Gateway& gateway);

			Gateway& m_Gateway;																							///< Gateway profile.

		private:
			std::unique_lock<std::mutex> m_Lock;																		///< Access lock.
			static std::mutex m_Mutex;																					///< Lock object.
		};

		/// Profile getter.
		/// @return Current Profile snapshot.
		Profile Get();

		/// Maps peripheral type hash to connector type hash and other way around
		/// @param id beacon hash
		/// @returns binder id
		uint32_t GetBinderTo(uint32_t);

		/// Helper to wrap calls to CreateProfileShnapshot
		class SnapshotProxy
		{
		public:
			/// Create a snapshot proxy
			/// @param profiler to wrap CreateProfileShnapshot calls
			SnapshotProxy(Profiler& profiler);

			/// Check if new version of snapshot is available.
			/// @param ob. Observer token. If observer is not registered function will always return true.
			/// @returns true if new snapshot is available.
			bool CheckUpdates();

			/// Create a snapshot
			/// @return std::nullopt if snaphot hasn't change since the last call
			json const& GetSnapshot() const;

		private:
			/// Proxied profiler
			Profiler& m_Profiler;

			/// helper state variable
			std::optional<size_t> m_PreviousHash;

			/// Current snapshot
			json m_CurrentSnapshot;
		};

		/// Create Snapshot proxy for this profiler
		/// @returns snapshot proxy for this profiler
		SnapshotProxy GetSnapshotProxy() { return SnapshotProxy(*this); }

	protected:
		std::optional<Gateway> m_Gateway;																				///< The "virtual gateway object".

		mutable std::mutex m_AccessMutex;																				///< Mutex for synchronization.

		/// Contains hashes of binders. This allows to call: auto tsConnectorhash = GetBinderTo(hashBeacona);. First hash in pair is Peripheral hash and second one is corresponding Connector.
		std::vector<std::pair<std::uint32_t, std::uint32_t>> m_BindersMappings;

		private:
			/// Dump snapshot file in regular intervals
			/// Creates a snapshot .tmp file and replaces previous snapshot file
			void DumpSnapshots();

			/// Restore Gateway and profiler state from snapshot
			void RestoreFromSnapshot();

			std::filesystem::path m_SnapshotPath;																		///< Snapshot dump path
	};
}
