using System.Globalization;
using Sunfish.Dav;
using Sunfish.Net;

namespace Sunfish.Cli.Tui;

/// <summary>
/// Pestana Explorar: otros Sunfish de la red (DNS-SD) o cualquier WebDAV por direccion, con
/// descarga y subida en los dos sentidos. Las operaciones de red van en segundo plano y la
/// pantalla se redibuja al terminar o al avanzar.
/// </summary>
internal sealed partial class TuiApp
{
    private List<DiscoveredServer>? _servers;
    private int _serverSel;
    private bool _searching;
    private DavClient? _dav;
    private string? _davUser, _davPassword;
    private string _davFolder = "/";
    private List<DavItem> _davItems = new();
    private int _davSel;
    private string? _davBusy;
    private string? _davMessage;

    private bool Browsing => _dav is not null;

    private string ExploreHint() => Browsing ? TuiTexts.KeysExploreBrowse : TuiTexts.KeysExploreServers;

    /// <summary>Teclas de la pestana; devuelve true si las ha usado (Esc vuelve a la lista de servidores).</summary>
    private async Task<bool> ExploreKeyAsync(ConsoleKeyInfo key)
    {
        if (_servers is null && !_searching)
            StartSearch();
        if (_davBusy is not null)
            return key.Key != ConsoleKey.Q; // durante una operacion solo se puede salir
        if (!Browsing)
        {
            switch (key.Key)
            {
                case ConsoleKey.UpArrow: _serverSel = Math.Max(0, _serverSel - 1); return true;
                case ConsoleKey.DownArrow: _serverSel = Math.Min(Math.Max(0, (_servers?.Count ?? 1) - 1), _serverSel + 1); return true;
                case ConsoleKey.R: StartSearch(); return true;
                case ConsoleKey.A:
                    _overlays.Add(new InputOverlay(TuiTexts.ExploreAddress, "", false, text => { Connect(text); return Task.CompletedTask; }));
                    return true;
                case ConsoleKey.Enter when _servers is { Count: > 0 }:
                    Connect(_servers[_serverSel].Url);
                    return true;
            }
            return false;
        }

        var item = _davItems.Count > 0 ? _davItems[Math.Clamp(_davSel, 0, _davItems.Count - 1)] : null;
        switch (key.Key)
        {
            case ConsoleKey.Escape:
                _dav?.Dispose();
                _dav = null;
                _davMessage = null;
                return true;
            case ConsoleKey.UpArrow: _davSel = Math.Max(0, _davSel - 1); return true;
            case ConsoleKey.DownArrow: _davSel = Math.Min(Math.Max(0, _davItems.Count - 1), _davSel + 1); return true;
            case ConsoleKey.PageUp: _davSel = Math.Max(0, _davSel - 10); return true;
            case ConsoleKey.PageDown: _davSel = Math.Min(Math.Max(0, _davItems.Count - 1), _davSel + 10); return true;
            case ConsoleKey.Backspace:
                if (_davFolder != "/")
                    Run(TuiTexts.ExploreLoading, () => LoadAsync(DavClient.Parent(_davFolder)));
                return true;
            case ConsoleKey.R:
                Run(TuiTexts.ExploreLoading, () => LoadAsync(_davFolder));
                return true;
            case ConsoleKey.Enter when item is { IsCollection: true }:
                Run(TuiTexts.ExploreLoading, () => LoadAsync(item.Path));
                return true;
            case ConsoleKey.D or ConsoleKey.Enter when item is not null:
                _overlays.Add(new InputOverlay(TuiTexts.ExploreDownloadTo, Environment.CurrentDirectory, false, text =>
                {
                    string target = text.Trim().Trim('"');
                    if (!Directory.Exists(target))
                    {
                        _overlays.Add(new MessageOverlay(Strings.ErrorNotAFolder(target)));
                        return Task.CompletedTask;
                    }
                    Run(TuiTexts.ExploreDownloading(item.Name), async () =>
                    {
                        await WithLoginAsync(c => c.DownloadTreeAsync(item, target, Progress()));
                        _davMessage = TuiTexts.ExploreDone;
                    });
                    return Task.CompletedTask;
                }));
                return true;
            case ConsoleKey.U:
                _overlays.Add(new InputOverlay(TuiTexts.ExploreUploadFrom, Environment.CurrentDirectory, false, text =>
                {
                    string source = text.Trim().Trim('"');
                    if (!File.Exists(source) && !Directory.Exists(source))
                    {
                        _overlays.Add(new MessageOverlay(Strings.ErrorNotAFolder(source)));
                        return Task.CompletedTask;
                    }
                    string folder = _davFolder;
                    Run(TuiTexts.ExploreUploading(Path.GetFileName(Path.TrimEndingDirectorySeparator(source))), async () =>
                    {
                        await WithLoginAsync(c => c.UploadTreeAsync(source, folder, Progress()));
                        await LoadAsync(folder);
                        _davMessage = TuiTexts.ExploreDone;
                    });
                    return Task.CompletedTask;
                }));
                return true;
            case ConsoleKey.M:
                _overlays.Add(new InputOverlay(TuiTexts.ExploreNewFolder, "", false, name =>
                {
                    if (name.Trim().Length == 0)
                        return Task.CompletedTask;
                    string folder = _davFolder;
                    Run(TuiTexts.ExploreLoading, async () =>
                    {
                        await WithLoginAsync(c => c.CreateFolderAsync(DavClient.Combine(folder, name.Trim())));
                        await LoadAsync(folder);
                    });
                    return Task.CompletedTask;
                }));
                return true;
            case ConsoleKey.Delete when item is not null:
                _overlays.Add(new ConfirmOverlay(TuiTexts.ExploreDeleteQuestion(item.Name), () =>
                {
                    string folder = _davFolder;
                    Run(TuiTexts.ExploreLoading, async () =>
                    {
                        await WithLoginAsync(c => c.DeleteAsync(item.Path, item.IsCollection));
                        await LoadAsync(folder);
                    });
                    return Task.CompletedTask;
                }));
                return true;
        }
        return false;
    }

