#include "HyperTwistRecognition/HyperTwistVisionClient.h"

#include "Dom/JsonObject.h"
#include "Http.h"
#include "HttpManager.h"
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "JsonObjectConverter.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"

namespace HyperTwistHttpVisionClientInternal
{
	template <typename TStruct>
	bool SerializeStruct(const TStruct& Value, FString& OutJson)
	{
		return FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, OutJson, 0, 0);
	}

	template <typename TStruct>
	bool DeserializeStruct(const FString& Json, TStruct& OutValue)
	{
		return !Json.IsEmpty() && FJsonObjectConverter::JsonObjectStringToUStruct(Json, &OutValue, 0, 0);
	}

	FString MakeSessionIdJson(const FString& SessionId)
	{
		FString Json;
		const TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
		Root->SetStringField(TEXT("sessionId"), SessionId);
		const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Json);
		FJsonSerializer::Serialize(Root, Writer);
		return Json;
	}

	FString BuildUrl(const FString& BaseUrl, const FString& Path)
	{
		if (BaseUrl.IsEmpty())
		{
			return FString();
		}

		const FString TrimmedBase = BaseUrl.EndsWith(TEXT("/")) ? BaseUrl.LeftChop(1) : BaseUrl;
		if (Path.IsEmpty())
		{
			return TrimmedBase;
		}

		const FString TrimmedPath = Path.StartsWith(TEXT("/")) ? Path.RightChop(1) : Path;
		return FString::Printf(TEXT("%s/%s"), *TrimmedBase, *TrimmedPath);
	}

	struct FHttpJsonResponse
	{
		bool bCompleted = false;
		bool bSucceeded = false;
		int32 StatusCode = 0;
		FString ResponseBody;
		FString Error;
	};

	bool ExecuteJsonRequest(
		const UHyperTwistHttpVisionClient& Client,
		const FString& Verb,
		const FString& Url,
		const FString& RequestJson,
		FHttpJsonResponse& OutResponse)
	{
		OutResponse = FHttpJsonResponse();

		if (Url.IsEmpty())
		{
			OutResponse.Error = TEXT("service-base-url-missing");
			return false;
		}

		const TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
		Request->SetURL(Url);
		Request->SetVerb(Verb);
		Request->SetHeader(TEXT("Accept"), TEXT("application/json"));
		Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));

		if (!Client.AuthorizationToken.IsEmpty())
		{
			Request->SetHeader(TEXT("Authorization"), FString::Printf(TEXT("Bearer %s"), *Client.AuthorizationToken));
		}

		if (!RequestJson.IsEmpty())
		{
			Request->SetContentAsString(RequestJson);
		}

		Request->OnProcessRequestComplete().BindLambda(
			[&OutResponse](FHttpRequestPtr, FHttpResponsePtr Response, bool bWasSuccessful)
			{
				OutResponse.bCompleted = true;

				if (Response.IsValid())
				{
					OutResponse.StatusCode = Response->GetResponseCode();
					OutResponse.ResponseBody = Response->GetContentAsString();
				}

				if (!bWasSuccessful)
				{
					OutResponse.Error = TEXT("request-failed");
					return;
				}

				if (!Response.IsValid())
				{
					OutResponse.Error = TEXT("response-missing");
					return;
				}

				if (!EHttpResponseCodes::IsOk(OutResponse.StatusCode))
				{
					OutResponse.Error = FString::Printf(TEXT("http-%d"), OutResponse.StatusCode);
					return;
				}

				OutResponse.bSucceeded = true;
			});

		if (!Request->ProcessRequest())
		{
			OutResponse.Error = TEXT("request-start-failed");
			return false;
		}

		const double TimeoutSeconds = FMath::Max(static_cast<double>(Client.RequestTimeoutSeconds), 0.1);
		const double Deadline = FPlatformTime::Seconds() + TimeoutSeconds;
		while (!OutResponse.bCompleted && FPlatformTime::Seconds() < Deadline)
		{
			FHttpModule::Get().GetHttpManager().Tick(0.01f);
			FPlatformProcess::Sleep(0.01f);
		}

		if (!OutResponse.bCompleted)
		{
			Request->CancelRequest();
			OutResponse.Error = TEXT("request-timeout");
			return false;
		}

		return OutResponse.bSucceeded;
	}
}

bool UHyperTwistHttpVisionClient::OpenVisionSession(const FHyperTwistVisionSessionConfig& Config, FString& OutError)
{
	OutError.Reset();

	FString RequestJson;
	if (!HyperTwistHttpVisionClientInternal::SerializeStruct(Config, RequestJson))
	{
		OutError = TEXT("request-serialize-failed");
		LastTransportError = OutError;
		return false;
	}

	HyperTwistHttpVisionClientInternal::FHttpJsonResponse Response;
	if (!HyperTwistHttpVisionClientInternal::ExecuteJsonRequest(
		*this,
		TEXT("POST"),
		HyperTwistHttpVisionClientInternal::BuildUrl(ServiceBaseUrl, OpenSessionPath),
		RequestJson,
		Response))
	{
		OutError = Response.Error;
		LastTransportError = OutError;
		return false;
	}

	LastTransportError.Reset();
	return true;
}

