/* Copyright (C) Itseez3D, Inc. - All Rights Reserved
* You may not use this file except in compliance with an authorized license
* Unauthorized copying of this file, via any medium is strictly prohibited
* Proprietary and confidential
* UNLESS REQUIRED BY APPLICABLE LAW OR AGREED BY ITSEEZ3D, INC. IN WRITING, SOFTWARE DISTRIBUTED UNDER THE LICENSE IS DISTRIBUTED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED
* See the License for the specific language governing permissions and limitations under the License.
* Written by Itseez3D, Inc. <support@avatarsdk.com>, July 2026
*/

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

namespace AvatarSDKMove
{
	public class Connection
	{
		private const float PollIntervalSeconds = 3.0f;
		private const string MoveType = "video";

		private const string MoveAnimationItem = "move_animation";
		private const string CompletedTransactionStatus = "completed";

		private const string NotSignedInMessage = "Sign in to your Avatar SDK Move account to generate animations.";
		private const string SessionExpiredMessage = "Your Avatar SDK Move session has expired. Please sign in again.";

		private static readonly Regex NextPageLinkRegex = new Regex(@"<(?<link>[^>]+)>\s*;\s*rel=""next""");

		private string tokenType = string.Empty;
		private string accessToken = string.Empty;
		private string playerUID = string.Empty;
		private string authenticatedServerUrl = string.Empty;

		public bool IsAuthenticated { get { return !string.IsNullOrEmpty(accessToken) && !string.IsNullOrEmpty(playerUID); } }

		public string PlayerUID { get { return playerUID; } }

		public async Task Authenticate()
		{
			await AuthenticateClient();

			UserInfoData userInfo = await GetUserInfo();
			playerUID = userInfo.player_uid;
		}

		private void ResetIfServerChanged()
		{
			if (authenticatedServerUrl == AvatarSDKMoveServer.ApiUrl)
				return;

			tokenType = string.Empty;
			accessToken = string.Empty;
			playerUID = string.Empty;
			authenticatedServerUrl = AvatarSDKMoveServer.ApiUrl;
		}

		private async Task EnsureAuthenticated(Action<string> onProgress)
		{
			ResetIfServerChanged();
			UserSession session = LoadSessionOrThrow();
			if (IsAuthenticated && session.playerUid == playerUID)
				return;

			onProgress("Authenticating...");
			await Authenticate();
		}

		public async Task AuthenticateClient()
		{
			ResetIfServerChanged();
			ClientCredentials credentials = AvatarSDKMoveServer.LoadClientCredentials();
			if (credentials == null || !credentials.IsValid)
				throw new Exception("Avatar SDK Move client credentials are missing from the plugin. This build of the plugin is incomplete.");

			var authForm = new Dictionary<string, string>
			{
				{ "grant_type", "client_credentials" },
			};

			var tokenRequest = new UnityWebRequest(GetUrl("o", "token"), UnityWebRequest.kHttpVerbPOST);
			tokenRequest.downloadHandler = new DownloadHandlerBuffer();
			tokenRequest.uploadHandler = CreateFormUpload(authForm);
			tokenRequest.SetRequestHeader("Authorization", CreateBasicAuthHeader(credentials));

			AccessData accessData = JsonUtility.FromJson<AccessData>(await SendAsText(tokenRequest));
			tokenType = accessData.token_type;
			accessToken = accessData.access_token;
		}

		public async Task<RequestCodeResult> RequestAuthenticationCode(string email)
		{
			await AuthenticateClient();

			var form = new Dictionary<string, string>
			{
				{ "email", email },
				{ "token", accessToken },
			};

			try
			{
				return JsonUtility.FromJson<RequestCodeResult>(await SendAsText(CreateAuthRequest(form)));
			}
			catch (RequestException ex) when (ex.IsTooManyRequests)
			{
				RequestCodeResult result = ParseOrDefault<RequestCodeResult>(ex.Body);
				result.success = false;
				return result;
			}
		}

		public async Task<AuthResult> SubmitAuthenticationCode(string email, string code)
		{
			ResetIfServerChanged();
			if (string.IsNullOrEmpty(accessToken))
				await AuthenticateClient();

			var form = new Dictionary<string, string>
			{
				{ "email", email },
				{ "token", accessToken },
				{ "code", code },
			};

			AuthResult result;
			try
			{
				result = JsonUtility.FromJson<AuthResult>(await SendAsText(CreateAuthRequest(form)));
			}
			catch (RequestException ex) when (ex.IsClientError)
			{
				Debug.LogWarning(ex.Message);
				return new AuthResult();
			}

			if (result.success)
				AuthUtils.StoreSession(new UserSession(email, result.token, result.refresh_token, result.player_uid));

			return result;
		}

