using Sunfish.Config;

namespace Sunfish.Shares;

/// <summary>Una entrada de un listado (real o virtual).</summary>
public sealed record FileEntry(string Name, bool IsDirectory, long Size, DateTime Modified, bool IsVirtual = false, bool IsLink = false);

/// <summary>
/// Acceso al disco de un share. Toda ruta pasa por <see cref="TryGetPhysicalPath"/>, que garantiza
/// que no se sale de la carpeta del share (ni con "..", ni con rutas absolutas, ni con enlaces).
/// </summary>
public static class ShareFileSystem
{
    private static readonly StringComparison PathComparison =
        OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;

    public static string RootOf(ShareConfig share) =>
        Path.TrimEndingDirectorySeparator(Path.GetFullPath(share.Path));

    /// <summary>Ruta real dentro del share, o null si los componentes no son validos o escaparian de el.</summary>
    public static string? TryGetPhysicalPath(ShareConfig share, IReadOnlyList<string> relative, bool aliases = false)
    {
        string root = RootOf(share);
        string current = root;
        foreach (var segment in relative)
        {
            if (!PathRules.IsValidSegment(segment))
                return null;
            string next = Path.Combine(current, segment);
            // Alias 8.3 (DOS, Windows 3.1): solo si el nombre tal cual no existe.
            if (aliases && !File.Exists(next) && !Directory.Exists(next) &&
                ShortNames.Resolve(current, segment) is { } real)
                next = Path.Combine(current, real);
            current = next;
            if (!IsInside(root, Path.GetFullPath(current)))
                return null;
            // Un enlace (simbolico o junction) solo se sigue si su destino final sigue dentro.
            FileSystemInfo info = Directory.Exists(current) ? new DirectoryInfo(current) : new FileInfo(current);
            if (info.Exists && info.LinkTarget is not null)
            {
                var target = info.ResolveLinkTarget(returnFinalTarget: true);
                if (target is null || !IsInside(root, Path.GetFullPath(target.FullName)))
                    return null;
            }
        }
        return current;
    }

    public static bool IsInside(string root, string fullPath)
    {
        if (string.Equals(root, fullPath, PathComparison))
            return true;
        string prefix = root.EndsWith(Path.DirectorySeparatorChar) ? root : root + Path.DirectorySeparatorChar;
        return fullPath.StartsWith(prefix, PathComparison);
    }

    /// <summary>
    /// Contenido de una carpeta para listar. Se omiten los ocultos (atributo Hidden/System en
    /// Windows, nombres que empiezan por '.') aunque se puedan pedir directamente por su URL.
    /// </summary>
    public static List<FileEntry> List(string physicalDirectory)
    {
        var entries = new List<FileEntry>();
        var dir = new DirectoryInfo(physicalDirectory);
        foreach (var info in dir.EnumerateFileSystemInfos())
        {
            if (IsHidden(info))
                continue;
            bool link = info.LinkTarget is not null;
            if (info is DirectoryInfo)
                entries.Add(new FileEntry(info.Name, true, 0, info.LastWriteTime, IsLink: link));
            else if (info is FileInfo file)
                entries.Add(new FileEntry(file.Name, false, file.Length, file.LastWriteTime, IsLink: link));
        }
        return entries;
    }

    private static bool IsHidden(FileSystemInfo info) =>
        info.Name.StartsWith('.') || info.Name == "Icon\r" || // icono de carpeta de Mac
        (info.Attributes & (FileAttributes.Hidden | FileAttributes.System)) != 0;
}
