using System.Globalization;
using Sunfish.Shares;

namespace Sunfish.Web;

/// <summary>Una fila del navegador de directorios.</summary>
internal sealed record ListingItem(FileEntry Entry, bool HasIndex, string? Alias = null, bool MacFork = false);

/// <summary>Lo que se puede hacer en la carpeta mostrada, con el token anti-CSRF de cada accion.</summary>
internal sealed class FolderActions
{
    public bool Upload { get; init; }
    public bool Rename { get; init; }
    public bool Delete { get; init; }
    public bool Zip { get; init; }
    public Func<string, string> Token { get; init; } = _ => "";

    public bool Any => Upload || Rename || Delete || Zip;
}

/// <summary>
/// Paginas del navegador de directorios. El mismo HTML sirve a todos: HTML 3.2 con tablas y
/// formularios, sin JavaScript; el modo moderno anade UTF-8, un CSS sencillo y un script que solo
/// mejora la subida (arrastrar y soltar, progreso, trozos). Los navegadores de texto reciben una
/// lista. La fuente es sin serifa (CSS en moderno, FONT FACE en clasico: Netscape no hereda la
/// fuente dentro de las tablas, por eso va en cada celda).
/// </summary>
internal static class DirectoryPage
{
    internal const string Font = "<FONT FACE=\"Verdana,Arial,Helvetica\">";
    internal const string EndFont = "</FONT>";

    private const string ModernStyle =
        "body{font-family:Verdana,\"DejaVu Sans\",Tahoma,Arial,Helvetica,sans-serif;max-width:1000px;margin:0 auto;padding:8px;color:#000;background:#fff}" +
        "table.list{border-collapse:collapse;width:100%}" +
        "table.list td,table.list th{padding:4px 6px}" +
        "table.list tr:nth-child(even){background:#f4f7fb}" +
        "a{text-decoration:none;color:#1a4f8a}a:hover{text-decoration:underline}" +
        ".extra{font-size:smaller;color:#555}" +
        "form{margin:6px 0}" +
        "body.sf-drop{outline:4px dashed #1a4f8a;outline-offset:-4px}" +
        "#sf-status{font-weight:bold}" +
        "@media (max-width:600px){.date{display:none}}";

    public sealed class Model
    {
        public string ServerName { get; init; } = "Sunfish";
        public IReadOnlyList<string> Segments { get; init; } = [];
        public IReadOnlyList<ListingItem> Items { get; init; } = [];
        public ClientProfile Profile { get; init; } = new(ClientKind.Classic, false);
        public string Sort { get; init; } = "n";
        public FolderActions Actions { get; init; } = new();
        public bool IsRoot => Segments.Count == 0;

        /// <summary>URL de la carpeta, codificada y con barra final.</summary>
        public string FolderUrl => UrlOf(Segments);
    }

    public static HtmlBuilder Render(Model m)
    {
        var html = new HtmlBuilder(classic: !m.Profile.IsModern);
        string title = m.IsRoot ? m.ServerName : m.ServerName + " - /" + string.Join('/', m.Segments) + "/";
        Header(html, title);
        if (m.Profile.IsText)
            RenderText(html, m);
        else
            RenderTable(html, m);
        RenderForms(html, m);
        if (!html.Classic && m.Actions.Upload)
            html.Raw("<script src=\"/$sunfish/res/sunfish.js\"></script>\n");
        Footer(html);
        return html;
    }

    public static HtmlBuilder RenderError(int status, string serverName, ClientProfile profile, string? detail = null)
    {
        var html = new HtmlBuilder(classic: !profile.IsModern);
        Header(html, serverName + " - " + status);
        html.Raw("<H2>").Raw(Font).Raw(status.ToString(CultureInfo.InvariantCulture)).Raw(" ").Text(Strings.ErrorTitle(status)).Raw(EndFont).Raw("</H2>\n");
        string text = detail ?? Strings.ErrorText(status);
        if (text.Length > 0)
            html.Raw("<P>").Raw(Font).Text(text).Raw(EndFont).Raw("</P>\n");
        html.Raw("<P>").Raw(Font).Raw("<A HREF=\"/").Raw(ProfileQuery(profile, true)).Raw("\">/</A>").Raw(EndFont).Raw("</P>\n");
        Footer(html);
        return html;
    }

