using Castle.Core.Logging;
using Newtonsoft.Json.Linq;
using Pilz.Extensions.Reflection;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Web;
using static Pilz.Net.Api.IApiServer;

namespace Pilz.Net.Api;

public class ApiServer(string apiUrl) : IApiServer
{
    protected readonly List<PrivateMessageHandler> handlers = [];
    protected readonly Dictionary<Type, IApiMessageSerializer> serializers = [];
    protected readonly HttpListener httpListener = new();

    public event OnCheckAuthenticationEventHandler? OnCheckAuthentication;
    public event OnCheckContextEventHandler? OnCheckContext;
    public event OnCheckContextEventHandler? OnCheckContextCompleted;

    protected record PrivateParameterInfo(string Name, int Index);

    protected record PrivateMessageHandler(string Url, bool UseRegEx, Delegate Handler, PrivateParameterInfo[] Parameters, ApiMessageHandlerAttribute Attribute);

    protected record PrivateApiResult(ApiResult Original, string? ResultJson);

    public string ApiUrl { get; } = apiUrl;

    public virtual bool EnableAuth { get; set; }

    public IApiMessageSerializer Serializer { get; set; } = new DefaultApiMessageSerializer();

    public ILogger Log { get; set; } = NullLogger.Instance;

    public bool DebugMode { get; set; }

    public bool AllowMultipleRequests { get; set; }

    public virtual void Start()
    {
        Log.Info("Start listening");
        httpListener.Prefixes.Add(ApiUrl + "/");
        httpListener.Start();
        Receive();
    }

    public virtual void Stop()
    {
        httpListener.Stop();
        Log.Info("Stopped listening");
    }

    public virtual void RegisterHandler<T>(T instance) where T : class
    {
        // Get all public instance methods
        var methods = typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public);