		public async Task<UserInfoData> GetUserInfo()
		{
			string userInfoText = await RetryOnUnauthorized(() => SendAsText(CreateUserInfoRequest(LoadSessionOrThrow().token)), RefreshSession);
			UserInfoData userInfo = JsonUtility.FromJson<UserInfoData>(userInfoText);

			UserSession session = AuthUtils.LoadSession();
			if (session != null && session.playerUid != userInfo.player_uid)
				AuthUtils.StoreSession(new UserSession(session.email, session.token, session.refreshToken, userInfo.player_uid));

			return userInfo;
		}

		private async Task RefreshSession()
		{
			UserSession session = LoadSessionOrThrow();

			var form = new Dictionary<string, string>
			{
				{ "refresh_token", session.refreshToken },
			};

			AuthResult result = null;
			try
			{
				result = JsonUtility.FromJson<AuthResult>(await SendAsText(CreateAuthRequest(form)));
			}
			catch (RequestException ex) when (ex.IsClientError)
			{
				Debug.LogWarning(ex.Message);
			}

			if (result != null && result.success)
			{
				string refreshToken = string.IsNullOrEmpty(result.refresh_token) ? session.refreshToken : result.refresh_token;
				string uid = string.IsNullOrEmpty(result.player_uid) ? session.playerUid : result.player_uid;
				AuthUtils.StoreSession(new UserSession(session.email, result.token, refreshToken, uid));
				return;
			}

			UserSession current = AuthUtils.LoadSession();
			if (current != null && current.refreshToken != session.refreshToken)
				return;

			AuthUtils.ClearSession();
			throw new SignInRequiredException(SessionExpiredMessage);
		}

		private static UserSession LoadSessionOrThrow()
		{
			UserSession session = AuthUtils.LoadSession();
			if (session == null)
				throw new SignInRequiredException(NotSignedInMessage);
			return session;
		}

		private static UnityWebRequest CreateUserInfoRequest(string sessionToken)
		{
			var request = new UnityWebRequest(AvatarSDKMoveServer.MetaPersonApiUrl + "/user_info/", UnityWebRequest.kHttpVerbGET);
			request.downloadHandler = new DownloadHandlerBuffer();
			request.SetRequestHeader("Authorization", "Token " + sessionToken);
			return request;
		}

		private static string CreateBasicAuthHeader(ClientCredentials credentials)
		{
			string pair = string.Format("{0}:{1}", credentials.clientId, credentials.clientSecret);
			return "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(pair));
		}

		private static UnityWebRequest CreateAuthRequest(Dictionary<string, string> form)
		{
			var request = new UnityWebRequest(AvatarSDKMoveServer.MetaPersonApiUrl + "/auth/", UnityWebRequest.kHttpVerbPOST);
			request.downloadHandler = new DownloadHandlerBuffer();
			request.uploadHandler = CreateFormUpload(form);
			return request;
		}

		private async Task<string> PurchaseGeneration(int duration, Action<string> onProgress)
		{
			UserInfoData userInfo = await GetUserInfo();
			if (!GenerationPricing.IsPurchaseRequired(userInfo))
				return null;

			int price = GenerationPricing.GetPrice(userInfo, duration);
			if (price > userInfo.balance)
				throw new NotEnoughCoinsException(price, userInfo.balance);

			onProgress(price > 0 ? string.Format("Paying {0} coins...", price) : "Using a free animation...");

			var form = new Dictionary<string, string>
			{
				{ "action", "buy" },
				{ "item", MoveAnimationItem },
				{ "force_charge", "on" },
			};
			if (price > 0)
				form.Add("price", price.ToString(CultureInfo.InvariantCulture));

			string transactionCode = Guid.NewGuid().ToString();
			TransactionData transaction;
			try
			{
				string transactionText = await RetryOnUnauthorized(
					() => SendAsText(CreatePurchaseRequest(transactionCode, form, LoadSessionOrThrow().token)), RefreshSession);
				transaction = JsonUtility.FromJson<TransactionData>(transactionText);
			}
			catch (RequestException ex) when (ex.IsClientError && !ex.IsUnauthorized)
			{
				Debug.LogWarning(ex.Message);
				transaction = ParseOrDefault<TransactionData>(ex.Body);
			}

			if (transaction.status != CompletedTransactionStatus || string.IsNullOrEmpty(transaction.txid))
			{
				string reason = transaction.error != null && !string.IsNullOrEmpty(transaction.error.message)
					? transaction.error.message
					: string.Format("transaction status '{0}'", transaction.status);
				throw new Exception("Could not pay for the animation: " + reason);
			}

			return transaction.txid;
		}

