using OpenIddict.Client;
using RobotNet.MapShares.Dtos;
using RobotNet.MapShares.Models;
using RobotNet.Script;
using RobotNet.Shares;
using Serialize.Linq.Serializers;
using System.Linq.Expressions;
using System.Net.Http.Headers;

namespace RobotNet.ScriptManager.Models;

public class ScriptMapManager(OpenIddictClientService openIddictClient, string MapManagerUrl, string[] MapManagerScopes) : IMapManager
{
    private string? CachedToken;
    private DateTime TokenExpiry;
    private static readonly ExpressionSerializer expressionSerializer = new(new Serialize.Linq.Serializers.JsonSerializer());

    public Task<IElement[]> FindElements(string map, string model)
        => FindElements(map, model, element => true);

    public Task<IElement[]> FindElements(string map, string model, Expression<Func<Script.Expressions.ElementProperties, bool>> expr)
        => FindElements(map, model, expr, true);

    private async Task<IElement[]> FindElements(string map, string model, Expression<Func<Script.Expressions.ElementProperties, bool>> expr, bool retry)
    {
        var accessToken = await RequestAccessToken();
        using var client = new HttpClient()
        {
            BaseAddress = new Uri(MapManagerUrl)
        };

        using var request = new HttpRequestMessage(HttpMethod.Post, $"api/ScriptElements");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        // Đưa modelExpression vào body của request
        request.Content = JsonContent.Create(new ElementExpressionModel
        {
            MapName = map,
            ModelName = model,
            Expression = expressionSerializer.SerializeText(expr),
        });

        using var response = await client.SendAsync(request);
        using var status = response.EnsureSuccessStatusCode();
        if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            if (retry)
            {
                CachedToken = null; // Clear cached token to force re-authentication
                return await FindElements(map, model, expr, false); // Retry without recall
            }
            else
            {
                throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
            }
        }
        else if (status.StatusCode != System.Net.HttpStatusCode.OK)
        {
            throw new HttpRequestException($"Failed to get elements: {status.ReasonPhrase}");
        }
        else
        {
            var result = await response.Content.ReadFromJsonAsync<MessageResult<IEnumerable<ElementDto>>>();
            if (result == null)
            {
                throw new InvalidOperationException("Failed to deserialize response from server");
            }
            else if (!result.IsSuccess)
            {
                throw new InvalidOperationException($"Error from server: {result.Message}");
            }
            else if (result.Data == null || !result.Data.Any())
            {
                return [];
            }

            return [.. result.Data.Select(e => new ScriptMapElement(map, e, UpdateIsOpenAsync, UpdatePropertiesAsync))];
        }
    }

    public Task<IElement> GetElement(string map, string name) => GetElement(map, name, true);

    private async Task<IElement> GetElement(string map, string name, bool retry)
    {
        if (string.IsNullOrEmpty(map) || string.IsNullOrEmpty(name))
        {
            throw new ArgumentException("Map name and element name cannot be null or empty");
        }

        var accessToken = await RequestAccessToken();

        using var client = new HttpClient()
        {
            BaseAddress = new Uri(MapManagerUrl)
        };
        using var request = new HttpRequestMessage(HttpMethod.Get, $"api/ScriptElements/{map}/element/{name}");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

        using var response = await client.SendAsync(request);
        using var status = response.EnsureSuccessStatusCode();
        if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            if (retry)
            {
                CachedToken = null; // Clear cached token to force re-authentication
                return await GetElement(map, name, false); // Retry without recall
            }
            else
            {
                throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
            }
        }
        else if (status.StatusCode != System.Net.HttpStatusCode.OK)
        {
            throw new HttpRequestException($"Failed to get element: {status.ReasonPhrase}");
        }
        else
        {
            var result = await response.Content.ReadFromJsonAsync<MessageResult<ElementDto>>();
            if (result == null)
            {
                throw new InvalidOperationException("Failed to deserialize response from server");
            }
            else if (!result.IsSuccess)
            {
                throw new InvalidOperationException($"Error from server: {result.Message}");
            }
            else if (result.Data == null)
            {
                throw new KeyNotFoundException("Element not found in response");
            }
            else
            {
                return new ScriptMapElement(map, result.Data, UpdateIsOpenAsync, UpdatePropertiesAsync);
            }
        }
    }

    private Task UpdateIsOpenAsync(string mapName, string elementName, bool isOpen) => UpdateIsOpenAsync(mapName, elementName, isOpen, true);

    private async Task UpdateIsOpenAsync(string mapName, string elementName, bool isOpen, bool retry)
    {
        var accessToken = await RequestAccessToken();
        using var client = new HttpClient()
        {
            BaseAddress = new Uri(MapManagerUrl)
        };

        using var request = new HttpRequestMessage(HttpMethod.Patch, $"api/ScriptElements/{mapName}/element/{elementName}/IsOpen?isOpen={isOpen}");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        //request.Content = JsonContent.Create(new { isOpen });

        using var response = await client.SendAsync(request);
        using var status = response.EnsureSuccessStatusCode();
        if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            if (retry)
            {
                CachedToken = null; // Clear cached token to force re-authentication
                await UpdateIsOpenAsync(mapName, elementName, isOpen, false); // Retry without recall
            }
            else
            {
                throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
            }
        }
        else if (status.StatusCode != System.Net.HttpStatusCode.OK)
        {
            throw new HttpRequestException($"Failed to update IsOpen: {status.ReasonPhrase}");
        }
        else
        {
            var result = await response.Content.ReadFromJsonAsync<MessageResult>();
            if (result == null)
            {
                throw new InvalidOperationException("Failed to deserialize response from server");
            }
            else if (!result.IsSuccess)
            {
                throw new InvalidOperationException($"Error from server: {result.Message}");
            }
        }
    }
    private Task UpdatePropertiesAsync(string mapName, string elementName, ElementPropertyUpdateModel model)
        => UpdatePropertiesAsync(mapName, elementName, model, true);

    private async Task UpdatePropertiesAsync(string mapName, string elementName, ElementPropertyUpdateModel model, bool retry)
    {
        var accessToken = await RequestAccessToken();
        using var client = new HttpClient()
        {
            BaseAddress = new Uri(MapManagerUrl)
        };

        using var request = new HttpRequestMessage(HttpMethod.Patch, $"api/ScriptElements/{mapName}/element/{elementName}");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        // Đưa modelExpression vào body của request
        request.Content = JsonContent.Create(model);

        using var response = await client.SendAsync(request);
        using var status = response.EnsureSuccessStatusCode();
        if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            if (retry)
            {
                CachedToken = null; // Clear cached token to force re-authentication
                await UpdatePropertiesAsync(mapName, elementName, model, false); // Retry without recall
            }
            else
            {
                throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
            }
        }
        else if (status.StatusCode != System.Net.HttpStatusCode.OK)
        {
            throw new HttpRequestException($"Failed to get elements: {status.ReasonPhrase}");
        }
        else
        {
            var result = await response.Content.ReadFromJsonAsync<MessageResult>();
            if (result == null)
            {
                throw new InvalidOperationException("Failed to deserialize response from server");
            }
            else if (!result.IsSuccess)
            {
                throw new InvalidOperationException($"Error from server: {result.Message}");
            }
        }
    }

    public async Task<string?> RequestAccessToken()
    {
        try
        {
            if (!string.IsNullOrEmpty(CachedToken) && DateTime.UtcNow < TokenExpiry)
            {
                return CachedToken;
            }

            var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new()
            {
                Scopes = [.. MapManagerScopes],
            });

            if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null)
            {
                return null;
            }
            else
            {
                TokenExpiry = result.AccessTokenExpirationDate.Value.UtcDateTime;
                CachedToken = result.AccessToken;
                return CachedToken;
            }
        }
        catch
        {
            return null;
        }
    }

    public Task<INode> GetNode(string map, string name) => GetNode(map, name, true);

    private async Task<INode> GetNode(string map, string name, bool retry)
    {
        if (string.IsNullOrEmpty(map) || string.IsNullOrEmpty(name))
        {
            throw new ArgumentException("Map name and element name cannot be null or empty");
        }

        var accessToken = await RequestAccessToken();

        using var client = new HttpClient()
        {
            BaseAddress = new Uri(MapManagerUrl)
        };
        using var request = new HttpRequestMessage(HttpMethod.Get, $"api/ScriptElements/{map}/node/{name}");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

        using var response = await client.SendAsync(request);
        using var status = response.EnsureSuccessStatusCode();
        if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            if (retry)
            {
                CachedToken = null; // Clear cached token to force re-authentication
                return await GetNode(map, name, false); // Retry without recall
            }
            else
            {
                throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
            }
        }
        else if (status.StatusCode != System.Net.HttpStatusCode.OK)
        {
            throw new HttpRequestException($"Failed to get element: {status.ReasonPhrase}");
        }
        else
        {
            var result = await response.Content.ReadFromJsonAsync<MessageResult<NodeDto>>();
            if (result == null)
            {
                throw new InvalidOperationException("Failed to deserialize response from server");
            }
            else if (!result.IsSuccess)
            {
                throw new InvalidOperationException($"Error from server: {result.Message}");
            }
            else if (result.Data == null)
            {
                throw new KeyNotFoundException("Element not found in response");
            }
            else
            {
                return new ScriptMapNode(result.Data.Id, result.Data.MapId, result.Data.Name, result.Data.X, result.Data.Y, result.Data.Theta);
            }
        }
    }
}