    /// <summary>Pagina para escribir el nombre nuevo (sin JavaScript).</summary>
    public static HtmlBuilder RenderRename(Model m, string name)
    {
        var html = new HtmlBuilder(classic: !m.Profile.IsModern);
        Header(html, m.ServerName + " - " + Strings.Rename);
        html.Raw("<H2>").Raw(Font).Text(Strings.RenameTitle(name)).Raw(EndFont).Raw("</H2>\n");
        FormStart(html, m, "rename", multipart: false);
        html.Raw("<INPUT TYPE=HIDDEN NAME=\"name\" VALUE=\"").Text(name).Raw("\">")
            .Raw(Font).Text(Strings.NewName).Raw(EndFont).Raw(" <INPUT TYPE=TEXT NAME=\"to\" SIZE=40 VALUE=\"").Text(name).Raw("\"> ")
            .Raw("<INPUT TYPE=SUBMIT VALUE=\"").Text(Strings.Rename).Raw("\"></FORM>\n");
        CancelLink(html, m);
        Footer(html);
        return html;
    }

    /// <summary>Confirmacion de borrado (sin JavaScript).</summary>
    public static HtmlBuilder RenderDelete(Model m, string name, bool isDirectory)
    {
        var html = new HtmlBuilder(classic: !m.Profile.IsModern);
        Header(html, m.ServerName + " - " + Strings.Delete);
        html.Raw("<H2>").Raw(Font).Text(Strings.Delete).Raw(EndFont).Raw("</H2>\n<P>").Raw(Font)
            .Text(isDirectory ? Strings.DeleteFolderQuestion(name) : Strings.DeleteFileQuestion(name)).Raw(EndFont).Raw("</P>\n");
        FormStart(html, m, "delete", multipart: false);
        html.Raw("<INPUT TYPE=HIDDEN NAME=\"name\" VALUE=\"").Text(name).Raw("\">")
            .Raw("<INPUT TYPE=SUBMIT VALUE=\"").Text(Strings.Delete).Raw("\"></FORM>\n");
        CancelLink(html, m);
        Footer(html);
        return html;
    }

    private static void CancelLink(HtmlBuilder html, Model m) =>
        html.Raw("<P>").Raw(Font).Raw("<A HREF=\"").Raw(m.FolderUrl).Raw(ProfileQuery(m.Profile, true)).Raw("\">")
            .Text(Strings.Cancel).Raw("</A>").Raw(EndFont).Raw("</P>\n");

    internal static void Header(HtmlBuilder html, string title)
    {
        html.Raw("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<HTML><HEAD><TITLE>")
            .Text(title).Raw("</TITLE>\n")
            .Raw("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"").Raw(html.ContentType).Raw("\">\n");
        if (!html.Classic)
            html.Raw("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<style>")
                .Raw(ModernStyle).Raw("</style>\n");
        html.Raw("</HEAD>\n<BODY BGCOLOR=\"#FFFFFF\" TEXT=\"#000000\" LINK=\"#1A4F8A\" VLINK=\"#5A3D8A\">\n");
    }

    internal static void Footer(HtmlBuilder html) =>
        html.Raw("<HR>\n").Raw(Font).Raw("<FONT SIZE=\"-1\">Sunfish 3.0</FONT>").Raw(EndFont).Raw("\n</BODY></HTML>\n");

