using System.Net.Sockets;
using System.Text;
using System.Threading.Channels;
using Sunfish.Auth;
using Sunfish.Config;
using Sunfish.Host;
using Sunfish.Log;
using Sunfish.Net;

namespace Sunfish.Cli.Tui;

/// <summary>
/// Interfaz interactiva de sunfish-cli: sirve y gestiona a la vez. Se redibuja la pantalla entera
/// en cada cambio (tecla, registro, estado). Las ventanas modales (preguntas, formularios) se
/// apilan sobre las pestanas.
/// </summary>
internal sealed partial class TuiApp
{
    private enum Tab { Status, Shares, Users, Explore, Log }
    private const int TabCount = 5;

    private abstract class Overlay;

    private sealed class InputOverlay(string prompt, string text, bool mask, Func<string, Task> accept) : Overlay
    {
        public string Prompt { get; } = prompt;
        public StringBuilder Text { get; } = new(text);
        public bool Mask { get; } = mask;
        public Func<string, Task> Accept { get; } = accept;
    }

    private sealed class ConfirmOverlay(string question, Func<Task> yes) : Overlay
    {
        public string Question { get; } = question;
        public Func<Task> Yes { get; } = yes;
    }

    private sealed class MessageOverlay(string text) : Overlay
    {
        public string Text { get; } = text;
    }

    /// <summary>Codigo QR de una direccion; las flechas pasan a la siguiente.</summary>
    private sealed class QrOverlay(List<string> urls) : Overlay
    {
        public List<string> Urls { get; } = urls;
        public int Index { get; set; }
    }

    private sealed class ShareForm(ShareConfig share, string? originalName) : Overlay
    {
        public ShareConfig Share { get; } = share;
        public string? OriginalName { get; } = originalName;
        public int Selected { get; set; }
    }

    private readonly SunfishHost _host;
    private readonly Channel<ConsoleKeyInfo?> _events = Channel.CreateUnbounded<ConsoleKeyInfo?>();
    private readonly List<Overlay> _overlays = new();
    private Tab _tab;
    private int _shareSel;
    private int _userSel;
    private bool _quit;
    private string? _status;
    private string[]? _icon;

    public TuiApp(SunfishHost host) => _host = host;

    private bool IsQuick => _host.Config.FilePath is null;

    public async Task RunAsync()
    {
        Console.TreatControlCAsInput = true;
        Terminal.Enter();
        _host.Log.Written += _ => _events.Writer.TryWrite(null);
        _host.Changed += () => _events.Writer.TryWrite(null);
        var keys = new Thread(ReadKeys) { IsBackground = true, Name = "tui-keys" };
        keys.Start();
        _ = WatchResizeAsync();
        try
        {
            while (!_quit)
            {
                Render();
                var ev = await _events.Reader.ReadAsync();
                await HandleAsync(ev);
                while (!_quit && _events.Reader.TryRead(out ev))
                    await HandleAsync(ev);
            }
        }
        finally
        {
            Terminal.Leave();
            Console.TreatControlCAsInput = false;
        }
    }

    private void ReadKeys()
    {
        while (!_quit)
        {
            try
            {
                _events.Writer.TryWrite(Console.ReadKey(intercept: true));
            }
            catch (InvalidOperationException)
            {
                return;
            }
        }
    }

    private async Task WatchResizeAsync()
    {
        int w = Terminal.Width, h = Terminal.Height;
        while (!_quit)
        {
            await Task.Delay(400);
            if (Terminal.Width != w || Terminal.Height != h)
            {
                w = Terminal.Width;
                h = Terminal.Height;
                _events.Writer.TryWrite(null);
            }
        }
    }

    // ------------------------------------------------------------------ teclas