    private void StartSearch()
    {
        _searching = true;
        _ = Task.Run(async () =>
        {
            try
            {
                _servers = await Discovery.BrowseAsync(TimeSpan.FromSeconds(2));
            }
            catch (Exception e) when (e is System.Net.Sockets.SocketException or OperationCanceledException)
            {
                _servers = new();
            }
            _searching = false;
            _serverSel = 0;
            _events.Writer.TryWrite(null);
        });
    }

    private void Connect(string address)
    {
        if (!DavClient.TryParseAddress(address, out var server, out var path))
        {
            _overlays.Add(new MessageOverlay(TuiTexts.ExploreBadAddress));
            return;
        }
        _dav?.Dispose();
        _dav = new DavClient(server, _davUser, _davPassword);
        _davItems = new();
        Run(TuiTexts.ExploreLoading, () => LoadAsync(path));
    }

    private async Task LoadAsync(string folder)
    {
        var items = await WithLoginAsync(c => c.ListAsync(folder));
        _davFolder = folder.Length > 1 ? folder.TrimEnd('/') : "/";
        _davItems = items;
        _davSel = 0;
        _davMessage = TuiTexts.ExploreItems(items.Count);
    }

    /// <summary>Con 401 se piden usuario y contrasena (en la TUI) y se repite.</summary>
    private async Task<T> WithLoginAsync<T>(Func<DavClient, Task<T>> action)
    {
        while (true)
        {
            try
            {
                return await action(_dav!);
            }
            catch (DavException e) when (e.Status == 401)
            {
                var credentials = await AskCredentialsAsync(_dav!.Server.Authority);
                if (credentials is null)
                    throw;
                (_davUser, _davPassword) = credentials.Value;
                var server = _dav.Server;
                _dav.Dispose();
                _dav = new DavClient(server, _davUser, _davPassword);
            }
        }
    }

    private Task WithLoginAsync(Func<DavClient, Task> action) =>
        WithLoginAsync<bool>(async c => { await action(c); return true; });

    /// <summary>Muestra dos preguntas y espera las respuestas (las teclas las atiende el bucle principal).</summary>
    private Task<(string, string)?> AskCredentialsAsync(string server)
    {
        var done = new TaskCompletionSource<(string, string)?>();
        _overlays.Add(new InputOverlay(TuiTexts.ExploreUser(server), _davUser ?? "", false, user =>
        {
            _overlays.Add(new InputOverlay(Strings.FieldPassword + ":", "", true, password =>
            {
                done.TrySetResult((user.Trim(), password));
                return Task.CompletedTask;
            }));
            return Task.CompletedTask;
        }));
        _events.Writer.TryWrite(null);
        // Si se cancela con Esc no hay respuesta: se da por cancelado a los dos minutos.
        _ = Task.Delay(TimeSpan.FromMinutes(2)).ContinueWith(_ => done.TrySetResult(null), TaskScheduler.Default);
        return done.Task;
    }