		private static UnityWebRequest CreatePurchaseRequest(string transactionCode, Dictionary<string, string> form, string sessionToken)
		{
			string url = string.Format("{0}/transactions/{1}/", AvatarSDKMoveServer.MetaPersonApiUrl, transactionCode);
			var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPUT);
			request.downloadHandler = new DownloadHandlerBuffer();
			request.uploadHandler = CreateFormUpload(form);
			request.SetRequestHeader("Authorization", "Token " + sessionToken);
			return request;
		}

		private static T ParseOrDefault<T>(string json) where T : new()
		{
			try
			{
				T result = JsonUtility.FromJson<T>(json);
				return result == null ? new T() : result;
			}
			catch (Exception)
			{
				return new T();
			}
		}

		public async Task<GeneratedAnimation> GenerateAnimation(string prompt, int duration, string format, Action<string> onProgress)
		{
			TextToAnimationData animationData = await StartAnimationGeneration(prompt, duration, format, onProgress);
			animationData = await AwaitAnimation(animationData, onProgress);

			onProgress("Downloading animation...");
			return new GeneratedAnimation
			{
				code = animationData.code,
				data = await DownloadAnimation(animationData)
			};
		}

		public async Task<TextToAnimationData> StartAnimationGeneration(string prompt, int duration, string format, Action<string> onProgress)
		{
			if (duration < AvatarSDKMoveSettings.MinAnimationDuration || duration > AvatarSDKMoveSettings.MaxAnimationDuration)
				throw new ArgumentOutOfRangeException(nameof(duration), duration, string.Format("Animation duration must be from {0} to {1} seconds.",
					AvatarSDKMoveSettings.MinAnimationDuration, AvatarSDKMoveSettings.MaxAnimationDuration));

			await EnsureAuthenticated(onProgress);

			string txid = await PurchaseGeneration(duration, onProgress);

			TextToAnimationRequestData requestData = string.IsNullOrEmpty(txid)
				? new TextToAnimationRequestData()
				: new PaidTextToAnimationRequestData { txid = txid };
			requestData.text_prompt = prompt;
			requestData.duration = duration;
			requestData.pipeline_subtype = "male";
			requestData.format = format;
			requestData.move_type = MoveType;
			string requestJson = JsonUtility.ToJson(requestData);

			string generateText = await SendApiRequest(() =>
			{
				UnityWebRequest generateRequest = CreateRequest(UnityWebRequest.kHttpVerbPOST, GetUrl("move"));
				generateRequest.uploadHandler = CreateJsonUpload(requestJson);
				return generateRequest;
			});
			return JsonUtility.FromJson<TextToAnimationData>(generateText);
		}

		public async Task<TextToAnimationData> AwaitAnimation(TextToAnimationData animationData, Action<string> onProgress)
		{
			var timeEstimate = new GenerationTimeEstimate(animationData.created_on, animationData.Duration);

			while (!animationData.IsCompleted)
			{
				if (animationData.IsFailed)
					throw new AnimationGenerationFailedException(animationData.code, animationData.status);

				onProgress(string.Format("{0}... {1}", animationData.status, timeEstimate.FormatTimeLeft()));

				await Delay(PollIntervalSeconds);

				animationData = await GetAnimation(animationData.url);
			}

			return animationData;
		}

		public async Task<TextToAnimationData> GetAnimation(string url)
		{
			await EnsureAuthenticated(IgnoreProgress);

			string statusText = await SendApiRequest(() => CreateRequest(UnityWebRequest.kHttpVerbGET, url));
			return JsonUtility.FromJson<TextToAnimationData>(statusText);
		}

		public async Task<AnimationsPage> GetAnimations(string pageUrl = null)
		{
			await EnsureAuthenticated(IgnoreProgress);

			string url = string.IsNullOrEmpty(pageUrl) ? GetUrl("move") + "?move_type=" + MoveType : pageUrl;
			return await RetryOnUnauthorized(() => SendAsAnimationsPage(CreateRequest(UnityWebRequest.kHttpVerbGET, url)), AuthenticateClient);
		}