    private async Task HandleAsync(ConsoleKeyInfo? ev)
    {
        if (ev is not { } key)
            return; // solo redibujar
        _status = null;
        if (_overlays.Count > 0)
        {
            await HandleOverlayAsync(_overlays[^1], key);
            return;
        }
        if (key.Key == ConsoleKey.Tab)
        {
            _tab = (Tab)(((int)_tab + (key.Modifiers.HasFlag(ConsoleModifiers.Shift) ? TabCount - 1 : 1)) % TabCount);
            return;
        }
        if (key.Key is >= ConsoleKey.F1 and <= ConsoleKey.F5)
        {
            _tab = (Tab)(key.Key - ConsoleKey.F1);
            return;
        }
        if (_tab == Tab.Explore && await ExploreKeyAsync(key))
            return;
        if (key.Key is ConsoleKey.Escape or ConsoleKey.Q || (key.Key == ConsoleKey.C && key.Modifiers.HasFlag(ConsoleModifiers.Control)))
        {
            _overlays.Add(new ConfirmOverlay(TuiTexts.QuitQuestion, () =>
            {
                _quit = true;
                return Task.CompletedTask;
            }));
            return;
        }
        switch (_tab)
        {
            case Tab.Status: await StatusKeyAsync(key); break;
            case Tab.Shares: await SharesKeyAsync(key); break;
            case Tab.Users: UsersKey(key); break;
        }
    }

    private async Task StatusKeyAsync(ConsoleKeyInfo key)
    {
        switch (key.Key)
        {
            case ConsoleKey.S:
                if (_host.Running)
                    await _host.StopAsync();
                else
                    StartServer();
                break;
            case ConsoleKey.P:
                AskPort(TuiTexts.PortPrompt, _host.Config.Server.HttpPort, (c, port) => c.Server.HttpPort = port);
                break;
            case ConsoleKey.T:
                AskPort(TuiTexts.FtpPortPrompt, _host.Config.Server.FtpPort, (c, port) => c.Server.FtpPort = port);
                break;
            case ConsoleKey.F:
                await ChangeAsync(c => c.Server.Ftp = !c.Server.Ftp);
                break;
            case ConsoleKey.C:
                var urls = QrUrls();
                _overlays.Add(urls.Count > 0 ? new QrOverlay(urls) : new MessageOverlay(TuiTexts.NoQr));
                break;
            case ConsoleKey.N:
                _overlays.Add(new InputOverlay(TuiTexts.NamePrompt, _host.Config.Server.Name, false, async text =>
                {
                    if (text.Trim().Length > 0)
                        await ChangeAsync(c => c.Server.Name = text.Trim());
                }));
                break;
        }
    }

    private void AskPort(string prompt, int current, Action<SunfishConfig, int> set)
    {
        _overlays.Add(new InputOverlay(prompt, current.ToString(), false, async text =>
        {
            if (!int.TryParse(text, out int port) || port is < 1 or > 65535)
            {
                _overlays.Add(new MessageOverlay(TuiTexts.BadPort));
                return;
            }
            await ChangeAsync(c => set(c, port));
        }));
    }

    private void StartServer()
    {
        try
        {
            _host.Start();
        }
        catch (SocketException e)
        {
            _overlays.Add(new MessageOverlay(Strings.CannotListen(_host.Config.Server.HttpPort, e.Message)));
        }
    }

    private async Task SharesKeyAsync(ConsoleKeyInfo key)
    {
        var shares = _host.Config.Shares;
        switch (key.Key)
        {
            case ConsoleKey.UpArrow: _shareSel = Math.Max(0, _shareSel - 1); break;
            case ConsoleKey.DownArrow: _shareSel = Math.Min(Math.Max(0, shares.Count - 1), _shareSel + 1); break;
            case ConsoleKey.A or ConsoleKey.Insert:
                _overlays.Add(new InputOverlay(TuiTexts.NewShareFolder, Environment.CurrentDirectory, false, text =>
                {
                    string path = text.Trim().Trim('"');
                    if (!Directory.Exists(path))
                    {
                        _overlays.Add(new MessageOverlay(Strings.ErrorNotAFolder(path)));
                        return Task.CompletedTask;
                    }
                    path = Path.GetFullPath(path);
                    string folderName = Path.GetFileName(Path.TrimEndingDirectorySeparator(path));
                    var share = new ShareConfig
                    {
                        Name = ShareSettings.SuggestName(_host.Config, folderName),
                        Location = ShareSettings.SuggestLocation(_host.Config, folderName),
                        Path = path,
                    };
                    _overlays.Add(new ShareForm(share, null));
                    return Task.CompletedTask;
                }));
                break;
            case ConsoleKey.Enter or ConsoleKey.E when shares.Count > 0:
                _overlays.Add(new ShareForm(shares[_shareSel].Clone(), shares[_shareSel].Name));
                break;
            case ConsoleKey.Spacebar when shares.Count > 0:
                string toggled = shares[_shareSel].Name;
                await ChangeAsync(c => c.Shares.First(s => s.Name == toggled).Enabled ^= true);
                break;
            case ConsoleKey.D or ConsoleKey.Delete when shares.Count > 0:
                string removed = shares[_shareSel].Name;
                _overlays.Add(new ConfirmOverlay(Strings.AskDeleteShare(removed), async () =>
                {
                    await ChangeAsync(c => c.Shares.RemoveAll(s => s.Name == removed));
                    _shareSel = Math.Max(0, _shareSel - 1);
                }));
                break;
        }
    }

