using System;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

namespace Furlumin.SDK.Editor
{
    [Serializable]
    internal sealed class SdkUser
    {
        public string   userId;
        public string   username;
        public string   displayName;
        public string   avatar;
        public string[] groups;
    }

    internal static class FurluminAuthManager
    {
        private const string ApiBase          = "https://api.furlumin.de";
        private const int    PortRangeMin     = 47832;
        private const int    PortRangeMax     = 47899;
        private const int    TimeoutMin       = 5;
        private const int    LoginCooldownSec = 30;
        private const int    MaxLoginAttempts = 5;

        private static string   s_token;
        private static SdkUser  s_user;
        private static DateTime s_lastLoginAttempt   = DateTime.MinValue;
        private static int      s_loginAttempts      = 0;
        private static DateTime s_attemptWindowStart = DateTime.MinValue;

        internal static bool    IsLoggedIn() =>
            !string.IsNullOrEmpty(s_token) && !IsTokenExpired(s_token);

        internal static SdkUser GetUser()  => s_user;
        internal static string  GetToken() => s_token;

        internal static void Logout()
        {
            s_token = null;
            s_user  = null;
        }

        internal static (bool allowed, string reason) CheckLoginRateLimit()
        {
            var now = DateTime.UtcNow;

            if ((now - s_attemptWindowStart).TotalSeconds > 120)
            {
                s_loginAttempts      = 0;
                s_attemptWindowStart = now;
            }

            if (s_loginAttempts >= MaxLoginAttempts)
                return (false, "Too many login attempts. Please wait a moment.");

            double elapsed = (now - s_lastLoginAttempt).TotalSeconds;
            if (s_lastLoginAttempt != DateTime.MinValue && elapsed < LoginCooldownSec)
            {
                int wait = LoginCooldownSec - (int)elapsed;
                return (false, $"Please wait {wait}s before trying again.");
            }

            return (true, null);
        }

        internal static async Task<(bool success, string error)> StartBrowserOAuthFlow(
            Action<int> onListening       = null,
            CancellationToken cancelToken = default)
        {
            var (allowed, reason) = CheckLoginRateLimit();
            if (!allowed) return (false, reason);

            s_lastLoginAttempt = DateTime.UtcNow;
            s_loginAttempts++;

            int port = FindFreePort();
            if (port < PortRangeMin || port > PortRangeMax)
                return (false, "No free port found in the allowed range.");

            var listener = new HttpListener();
            listener.Prefixes.Add("http://localhost:" + port + "/");

            try { listener.Start(); }
            catch (Exception ex) { return (false, "Listener error: " + ex.Message); }

            onListening?.Invoke(port);
            Application.OpenURL(ApiBase + "/auth/discord?sdk=1&port=" + port);

            var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);

            var thread = new Thread(() =>
            {
                try
                {
                    var ctx    = listener.GetContext();
                    string rawUrl = ctx.Request.RawUrl ?? "";
                    string host   = ctx.Request.UserHostName ?? "";

                    if (!host.StartsWith("localhost") && !host.StartsWith("127.0.0.1"))
                    {
                        ctx.Response.StatusCode = 403;
                        ctx.Response.Close();
                        tcs.TrySetResult("");
                        return;
                    }

                    string userAgent = ctx.Request.UserAgent ?? "";
                    if (string.IsNullOrEmpty(userAgent))
                    {
                        ctx.Response.StatusCode = 400;
                        ctx.Response.Close();
                        tcs.TrySetResult("");
                        return;
                    }

                    string token = ExtractQueryParam(rawUrl, "token");

                    if (!IsWellFormedJwt(token))
                    {
                        ctx.Response.StatusCode = 400;
                        ctx.Response.Close();
                        tcs.TrySetResult("");
                        return;
                    }

                    byte[] buf = Encoding.UTF8.GetBytes(BuildSuccessHtml());
                    ctx.Response.ContentType     = "text/html; charset=utf-8";
                    ctx.Response.ContentLength64 = buf.Length;
                    ctx.Response.AddHeader("Cache-Control", "no-store");
                    ctx.Response.OutputStream.Write(buf, 0, buf.Length);
                    ctx.Response.Close();

                    tcs.TrySetResult(token);
                }
                catch (HttpListenerException) { tcs.TrySetResult(""); }
                catch (Exception ex)          { tcs.TrySetException(ex); }
            });
            thread.IsBackground = true;
            thread.Start();