    /// <summary>Lanza una operacion de red en segundo plano con un mensaje de espera.</summary>
    private void Run(string busy, Func<Task> action)
    {
        _davBusy = busy;
        _ = Task.Run(async () =>
        {
            try
            {
                await action();
            }
            catch (DavException e)
            {
                _davMessage = e.Status == 403 ? Strings.ErrorText(403) : e.Message;
            }
            catch (Exception e) when (e is HttpRequestException or IOException or UnauthorizedAccessException or TaskCanceledException)
            {
                _davMessage = e.Message;
            }
            finally
            {
                _davBusy = null;
                _events.Writer.TryWrite(null);
            }
        });
    }

    private IProgress<long> Progress()
    {
        long total = 0;
        string busy = _davBusy ?? "";
        return new Progress<long>(n =>
        {
            total += n;
            _davBusy = $"{busy} {FormatSize(total)}";
            _events.Writer.TryWrite(null);
        });
    }

    private List<string> ExploreBody(int width, int height)
    {
        if (_servers is null && !_searching)
            StartSearch();
        var lines = new List<string>();
        if (!Browsing)
        {
            lines.Add(Terminal.Bold + TuiTexts.ExploreServersTitle + Terminal.Reset);
            lines.Add(Frame + new string('─', width) + Terminal.Reset);
            if (_searching)
                lines.Add(Muted + TuiTexts.ExploreSearching + Terminal.Reset);
            else if (_servers is { Count: 0 })
                lines.Add(Muted + TuiTexts.ExploreNoneFound + Terminal.Reset);
            for (int i = 0; _servers is not null && i < _servers.Count; i++)
            {
                var s = _servers[i];
                string row = $"{(s.IsSunfish ? "◆" : "◇")} {Terminal.Fit(s.Name, 30)} {s.Address}:{s.Port}";
                lines.Add(i == _serverSel ? Terminal.Reverse + Terminal.Fit(row, width) + Terminal.Reset : row);
            }
            AddExploreStatus(lines);
            return lines;
        }

        lines.Add(Terminal.Bold + Accent + _dav!.Server.Authority + Terminal.Reset + _davFolder);
        lines.Add(Frame + new string('─', width) + Terminal.Reset);
        int room = Math.Max(3, height - 5);
        int first = Math.Clamp(_davSel - room / 2, 0, Math.Max(0, _davItems.Count - room));
        if (_davFolder != "/")
            lines.Add(Muted + "▲ .." + Terminal.Reset);
        int nameWidth = Math.Max(10, width - 32);
        for (int i = first; i < Math.Min(_davItems.Count, first + room); i++)
        {
            var e = _davItems[i];
            string row = (e.IsCollection ? Accent + "▸ " + Terminal.Reset : "  ") + Terminal.Fit(e.Name + (e.IsCollection ? "/" : ""), nameWidth) + " " +
                         Terminal.Fit(e.IsCollection ? "" : FormatSize(e.Size), 10) + " " +
                         Terminal.Fit(e.Modified?.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) ?? "", 16);
            lines.Add(i == _davSel ? Terminal.Reverse + Terminal.FitVisible(row, width) + Terminal.Reset : row);
        }
        if (_davItems.Count == 0 && _davBusy is null)
            lines.Add(Muted + Strings.EmptyFolder + Terminal.Reset);
        AddExploreStatus(lines);
        return lines;
    }

    private void AddExploreStatus(List<string> lines)
    {
        lines.Add("");
        if (_davBusy is not null)
            lines.Add(Accent + "⟳ " + _davBusy + Terminal.Reset);
        else if (_davMessage is not null)
            lines.Add(Muted + _davMessage + Terminal.Reset);
    }

    private static string FormatSize(long bytes)
    {
        string[] units = ["B", "KB", "MB", "GB", "TB"];
        double value = bytes;
        int unit = 0;
        while (value >= 1024 && unit < units.Length - 1)
        {
            value /= 1024;
            unit++;
        }
        return unit == 0 ? $"{bytes} B" : value.ToString("0.0", CultureInfo.InvariantCulture) + " " + units[unit];
    }
}
