using Microsoft.Extensions.Logging;
using SharpChat.Auth;
using SharpChat.Bans;
using SharpChat.Configuration;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using ZLogger;

namespace SharpChat.Flashii;

public class FlashiiClient(ILogger logger, 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> AuthVerify(IPAddress remoteAddr, string scheme, string token) {
        logger.ZLogInformation($"Verifying authentication data for {remoteAddr}...");
        logger.ZLogTrace($"AuthVerify({remoteAddr}, {scheme}, {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);
        logger.ZLogTrace($"AuthVerify() -> HTTP {response.StatusCode}");
        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 AuthBumpUsersOnline(IEnumerable<(IPAddress remoteAddr, string userId)> entries) {
        if(!entries.Any())
            return;

        logger.ZLogInformation($"Bumping online users list...");

        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);
        logger.ZLogTrace($"AuthBumpUsersOnline() -> HTTP {response.StatusCode}");
        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 BanCreate(
        BanKind kind,
        TimeSpan duration,
        IPAddress remoteAddr,
        string? userId = null,
        string? reason = null,
        IPAddress? issuerRemoteAddr = null,
        string? issuerUserId = null
    ) {
        logger.ZLogInformation($"Creating ban of kind {kind} with duration {duration} for {remoteAddr}/{userId} issued by {issuerRemoteAddr}/{issuerUserId}...");
        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);
        logger.ZLogTrace($"BanCreate() -> HTTP {response.StatusCode}");
        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> BanRevoke(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));

        logger.ZLogInformation($"Revoking ban of kind {info.Kind} issued on {target}...");

        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;

        logger.ZLogTrace($"BanRevoke() -> HTTP {response.StatusCode}");
        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?> BanGet(string? userIdOrName = null, IPAddress? remoteAddr = null) {
        userIdOrName ??= "0";
        remoteAddr ??= IPAddress.None;

        logger.ZLogInformation($"Requesting ban info for {remoteAddr}/{userIdOrName}...");

        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);
        logger.ZLogTrace($"BanGet() -> HTTP {response.StatusCode}");
        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[]> BanGetList() {
        logger.ZLogInformation($"Requesting ban list...");

        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);
        logger.ZLogTrace($"BanGetList() -> HTTP {response.StatusCode}");
        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));
        })];
    }
}