    private static void RenderTable(HtmlBuilder html, Model m)
    {
        string q = ProfileQuery(m.Profile, first: true);

        html.Raw("<TABLE BORDER=0 CELLPADDING=2 CELLSPACING=0 WIDTH=\"100%\"><TR>")
            .Raw("<TD WIDTH=52 VALIGN=TOP><A HREF=\"/").Raw(q).Raw("\"><IMG SRC=\"/$sunfish/res/logo.gif\" WIDTH=48 HEIGHT=48 BORDER=0 ALT=\"Sunfish\"></A></TD>")
            .Raw("<TD VALIGN=MIDDLE>").Raw(Font).Raw("<FONT SIZE=\"+2\"><B>").Text(m.ServerName).Raw("</B></FONT><BR>");
        Breadcrumb(html, m, q);
        html.Raw(EndFont).Raw("</TD></TR></TABLE>\n<HR>\n");

        bool rowActions = m.Actions.Rename || m.Actions.Delete;
        html.Raw("<TABLE CLASS=\"list\" BORDER=0 CELLPADDING=3 CELLSPACING=0 WIDTH=\"100%\">\n")
            .Raw("<TR BGCOLOR=\"#DDE8F4\">");
        HeaderCell(html, "LEFT", "n", Strings.ColName, m, "");
        HeaderCell(html, "RIGHT", "s", Strings.ColSize, m, "");
        HeaderCell(html, "LEFT", "d", Strings.ColDate, m, " CLASS=\"date\"");
        if (rowActions)
            html.Raw("<TH></TH>");
        html.Raw("</TR>\n");
        string emptyActions = rowActions ? "<TD></TD>" : "";

        if (!m.IsRoot)
        {
            html.Raw("<TR><TD>").Raw(Font).Raw("<A HREF=\"../").Raw(q).Raw("\"><IMG SRC=\"/$sunfish/res/up.gif\" WIDTH=16 HEIGHT=16 BORDER=0 ALT=\"[..]\"> ..</A>")
                .Raw(EndFont).Raw("</TD><TD></TD><TD CLASS=\"date\"></TD>").Raw(emptyActions).Raw("</TR>\n");
        }

        int row = 0;
        foreach (var item in Sorted(m.Items, m.Sort))
        {
            var e = item.Entry;
            string shown = item.Alias ?? e.Name;
            string href = HtmlBuilder.UrlSegment(shown) + (e.IsDirectory ? "/" : "");
            string bg = row++ % 2 == 1 && html.Classic ? " BGCOLOR=\"#F4F7FB\"" : "";
            html.Raw("<TR").Raw(bg).Raw("><TD>").Raw(Font)
                .Raw("<A HREF=\"").Raw(href).Raw(q).Raw("\">")
                .Raw(e.IsDirectory
                    ? "<IMG SRC=\"/$sunfish/res/folder.gif\" WIDTH=16 HEIGHT=16 BORDER=0 ALT=\"[DIR]\"> "
                    : "<IMG SRC=\"/$sunfish/res/file.gif\" WIDTH=16 HEIGHT=16 BORDER=0 ALT=\"[   ]\"> ")
                .Text(shown).Raw(e.IsDirectory ? "/" : "").Raw("</A>");
            Extras(html, m, item, href);
            if (item.HasIndex)
                html.Raw(" <FONT SIZE=\"-1\" CLASS=\"extra\">[<A HREF=\"").Raw(href).Raw("?$list").Raw(ProfileQuery(m.Profile, first: false))
                    .Raw("\">").Text(Strings.FolderContents).Raw("</A>]</FONT>");
            html.Raw(EndFont).Raw("</TD><TD ALIGN=RIGHT NOWRAP>").Raw(Font)
                .Text(e.IsDirectory ? "" : FormatSize(e.Size)).Raw(EndFont)
                .Raw("</TD><TD NOWRAP CLASS=\"date\">").Raw(Font)
                .Text(e.IsVirtual ? "" : FormatDate(e.Modified)).Raw(EndFont).Raw("</TD>");
            if (rowActions)
            {
                html.Raw("<TD NOWRAP>").Raw(Font).Raw("<FONT SIZE=\"-1\" CLASS=\"extra\">");
                if (!e.IsVirtual)
                    RowActionLinks(html, m, e.Name);
                html.Raw("</FONT>").Raw(EndFont).Raw("</TD>");
            }
            html.Raw("</TR>\n");
        }
        if (m.Items.Count == 0)
            html.Raw("<TR><TD COLSPAN=3>").Raw(Font).Raw("<I>").Text(Strings.EmptyFolder).Raw("</I>").Raw(EndFont).Raw("</TD>").Raw(emptyActions).Raw("</TR>\n");
        html.Raw("</TABLE>\n");
    }

