#include "HyperTwistRecognition/HyperTwistVoiceClient.h"

#include "HyperTwistBootstrap/HyperTwistContractLibrary.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 HyperTwistHttpVoiceClientInternal
{
	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 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 FHttpResponseState
	{
		bool bCompleted = false;
		bool bSucceeded = false;
		int32 StatusCode = 0;
		FString ResponseBody;
		TArray<uint8> ResponseBytes;
		FString Error;
	};

	bool ExecuteRequest(
		const UHyperTwistHttpVoiceClient& Client,
		const FString& Verb,
		const FString& Url,
		const FString& AcceptHeader,
		const FString& ContentType,
		const FString& RequestBody,
		FHttpResponseState& OutResponse)
	{
		OutResponse = FHttpResponseState();

		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"), AcceptHeader);
		if (!ContentType.IsEmpty())
		{
			Request->SetHeader(TEXT("Content-Type"), ContentType);
		}

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

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

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

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

				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 ReadLe16(const TArray<uint8>& Bytes, const int32 Offset, uint16& OutValue)
	{
		if (Offset < 0 || Offset + 1 >= Bytes.Num())
		{
			return false;
		}

		OutValue = static_cast<uint16>(Bytes[Offset]) | (static_cast<uint16>(Bytes[Offset + 1]) << 8);
		return true;
	}

	bool ReadLe32(const TArray<uint8>& Bytes, const int32 Offset, uint32& OutValue)
	{
		if (Offset < 0 || Offset + 3 >= Bytes.Num())
		{
			return false;
		}

		OutValue = static_cast<uint32>(Bytes[Offset])
			| (static_cast<uint32>(Bytes[Offset + 1]) << 8)
			| (static_cast<uint32>(Bytes[Offset + 2]) << 16)
			| (static_cast<uint32>(Bytes[Offset + 3]) << 24);
		return true;
	}

	bool ParseWavMetadata(
		const TArray<uint8>& Bytes,
		int32& OutSampleRateHz,
		int32& OutChannelCount,
		int32& OutDurationMs)
	{
		OutSampleRateHz = 0;
		OutChannelCount = 0;
		OutDurationMs = 0;

		if (Bytes.Num() < 44
			|| Bytes[0] != 'R'
			|| Bytes[1] != 'I'
			|| Bytes[2] != 'F'
			|| Bytes[3] != 'F'
			|| Bytes[8] != 'W'
			|| Bytes[9] != 'A'
			|| Bytes[10] != 'V'
			|| Bytes[11] != 'E')
		{
			return false;
		}

		int32 Offset = 12;
		uint16 Channels = 0;
		uint32 SampleRate = 0;
		uint16 BitsPerSample = 0;
		uint32 DataSize = 0;

		while (Offset + 8 <= Bytes.Num())
		{
			const ANSICHAR ChunkIdChars[5] = {
				static_cast<ANSICHAR>(Bytes[Offset]),
				static_cast<ANSICHAR>(Bytes[Offset + 1]),
				static_cast<ANSICHAR>(Bytes[Offset + 2]),
				static_cast<ANSICHAR>(Bytes[Offset + 3]),
				0
			};
			const FString ChunkId(ANSI_TO_TCHAR(ChunkIdChars));

			uint32 ChunkSize = 0;
			if (!ReadLe32(Bytes, Offset + 4, ChunkSize))
			{
				return false;
			}

			const int32 ChunkDataOffset = Offset + 8;
			if (ChunkDataOffset + static_cast<int32>(ChunkSize) > Bytes.Num())
			{
				return false;
			}

			if (ChunkId == TEXT("fmt "))
			{
				uint16 AudioFormat = 0;
				if (!ReadLe16(Bytes, ChunkDataOffset, AudioFormat)
					|| !ReadLe16(Bytes, ChunkDataOffset + 2, Channels)
					|| !ReadLe32(Bytes, ChunkDataOffset + 4, SampleRate)
					|| !ReadLe16(Bytes, ChunkDataOffset + 14, BitsPerSample))
				{
					return false;
				}

				if (AudioFormat != 1)
				{
					return false;
				}
			}
			else if (ChunkId == TEXT("data"))
			{
				DataSize = ChunkSize;
			}

			Offset = ChunkDataOffset + static_cast<int32>(ChunkSize);
			if ((ChunkSize % 2U) != 0U)
			{
				++Offset;
			}
		}

		if (Channels <= 0 || SampleRate <= 0 || BitsPerSample <= 0 || DataSize <= 0)
		{
			return false;
		}

		const int32 BytesPerSampleFrame = Channels * (BitsPerSample / 8);
		if (BytesPerSampleFrame <= 0)
		{
			return false;
		}

		const double FrameCount = static_cast<double>(DataSize) / static_cast<double>(BytesPerSampleFrame);
		OutSampleRateHz = static_cast<int32>(SampleRate);
		OutChannelCount = static_cast<int32>(Channels);
		OutDurationMs = FMath::Max(
			0,
			FMath::RoundToInt(static_cast<float>((FrameCount * 1000.0) / static_cast<double>(SampleRate)))
		);
		return true;
	}
}

TArray<FHyperTwistVoiceProfileSummary> UHyperTwistHttpVoiceClient::ListVoiceProfiles() const
{
	HyperTwistHttpVoiceClientInternal::FHttpResponseState Response;
	if (!HyperTwistHttpVoiceClientInternal::ExecuteRequest(
		*this,
		TEXT("GET"),
		HyperTwistHttpVoiceClientInternal::BuildUrl(ServiceBaseUrl, ProfilesPath),
		TEXT("application/json"),
		FString(),
		FString(),
		Response))
	{
		LastTransportError = Response.Error;
		return {};
	}

	FHyperTwistVoiceProfileCatalog Catalog;
	if (!HyperTwistHttpVoiceClientInternal::DeserializeStruct(Response.ResponseBody, Catalog)
		|| !Catalog.IsStructurallyValid())
	{
		LastTransportError = TEXT("invalid-provider-response");
		return {};
	}

	LastTransportError.Reset();
	return Catalog.Profiles;
}

FHyperTwistNarrationSynthesisResult UHyperTwistHttpVoiceClient::SynthesizeNarration(
	const FHyperTwistNarrationSynthesisRequest& Request)
{
	FHyperTwistNarrationSynthesisResult Result;
	Result.RequestId = Request.RequestId;
	Result.NarrationContractId = Request.NarrationContractId;
	Result.ServiceLaneId = Request.ServiceLaneId;
	Result.OutputRouteId = Request.OutputRouteId;
	Result.VoiceProfileId = Request.VoiceProfileId;
	Result.LanguageCode = Request.LanguageCode;
	Result.OrchestrationProfileId = Request.OrchestrationProfile.OrchestrationProfileId;
	Result.ModelBindingId = Request.OrchestrationProfile.ModelBindingId;
	Result.VocoderBindingId = Request.OrchestrationProfile.VocoderBindingId;
	Result.SpeakerProfileId = Request.SpeakerProfileId.IsEmpty()
		? Request.OrchestrationProfile.DefaultSpeakerProfileId
		: Request.SpeakerProfileId;
	Result.SubtitleText = Request.SubtitleSeedText.IsEmpty() ? Request.ScriptText : Request.SubtitleSeedText;
	Result.AudioEncoding = TEXT("wav-pcm16-mono");
	Result.AppliedLengthScale = Request.LengthScale;
	Result.AppliedNoiseScale = Request.NoiseScale;
	Result.AppliedNoiseW = Request.NoiseW;
	Result.AppliedSentenceSilenceSeconds = Request.SentenceSilenceSeconds;
	Result.bUsedSentenceSplitting = Request.bSplitSentences || Request.OrchestrationProfile.bEnableSentenceSplitting;
	Result.bUsedSpeakerSelection =
		Request.OrchestrationProfile.bEnableSpeakerSelection && !Result.SpeakerProfileId.IsEmpty();
	Result.bUsedSpeakerReferenceConditioning =
		Request.OrchestrationProfile.bEnableSpeakerReferenceConditioning && !Request.ReferenceVoiceSampleRef.IsEmpty();

	if (!Request.IsStructurallyValid())
	{
		Result.Warnings.Add(TEXT("narration-request-invalid"));
		return Result;
	}

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

	HyperTwistHttpVoiceClientInternal::FHttpResponseState Response;
	if (!HyperTwistHttpVoiceClientInternal::ExecuteRequest(
		*this,
		TEXT("POST"),
		HyperTwistHttpVoiceClientInternal::BuildUrl(ServiceBaseUrl, SynthesizePath),
		TEXT("audio/wav"),
		TEXT("application/json"),
		RequestJson,
		Response))
	{
		LastTransportError = Response.Error;
		Result.Warnings.Add(LastTransportError);
		return Result;
	}

	int32 SampleRateHz = 0;
	int32 ChannelCount = 0;
	int32 DurationMs = 0;
	if (!HyperTwistHttpVoiceClientInternal::ParseWavMetadata(
		Response.ResponseBytes,
		SampleRateHz,
		ChannelCount,
		DurationMs))
	{
		LastTransportError = TEXT("invalid-provider-audio");
		Result.Warnings.Add(LastTransportError);
		return Result;
	}

	Result.SampleRateHz = SampleRateHz;
	Result.ChannelCount = ChannelCount;
	Result.DurationMs = DurationMs;
	Result.AudioBytes = Response.ResponseBytes;
	LastTransportError.Reset();
	return Result;
}

FHyperTwistVoiceServiceHealth UHyperTwistHttpVoiceClient::GetVoiceServiceHealth() const
{
	const auto BuildUnavailableHealth = [this]() -> FHyperTwistVoiceServiceHealth
	{
		FHyperTwistVoiceServiceHealth Health = UHyperTwistContractLibrary::MakeMockVoiceServiceHealth();
		Health.ProviderLabel = ProviderLabel;
		Health.ServiceVersion = TEXT("provider-unavailable/v1");
		Health.ServiceEndpoint = ServiceBaseUrl;
		Health.Capabilities.AddUnique(TEXT("transport:http"));
		Health.Capabilities.AddUnique(TEXT("provider-backed-voice"));
		Health.bReady = false;
		Health.LastError = LastTransportError;
		return Health;
	};

	HyperTwistHttpVoiceClientInternal::FHttpResponseState Response;
	if (!HyperTwistHttpVoiceClientInternal::ExecuteRequest(
		*this,
		TEXT("GET"),
		HyperTwistHttpVoiceClientInternal::BuildUrl(ServiceBaseUrl, HealthPath),
		TEXT("application/json"),
		FString(),
		FString(),
		Response))
	{
		LastTransportError = Response.Error;
		return BuildUnavailableHealth();
	}

	FHyperTwistVoiceServiceHealth Health;
	if (!HyperTwistHttpVoiceClientInternal::DeserializeStruct(Response.ResponseBody, Health)
		|| !Health.IsStructurallyValid())
	{
		LastTransportError = TEXT("invalid-provider-response");
		return BuildUnavailableHealth();
	}

	const FHyperTwistVoiceServiceHealth DefaultHealth =
		UHyperTwistContractLibrary::MakeMockVoiceServiceHealth();

	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-voice"));
	Health.Capabilities.AddUnique(TEXT("voiceCatalog"));
	Health.Capabilities.AddUnique(TEXT("offlineNarration"));
	Health.Capabilities.AddUnique(TEXT("wavOutput"));
	Health.Capabilities.AddUnique(TEXT("subtitleSeed"));
	Health.Capabilities.AddUnique(TEXT("multiSpeakerNarration"));
	Health.Capabilities.AddUnique(TEXT("multiLingualNarration"));
	Health.Capabilities.AddUnique(TEXT("speakerReferenceConditioning"));
	Health.Capabilities.AddUnique(TEXT("sentenceSplitNarration"));
	Health.Capabilities.AddUnique(TEXT("modelBindingSelection"));
	Health.Capabilities.AddUnique(TEXT("vocoderBindingSelection"));
	Health.Capabilities.AddUnique(TEXT("downloadableVoiceAssetReview"));
	Health.Capabilities.AddUnique(TEXT("voiceModelCardReview"));
	Health.Capabilities.AddUnique(TEXT("downloadableVoiceModelReview"));
	Health.Capabilities.AddUnique(TEXT("modelLicenseSelectionReview"));
	Health.Capabilities.AddUnique(TEXT("termsAcceptanceReview"));
	Health.SupportedOrchestrationProfiles.AddUnique(TEXT("local-basic-narration-v1"));
	Health.SupportedOrchestrationProfiles.AddUnique(TEXT("python-multispeaker-narration-v1"));
	Health.SupportedModelBindingIds.AddUnique(TEXT("voice-model/local-offline-medium-v1"));
	Health.SupportedModelBindingIds.AddUnique(TEXT("voice-model/multilingual-premium-v1"));
	Health.SupportedVocoderBindingIds.AddUnique(TEXT("vocoder/default-mono-v1"));
	Health.SupportedVocoderBindingIds.AddUnique(TEXT("vocoder/high-fidelity-v1"));
	if (Health.VoiceAssetReviewProfileId.IsEmpty()
		|| !Health.VoiceAssetReviewProfileDefinition.IsStructurallyValid())
	{
		Health.VoiceAssetReviewProfileId = DefaultHealth.VoiceAssetReviewProfileId;
		Health.VoiceAssetReviewProfileDefinition = DefaultHealth.VoiceAssetReviewProfileDefinition;
	}
	if (Health.SupportedVoiceAssetIds.Num() <= 0 || Health.SupportedVoiceAssets.Num() <= 0)
	{
		Health.SupportedVoiceAssetIds = DefaultHealth.SupportedVoiceAssetIds;
		Health.SupportedVoiceAssets = DefaultHealth.SupportedVoiceAssets;
	}
	if (Health.VoiceModelReviewProfileId.IsEmpty()
		|| !Health.VoiceModelReviewProfileDefinition.IsStructurallyValid())
	{
		Health.VoiceModelReviewProfileId = DefaultHealth.VoiceModelReviewProfileId;
		Health.VoiceModelReviewProfileDefinition = DefaultHealth.VoiceModelReviewProfileDefinition;
	}
	if (Health.SupportedVoiceModelReviewIds.Num() <= 0 || Health.SupportedVoiceModelReviews.Num() <= 0)
	{
		Health.SupportedVoiceModelReviewIds = DefaultHealth.SupportedVoiceModelReviewIds;
		Health.SupportedVoiceModelReviews = DefaultHealth.SupportedVoiceModelReviews;
	}
	LastTransportError.Reset();
	Health.LastError.Reset();
	return Health;
}
