using OpenIddict.Client;
using RobotNet.MapShares.Models;
using RobotNet.RobotShares.Models;
using RobotNet.Script;
using RobotNet.Script.Expressions;
using RobotNet.ScriptManager.Clients;
using RobotNet.ScriptManager.Helpers;
using RobotNet.Shares;
using Serialize.Linq.Serializers;
using System.Linq.Expressions;
using System.Net.Http.Headers;

namespace RobotNet.ScriptManager.Models;

public class ScriptRobotManager(OpenIddictClientService openIddictClient, string RobotManagerUrl, string[] RobotManagerScopes) : IRobotManager
{
    private string? CachedToken;
    private DateTime TokenExpiry;
    private static readonly ExpressionSerializer expressionSerializer = new(new Serialize.Linq.Serializers.JsonSerializer());

    public async Task<IRobot?> GetRobotById(string robotId)
    {
        var robot = new RobotManagerHubClient(robotId, $"{RobotManagerUrl}/hubs/robot-manager", async () =>
        {
            var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new()
            {
                Scopes = [.. RobotManagerScopes],
            });
            if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null)
            {
                return null;
            }
            else
            {
                return result.AccessToken;
            }
        });
        await robot.StartAsync();
        return robot;
    }

    public Task<RobotState> GetRobotState(string robotId) => GetRobotState(robotId, true);
    public async Task<RobotState> GetRobotState(string robotId, bool retry)
    {
        var accessToken = await RequestAccessToken();
        if (string.IsNullOrEmpty(accessToken))
        {
            throw new ArgumentException("Failed to get access token");
        }

        using var client = new HttpClient()
        {
            BaseAddress = new Uri(RobotManagerUrl)
        };
        using var request = new HttpRequestMessage(HttpMethod.Get, $"api/RobotManager/State/{robotId}");
        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)
            {
                // Retry with a new access token
                CachedToken = null; // Clear cached token to force a new request
                return await GetRobotState(robotId, false);
            }
            else
            {
                throw new UnauthorizedAccessException("Access token is invalid or expired.");
            }
        }
        else if (status.StatusCode != System.Net.HttpStatusCode.OK)
        {
            throw new HttpRequestException($"Failed to get robot state: {status.ReasonPhrase}");
        }
        else
        {
            var state = await response.Content.ReadFromJsonAsync<MessageResult<RobotStateModel>>();
            if (state == null)
            {
                throw new InvalidOperationException("Failed to deserialize robot state response");
            }
            else if (!state.IsSuccess)
            {
                throw new InvalidOperationException($"Robot Manager error: {state.Message}");
            }
            else
            {
                if (state.Data == null)
                {
                    throw new InvalidOperationException("Robot state data is null.");
                }
                return VDA5050ScriptHelper.ConvertToRobotState(state.Data);
            }
        }
    }

    public Task<IEnumerable<string>> SearchRobots(string map, string model)
        => SearchRobots(map, model, robot => true, true);

    public Task<IEnumerable<string>> SearchRobots(string map, string model, Expression<Func<RobotState, bool>> expr)
        => SearchRobots(map, model, expr, true);

    public async Task<IEnumerable<string>> SearchRobots(string map, string model, Expression<Func<RobotState, bool>> expr, bool retry)
    {
        var accessToken = await RequestAccessToken();
        using var client = new HttpClient()
        {
            BaseAddress = new Uri(RobotManagerUrl)
        };
        using var request = new HttpRequestMessage(HttpMethod.Post, $"api/RobotManager/Search");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

        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)
            {
                // Retry with a new access token
                CachedToken = null; // Clear cached token to force a new request
                return await SearchRobots(map, model, expr, false);
            }
            else
            {
                throw new UnauthorizedAccessException("Access token is invalid or expired.");
            }
        }
        else if (response.EnsureSuccessStatusCode().StatusCode != System.Net.HttpStatusCode.OK)
        {
            throw new HttpRequestException($"Failed to search robots: {response.ReasonPhrase}");
        }
        else
        {
            var result = await response.Content.ReadFromJsonAsync<MessageResult<string[]>>() ?? throw new Exception("Failed to convert result from Robot Manager");
            if (result.IsSuccess)
            {
                return result.Data ?? [];
            }
            else
            {
                throw new Exception($"Robot Manager error: {result.Message}");
            }
        }
    }

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

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

            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;
        }
    }
}