    private static void RenderText(HtmlBuilder html, Model m)
    {
        string q = ProfileQuery(m.Profile, first: true);
        html.Raw("<H1>").Text(m.ServerName).Raw("</H1>\n<P>");
        Breadcrumb(html, m, q);
        html.Raw("</P>\n<UL>\n");
        if (!m.IsRoot)
            html.Raw("<LI><A HREF=\"../").Raw(q).Raw("\">..</A> (").Text(Strings.ParentFolder).Raw(")\n");
        foreach (var item in Sorted(m.Items, m.Sort))
        {
            var e = item.Entry;
            string shown = item.Alias ?? e.Name;
            string href = HtmlBuilder.UrlSegment(shown) + (e.IsDirectory ? "/" : "");
            html.Raw("<LI><A HREF=\"").Raw(href).Raw(q).Raw("\">").Text(shown).Raw(e.IsDirectory ? "/" : "").Raw("</A>");
            Extras(html, m, item, href);
            if (!e.IsDirectory)
                html.Raw(" ").Text(FormatSize(e.Size));
            if (!e.IsVirtual)
            {
                html.Raw(" ").Text(FormatDate(e.Modified));
                RowActionLinks(html, m, e.Name);
            }
            html.Raw("\n");
        }
        if (m.Items.Count == 0)
            html.Raw("<LI>").Text(Strings.EmptyFolder).Raw("\n");
        html.Raw("</UL>\n");
    }

    /// <summary>
    /// Tras el nombre: el nombre largo si se muestra un alias 8.3, y para quien no es un Mac
    /// clasico los enlaces para bajar el fichero con su resource fork (MacBinary o BinHex).
    /// </summary>
    private static void Extras(HtmlBuilder html, Model m, ListingItem item, string href)
    {
        if (item.Alias is not null && item.Alias != item.Entry.Name)
            html.Raw(" <FONT SIZE=\"-1\" CLASS=\"extra\">(").Text(item.Entry.Name).Raw(")</FONT>");
        if (item.MacFork && m.Profile.Kind != ClientKind.Mac)
        {
            string pq = ProfileQuery(m.Profile, first: false);
            html.Raw(" <FONT SIZE=\"-1\" CLASS=\"extra\">[<A HREF=\"").Raw(href).Raw("?as=macbin").Raw(pq).Raw("\">.bin</A>]")
                .Raw(" [<A HREF=\"").Raw(href).Raw("?as=hqx").Raw(pq).Raw("\">.hqx</A>]</FONT>");
        }
    }

    private static void RowActionLinks(HtmlBuilder html, Model m, string name)
    {
        string encoded = Uri.EscapeDataString(name);
        string pq = ProfileQuery(m.Profile, first: false);
        if (m.Actions.Rename)
            html.Raw(" [<A HREF=\"?$rename=").Raw(encoded).Raw(pq).Raw("\">").Text(Strings.RenameShort).Raw("</A>]");
        if (m.Actions.Delete)
            html.Raw(" [<A HREF=\"?$delete=").Raw(encoded).Raw(pq).Raw("\">").Text(Strings.DeleteShort).Raw("</A>]");
    }