FHyperTwistVisionPreviewResult UHyperTwistHttpVisionClient::SubmitVisionFrame(const FHyperTwistVisionFrameEnvelope& Frame)
{
	FHyperTwistVisionPreviewResult Result;
	Result.SessionId = Frame.SessionId;
	Result.FrameId = Frame.FrameId;
	Result.PreviewState.State.SourceSessionId = Frame.SessionId;

	FString RequestJson;
	if (!HyperTwistHttpVisionClientInternal::SerializeStruct(Frame, RequestJson))
	{
		LastTransportError = TEXT("request-serialize-failed");
		Result.Guidance = TEXT("service-unavailable");
		return Result;
	}

	HyperTwistHttpVisionClientInternal::FHttpJsonResponse Response;
	if (!HyperTwistHttpVisionClientInternal::ExecuteJsonRequest(
		*this,
		TEXT("POST"),
		HyperTwistHttpVisionClientInternal::BuildUrl(ServiceBaseUrl, SubmitFramePath),
		RequestJson,
		Response))
	{
		LastTransportError = Response.Error;
		Result.Guidance = TEXT("service-unavailable");
		return Result;
	}

	if (!HyperTwistHttpVisionClientInternal::DeserializeStruct(Response.ResponseBody, Result))
	{
		LastTransportError = TEXT("invalid-provider-response");
		Result = FHyperTwistVisionPreviewResult();
		Result.SessionId = Frame.SessionId;
		Result.FrameId = Frame.FrameId;
		Result.PreviewState.State.SourceSessionId = Frame.SessionId;
		Result.Guidance = TEXT("service-unavailable");
		return Result;
	}

	if (Result.SessionId.IsEmpty())
	{
		Result.SessionId = Frame.SessionId;
	}

	if (Result.FrameId.IsEmpty())
	{
		Result.FrameId = Frame.FrameId;
	}

	if (Result.PreviewState.State.SourceSessionId.IsEmpty())
	{
		Result.PreviewState.State.SourceSessionId = Frame.SessionId;
	}

	LastTransportError.Reset();
	return Result;
}

FHyperTwistVisionCommitResult UHyperTwistHttpVisionClient::CommitVisionObservation(const FHyperTwistVisionCommitRequest& Request)
{
	FHyperTwistVisionCommitResult Result;
	Result.SessionId = Request.SessionId;
	Result.CommitKind = Request.CommitKind;
	Result.Snapshot.State.SourceSessionId = Request.SessionId;

	FString RequestJson;
	if (!HyperTwistHttpVisionClientInternal::SerializeStruct(Request, RequestJson))
	{
		LastTransportError = TEXT("request-serialize-failed");
		Result.Warnings.Add(LastTransportError);
		return Result;
	}

	HyperTwistHttpVisionClientInternal::FHttpJsonResponse Response;
	if (!HyperTwistHttpVisionClientInternal::ExecuteJsonRequest(
		*this,
		TEXT("POST"),
		HyperTwistHttpVisionClientInternal::BuildUrl(ServiceBaseUrl, CommitObservationPath),
		RequestJson,
		Response))
	{
		LastTransportError = Response.Error;
		Result.Warnings.Add(LastTransportError);
		return Result;
	}

	if (!HyperTwistHttpVisionClientInternal::DeserializeStruct(Response.ResponseBody, Result))
	{
		LastTransportError = TEXT("invalid-provider-response");
		Result = FHyperTwistVisionCommitResult();
		Result.SessionId = Request.SessionId;
		Result.CommitKind = Request.CommitKind;
		Result.Snapshot.State.SourceSessionId = Request.SessionId;
		Result.Warnings.Add(LastTransportError);
		return Result;
	}

	if (Result.SessionId.IsEmpty())
	{
		Result.SessionId = Request.SessionId;
	}

	if (Result.CommitKind.IsEmpty())
	{
		Result.CommitKind = Request.CommitKind;
	}

	if (Result.Snapshot.State.SourceSessionId.IsEmpty())
	{
		Result.Snapshot.State.SourceSessionId = Request.SessionId;
	}

	LastTransportError.Reset();
	return Result;
}