		public async Task<byte[]> DownloadAnimation(TextToAnimationData animationData)
		{
			await EnsureAuthenticated(IgnoreProgress);

			string resultUrl = animationData.result_url;
			return await RetryOnUnauthorized(() => SendAsData(CreateRequest(UnityWebRequest.kHttpVerbGET, resultUrl)), AuthenticateClient);
		}

		private static void IgnoreProgress(string message)
		{
		}

		private static async Task<AnimationsPage> SendAsAnimationsPage(UnityWebRequest request)
		{
			using (request)
			{
				await SendRequest(request);
				EnsureSuccess(request);

				var list = JsonUtility.FromJson<TextToAnimationDataList>("{\"items\":" + request.downloadHandler.text + "}");
				return new AnimationsPage
				{
					animations = list.items ?? new TextToAnimationData[0],
					nextPageUrl = ParseNextPageUrl(request.GetResponseHeader("Link"))
				};
			}
		}

		private static string ParseNextPageUrl(string linkHeader)
		{
			if (string.IsNullOrEmpty(linkHeader))
				return null;

			Match match = NextPageLinkRegex.Match(linkHeader);
			return match.Success ? match.Groups["link"].Value : null;
		}

		private string GetUrl(params string[] urlTokens)
		{
			return string.Format("{0}/{1}/", AvatarSDKMoveServer.ApiUrl, string.Join("/", urlTokens));
		}

		private UnityWebRequest CreateRequest(string method, string url)
		{
			var request = new UnityWebRequest(url, method);
			request.downloadHandler = new DownloadHandlerBuffer();
			request.SetRequestHeader("Authorization", string.Format("{0} {1}", tokenType, accessToken));
			request.SetRequestHeader("X-User-Agent", string.Format("avatar sdk move unity plugin/{0}", Application.version));
			if (!string.IsNullOrEmpty(playerUID))
				request.SetRequestHeader("X-PlayerUID", playerUID);
			return request;
		}

		private static UploadHandler CreateFormUpload(Dictionary<string, string> form)
		{
			var body = new StringBuilder();
			foreach (KeyValuePair<string, string> field in form)
			{
				if (body.Length > 0)
					body.Append('&');

				body.Append(UnityWebRequest.EscapeURL(field.Key));
				body.Append('=');
				body.Append(UnityWebRequest.EscapeURL(field.Value));
			}

			var upload = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body.ToString()));
			upload.contentType = "application/x-www-form-urlencoded";
			return upload;
		}

		private static UploadHandler CreateJsonUpload(string json)
		{
			var upload = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json));
			upload.contentType = "application/json";
			return upload;
		}

		private Task<string> SendApiRequest(Func<UnityWebRequest> createRequest)
		{
			return RetryOnUnauthorized(() => SendAsText(createRequest()), AuthenticateClient);
		}

		private static async Task<T> RetryOnUnauthorized<T>(Func<Task<T>> send, Func<Task> authorize)
		{
			try
			{
				return await send();
			}
			catch (RequestException ex) when (ex.IsUnauthorized)
			{
				await authorize();
				return await send();
			}
		}

		private static async Task<string> SendAsText(UnityWebRequest request)
		{
			using (request)
			{
				await SendRequest(request);
				EnsureSuccess(request);
				return request.downloadHandler.text;
			}
		}

		private static async Task<byte[]> SendAsData(UnityWebRequest request)
		{
			using (request)
			{
				await SendRequest(request);
				EnsureSuccess(request);
				return request.downloadHandler.data;
			}
		}

		private static async Task Delay(float seconds)
		{
			float endTime = Time.realtimeSinceStartup + seconds;
			while (Time.realtimeSinceStartup < endTime)
				await Task.Yield();
		}

		private static Task SendRequest(UnityWebRequest request)
		{
			var completionSource = new TaskCompletionSource<bool>();

			UnityWebRequestAsyncOperation operation = request.SendWebRequest();
			if (operation.isDone)
				completionSource.SetResult(true);
			else
				operation.completed += _ => completionSource.SetResult(true);

			return completionSource.Task;
		}

		private static void EnsureSuccess(UnityWebRequest request)
		{
			if (request.result == UnityWebRequest.Result.Success)
				return;

			string body = request.downloadHandler == null ? string.Empty : request.downloadHandler.text;
			throw new RequestException(request.url, request.responseCode, request.error, body);
		}
	}
}