        // Register each method
        foreach (var method in methods)
            RegisterHandler(method.CreateDelegate(instance), false);
    }

    public virtual void RegisterHandler(Delegate handler)
    {
        RegisterHandler(handler, true);
    }

    public virtual void RegisterHandler(Delegate handler, bool throwOnError)
    {
        var method = handler.Method;

        // Sanity checks
        if (method.GetCustomAttribute<ApiMessageHandlerAttribute>() is not ApiMessageHandlerAttribute attribute
            || !method.ReturnType.IsAssignableTo(typeof(ApiResult)))
        {
            if (throwOnError)
                throw new NotSupportedException("The first parameter needs to be of type ApiMessage and must return an ApiResult object and the method must have the MessageHandlerAttribute.");
            return;
        }

        // Resolves parameters
        var url = attribute.Route;
        var useRegEx = false;
        var nextBreacket = url.IndexOf('{');
        var parameters = new List<PrivateParameterInfo>();
        while (nextBreacket != -1)
        {
            var endBreacket = url.IndexOf('}', nextBreacket + 1);
            if (endBreacket != -1)
            {
                var name = url.Substring(nextBreacket + 1, endBreacket - nextBreacket - 1);
                const string regex = "[A-Za-z0-9%]+";
                url = url.Replace(url.Substring(nextBreacket, endBreacket - nextBreacket + 1), regex);

                var index = url[..(nextBreacket + 1)].Split('/').Length - 1;
                parameters.Add(new(name, index));

                useRegEx = true;
                nextBreacket = url.IndexOf('{', endBreacket + 1);
            }
        }
        if (useRegEx)
            url = url.Replace(".", "\\."); // Escape special characters

        // Add handler
        Log.InfoFormat("Added handler for {0}", attribute.Route);
        handlers.Add(new(url, useRegEx, handler, [.. parameters], attribute));
    }

    protected void Receive()
    {
        if (httpListener.IsListening)
            httpListener?.BeginGetContext(ListenerCallback, null);
    }

    protected void ListenerCallback(IAsyncResult result)
    {
        HttpListenerContext? context;

        // Skip if not lisstening anymore
        if (!httpListener.IsListening)
            return;

        // Get context
        try
        {
            context = httpListener.EndGetContext(result);
        }
        catch (Exception ex)
        {
            Log.ErrorFormat("Error at getting context: ", ex.ToString());
            context = null;
        }

        // Immitatly listen for new request
        if (AllowMultipleRequests)
            Receive();

        // Cancel if we don't have a context
        if (context is null)
            return;

        // Check context
        OnCheckContext?.Invoke(this, new(context));
        try
        {
            CheckContext(context);
        }
        catch (Exception ex)
        {
            Log.ErrorFormat("Error at checking context: ", ex.ToString());
            if (DebugMode)
                throw;
        }
        finally
        {
            OnCheckContextCompleted?.Invoke(this, new(context));
        }

        // Listen for new request
        if (!AllowMultipleRequests)
            Receive();
    }

    protected virtual void CheckContext(HttpListenerContext context)
    {
        Log.Info("Start handling request");

        void close()
        {
            Log.Info("End handling request");
            context.Response.OutputStream.Close();
        }

        // Parse url
        Log.Debug("Parse url");
        var path = context.Request.Url?.PathAndQuery.Replace(ApiUrl, string.Empty);
        if (string.IsNullOrWhiteSpace(path))
        {
            Log.Info("Request has no path");
            close();
            return;
        }

        // Find handler
        Log.Debug("Find handler");
        if (!TryGetHandler(path, context.Request.HttpMethod, out var handler))
        {
            Log.Info("Request handler couldn't be found");
            close();
            return;
        }

        // Get auth key
        Log.Debug("Get auth key");
        if (context.Request.Headers.Get("API-AUTH-KEY") is not string authKey)
            authKey = null!;

        // Read input content
        Log.Debug("Read input content");
        string? contentJson;
        if (context.Request.ContentType is string contentType
            && contentType.Contains("application/json")
            && context.Request.ContentLength64 > 0)
        {
            try
            {
                using StreamReader input = new(context.Request.InputStream);
                contentJson = input.ReadToEnd();
            }
            catch (OutOfMemoryException)
            {
                Log.Error("Error reading remote data due to missing memory");
                close();
                return;
            }
            catch (Exception ex)
            {
                Log.Error("Error reading remote data", ex);
                close();
                return;
            }
        }
        else
            contentJson = null;

        // Handle message
        Log.Debug("Handle mssage");
        if (HandleMessage(path, context.Request.HttpMethod, handler, contentJson, authKey) is not PrivateApiResult result)
        {
            Log.Warn("Request couldn't be handled");
            close();
            return;
        }

        // Set response parameters
        Log.Debug("Set response parameters");
        context.Response.StatusCode = (int)result.Original.StatusCode;

        // Write response content
        Log.Debug("Create response");
        if (result.ResultJson is not null)
        {
            Log.Info("Sending response");
            context.Response.ContentType = "application/json";
            using StreamWriter output = new(context.Response.OutputStream);
            output.Write(result.ResultJson);
        }

        Log.Debug("Finish response");
        close();
    }

    protected virtual PrivateApiResult? HandleMessage(string url, string method, PrivateMessageHandler handler, string? json, string? authKey)
    {
        // Check authentication
        Log.Debug("Check authentication");
        var isAuthenticated = false;
        if (!string.IsNullOrWhiteSpace(authKey) && DecodeAuthKey(authKey) is string authKeyDecoded)
            isAuthenticated = CheckAuthentication(authKeyDecoded, handler.Handler);
        else
            authKeyDecoded = null!;
        if (handler.Attribute.RequiesAuth && !isAuthenticated)
            return new(ApiResult.Unauthorized(), null);

        // Get required infos
        Log.Debug("Identify message parameter type and serializer");
        var targetType = handler.Handler.Method.GetParameters().FirstOrDefault(p => p.ParameterType.IsAssignableTo(typeof(ApiMessage)))?.ParameterType;
        var serializer = GetSerializer(handler.Attribute.Serializer);

        // Deserialize
        Log.Debug("Deserialize message");
        ApiMessage? message;
        if (json != null && targetType != null)
            message = serializer.Deserialize(json, targetType);
        else
            message = null;

        // Invoke handler
        Log.Debug("Invoke handler");
        var parameters = BuildParameters(url, handler, () => message, () => new(message, isAuthenticated, authKeyDecoded, url, method));
        if (handler.Handler.DynamicInvoke(parameters) is not ApiResult result)
            return new(ApiResult.InternalServerError(), null);

        // Return result without message
        Log.Debug("Check message");
        if (result.Message is null)
            return new(result, null);

        // Serializer
        Log.Debug("Serialize message");
        if (serializer.Serialize(result.Message) is not string resultStr)
            return new(ApiResult.InternalServerError(), null);

        // Return result with message
        Log.Debug("Complete result");
        return new(result, resultStr);
    }

    protected virtual bool TryGetHandler(string url, string method, [NotNullWhen(true)] out PrivateMessageHandler? handler)
    {
        handler = handlers.FirstOrDefault(handler =>
        {
            if (!handler.Attribute.Methods.Contains(method))
                return false;

            if (handler.UseRegEx)
                return Regex.IsMatch(url, handler.Url, RegexOptions.IgnoreCase);

            return handler.Url.Equals(url, StringComparison.InvariantCultureIgnoreCase);
        });

        return handler != null;
    }

    protected virtual object?[]? BuildParameters(string url, PrivateMessageHandler handler, Func<ApiMessage?> getMessage, Func<ApiRequestInfo> getRequestInfo)
    {
        var infos = handler.Handler.Method.GetParameters();
        var objs = new List<object?>();

        foreach (var info in infos)
        {
            if (info.ParameterType.IsAssignableTo(typeof(ApiMessage)))
                objs.Add(getMessage());
            else if (info.ParameterType.IsAssignableTo(typeof(ApiRequestInfo)))
                objs.Add(getRequestInfo());
            else if (handler.Parameters.FirstOrDefault(p => p.Name.Equals(info.Name, StringComparison.InvariantCultureIgnoreCase)) is PrivateParameterInfo parameterInfo
                && url.Split('/').ElementAtOrDefault(parameterInfo.Index) is string parameterValue)
                objs.Add(Convert.ChangeType(HttpUtility.UrlDecode(parameterValue), info.ParameterType)); // or Uri.UnescapeDataString(); maybe run this line twice?
            else
                objs.Add(null);
        }

        return [.. objs];
    }

    protected virtual IApiMessageSerializer GetSerializer(Type? t)
    {
        if (t is not null)
        {
            if (serializers.TryGetValue(t, out var s) && s is not null)
                return s;
            else if (Activator.CreateInstance(t) is IApiMessageSerializer ss)
            {
                serializers.Add(t, ss);
                return ss;
            }
        }
        return Serializer;
    }

    protected virtual bool CheckAuthentication(string authKey, Delegate? handler)
    {
        if (OnCheckAuthentication != null)
        {
            var args = new ApiAuthCheckEventArgs(authKey, handler);
            OnCheckAuthentication?.Invoke(this, args);
            return args.Valid;
        }
        return false;
    }

    protected virtual string? DecodeAuthKey(string authKey)
    {
        return authKey;
    }
}