FHyperTwistVisionFinalizeResult UHyperTwistHttpVisionClient::FinalizeVisionSession(const FString& SessionId)
{
	FHyperTwistVisionFinalizeResult Result;
	Result.SessionId = SessionId;
	Result.FinalState.SourceSessionId = SessionId;
	Result.FinalSnapshot.State.SourceSessionId = SessionId;

	HyperTwistHttpVisionClientInternal::FHttpJsonResponse Response;
	if (!HyperTwistHttpVisionClientInternal::ExecuteJsonRequest(
		*this,
		TEXT("POST"),
		HyperTwistHttpVisionClientInternal::BuildUrl(ServiceBaseUrl, FinalizeSessionPath),
		HyperTwistHttpVisionClientInternal::MakeSessionIdJson(SessionId),
		Response))
	{
		LastTransportError = Response.Error;
		Result.NormalizationWarnings.Add(LastTransportError);
		return Result;
	}

	if (!HyperTwistHttpVisionClientInternal::DeserializeStruct(Response.ResponseBody, Result))
	{
		LastTransportError = TEXT("invalid-provider-response");
		Result = FHyperTwistVisionFinalizeResult();
		Result.SessionId = SessionId;
		Result.FinalState.SourceSessionId = SessionId;
		Result.FinalSnapshot.State.SourceSessionId = SessionId;
		Result.NormalizationWarnings.Add(LastTransportError);
		return Result;
	}

	if (Result.SessionId.IsEmpty())
	{
		Result.SessionId = SessionId;
	}

	if (Result.FinalState.SourceSessionId.IsEmpty())
	{
		Result.FinalState.SourceSessionId = SessionId;
	}

	if (Result.FinalSnapshot.State.SourceSessionId.IsEmpty())
	{
		Result.FinalSnapshot.State.SourceSessionId = SessionId;
	}

	LastTransportError.Reset();
	return Result;
}

bool UHyperTwistHttpVisionClient::CloseVisionSession(const FString& SessionId, FString& OutError)
{
	OutError.Reset();

	HyperTwistHttpVisionClientInternal::FHttpJsonResponse Response;
	if (!HyperTwistHttpVisionClientInternal::ExecuteJsonRequest(
		*this,
		TEXT("POST"),
		HyperTwistHttpVisionClientInternal::BuildUrl(ServiceBaseUrl, CloseSessionPath),
		HyperTwistHttpVisionClientInternal::MakeSessionIdJson(SessionId),
		Response))
	{
		OutError = Response.Error;
		LastTransportError = OutError;
		return false;
	}

	LastTransportError.Reset();
	return true;
}

FHyperTwistVisionServiceHealth UHyperTwistHttpVisionClient::GetVisionServiceHealth() const
{
	FHyperTwistVisionServiceHealth Health;
	Health.ProviderLabel = ProviderLabel;
	Health.ServiceVersion = TEXT("provider-unavailable/v1");
	Health.ServiceEndpoint = ServiceBaseUrl;
	Health.bReady = false;
	Health.Capabilities = {
		TEXT("transport:http"),
		TEXT("provider-backed-recognition"),
		TEXT("calibrationProfiles"),
		TEXT("orderedFaceObservation")
	};

	HyperTwistHttpVisionClientInternal::FHttpJsonResponse Response;
	if (!HyperTwistHttpVisionClientInternal::ExecuteJsonRequest(
		*this,
		TEXT("GET"),
		HyperTwistHttpVisionClientInternal::BuildUrl(ServiceBaseUrl, HealthPath),
		FString(),
		Response))
	{
		LastTransportError = Response.Error;
		Health.LastError = LastTransportError;
		return Health;
	}

	if (!HyperTwistHttpVisionClientInternal::DeserializeStruct(Response.ResponseBody, Health))
	{
		LastTransportError = TEXT("invalid-provider-response");
		Health = FHyperTwistVisionServiceHealth();
		Health.ProviderLabel = ProviderLabel;
		Health.ServiceVersion = TEXT("provider-unavailable/v1");
		Health.ServiceEndpoint = ServiceBaseUrl;
		Health.bReady = false;
		Health.Capabilities = {
			TEXT("transport:http"),
			TEXT("provider-backed-recognition"),
			TEXT("calibrationProfiles"),
			TEXT("orderedFaceObservation")
		};
		Health.LastError = LastTransportError;
		return Health;
	}

	if (Health.ProviderLabel.IsEmpty())
	{
		Health.ProviderLabel = ProviderLabel;
	}

	if (Health.ServiceEndpoint.IsEmpty())
	{
		Health.ServiceEndpoint = ServiceBaseUrl;
	}

	Health.Capabilities.AddUnique(TEXT("transport:http"));
	Health.Capabilities.AddUnique(TEXT("provider-backed-recognition"));
	Health.Capabilities.AddUnique(TEXT("calibrationProfiles"));
	Health.Capabilities.AddUnique(TEXT("orderedFaceObservation"));
	Health.LastError.Reset();
	LastTransportError.Reset();
	return Health;
}
