using SharpChat.Auth;
using SharpChat.Bans;
using SharpChat.Configuration;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace SharpChat.Flashii {
    public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, BansClient {
        private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat";
        private readonly CachedValue<string> BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);

        private const string DEFAULT_SECRET_KEY = "woomy";
        private readonly CachedValue<string> SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);

        private string CreateStringSignature(string str) {
            return CreateBufferSignature(Encoding.UTF8.GetBytes(str));
        }

        private string CreateBufferSignature(byte[] bytes) {
            using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey!));
            return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2")));
        }

        private const string AUTH_VERIFY_URL = "{0}/verify";
        private const string AUTH_VERIFY_SIG = "verify#{0}#{1}#{2}";

        public async Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token) {
            string remoteAddrStr = remoteAddr.ToString();

            HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) {
                Content = new FormUrlEncodedContent(new Dictionary<string, string> {
                    { "method", scheme },
                    { "token", token },
                    { "ipaddr", remoteAddrStr },
                }),
                Headers = {
                    { "X-SharpChat-Signature", CreateStringSignature(string.Format(AUTH_VERIFY_SIG, scheme, token, remoteAddrStr)) },
                },
            };

            using HttpResponseMessage response = await httpClient.SendAsync(request);
            response.EnsureSuccessStatusCode();

            using Stream stream = await response.Content.ReadAsStreamAsync();
            FlashiiAuthResult? authResult = await JsonSerializer.DeserializeAsync<FlashiiAuthResult>(stream);
            if(authResult?.Success != true)
                throw new AuthFailedException(authResult?.Reason ?? "none");

            return authResult;
        }

        private const string AUTH_BUMP_USERS_ONLINE_URL = "{0}/bump";

        public async Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries) {
            if(!entries.Any())
                return;

            string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
            StringBuilder sb = new();
            sb.AppendFormat("bump#{0}", now);

            Dictionary<string, string> formData = new() {
                { "t", now },
            };

            foreach(var (remoteAddr, userId) in entries) {
                string remoteAddrStr = remoteAddr.ToString();
                sb.AppendFormat("#{0}:{1}", userId, remoteAddrStr);
                formData.Add(string.Format("u[{0}]", userId), remoteAddrStr);
            }

            HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_BUMP_USERS_ONLINE_URL, BaseURL)) {
                Headers = {
                    { "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) }
                },
                Content = new FormUrlEncodedContent(formData),
            };

            using HttpResponseMessage response = await httpClient.SendAsync(request);
            response.EnsureSuccessStatusCode();
        }

        private const string BANS_CREATE_URL = "{0}/bans/create";
        private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}";

        public async Task BanCreateAsync(
            BanKind kind,
            TimeSpan duration,
            IPAddress remoteAddr,
            string? userId = null,
            string? reason = null,
            IPAddress? issuerRemoteAddr = null,
            string? issuerUserId = null
        ) {
            if(duration <= TimeSpan.Zero || kind != BanKind.User)
                return;

            issuerUserId ??= string.Empty;
            userId ??= string.Empty;
            reason ??= string.Empty;
            issuerRemoteAddr ??= IPAddress.IPv6None;

            string isPerma = duration == TimeSpan.MaxValue ? "1" : "0";
            string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString();
            string remoteAddrStr = remoteAddr.ToString();
            string issuerRemoteAddrStr = issuerRemoteAddr.ToString();

            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
            string sig = string.Format(
                BANS_CREATE_SIG,
                now, userId, remoteAddrStr,
                issuerUserId, issuerRemoteAddrStr,
                durationStr, isPerma, reason
            );

            HttpRequestMessage request = new(HttpMethod.Post, string.Format(BANS_CREATE_URL, BaseURL)) {
                Headers = {
                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
                },
                Content = new FormUrlEncodedContent(new Dictionary<string, string> {
                    { "t", now },
                    { "ui", userId },
                    { "ua", remoteAddrStr },
                    { "mi", issuerUserId },
                    { "ma", issuerRemoteAddrStr },
                    { "d", durationStr },
                    { "p", isPerma },
                    { "r", reason },
                }),
            };

            using HttpResponseMessage response = await httpClient.SendAsync(request);

            response.EnsureSuccessStatusCode();
        }

        private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}";
        private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}";

        public async Task<bool> BanRevokeAsync(BanInfo info) {
            string type;
            string target;

            if(info is UserBanInfo ubi) {
                if(info.Kind != BanKind.User)
                    throw new ArgumentException("info argument is an instance of UserBanInfo but Kind was not set to BanKind.User", nameof(info));

                type = "user";
                target = ubi.UserId;
            } else if(info is IPAddressBanInfo iabi) {
                if(info.Kind != BanKind.IPAddress)
                    throw new ArgumentException("info argument is an instance of IPAddressBanInfo but Kind was not set to BanKind.IPAddress", nameof(info));

                type = "addr";
                target = iabi.Address.ToString();
            } else throw new ArgumentException("info argument is set to unsupported implementation", nameof(info));

            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
            string url = string.Format(BANS_REVOKE_URL, BaseURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now));
            string sig = string.Format(BANS_REVOKE_SIG, now, type, target);

            HttpRequestMessage request = new(HttpMethod.Delete, url) {
                Headers = {
                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
                },
            };

            using HttpResponseMessage response = await httpClient.SendAsync(request);
            if(response.StatusCode == HttpStatusCode.NotFound)
                return false;

            response.EnsureSuccessStatusCode();

            return response.StatusCode == HttpStatusCode.NoContent;
        }

        private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}";
        private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}";

        public async Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null) {
            userIdOrName ??= "0";
            remoteAddr ??= IPAddress.None;

            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
            bool usingUserName = string.IsNullOrEmpty(userIdOrName) || userIdOrName.Any(c => c is < '0' or > '9');
            string remoteAddrStr = remoteAddr.ToString();
            string usingUserNameStr = usingUserName ? "1" : "0";
            string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userIdOrName), Uri.EscapeDataString(remoteAddrStr), Uri.EscapeDataString(now), Uri.EscapeDataString(usingUserNameStr));
            string sig = string.Format(BANS_CHECK_SIG, now, userIdOrName, remoteAddrStr, usingUserNameStr);

            HttpRequestMessage request = new(HttpMethod.Get, url) {
                Headers = {
                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
                },
            };

            using HttpResponseMessage response = await httpClient.SendAsync(request);
            response.EnsureSuccessStatusCode();

            using Stream stream = await response.Content.ReadAsStreamAsync();
            FlashiiRawBanInfo? rawBanInfo = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo>(stream);
            if(rawBanInfo?.IsBanned != true || rawBanInfo.HasExpired)
                return null;

            return rawBanInfo.RemoteAddress is null or "::"
                ? new FlashiiUserBanInfo(rawBanInfo)
                : new FlashiiIPAddressBanInfo(rawBanInfo);
        }

        private const string BANS_LIST_URL = "{0}/bans/list?x={1}";
        private const string BANS_LIST_SIG = "list#{0}";

        public async Task<BanInfo[]> BanGetListAsync() {
            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
            string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now));
            string sig = string.Format(BANS_LIST_SIG, now);

            HttpRequestMessage request = new(HttpMethod.Get, url) {
                Headers = {
                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
                },
            };

            using HttpResponseMessage response = await httpClient.SendAsync(request);
            response.EnsureSuccessStatusCode();

            using Stream stream = await response.Content.ReadAsStreamAsync();
            FlashiiRawBanInfo[]? list = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo[]>(stream);
            if(list is null || list.Length < 1)
                return [];

            return [.. list.Where(b => b?.IsBanned == true && !b.HasExpired).Select(b => {
                return (BanInfo)(b.RemoteAddress is null or "::"
                    ? new FlashiiUserBanInfo(b) : new FlashiiIPAddressBanInfo(b));
            })];
        }
    }
}