using System.Globalization;
using System.Net;
using Sunfish.Shares;

namespace Sunfish.Config;

/// <summary>Seccion [Server].</summary>
public sealed class ServerSettings
{
    public string Name { get; set; } = "Sunfish";
    public bool Http { get; set; } = true;
    public int HttpPort { get; set; } = 8080;
    public bool Ftp { get; set; } = true;
    public int FtpPort { get; set; } = 2121;

    /// <summary>Rango de puertos para el modo pasivo, "50000-50050" (vacio: cualquiera libre).</summary>
    public string FtpPassivePorts { get; set; } = "50000-50050";

    /// <summary>IP que se anuncia en PASV si el servidor esta detras de NAT (null: la local).</summary>
    public IPAddress? FtpPublicAddress { get; set; }

    /// <summary>Juego de caracteres de los nombres para clientes que no piden UTF-8.</summary>
    public string FtpCharset { get; set; } = "iso-8859-1";

    public List<IPAddress> Bind { get; set; } = new() { IPAddress.Any, IPAddress.IPv6Any };
    public bool RootMenu { get; set; } = true;
    public bool SelfDownload { get; set; } = true;
    public bool Announce { get; set; } = true;
}

/// <summary>Seccion [Share nombre]: una carpeta publicada en una location.</summary>
public sealed class ShareConfig
{
    public string Name { get; set; } = "";
    public string Location { get; set; } = "/";
    public string Path { get; set; } = "";
    public string Index { get; set; } = "index.html";
    public bool Enabled { get; set; } = true;
    public bool Listing { get; set; } = true;
    public bool Upload { get; set; }
    public bool Overwrite { get; set; }
    public bool Rename { get; set; }
    public bool Delete { get; set; }
    public bool Zip { get; set; }
    public bool Ftp { get; set; } = true;
    public bool WebDav { get; set; } = true;
    public bool Mac { get; set; } = true;
    public bool ShortNames { get; set; } = true;
    public List<string> Users { get; set; } = new();

    public ShareConfig Clone()
    {
        var copy = (ShareConfig)MemberwiseClone();
        copy.Users = new List<string>(Users);
        return copy;
    }
}

/// <summary>Seccion [User nombre].</summary>
public sealed class UserConfig
{
    public string Name { get; set; } = "";
    public string PasswordHash { get; set; } = "";
}

/// <summary>Seccion [Admin].</summary>
public sealed class AdminSettings
{
    public bool Enabled { get; set; }
    public string PasswordHash { get; set; } = "";
    public List<string> AllowFrom { get; set; } = new() { "127.0.0.1", "::1" };
}

/// <summary>
/// Configuracion completa (sunfish.conf). Se lee de un INI y se vuelve a escribir sobre el
/// mismo documento, asi se conservan comentarios y orden.
/// </summary>
public sealed class SunfishConfig
{
    private readonly IniDocument _doc;

    public SunfishConfig() : this(new IniDocument()) { }

    private SunfishConfig(IniDocument doc) => _doc = doc;

    public ServerSettings Server { get; } = new();
    public AdminSettings Admin { get; } = new();
    public List<ShareConfig> Shares { get; } = new();
    public List<UserConfig> Users { get; } = new();

    /// <summary>Fichero del que se leyo (null si se creo en memoria).</summary>
    public string? FilePath { get; set; }

    /// <summary>Errores encontrados al leer; la configuracion se usa igualmente.</summary>
    public List<string> Errors { get; } = new();

    /// <summary>
    /// Copia independiente (mismo texto, comentarios incluidos) para editarla sin tocar la que
    /// esta sirviendo; se aplica despues con SunfishHost.ApplyAsync.
    /// </summary>
    public SunfishConfig Clone()
    {
        var copy = Parse(ToText());
        copy.FilePath = FilePath;
        return copy;
    }

    public static SunfishConfig Load(string path)
    {
        var config = Parse(File.ReadAllText(path));
        config.FilePath = path;
        return config;
    }

    public static SunfishConfig Parse(string text)
    {
        var doc = IniDocument.Parse(text);
        var config = new SunfishConfig(doc);
        foreach (var (line, _) in doc.Errors)
            config.Errors.Add(Strings.ConfigLine(line, Strings.ConfigBadLine));
        foreach (var section in doc.Sections)
        {
            var reader = new SectionReader(section, config.Errors);
            switch (section.Kind.ToLowerInvariant())
            {
                case "server":
                    config.ReadServer(reader);
                    break;
                case "admin":
                    config.ReadAdmin(reader);
                    break;
                case "share":
                    config.Shares.Add(ReadShare(reader, section.Title));
                    break;
                case "user":
                    config.Users.Add(new UserConfig { Name = section.Title, PasswordHash = reader.String("PasswordHash", "") });
                    break;
                default:
                    config.Errors.Add(Strings.ConfigLine(section.LineNumber, Strings.ConfigUnknownSection(section.Name)));
                    break;
            }
        }
        config.Validate();
        return config;
    }