    /// <summary>
    /// Formularios de la carpeta. Los campos ocultos van antes que el fichero: los navegadores los
    /// envian en ese orden y el servidor valida el token antes de escribir nada en disco.
    /// </summary>
    private static void RenderForms(HtmlBuilder html, Model m)
    {
        var a = m.Actions;
        if (!a.Any)
            return;
        html.Raw("<HR>\n");
        if (a.Upload)
        {
            FormStart(html, m, "upload", multipart: true);
            html.Raw(Font).Text(Strings.UploadHint).Raw(":").Raw(EndFont).Raw("<BR>")
                .Raw("<INPUT TYPE=FILE NAME=\"f\" MULTIPLE> <INPUT TYPE=SUBMIT VALUE=\"").Text(Strings.Upload).Raw("\"> ")
                .Raw("<SPAN ID=\"sf-status\"></SPAN></FORM>\n");
            FormStart(html, m, "mkdir", multipart: false);
            html.Raw(Font).Text(Strings.NewFolder).Raw(":").Raw(EndFont)
                .Raw(" <INPUT TYPE=TEXT NAME=\"name\" SIZE=24> <INPUT TYPE=SUBMIT VALUE=\"").Text(Strings.Create).Raw("\"></FORM>\n");
        }
        if (a.Zip)
        {
            html.Raw("<P>").Raw(Font).Raw("<A HREF=\"?$zip").Raw(ProfileQuery(m.Profile, first: false)).Raw("\">[ZIP] ")
                .Text(Strings.DownloadZip).Raw("</A>").Raw(EndFont).Raw("</P>\n");
        }
    }

    private static void FormStart(HtmlBuilder html, Model m, string action, bool multipart)
    {
        html.Raw("<FORM METHOD=POST ACTION=\"").Raw(m.FolderUrl).Raw(ProfileQuery(m.Profile, true)).Raw("\"")
            .Raw(multipart ? " ENCTYPE=\"multipart/form-data\"" : "");
        if (action == "upload")
            html.Raw(" ID=\"sf-upload\" DATA-UPLOADING=\"").Text(Strings.Uploading).Raw("\" DATA-DONE=\"").Text(Strings.UploadDone).Raw("\"");
        html.Raw(">")
            .Raw("<INPUT TYPE=HIDDEN NAME=\"a\" VALUE=\"").Raw(action).Raw("\">")
            .Raw("<INPUT TYPE=HIDDEN NAME=\"t\" VALUE=\"").Raw(m.Actions.Token(action)).Raw("\">");
    }

    private static void Breadcrumb(HtmlBuilder html, Model m, string q)
    {
        html.Raw("<A HREF=\"/").Raw(q).Raw("\">/</A>");
        string link = "/";
        foreach (var segment in m.Segments)
        {
            link += HtmlBuilder.UrlSegment(segment) + "/";
            html.Raw(" <A HREF=\"").Raw(link).Raw(q).Raw("\">").Text(segment).Raw("</A> /");
        }
    }

    private static void HeaderCell(HtmlBuilder html, string align, string sort, string label, Model m, string extra)
    {
        html.Raw("<TH ALIGN=").Raw(align).Raw(extra).Raw(">").Raw(Font)
            .Raw("<A HREF=\"?sort=").Raw(sort).Raw(ProfileQuery(m.Profile, first: false)).Raw("\">")
            .Text(label).Raw("</A>").Raw(EndFont).Raw("</TH>");
    }

    /// <summary>"?p=dos" si el perfil se forzo (los clientes sin cookies lo pierden si no).</summary>
    internal static string ProfileQuery(ClientProfile profile, bool first) =>
        profile.Forced ? (first ? "?p=" : "&p=") + profile.Code : "";

    internal static string UrlOf(IReadOnlyList<string> segments) =>
        "/" + string.Concat(segments.Select(s => HtmlBuilder.UrlSegment(s) + "/"));

    internal static IEnumerable<ListingItem> Sorted(IEnumerable<ListingItem> items, string sort)
    {
        var dirsFirst = items.OrderBy(i => i.Entry.IsDirectory ? 0 : 1);
        return sort switch
        {
            "s" => dirsFirst.ThenBy(i => i.Entry.Size).ThenBy(i => i.Entry.Name, StringComparer.OrdinalIgnoreCase),
            "d" => dirsFirst.ThenByDescending(i => i.Entry.Modified).ThenBy(i => i.Entry.Name, StringComparer.OrdinalIgnoreCase),
            _ => dirsFirst.ThenBy(i => i.Entry.Name, StringComparer.OrdinalIgnoreCase),
        };
    }

    internal 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.ToString(CultureInfo.InvariantCulture) + " B"
            : value.ToString("0.0", CultureInfo.InvariantCulture) + " " + units[unit];
    }

    internal static string FormatDate(DateTime time) =>
        time.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
}
