using Dalamud.Utility;
using LightlessSync.API.Routes;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json;

namespace LightlessSync.Services.ServerConfiguration;

public class ServerConfigurationManager
{
    private readonly ServerConfigService _configService;
    private readonly DalamudUtilService _dalamudUtil;
    private readonly LightlessConfigService _lightlessConfigService;
    private readonly HttpClient _httpClient;
    private readonly ILogger<ServerConfigurationManager> _logger;
    private readonly LightlessMediator _lightlessMediator;
    private readonly NotesConfigService _notesConfig;
    private readonly PairTagConfigService _pairTagConfig;
    private readonly SyncshellTagConfigService _syncshellTagConfig;
    private readonly int _maxCharactersFolder = 20;

    public ServerConfigurationManager(ILogger<ServerConfigurationManager> logger, ServerConfigService configService,
        PairTagConfigService pairTagConfig, SyncshellTagConfigService syncshellTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil,
        LightlessConfigService lightlessConfigService, HttpClient httpClient, LightlessMediator lightlessMediator)
    {
        _logger = logger;
        _configService = configService;
        _pairTagConfig = pairTagConfig;
        _syncshellTagConfig = syncshellTagConfig;
        _notesConfig = notesConfig;
        _dalamudUtil = dalamudUtil;
        _lightlessConfigService = lightlessConfigService;
        _httpClient = httpClient;
        _lightlessMediator = lightlessMediator;
        EnsureMainExists();
    }

    public string CurrentApiUrl => CurrentServer.ServerUri;
    public ServerStorage CurrentServer => _configService.Current.ServerStorage[CurrentServerIndex];
    public bool SendCensusData
    {
        get
        {
            return _configService.Current.SendCensusData;
        }
        set
        {
            _configService.Current.SendCensusData = value;
            _configService.Save();
        }
    }

    public bool ShownCensusPopup
    {
        get
        {
            return _configService.Current.ShownCensusPopup;
        }
        set
        {
            _configService.Current.ShownCensusPopup = value;
            _configService.Save();
        }
    }

    public int CurrentServerIndex
    {
        set
        {
            _configService.Current.CurrentServer = value;
            _configService.Save();
        }
        get
        {
            if (_configService.Current.CurrentServer < 0)
            {
                _configService.Current.CurrentServer = 0;
                _configService.Save();
            }

            return _configService.Current.CurrentServer;
        }
    }