    private void UsersKey(ConsoleKeyInfo key)
    {
        var users = _host.Config.Users;
        switch (key.Key)
        {
            case ConsoleKey.UpArrow: _userSel = Math.Max(0, _userSel - 1); break;
            case ConsoleKey.DownArrow: _userSel = Math.Min(Math.Max(0, users.Count - 1), _userSel + 1); break;
            case ConsoleKey.A or ConsoleKey.Insert:
                _overlays.Add(new InputOverlay(TuiTexts.NewUserName, "", false, name =>
                {
                    name = name.Trim();
                    if (!ShareSettings.IsValidName(name))
                        _overlays.Add(new MessageOverlay(Strings.ErrorBadName(name)));
                    else if (_host.Config.Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
                        _overlays.Add(new MessageOverlay(Strings.ErrorNameInUse(name)));
                    else
                        AskPassword(name, isNew: true);
                    return Task.CompletedTask;
                }));
                break;
            case ConsoleKey.P when users.Count > 0:
                AskPassword(users[_userSel].Name, isNew: false);
                break;
            case ConsoleKey.D or ConsoleKey.Delete when users.Count > 0:
                string removed = users[_userSel].Name;
                _overlays.Add(new ConfirmOverlay(Strings.AskDeleteUser(removed), async () =>
                {
                    await ChangeAsync(c =>
                    {
                        c.Users.RemoveAll(u => u.Name == removed);
                        foreach (var s in c.Shares)
                            s.Users.RemoveAll(u => u.Equals(removed, StringComparison.OrdinalIgnoreCase));
                    });
                    _userSel = Math.Max(0, _userSel - 1);
                }));
                break;
        }
    }

    private void AskPassword(string name, bool isNew)
    {
        _overlays.Add(new InputOverlay(TuiTexts.NewPassword(name), "", true, first =>
        {
            if (first.Length == 0)
            {
                _overlays.Add(new MessageOverlay(Strings.ErrorPasswordEmpty));
                return Task.CompletedTask;
            }
            _overlays.Add(new InputOverlay(TuiTexts.RepeatPassword, "", true, async second =>
            {
                if (second != first)
                {
                    _overlays.Add(new MessageOverlay(TuiTexts.PasswordMismatch));
                    return;
                }
                string hash = Passwords.Hash(first);
                await ChangeAsync(c =>
                {
                    if (isNew)
                        c.Users.Add(new UserConfig { Name = name, PasswordHash = hash });
                    else
                        c.Users.First(u => u.Name == name).PasswordHash = hash;
                });
            }));
            return Task.CompletedTask;
        }));
    }

    private async Task HandleOverlayAsync(Overlay overlay, ConsoleKeyInfo key)
    {
        switch (overlay)
        {
            case MessageOverlay:
                _overlays.Remove(overlay);
                break;

            case ConfirmOverlay confirm:
                _overlays.Remove(overlay);
                if (TuiTexts.IsYes(key.KeyChar))
                    await confirm.Yes();
                break;

            case InputOverlay input:
                if (key.Key == ConsoleKey.Escape)
                    _overlays.Remove(overlay);
                else if (key.Key == ConsoleKey.Enter)
                {
                    _overlays.Remove(overlay);
                    await input.Accept(input.Text.ToString());
                }
                else if (key.Key == ConsoleKey.Backspace)
                {
                    if (input.Text.Length > 0)
                        input.Text.Length--;
                }
                else if (!char.IsControl(key.KeyChar))
                    input.Text.Append(key.KeyChar);
                break;

            case ShareForm form:
                await HandleFormAsync(form, key);
                break;

            case QrOverlay qr:
                if (key.Key is ConsoleKey.RightArrow or ConsoleKey.DownArrow or ConsoleKey.Tab or ConsoleKey.Spacebar)
                    qr.Index = (qr.Index + 1) % qr.Urls.Count;
                else if (key.Key is ConsoleKey.LeftArrow or ConsoleKey.UpArrow)
                    qr.Index = (qr.Index + qr.Urls.Count - 1) % qr.Urls.Count;
                else
                    _overlays.Remove(overlay);
                break;
        }
    }

    /// <summary>Direcciones para el QR: cada share activo en cada interfaz (HTTP y, si esta, FTP).</summary>
    private List<string> QrUrls()
    {
        var urls = new List<string>();
        var addresses = LocalAddresses.Get();
        foreach (var share in _host.Config.Shares.Where(s => s.Enabled))
            foreach (var a in addresses)
            {
                if (_host.HttpRunning)
                    urls.Add(LocalAddresses.HttpUrl(a.Address, _host.HttpPort, share));
                if (_host.FtpRunning && share.Ftp)
                    urls.Add(LocalAddresses.FtpUrl(a.Address, _host.FtpPort, share));
            }
        return urls;
    }

    /// <summary>
    /// El QR ocupa el cuerpo: medios bloques negros sobre fondo blanco (con el fondo oscuro del
    /// terminal los moviles no lo leen), centrado, con la direccion debajo.
    /// </summary>
    private static List<string> QrBody(QrOverlay qr, int width, int height)
    {
        string url = qr.Urls[qr.Index];
        var code = Qr.QrImage.HalfBlocks(Qr.QrCode.Encode(url), 2).ToList();
        var lines = new List<string>();
        int top = Math.Max(0, (height - code.Count - 2) / 2);
        for (int i = 0; i < top; i++)
            lines.Add("");
        if (code.Count + 2 > height || code[0].Length > width)
        {
            lines.Add(TuiTexts.QrTooSmall);
            lines.Add(url);
            return lines;
        }
        string pad = new string(' ', Math.Max(0, (width - code[0].Length) / 2));
        foreach (var row in code)
            lines.Add(pad + Terminal.Fg(0, 0, 0) + Terminal.Bg(255, 255, 255) + row + Terminal.Reset);
        string caption = $"{url}   ({qr.Index + 1}/{qr.Urls.Count})";
        lines.Add(new string(' ', Math.Max(0, (width - caption.Length) / 2)) + Terminal.Bold + caption + Terminal.Reset);
        return lines;
    }

    // ------------------------------------------------------------------ formulario de share

    private static readonly string[] TextFields = ["Name", "Location", "Path", "Index", "Users"];
    private static readonly string[] BoolFields = ["Enabled", "Listing", "Upload", "Overwrite", "Rename", "Delete", "Zip"];
    private static int FormRows => TextFields.Length + BoolFields.Length + 2;

    private async Task HandleFormAsync(ShareForm form, ConsoleKeyInfo key)
    {
        switch (key.Key)
        {
            case ConsoleKey.Escape:
                _overlays.Remove(form);
                return;
            case ConsoleKey.UpArrow:
                form.Selected = (form.Selected + FormRows - 1) % FormRows;
                return;
            case ConsoleKey.DownArrow or ConsoleKey.Tab:
                form.Selected = (form.Selected + 1) % FormRows;
                return;
            case ConsoleKey.Enter or ConsoleKey.Spacebar:
                break;
            default:
                return;
        }

        int i = form.Selected;
        var s = form.Share;
        if (i < TextFields.Length)
        {
            if (key.Key != ConsoleKey.Enter)
                return;
            string field = TextFields[i];
            _overlays.Add(new InputOverlay(FieldLabel(field), FieldValue(s, field), false, text =>
            {
                text = text.Trim();
                if (field == "Name")
                {
                    if (!ShareSettings.IsValidName(text))
                        _overlays.Add(new MessageOverlay(Strings.ErrorBadName(text)));
                    else
                        s.Name = text;
                }
                else if (ShareSettings.TrySet(s, field, text) is { } error)
                {
                    _overlays.Add(new MessageOverlay(error));
                }
                return Task.CompletedTask;
            }));
            return;
        }
        i -= TextFields.Length;
        if (i < BoolFields.Length)
        {
            ShareSettings.TrySet(s, BoolFields[i], (!FieldBool(s, BoolFields[i])).ToString());
            return;
        }
        i -= BoolFields.Length;
        if (i == 1 || key.Key != ConsoleKey.Enter)
        {
            if (i == 1)
                _overlays.Remove(form);
            return;
        }

        // Guardar
        if (ValidateShare(form) is { } problem)
        {
            _overlays.Add(new MessageOverlay(problem));
            return;
        }
        _overlays.Remove(form);
        await ChangeAsync(c =>
        {
            int index = form.OriginalName is null ? -1 : c.Shares.FindIndex(x => x.Name == form.OriginalName);
            if (index >= 0)
                c.Shares[index] = s;
            else
                c.Shares.Add(s);
        });
        _shareSel = Math.Max(0, _host.Config.Shares.FindIndex(x => x.Name == s.Name));
    }

    private string? ValidateShare(ShareForm form) =>
        ShareSettings.Validate(_host.Config, form.Share, form.OriginalName);

    private static string FieldLabel(string field) => field switch
    {
        "Name" => Strings.FieldName,
        "Location" => Strings.FieldLocation,
        "Path" => Strings.FieldPath,
        "Index" => Strings.FieldIndex,
        "Users" => Strings.FieldUsers,
        "Enabled" => Strings.OptEnabled,
        "Listing" => Strings.OptListing,
        "Upload" => Strings.OptUpload,
        "Overwrite" => Strings.OptOverwrite,
        "Rename" => Strings.OptRename,
        "Delete" => Strings.OptDelete,
        "Zip" => Strings.OptZip,
        _ => field,
    };

    private static string FieldValue(ShareConfig s, string field) => field switch
    {
        "Name" => s.Name,
        "Location" => s.Location,
        "Path" => s.Path,
        "Index" => s.Index,
        "Users" => string.Join(", ", s.Users),
        _ => "",
    };

    private static bool FieldBool(ShareConfig s, string field) => field switch
    {
        "Enabled" => s.Enabled,
        "Listing" => s.Listing,
        "Upload" => s.Upload,
        "Overwrite" => s.Overwrite,
        "Rename" => s.Rename,
        "Delete" => s.Delete,
        "Zip" => s.Zip,
        _ => false,
    };

    /// <summary>Cambia una copia de la configuracion, la guarda (si hay fichero) y la aplica.</summary>
    private async Task ChangeAsync(Action<SunfishConfig> change)
    {
        var copy = _host.Config.Clone();
        change(copy);
        bool ok = IsQuick ? await _host.ApplyAsync(copy) : await _host.SaveAndApplyAsync(copy);
        _status = IsQuick ? TuiTexts.NotSavedQuick : ok ? TuiTexts.Saved : null;
    }

    // ------------------------------------------------------------------ dibujo
    // La TUI solo se usa en terminales modernos: marco de caja con esquinas redondeadas, pestanas
    // en el borde superior y atajos en el inferior. (La web es la que se mantiene compatible con
    // equipos antiguos.)

    private static readonly string Frame = Terminal.Fg(90, 140, 190);
    private static readonly string Accent = Terminal.Fg(120, 200, 255);
    private static readonly string Muted = Terminal.Fg(130, 130, 140);

    private void Render()
    {
        int width = Terminal.Width;
        int height = Terminal.Height;
        int inner = width - 4;             // "│ " + contenido + " │"
        int bodyHeight = height - 4;       // borde superior, separador, estado, borde inferior

        var body = _tab switch
        {
            Tab.Status => StatusBody(inner, bodyHeight),
            Tab.Shares => SharesBody(inner),
            Tab.Users => UsersBody(inner),
            Tab.Explore => ExploreBody(inner, bodyHeight),
            _ => LogBody(inner, bodyHeight),
        };
        if (_overlays.Count > 0 && _overlays[^1] is QrOverlay qr)
            body = QrBody(qr, inner, bodyHeight);
        var lines = body.Take(bodyHeight).ToList();
        while (lines.Count < bodyHeight)
            lines.Add("");
        if (_overlays.Count > 0 && _overlays[^1] is not QrOverlay)
            DrawOverlay(lines, _overlays[^1], inner, bodyHeight);

        string hint = _overlays.Count > 0 ? OverlayHint(_overlays[^1]) : _tab switch
        {
            Tab.Status => TuiTexts.KeysStatus,
            Tab.Shares => TuiTexts.KeysShares,
            Tab.Users => TuiTexts.KeysUsers,
            Tab.Explore => ExploreHint(),
            _ => TuiTexts.KeysLog,
        };

        var sb = new StringBuilder("\e[H");
        sb.Append(TopBorder(width)).Append("\e[K\r\n");
        foreach (var line in lines)
            sb.Append(Frame).Append("│ ").Append(Terminal.Reset).Append(Terminal.FitVisible(line, inner))
              .Append(Terminal.Reset).Append(Frame).Append(" │").Append(Terminal.Reset).Append("\e[K\r\n");
        sb.Append(Frame).Append('├').Append(new string('─', width - 2)).Append('┤').Append(Terminal.Reset).Append("\e[K\r\n");
        sb.Append(Frame).Append("│ ").Append(Terminal.Reset).Append(Muted).Append(Terminal.Fit(_status ?? "", inner))
          .Append(Terminal.Reset).Append(Frame).Append(" │").Append(Terminal.Reset).Append("\e[K\r\n");
        sb.Append(BottomBorder(width, hint + "  ·  " + TuiTexts.KeysCommon)).Append("\e[K");
        sb.Append(Terminal.Reset);
        Terminal.Draw(sb.ToString());
    }

    /// <summary>╭─ Sunfish 3.0 ─┤ F1 Estado ├─ F2 Shares ─ ... ─ ● En marcha :8080 ─╮</summary>
    private string TopBorder(int width)
    {
        string[] names = [Strings.AppStatus, Strings.AppShares, Strings.AppUsers, TuiTexts.ExploreTab, Strings.AppLog];
        var sb = new StringBuilder(Frame + "╭─ " + Terminal.Reset + Terminal.Bold + Accent + "Sunfish 3.0" + Terminal.Reset + Frame + " ─");
        int used = 3 + 11 + 2;
        for (int i = 0; i < names.Length; i++)
        {
            string label = $" F{i + 1} {names[i]} ";
            if (i == (int)_tab)
                sb.Append('┤').Append(Terminal.Reset).Append(Terminal.Reverse).Append(Terminal.Bold).Append(label)
                  .Append(Terminal.Reset).Append(Frame).Append('├');
            else
                sb.Append('─').Append(Terminal.Reset).Append(label).Append(Frame).Append('─');
            used += label.Length + 2;
        }
        string ports = (_host.HttpRunning ? $" http:{_host.HttpPort}" : "") + (_host.FtpRunning ? $" ftp:{_host.FtpPort}" : "");
        string state = _host.Running ? $" ● {Strings.AppRunning}{ports} " : $" ○ {Strings.AppStopped} ";
        string color = _host.Running ? Terminal.Fg(90, 200, 110) : Terminal.Fg(230, 90, 80);
        int gap = width - used - state.Length - 2;
        if (gap >= 1)
            sb.Append(new string('─', gap)).Append(Terminal.Reset).Append(color).Append(state).Append(Frame).Append("─╮");
        else
            sb.Append(new string('─', Math.Max(0, width - used - 1))).Append('╮');
        sb.Append(Terminal.Reset);
        return sb.ToString();
    }

    /// <summary>╰─ atajos ──────────────╯</summary>
    private static string BottomBorder(int width, string hint)
    {
        string text = Terminal.Fit(" " + hint + " ", Math.Max(0, width - 5)).TrimEnd() + " ";
        int rest = Math.Max(0, width - 3 - text.Length);
        return Frame + "╰─" + Terminal.Reset + Muted + text + Terminal.Reset + Frame + new string('─', rest) + "╯" + Terminal.Reset;
    }

    /// <summary>":8080", ":2121 (off)" o ":2121 !" si debia estar en marcha y no lo esta (puerto ocupado).</summary>
    private string Service(bool running, bool enabled, int port)
    {
        if (running)
            return $"{Terminal.Fg(90, 200, 110)}:{port}{Terminal.Reset}";
        if (!enabled)
            return $"{Muted}:{port} (off){Terminal.Reset}";
        return _host.Running ? $"{Terminal.Fg(230, 90, 80)}:{port} !{Terminal.Reset}" : $":{port}";
    }

    private List<string> StatusBody(int width, int height)
    {
        var info = new List<string>
        {
            Terminal.Bold + Accent + _host.Config.Server.Name + Terminal.Reset,
            (_host.Running ? Terminal.Fg(90, 200, 110) + "● " + Strings.AppRunning : Terminal.Fg(230, 90, 80) + "○ " + Strings.AppStopped) +
                Terminal.Reset + $"   HTTP: {Service(_host.HttpRunning, _host.Config.Server.Http, _host.Config.Server.HttpPort)}" +
                $"   FTP: {Service(_host.FtpRunning, _host.Config.Server.Ftp, _host.Config.Server.FtpPort)}",
            Muted + (IsQuick ? Strings.AppQuickShare(_host.Config.Shares.FirstOrDefault()?.Path ?? "") : TuiTexts.Config(_host.Config.FilePath!)) + Terminal.Reset,
            "",
            Terminal.Bold + Strings.AppAddresses + Terminal.Reset,
        };
        var shares = _host.Config.Shares.Where(s => s.Enabled).ToList();
        if (shares.Count == 0)
            info.Add("  " + Strings.AppNoShares);
        var addresses = LocalAddresses.Get();
        foreach (var share in shares)
        {
            info.Add($"  {Accent}▸{Terminal.Reset} {share.Name}");
            foreach (var address in addresses)
            {
                if (_host.HttpRunning || !_host.Running)
                    info.Add($"    {LocalAddresses.HttpUrl(address.Address, _host.HttpPort, share)}  {Muted}{address.Interface}{Terminal.Reset}");
                if (_host.FtpRunning && share.Ftp)
                    info.Add($"    {LocalAddresses.FtpUrl(address.Address, _host.FtpPort, share)}  {Muted}{address.Interface}{Terminal.Reset}");
            }
        }

        bool showIcon = width >= 96 && height >= 24;
        if (!showIcon)
            return info;

        _icon ??= TuiIcon.Render();
        var lines = new List<string>();
        int count = Math.Max(TuiIcon.Rows + 1, info.Count);
        for (int i = 0; i < count; i++)
        {
            string left = i >= 1 && i <= TuiIcon.Rows ? _icon[i - 1] + "  " : new string(' ', TuiIcon.Columns + 2);
            lines.Add(left + (i < info.Count ? info[i] : ""));
        }
        return lines;
    }

    private List<string> SharesBody(int width)
    {
        var lines = new List<string>();
        var shares = _host.Config.Shares;
        int nameW = 14, locW = 16, permW = 7, usersW = 12;
        int pathW = Math.Max(10, width - nameW - locW - permW - usersW - 7);
        string Row(string mark, string name, string loc, string path, string perms, string users) =>
            mark + " " + Terminal.Fit(name, nameW) + " " + Terminal.Fit(loc, locW) + " " +
            Terminal.Fit(path, pathW) + " " + Terminal.Fit(perms, permW) + " " + Terminal.Fit(users, usersW);

        lines.Add(Terminal.Bold + Row(" ", Strings.FieldName, "Location", Strings.FieldPath, TuiTexts.ColPerms, TuiTexts.ColUsers) + Terminal.Reset);
        lines.Add(Frame + new string('─', width) + Terminal.Reset);
        if (shares.Count == 0)
            lines.Add(Muted + Strings.AppNoShares + Terminal.Reset);
        for (int i = 0; i < shares.Count; i++)
        {
            var s = shares[i];
            string perms = (s.Listing ? "L" : "·") + (s.Upload ? "U" : "·") + (s.Overwrite ? "O" : "·") +
                           (s.Rename ? "R" : "·") + (s.Delete ? "D" : "·") + (s.Zip ? "Z" : "·");
            string row = Row(s.Enabled ? "●" : "○", s.Name, s.Location, s.Path, perms, string.Join(",", s.Users));
            lines.Add(i == _shareSel ? Terminal.Reverse + row + Terminal.Reset : (s.Enabled ? row : Muted + row + Terminal.Reset));
        }
        lines.Add("");
        lines.Add(Muted + TuiTexts.Permissions + Terminal.Reset);
        return lines;
    }

    private List<string> UsersBody(int width)
    {
        var lines = new List<string>();
        var users = _host.Config.Users;
        if (users.Count == 0)
            lines.Add(Muted + TuiTexts.NoUsers + Terminal.Reset);
        for (int i = 0; i < users.Count; i++)
        {
            var u = users[i];
            var shares = _host.Config.Shares.Where(s => s.Users.Contains(u.Name, StringComparer.OrdinalIgnoreCase)).Select(s => s.Name);
            string row = "◆ " + Terminal.Fit(u.Name, 20) + " " + Terminal.Fit(TuiTexts.UsedIn(string.Join(", ", shares)), Math.Max(10, width - 24));
            lines.Add(i == _userSel ? Terminal.Reverse + row + Terminal.Reset : row);
        }
        return lines;
    }

    private List<string> LogBody(int width, int height)
    {
        var entries = _host.Log.Recent();
        return entries.Skip(Math.Max(0, entries.Length - height)).Select(e =>
        {
            string color = e.Level switch
            {
                LogLevel.Error => Terminal.Fg(230, 90, 80),
                LogLevel.Warning => Terminal.Fg(230, 190, 80),
                LogLevel.Detail => Muted,
                _ => "",
            };
            return Muted + $"{e.Time:HH:mm:ss} " + Terminal.Reset + color + Terminal.Fit(e.Message, Math.Max(10, width - 9)) + Terminal.Reset;
        }).ToList();
    }

    private static string OverlayHint(Overlay overlay) => overlay switch
    {
        ShareForm => TuiTexts.KeysForm,
        QrOverlay => TuiTexts.KeysQr,
        InputOverlay => TuiTexts.KeysInput,
        ConfirmOverlay => TuiTexts.YesNo,
        _ => "",
    };

    /// <summary>Recuadro redondeado centrado sobre el cuerpo; tapa las lineas que ocupa.</summary>
    private void DrawOverlay(List<string> lines, Overlay overlay, int width, int height)
    {
        int boxWidth = Math.Min(width - 2, 76);
        string title = "";
        var content = new List<(string Text, bool Highlight)>();
        switch (overlay)
        {
            case MessageOverlay m:
                content.Add((m.Text, false));
                break;
            case ConfirmOverlay c:
                content.Add((c.Question + " " + TuiTexts.YesNo, false));
                break;
            case InputOverlay input:
                title = input.Prompt;
                string value = input.Mask ? new string('•', input.Text.Length) : input.Text.ToString();
                int room = boxWidth - 6;
                if (value.Length > room)
                    value = "…" + value[^(room - 1)..];
                content.Add(("› " + value + "▏", true));
                break;
            case ShareForm form:
                title = TuiTexts.EditShare(form.Share.Name);
                int row = 0;
                foreach (var field in TextFields)
                    content.Add(($"{Terminal.Fit(FieldLabel(field), 34)} {FieldValue(form.Share, field)}", form.Selected == row++));
                content.Add((new string('─', boxWidth - 4), false));
                foreach (var field in BoolFields)
                    content.Add(($"{(FieldBool(form.Share, field) ? "☑" : "☐")} {FieldLabel(field)}", form.Selected == row++));
                content.Add((new string('─', boxWidth - 4), false));
                content.Add((TuiTexts.Save, form.Selected == row++));
                content.Add((TuiTexts.Cancel, form.Selected == row));
                if (form.Share.Upload && form.Share.Listing)
                    content.Add(("⚠ " + Strings.WarnUploadListing, false));
                break;
        }

        int boxHeight = content.Count + 2;
        int top = Math.Max(0, (height - boxHeight) / 2);
        int left = Math.Max(0, (width - boxWidth) / 2);
        string pad = new(' ', left);
        string titleText = title.Length > 0 ? Terminal.Fit(" " + title + " ", Math.Min(title.Length + 2, boxWidth - 4)) : "";
        string topLine = Accent + "╭─" + Terminal.Reset + Terminal.Bold + titleText + Terminal.Reset + Accent +
                         new string('─', Math.Max(0, boxWidth - 3 - titleText.Length)) + "╮" + Terminal.Reset;
        string bottomLine = Accent + "╰" + new string('─', boxWidth - 2) + "╯" + Terminal.Reset;
        void Put(int index, string text)
        {
            if (index >= 0 && index < lines.Count)
                lines[index] = pad + text;
        }
        Put(top, topLine);
        for (int i = 0; i < content.Count; i++)
        {
            var (text, highlight) = content[i];
            string innerText = Terminal.Fit(" " + text, boxWidth - 2);
            Put(top + 1 + i, Accent + "│" + Terminal.Reset + (highlight ? Terminal.Reverse + innerText + Terminal.Reset : innerText) + Accent + "│" + Terminal.Reset);
        }
        Put(top + 1 + content.Count, bottomLine);
    }
}