            using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(TimeoutMin));
            using var linked     = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancelToken);
            Task timeout = Task.Delay(Timeout.Infinite, linked.Token)
                               .ContinueWith(_ => { }, TaskContinuationOptions.None);

            var completed = await Task.WhenAny(tcs.Task, timeout);
            listener.Stop();

            if (completed != tcs.Task)
                return (false, "Timeout - please try again.");

            string received;
            try   { received = await tcs.Task; }
            catch (Exception ex) { return (false, ex.Message); }

            if (string.IsNullOrEmpty(received))
                return (false, "No valid token received.");

            if (!IsWellFormedJwt(received))
                return (false, "Invalid token format.");

            StoreToken(received);
            s_loginAttempts = 0;
            return (true, null);
        }

        private static void StoreToken(string token)
        {
            s_token = token;
            try
            {
                string payload = DecodeJwtPayload(token);
                if (payload == null) return;
                s_user = new SdkUser
                {
                    userId      = ExtractString(payload, "userId"),
                    username    = ExtractString(payload, "username"),
                    displayName = ExtractString(payload, "displayName"),
                    avatar      = ExtractString(payload, "avatar"),
                    groups      = ExtractStringArray(payload, "groups"),
                };
            }
            catch { }
        }

        private static string DecodeJwtPayload(string token)
        {
            string[] parts = token.Split('.');
            if (parts.Length != 3) return null;
            string pad = parts[1].Replace('-', '+').Replace('_', '/');
            switch (pad.Length % 4)
            {
                case 2: pad += "=="; break;
                case 3: pad += "=";  break;
            }
            return Encoding.UTF8.GetString(Convert.FromBase64String(pad));
        }

        private static bool IsWellFormedJwt(string token)
        {
            if (string.IsNullOrEmpty(token)) return false;
            string[] parts = token.Split('.');
            return parts.Length == 3
                && parts[0].Length > 4
                && parts[1].Length > 4
                && parts[2].Length > 4;
        }

        private static bool IsTokenExpired(string token)
        {
            try
            {
                string payload = DecodeJwtPayload(token);
                if (payload == null) return true;
                var match = Regex.Match(payload, "\"exp\"\\s*:\\s*(\\d+)");
                if (!match.Success) return true;
                long exp = long.Parse(match.Groups[1].Value);
                return DateTimeOffset.FromUnixTimeSeconds(exp) <= DateTimeOffset.UtcNow;
            }
            catch { return true; }
        }

        private static int FindFreePort()
        {
            for (int p = PortRangeMin; p <= PortRangeMax; p++)
            {
                try
                {
                    var l = new HttpListener();
                    l.Prefixes.Add("http://localhost:" + p + "/");
                    l.Start();
                    l.Stop();
                    return p;
                }
                catch { }
            }
            return -1;
        }

        private static string ExtractQueryParam(string rawUrl, string key)
        {
            int start = rawUrl.IndexOf(key + "=", StringComparison.Ordinal);
            if (start < 0) return null;
            start += key.Length + 1;
            int end    = rawUrl.IndexOf('&', start);
            string val = end < 0 ? rawUrl.Substring(start) : rawUrl.Substring(start, end - start);
            return Uri.UnescapeDataString(val);
        }

        private static string ExtractString(string json, string key)
        {
            var m = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\"([^\"]*)\"");
            return m.Success ? m.Groups[1].Value : null;
        }

        private static string[] ExtractStringArray(string json, string key)
        {
            var m = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\\[([^\\]]*)\\]");
            if (!m.Success) return new string[0];
            var items  = Regex.Matches(m.Groups[1].Value, "\"([^\"]*)\"");
            var result = new string[items.Count];
            for (int i = 0; i < items.Count; i++)
                result[i] = items[i].Groups[1].Value;
            return result;
        }

        private static string BuildSuccessHtml() =>
            "<!DOCTYPE html><html><head><meta charset='utf-8'/>" +
            "<title>Furlumin SDK</title>" +
            "<style>*{margin:0;padding:0;box-sizing:border-box}" +
            "body{font-family:system-ui,sans-serif;background:#060f1e;color:#eaf2ff;" +
            "display:flex;align-items:center;justify-content:center;height:100vh;text-align:center}" +
            ".card{padding:2.5rem 3rem;background:rgba(255,255,255,.04);" +
            "border:1px solid rgba(255,255,255,.08);border-radius:18px}" +
            ".icon{font-size:2.5rem;margin-bottom:1rem}" +
            "h2{font-size:1.3rem;font-weight:800;margin-bottom:.5rem}" +
            "p{font-size:.88rem;color:rgba(234,242,255,.45)}</style></head>" +
            "<body><div class='card'><div class='icon'>&#10003;</div>" +
            "<h2>Logged in!</h2>" +
            "<p>You can close this window and return to Unity.</p>" +
            "</div></body></html>";
    }
}