    public (string OAuthToken, string UID)? GetOAuth2(out bool hasMulti, int serverIdx = -1)
    {
        ServerStorage? currentServer;
        currentServer = serverIdx == -1 ? CurrentServer : GetServerByIndex(serverIdx);
        if (currentServer == null)
        {
            currentServer = new();
            Save();
        }
        hasMulti = false;

        var charaName = _dalamudUtil.GetPlayerName();
        var worldId = _dalamudUtil.GetHomeWorldId();
        var cid = _dalamudUtil.GetCID();

        var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName) && f.WorldId == worldId);
        if (auth.Count >= 2)
        {
            _logger.LogTrace("GetOAuth2 accessed, returning null because multiple ({count}) identical characters.", auth.Count);
            hasMulti = true;
            return null;
        }

        if (auth.Count == 0)
        {
            _logger.LogTrace("GetOAuth2 accessed, returning null because no set up characters for {chara} on {world}", charaName, worldId);
            return null;
        }

        if (auth.Single().LastSeenCID != cid)
        {
            auth.Single().LastSeenCID = cid;
            _logger.LogTrace("GetOAuth2 accessed, updating CID for {chara} on {world} to {cid}", charaName, worldId, cid);
            Save();
        }

        if (!string.IsNullOrEmpty(auth.Single().UID) && !string.IsNullOrEmpty(currentServer.OAuthToken))
        {
            _logger.LogTrace("GetOAuth2 accessed, returning {key} ({keyValue}) for {chara} on {world}", auth.Single().UID, string.Join("", currentServer.OAuthToken.Take(10)), charaName, worldId);
            return (currentServer.OAuthToken, auth.Single().UID!);
        }

        _logger.LogTrace("GetOAuth2 accessed, returning null because no UID found for {chara} on {world} or OAuthToken is not configured.", charaName, worldId);

        return null;
    }

    public string? GetSecretKey(out bool hasMulti, int serverIdx = -1)
    {
        ServerStorage? currentServer;
        currentServer = serverIdx == -1 ? CurrentServer : GetServerByIndex(serverIdx);
        if (currentServer == null)
        {
            currentServer = new();
            Save();
        }
        hasMulti = false;

        var charaName = _dalamudUtil.GetPlayerName();
        var worldId = _dalamudUtil.GetHomeWorldId();
        var cid = _dalamudUtil.GetCID();
        if (!currentServer.Authentications.Any() && currentServer.SecretKeys.Any())
        {
            currentServer.Authentications.Add(new Authentication()
            {
                CharacterName = charaName,
                WorldId = worldId,
                LastSeenCID = cid,
                SecretKeyIdx = currentServer.SecretKeys.Last().Key,
            });

            Save();
        }

        var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName, StringComparison.Ordinal) && f.WorldId == worldId);
        if (auth.Count >= 2)
        {
            _logger.LogTrace("GetSecretKey accessed, returning null because multiple ({count}) identical characters.", auth.Count);
            hasMulti = true;
            return null;
        }

        if (auth.Count == 0)
        {
            _logger.LogTrace("GetSecretKey accessed, returning null because no set up characters for {chara} on {world}", charaName, worldId);
            return null;
        }

        if (auth.Single().LastSeenCID != cid)
        {
            auth.Single().LastSeenCID = cid;
            _logger.LogTrace("GetSecretKey accessed, updating CID for {chara} on {world} to {cid}", charaName, worldId, cid);
            Save();
        }

        if (currentServer.SecretKeys.TryGetValue(auth.Single().SecretKeyIdx, out var secretKey))
        {
            _logger.LogTrace("GetSecretKey accessed, returning {key} ({keyValue}) for {chara} on {world}", secretKey.FriendlyName, string.Join("", secretKey.Key.Take(10)), charaName, worldId);
            return secretKey.Key;
        }

        _logger.LogTrace("GetSecretKey accessed, returning null because no fitting key found for {chara} on {world} for idx {idx}.", charaName, worldId, auth.Single().SecretKeyIdx);

        return null;
    }

    public string[] GetServerApiUrls()
    {
        return _configService.Current.ServerStorage.Select(v => v.ServerUri).ToArray();
    }

    public ServerStorage GetServerByIndex(int idx)
    {
        try
        {
            return _configService.Current.ServerStorage[idx];
        }
        catch
        {
            _configService.Current.CurrentServer = 0;
            EnsureMainExists();
            return CurrentServer!;
        }
    }

    public string GetDiscordUserFromToken(ServerStorage server)
    {
        JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
        if (string.IsNullOrEmpty(server.OAuthToken)) return string.Empty;
        try
        {
            var token = handler.ReadJwtToken(server.OAuthToken);
            return token.Claims.First(f => string.Equals(f.Type, "discord_user", StringComparison.Ordinal)).Value!;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Could not read jwt, resetting it");
            server.OAuthToken = null;
            Save();
            return string.Empty;
        }
    }

    public string[] GetServerNames()
    {
        return _configService.Current.ServerStorage.Select(v => v.ServerName).ToArray();
    }

    public bool HasValidConfig()
    {
        return CurrentServer != null && CurrentServer.Authentications.Count > 0;
    }

    public void Save()
    {
        var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown";
        _logger.LogDebug("{caller} Calling config save", caller);
        _configService.Save();
    }

    public void SelectServer(int idx)
    {
        var previousIndex = _configService.Current.CurrentServer;
        _configService.Current.CurrentServer = idx;
        CurrentServer!.FullPause = false;
        Save();

        if (previousIndex != idx)
        {
            var serverUrl = CurrentServer.ServerUri;
            _lightlessMediator.Publish(new ActiveServerChangedMessage(serverUrl));
        }
    }

    internal void AddCurrentCharacterToServer(int serverSelectionIndex = -1)
    {
        if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex;
        var server = GetServerByIndex(serverSelectionIndex);
        if (server.Authentications.Exists(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerName(), StringComparison.Ordinal)
                && c.WorldId == _dalamudUtil.GetHomeWorldId()))
            return;

        server.Authentications.Add(new Authentication()
        {
            CharacterName = _dalamudUtil.GetPlayerName(),
            WorldId = _dalamudUtil.GetHomeWorldId(),
            SecretKeyIdx = !server.UseOAuth2 ? server.SecretKeys.Last().Key : -1,
            LastSeenCID = _dalamudUtil.GetCID()
        });
        Save();
    }

    internal void AddEmptyCharacterToServer(int serverSelectionIndex)
    {
        var server = GetServerByIndex(serverSelectionIndex);
        server.Authentications.Add(new Authentication()
        {
            SecretKeyIdx = server.SecretKeys.Count != 0 ? server.SecretKeys.First().Key : -1,
        });
        Save();
    }

    internal void AddOpenPairTag(string tag)
    {
        CurrentPairTagStorage().OpenPairTags.Add(tag);
        _pairTagConfig.Save();
    }

    internal void AddServer(ServerStorage serverStorage)
    {
        _configService.Current.ServerStorage.Add(serverStorage);
        Save();
    }

    internal void AddPairTag(string tag)
    {
        if (tag.Length <= _maxCharactersFolder)
        {
            CurrentPairTagStorage().ServerAvailablePairTags.Add(tag);
            _pairTagConfig.Save();
            _lightlessMediator.Publish(new RefreshUiMessage());
        }
        else
        {
            _logger.LogInformation("Couldn't save/add {tag}. Name too long to be saved", tag);
        }
    }

    internal void AddSyncshellTag(string tag)
    {
        if (tag.Length <= _maxCharactersFolder)
        {
            CurrentSyncshellTagStorage().ServerAvailableSyncshellTags.Add(tag);
            _syncshellTagConfig.Save();
            _lightlessMediator.Publish(new RefreshUiMessage());
        }
        else
        {
            _logger.LogInformation("Couldn't save/add {tag}. Name too long to be saved", tag);
        }
    }

    internal void AddTagForUid(string uid, string tagName)
    {
        if (CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
        {
            tags.Add(tagName);
            _lightlessMediator.Publish(new RefreshUiMessage());
        }
        else
        {
            CurrentPairTagStorage().UidServerPairedUserTags[uid] = [tagName];
        }

        _pairTagConfig.Save();
    }

    internal void AddTagForSyncshell(string syncshellName, string tagName)
    {
        if (CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(syncshellName, out var tags))
        {
            tags.Add(tagName);
            _lightlessMediator.Publish(new RefreshUiMessage());
        }
        else
        {
            CurrentSyncshellTagStorage().SyncshellPairedTags[syncshellName] = [tagName];
        }

        _syncshellTagConfig.Save();
    }

    internal bool ContainsOpenPairTag(string tag) => CurrentPairTagStorage().OpenPairTags.Contains(tag);

    internal bool ContainsPairTag(string uid, string tag)
    {
        if (CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
        {
            return tags.Contains(tag, StringComparer.Ordinal);
        }

        return false;
    }

    internal bool ContainsSyncshellTag(string name, string tag)
    {
        if (CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(name, out var tags))
        {
            return tags.Contains(tag, StringComparer.Ordinal);
        }

        return false;
    }

    internal void DeleteServer(ServerStorage selectedServer)
    {
        if (Array.IndexOf(_configService.Current.ServerStorage.ToArray(), selectedServer) <
            _configService.Current.CurrentServer)
        {
            _configService.Current.CurrentServer--;
        }

        _configService.Current.ServerStorage.Remove(selectedServer);
        Save();
    }

    internal string? GetNoteForGid(string gID)
    {
        if (CurrentNotesStorage().GidServerComments.TryGetValue(gID, out var note))
        {
            if (string.IsNullOrEmpty(note)) return null;
            return note;
        }

        return null;
    }

    internal string? GetNoteForUid(string uid)
    {
        if (CurrentNotesStorage().UidServerComments.TryGetValue(uid, out var note))
        {
            if (string.IsNullOrEmpty(note)) return null;
            return note;
        }
        return null;
    }

    internal HashSet<string> GetServerAvailablePairTags() => CurrentPairTagStorage().ServerAvailablePairTags;

    internal HashSet<string> GetServerAvailableSyncshellTags() => CurrentSyncshellTagStorage().ServerAvailableSyncshellTags;

    internal Dictionary<string, List<string>> GetUidServerPairedUserTags() => CurrentPairTagStorage().UidServerPairedUserTags;

    internal HashSet<string> GetUidsForPairTag(string tag) => CurrentPairTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);

    internal HashSet<string> GetNamesForSyncshellTag(string tag) => CurrentSyncshellTagStorage().SyncshellPairedTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);

    internal bool HasPairTags(string uid) => CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags) && tags.Count != 0;

    internal bool HasSyncshellTags(string name) => CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(name, out var tags) && tags.Count != 0;

    internal void RemoveCharacterFromServer(int serverSelectionIndex, Authentication item)
    {
        var server = GetServerByIndex(serverSelectionIndex);
        server.Authentications.Remove(item);
        Save();
    }

    internal void RemoveOpenPairTag(string tag)
    {
        CurrentPairTagStorage().OpenPairTags.Remove(tag);
        _pairTagConfig.Save();
    }

    internal void RemovePairTag(string tag)
    {
        RemoveTag(CurrentPairTagStorage().ServerAvailablePairTags, tag);
        _pairTagConfig.Save();
        _lightlessMediator.Publish(new RefreshUiMessage());
    }

    internal void RemoveSyncshellTag(string tag)
    {
        RemoveTag(CurrentSyncshellTagStorage().ServerAvailableSyncshellTags, tag, true);
        _syncshellTagConfig.Save();
        _lightlessMediator.Publish(new RefreshUiMessage());
    }

    internal void RemoveTag(HashSet<string> storage, string tag, bool syncshell = false)
    {
        storage.Remove(tag);
        if (syncshell)
        {
            foreach (var uid in GetNamesForSyncshellTag(tag))
            {
                RemoveTagForSyncshell(uid, tag, save: false);
            }
        }
        else
        {
            foreach (var uid in GetUidsForPairTag(tag))
            {
                RemoveTagForUid(uid, tag, save: false);
            }
        }
    }

    internal void RemoveTagForUid(string uid, string tagName, bool save = true)
    {
        if (CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
        {
            tags.Remove(tagName);

            if (save)
            {
                _pairTagConfig.Save();
                _lightlessMediator.Publish(new RefreshUiMessage());
            }
        }
    }

    internal void RemoveTagForSyncshell(string name, string tagName, bool save = true)
    {
        if (CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(name, out var tags))
        {
            tags.Remove(tagName);

            if (save)
            {
                _syncshellTagConfig.Save();
                _lightlessMediator.Publish(new RefreshUiMessage());
            }
        }
    }

    internal void RenamePairTag(string oldName, string newName) => RenameTag(CurrentPairTagStorage().UidServerPairedUserTags, CurrentPairTagStorage().ServerAvailablePairTags, oldName, newName);

    internal void RenameSyncshellTag(string oldName, string newName) => RenameTag(CurrentSyncshellTagStorage().SyncshellPairedTags, CurrentSyncshellTagStorage().ServerAvailableSyncshellTags, oldName, newName);

    internal void RenameTag(Dictionary<string, List<string>> tags, HashSet<string> storage, string oldName, string newName)
    {
        if (newName.Length < _maxCharactersFolder)
        {
            storage.Remove(oldName);
            storage.Add(newName);
            foreach (var existingTags in tags.Select(k => k.Value))
            {
                if (existingTags.Remove(oldName))
                    existingTags.Add(newName);
            }
            _lightlessMediator.Publish(new RefreshUiMessage());
        }
        else
        {
            _logger.LogInformation("Couldn't save/add {tag}. Name too long to be saved", newName);
        }
    }

    internal void SaveNotes() => _notesConfig.Save();

    internal void SetNoteForGid(string gid, string note, bool save = true)
    {
        if (string.IsNullOrEmpty(gid)) return;

        CurrentNotesStorage().GidServerComments[gid] = note;
        if (save)
            _notesConfig.Save();
    }

    internal void SetNoteForUid(string uid, string note, bool save = true)
    {
        if (string.IsNullOrEmpty(uid)) return;

        CurrentNotesStorage().UidServerComments[uid] = note;
        if (save)
            _notesConfig.Save();
    }

    internal void AutoPopulateNoteForUid(string uid, string note)
    {
        if (!_lightlessConfigService.Current.AutoPopulateEmptyNotesFromCharaName
            || GetNoteForUid(uid) != null)
            return;

        SetNoteForUid(uid, note, save: true);
    }

    private ServerNotesStorage CurrentNotesStorage()
    {
        TryCreateCurrentNotesStorage();
        return _notesConfig.Current.ServerNotes[CurrentApiUrl];
    }

    private PairTagStorage CurrentPairTagStorage()
    {
        TryCreateCurrentPairTagStorage();
        return _pairTagConfig.Current.ServerTagStorage[CurrentApiUrl];
    }

    private SyncshellTagStorage CurrentSyncshellTagStorage()
    {
        TryCreateCurrentSyncshellTagStorage();
        return _syncshellTagConfig.Current.ServerTagStorage[CurrentApiUrl];
    }

    private void EnsureMainExists()
    {
        if (_configService.Current.ServerStorage.Count == 0 || !string.Equals(_configService.Current.ServerStorage[0].ServerUri, ApiController.MainServiceUri, StringComparison.OrdinalIgnoreCase))
        {
            _configService.Current.ServerStorage.Insert(0, new ServerStorage() { ServerUri = ApiController.MainServiceUri, ServerName = ApiController.MainServer, UseOAuth2 = true });
        }
        Save();
    }

    private void TryCreateCurrentNotesStorage()
    {
        if (!_notesConfig.Current.ServerNotes.ContainsKey(CurrentApiUrl))
        {
            _notesConfig.Current.ServerNotes[CurrentApiUrl] = new();
        }
    }

    private void TryCreateCurrentPairTagStorage()
    {
        if (!_pairTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
        {
            _pairTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
        }
    }

    private void TryCreateCurrentSyncshellTagStorage()
    {
        if (!_syncshellTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
        {
            _syncshellTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
        }
    }

    public async Task<Dictionary<string, string>> GetUIDsWithDiscordToken(string serverUri, string token)
    {
        try
        {
            var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
            var oauthCheckUri = LightlessAuth.GetUIDsFullPath(new Uri(baseUri));
            using var request = new HttpRequestMessage(HttpMethod.Get, oauthCheckUri);
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
            using var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
            var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
            return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? [];
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failure getting UIDs");
            return [];
        }
    }

    public async Task<Uri?> CheckDiscordOAuth(string serverUri)
    {
        try
        {
            var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
            var oauthCheckUri = LightlessAuth.GetDiscordOAuthEndpointFullPath(new Uri(baseUri));
            var response = await _httpClient.GetFromJsonAsync<Uri?>(oauthCheckUri).ConfigureAwait(false);
            return response;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failure checking for Discord Auth");
            return null;
        }
    }

    public async Task<string?> GetDiscordOAuthToken(Uri discordAuthUri, string serverUri, CancellationToken token)
    {
        var sessionId = BitConverter.ToString(RandomNumberGenerator.GetBytes(64)).Replace("-", "").ToLower();
        Util.OpenLink(discordAuthUri.ToString() + "?sessionId=" + sessionId);

        string? discordToken = null;
        using CancellationTokenSource timeOutCts = new();
        timeOutCts.CancelAfter(TimeSpan.FromSeconds(60));
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeOutCts.Token, token);
        try
        {
            var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
            var oauthCheckUri = LightlessAuth.GetDiscordOAuthTokenFullPath(new Uri(baseUri), sessionId);
            var response = await _httpClient.GetAsync(oauthCheckUri, linkedCts.Token).ConfigureAwait(false);
            discordToken = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failure getting Discord Token");
            return null;
        }

        if (discordToken == null)
            return null;

        return discordToken;
    }

    public HttpTransportType GetTransport()
    {
        return CurrentServer.HttpTransportType;
    }

    public void SetTransportType(HttpTransportType httpTransportType)
    {
        CurrentServer.HttpTransportType = httpTransportType;
        Save();
    }
}