    private void ReadServer(SectionReader r)
    {
        Server.Name = r.String("Name", Server.Name);
        Server.Http = r.Bool("Http", Server.Http);
        Server.HttpPort = r.Port("HttpPort", Server.HttpPort);
        Server.Ftp = r.Bool("Ftp", Server.Ftp);
        Server.FtpPort = r.Port("FtpPort", Server.FtpPort);
        Server.FtpPassivePorts = r.String("FtpPassivePorts", Server.FtpPassivePorts);
        Server.FtpCharset = r.String("FtpCharset", Server.FtpCharset);
        if (r.String("FtpPublicAddress", "") is { Length: > 0 } publicAddress)
        {
            if (IPAddress.TryParse(publicAddress, out var parsed))
                Server.FtpPublicAddress = parsed;
            else
                r.Error(Strings.ConfigBadAddress(publicAddress));
        }
        Server.RootMenu = r.Bool("RootMenu", Server.RootMenu);
        Server.SelfDownload = r.Bool("SelfDownload", Server.SelfDownload);
        Server.Announce = r.Bool("Announce", Server.Announce);
        var bind = r.List("Bind");
        if (bind is not null)
        {
            var addresses = new List<IPAddress>();
            foreach (var item in bind)
            {
                if (IPAddress.TryParse(item, out var address))
                    addresses.Add(address);
                else
                    r.Error(Strings.ConfigBadAddress(item));
            }
            if (addresses.Count > 0)
                Server.Bind = addresses;
        }
    }

    private void ReadAdmin(SectionReader r)
    {
        Admin.Enabled = r.Bool("Enabled", Admin.Enabled);
        Admin.PasswordHash = r.String("PasswordHash", "");
        Admin.AllowFrom = r.List("AllowFrom") ?? Admin.AllowFrom;
    }

    private static ShareConfig ReadShare(SectionReader r, string name)
    {
        var s = new ShareConfig { Name = name };
        s.Location = r.String("Location", s.Location);
        s.Path = r.String("Path", s.Path);
        s.Index = r.String("Index", s.Index);
        s.Enabled = r.Bool("Enabled", s.Enabled);
        s.Listing = r.Bool("Listing", s.Listing);
        s.Upload = r.Bool("Upload", s.Upload);
        s.Overwrite = r.Bool("Overwrite", s.Overwrite);
        s.Rename = r.Bool("Rename", s.Rename);
        s.Delete = r.Bool("Delete", s.Delete);
        s.Zip = r.Bool("Zip", s.Zip);
        s.Ftp = r.Bool("Ftp", s.Ftp);
        s.WebDav = r.Bool("WebDav", s.WebDav);
        s.Mac = r.Bool("Mac", s.Mac);
        s.ShortNames = r.Bool("ShortNames", s.ShortNames);
        s.Users = r.List("Users") ?? s.Users;
        return s;
    }

