/* 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.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

namespace AvatarSDKMove
{
	public class Connection
	{
		private const string CompletedStatus = "Completed";
		private const float PollIntervalSeconds = 1.0f;

		private static readonly HashSet<string> BadFinalStatuses = new HashSet<string> { "Failed", "Timed Out" };

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

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

		public string PlayerUID { get { return playerUID; } }

		public async Task Authenticate()
		{
			UserSession session = AuthUtils.LoadCurrentSession();
			if (session == null || !session.IsValid)
				throw new Exception("Avatar SDK Move is not authenticated. Use Avatar SDK Move -> Account to sign in with your email.");

			await AuthenticateClient();

			UserInfoData userInfo = await GetUserInfoRefreshingIfNeeded(session);
			if (!userInfo.move_bundle_creation_allowed)
				throw new Exception("Animation generation is not enabled for this account. Contact " + AvatarSDKMoveSettings.SupportEmail + ".");

			playerUID = userInfo.player_uid;
			PlayerUidStorage.Store(session.email, playerUID);
		}

		private async Task EnsureAuthenticated(Action<string> onProgress)
		{
			if (IsAuthenticated)
				return;

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

		public async Task AuthenticateClient()
		{
			tokenType = string.Empty;
			accessToken = string.Empty;
			playerUID = string.Empty;

			ClientCredentials credentials = AuthUtils.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)
		{
			var form = new Dictionary<string, string>
			{
				{ "email", email },
				{ "token", accessToken },
				{ "product", AvatarSDKMoveSettings.Product },
			};

			return JsonUtility.FromJson<RequestCodeResult>(await SendAsText(CreateAuthRequest(form)));
		}

		public async Task<AuthResult> SubmitAuthenticationCode(string email, string code)
		{
			var form = new Dictionary<string, string>
			{
				{ "email", email },
				{ "token", accessToken },
				{ "code", code },
				{ "product", AvatarSDKMoveSettings.Product },
			};

			AuthResult result = JsonUtility.FromJson<AuthResult>(await SendAsText(CreateAuthRequest(form)));
			if (result.success)
				playerUID = result.player_uid;

			return result;
		}

		public async Task<AuthResult> RefreshSession(string refreshToken)
		{
			var form = new Dictionary<string, string>
			{
				{ "refresh_token", refreshToken },
				{ "product", AvatarSDKMoveSettings.Product },
			};

			AuthResult result = JsonUtility.FromJson<AuthResult>(await SendAsText(CreateAuthRequest(form)));
			if (result.success)
				playerUID = result.player_uid;

			return result;
		}

		private async Task<UserInfoData> GetUserInfoRefreshingIfNeeded(UserSession session)
		{
			try
			{
				return await GetUserInfo(session.token);
			}
			catch (Exception)
			{
				AuthResult result = await RefreshSession(session.refreshToken);
				if (!result.success)
					throw new Exception("The Avatar SDK Move session has expired. Sign in again with Avatar SDK Move -> Account.");

				var refreshed = new UserSession(session.email, result.token, result.refresh_token);
				AuthUtils.StoreCachedSession(refreshed);

				return await GetUserInfo(refreshed.token);
			}
		}

		public async Task<UserInfoData> GetUserInfo(string sessionToken)
		{
			var request = new UnityWebRequest(AvatarSDKMoveSettings.MetaPersonAuthUrl + "/user_info/", UnityWebRequest.kHttpVerbGET);
			request.downloadHandler = new DownloadHandlerBuffer();
			request.SetRequestHeader("Authorization", "Token " + sessionToken);

			return JsonUtility.FromJson<UserInfoData>(await SendAsText(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(AvatarSDKMoveSettings.MetaPersonAuthUrl + "/auth/", UnityWebRequest.kHttpVerbPOST);
			request.downloadHandler = new DownloadHandlerBuffer();
			request.uploadHandler = CreateFormUpload(form);
			return request;
		}

		public async Task<GeneratedAnimation> GenerateAnimation(string prompt, float duration, string format, Action<string> onProgress)
		{
			await EnsureAuthenticated(onProgress);

			var requestData = new TextToAnimationRequestData
			{
				text_prompt = prompt,
				duration = duration,
				pipeline_subtype = "male",
				format = format
			};

			UnityWebRequest generateRequest = CreateRequest(UnityWebRequest.kHttpVerbPOST, GetUrl("move"));
			generateRequest.uploadHandler = CreateJsonUpload(JsonUtility.ToJson(requestData));

			TextToAnimationData animationData = JsonUtility.FromJson<TextToAnimationData>(await SendAsText(generateRequest));

			while (animationData.status != CompletedStatus)
			{
				if (BadFinalStatuses.Contains(animationData.status))
					throw new Exception(string.Format("Animation generation failed, status: {0}", animationData.status));

				onProgress(string.Format("{0}... {1:0}%", animationData.status, animationData.progress));

				await Delay(PollIntervalSeconds);

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

			onProgress("Downloading animation...");

			return new GeneratedAnimation
			{
				code = animationData.code,
				data = await SendAsData(CreateRequest(UnityWebRequest.kHttpVerbGET, animationData.result_url))
			};
		}

		private string GetUrl(params string[] urlTokens)
		{
			return string.Format("{0}/{1}/", AvatarSDKMoveSettings.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 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 Exception(string.Format("Request to {0} failed: {1} {2} {3}", request.url, request.responseCode, request.error, body));
		}
	}
}