    /// <summary>Comprueba lo que no depende de una sola clave: locations validas y unicas, rutas.</summary>
    private void Validate()
    {
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var share in Shares)
        {
            if (!LocationPath.TryNormalize(share.Location, out var normalized))
            {
                Errors.Add(Strings.ConfigBadLocation(share.Name, share.Location));
                share.Enabled = false;
                continue;
            }
            share.Location = normalized;
            if (string.IsNullOrWhiteSpace(share.Path))
            {
                Errors.Add(Strings.ConfigMissingPath(share.Name));
                share.Enabled = false;
            }
            if (share.Enabled && !seen.Add(normalized))
            {
                Errors.Add(Strings.ConfigDuplicateLocation(share.Name, share.Location));
                share.Enabled = false;
            }
        }
    }

    /// <summary>Texto INI con los valores actuales, sobre el documento original.</summary>
    public string ToText()
    {
        WriteServer(_doc.GetOrAdd("Server"));
        var admin = _doc.Find("Admin");
        if (admin is not null || Admin.Enabled || Admin.PasswordHash.Length > 0)
            WriteAdmin(admin ?? _doc.GetOrAdd("Admin"));

        SyncSections("User", Users.Select(u => u.Name), (section, name) =>
        {
            var user = Users.First(u => u.Name == name);
            section.Set("PasswordHash", user.PasswordHash);
        });
        SyncSections("Share", Shares.Select(s => s.Name), (section, name) =>
            WriteShare(section, Shares.First(s => s.Name == name)));

        return _doc.ToText(OperatingSystem.IsWindows() ? "\r\n" : "\n");
    }

    /// <summary>Guarda de forma atomica: fichero temporal y renombrado.</summary>
    public void Save(string? path = null)
    {
        path ??= FilePath ?? throw new InvalidOperationException("No config path");
        string full = System.IO.Path.GetFullPath(path);
        string? dir = System.IO.Path.GetDirectoryName(full);
        if (dir is not null)
            Directory.CreateDirectory(dir);
        string temp = full + ".tmp";
        File.WriteAllText(temp, ToText());
        File.Move(temp, full, overwrite: true);
        FilePath = full;
    }

    private void SyncSections(string kind, IEnumerable<string> names, Action<IniSection, string> write)
    {
        var wanted = names.ToList();
        foreach (var section in _doc.Sections.Where(s => s.Kind.Equals(kind, StringComparison.OrdinalIgnoreCase)).ToList())
        {
            if (!wanted.Contains(section.Title, StringComparer.OrdinalIgnoreCase))
                _doc.Remove(section);
        }
        foreach (var name in wanted)
            write(_doc.GetOrAdd($"{kind} {name}"), name);
    }

    private void WriteServer(IniSection s)
    {
        var d = new ServerSettings();
        SetIf(s, "Name", Server.Name, d.Name);
        SetIf(s, "Http", Server.Http, d.Http);
        SetIf(s, "HttpPort", Server.HttpPort, d.HttpPort);
        SetIf(s, "Ftp", Server.Ftp, d.Ftp);
        SetIf(s, "FtpPort", Server.FtpPort, d.FtpPort);
        SetIf(s, "FtpPassivePorts", Server.FtpPassivePorts, d.FtpPassivePorts);
        SetIf(s, "FtpPublicAddress", Server.FtpPublicAddress?.ToString() ?? "", "");
        SetIf(s, "FtpCharset", Server.FtpCharset, d.FtpCharset);
        SetIf(s, "Bind", string.Join(", ", Server.Bind), string.Join(", ", d.Bind));
        SetIf(s, "RootMenu", Server.RootMenu, d.RootMenu);
        SetIf(s, "SelfDownload", Server.SelfDownload, d.SelfDownload);
        SetIf(s, "Announce", Server.Announce, d.Announce);
    }

    private void WriteAdmin(IniSection s)
    {
        s.Set("Enabled", Format(Admin.Enabled));
        s.Set("PasswordHash", Admin.PasswordHash);
        s.Set("AllowFrom", string.Join(", ", Admin.AllowFrom));
    }

    private static void WriteShare(IniSection s, ShareConfig share)
    {
        var d = new ShareConfig();
        s.Set("Location", share.Location);
        s.Set("Path", share.Path);
        SetIf(s, "Index", share.Index, d.Index);
        SetIf(s, "Enabled", share.Enabled, d.Enabled);
        SetIf(s, "Listing", share.Listing, d.Listing);
        SetIf(s, "Upload", share.Upload, d.Upload);
        SetIf(s, "Overwrite", share.Overwrite, d.Overwrite);
        SetIf(s, "Rename", share.Rename, d.Rename);
        SetIf(s, "Delete", share.Delete, d.Delete);
        SetIf(s, "Zip", share.Zip, d.Zip);
        SetIf(s, "Ftp", share.Ftp, d.Ftp);
        SetIf(s, "WebDav", share.WebDav, d.WebDav);
        SetIf(s, "Mac", share.Mac, d.Mac);
        SetIf(s, "ShortNames", share.ShortNames, d.ShortNames);
        SetIf(s, "Users", string.Join(", ", share.Users), "");
    }

    /// <summary>Escribe la clave si ya estaba en el fichero o si el valor no es el de por defecto.</summary>
    private static void SetIf(IniSection s, string key, string value, string defaultValue)
    {
        if (s.Has(key) || value != defaultValue)
            s.Set(key, value);
    }

    private static void SetIf(IniSection s, string key, bool value, bool defaultValue)
    {
        // "yes", "on" o "1" se respetan si siguen significando lo mismo.
        if (s.Get(key) is { } current && TryParseBool(current, out bool same) && same == value)
            return;
        SetIf(s, key, Format(value), Format(defaultValue));
    }

    private static bool TryParseBool(string text, out bool value)
    {
        switch (text.Trim().ToLowerInvariant())
        {
            case "true": case "yes": case "on": case "1": value = true; return true;
            case "false": case "no": case "off": case "0": value = false; return true;
            default: value = false; return false;
        }
    }

    private static void SetIf(IniSection s, string key, int value, int defaultValue) =>
        SetIf(s, key, value.ToString(CultureInfo.InvariantCulture), defaultValue.ToString(CultureInfo.InvariantCulture));

    private static string Format(bool value) => value ? "true" : "false";

    /// <summary>Lectura tipada de una seccion, acumulando errores con su numero de linea.</summary>
    private sealed class SectionReader(IniSection section, List<string> errors)
    {
        private int _line = section.LineNumber;

        public void Error(string message) => errors.Add(Strings.ConfigLine(_line, message));

        private string? Raw(string key)
        {
            var entry = section.FindEntry(key);
            if (entry is null)
                return null;
            _line = entry.Number;
            return entry.Value;
        }

        public string String(string key, string defaultValue) => Raw(key) ?? defaultValue;

        public bool Bool(string key, bool defaultValue)
        {
            string? value = Raw(key);
            if (value is null)
                return defaultValue;
            if (TryParseBool(value, out bool result))
                return result;
            Error(Strings.ConfigBadBool(key, value));
            return defaultValue;
        }

        public int Port(string key, int defaultValue)
        {
            string? value = Raw(key);
            if (value is null)
                return defaultValue;
            if (int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out int port) && port is >= 0 and <= 65535)
                return port;
            Error(Strings.ConfigBadNumber(key, value));
            return defaultValue;
        }

        public List<string>? List(string key)
        {
            string? value = Raw(key);
            if (value is null)
                return null;
            return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
        }
    }
}
