namespace Sunfish.Shares;

public enum FileOpResult { Ok, InvalidName, NotFound, Exists, NoSpace, Conflict, Failed }

/// <summary>
/// Cambios en disco comunes a web, FTP y WebDAV. Los nombres ya llegan validados por la capa de
/// protocolo, pero se vuelven a comprobar aqui. Las subidas se escriben en un temporal oculto de
/// la misma carpeta y se renombran al terminar: una subida cortada no deja un fichero a medias
/// ni pisa el existente.
/// </summary>
public static class FileOperations
{
    public const string TempPrefix = ".sunfish-";

    /// <summary>Nombre aceptable para crear: valido en el sistema y que no empiece por '.'.</summary>
    public static bool IsCreatableName(string name) =>
        PathRules.IsValidSegment(name) && !name.StartsWith('.') && name.Length <= 255;

    /// <summary>
    /// Metadatos que macOS escribe en volumenes sin forks (._nombre = AppleDouble, .DS_Store).
    /// Por WebDAV se aceptan aunque empiecen por '.'; no salen en los listados.
    /// </summary>
    public static bool IsMacMetadataName(string name) =>
        PathRules.IsValidSegment(name) && name.Length <= 255 &&
        ((name.StartsWith("._", StringComparison.Ordinal) && name.Length > 2) || name == ".DS_Store") &&
        !name.StartsWith(TempPrefix, StringComparison.Ordinal);

    /// <summary>
    /// Nombre de un fichero subido: sin la ruta que envian algunos navegadores (IE antiguo manda
    /// "C:\WINDOWS\a.txt"; Mac clasico puede usar ':') y sin espacios sobrantes.
    /// </summary>
    public static string CleanUploadName(string fileName)
    {
        int cut = fileName.LastIndexOfAny(['\\', '/', ':']);
        return (cut >= 0 ? fileName[(cut + 1)..] : fileName).Trim();
    }

    public static FileOpResult CreateFolder(string directory, string name)
    {
        if (!IsCreatableName(name))
            return FileOpResult.InvalidName;
        string target = Path.Combine(directory, name);
        if (Directory.Exists(target) || File.Exists(target))
            return FileOpResult.Exists;
        try
        {
            Directory.CreateDirectory(target);
            return FileOpResult.Ok;
        }
        catch (Exception e) when (e is IOException or UnauthorizedAccessException)
        {
            return FileOpResult.Failed;
        }
    }

    /// <summary>Borra un fichero o una carpeta con todo su contenido (los enlaces se borran, no se siguen).</summary>
    public static FileOpResult Delete(string path)
    {
        try
        {
            if (File.Exists(path))
            {
                File.Delete(path);
                DeleteCompanion(path);
            }
            else if (Directory.Exists(path))
                Directory.Delete(path, recursive: true);
            else
                return FileOpResult.NotFound;
            return FileOpResult.Ok;
        }
        catch (Exception e) when (e is IOException or UnauthorizedAccessException)
        {
            return FileOpResult.Failed;
        }
    }

    /// <summary>Cambia el nombre dentro de la misma carpeta; no sustituye nada existente.</summary>
    public static FileOpResult Rename(string path, string newName)
    {
        if (!IsCreatableName(newName))
            return FileOpResult.InvalidName;
        string? directory = Path.GetDirectoryName(path);
        if (directory is null)
            return FileOpResult.Failed;
        string target = Path.Combine(directory, newName);
        bool sameItem = string.Equals(target, path, StringComparison.OrdinalIgnoreCase);
        if (!sameItem && (File.Exists(target) || Directory.Exists(target)))
            return FileOpResult.Exists;
        try
        {
            if (File.Exists(path))
            {
                File.Move(path, target);
                MoveCompanion(path, target);
            }
            else if (Directory.Exists(path))
                Directory.Move(path, target); // tambien vale para cambiar solo mayusculas en Windows
            else
                return FileOpResult.NotFound;
            return FileOpResult.Ok;
        }
        catch (Exception e) when (e is IOException or UnauthorizedAccessException)
        {
            return FileOpResult.Failed;
        }
    }

    /// <summary>
    /// Mueve (o renombra) a otra ruta ya validada del mismo share; no sustituye nada existente.
    /// No mueve una carpeta dentro de si misma.
    /// </summary>
    public static FileOpResult Move(string source, string target)
    {
        string? name = Path.GetFileName(target);
        if (name is null || !IsCreatableName(name))
            return FileOpResult.InvalidName;
        bool sameItem = string.Equals(source, target, StringComparison.OrdinalIgnoreCase);
        if (!sameItem && (File.Exists(target) || Directory.Exists(target)))
            return FileOpResult.Exists;
        string? parent = Path.GetDirectoryName(target);
        if (parent is null || !Directory.Exists(parent))
            return FileOpResult.NotFound;
        try
        {
            if (File.Exists(source))
            {
                File.Move(source, target);
                MoveCompanion(source, target);
            }
            else if (Directory.Exists(source))
            {
                if (target.StartsWith(Path.TrimEndingDirectorySeparator(source) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
                    return FileOpResult.Conflict;
                Directory.Move(source, target);
            }
            else
                return FileOpResult.NotFound;
            return FileOpResult.Ok;
        }
        catch (Exception e) when (e is IOException or UnauthorizedAccessException)
        {
            return FileOpResult.Failed;
        }
    }

    /// <summary>Borra una carpeta solo si esta vacia (RMD de FTP).</summary>
    public static FileOpResult DeleteEmptyFolder(string path)
    {
        if (!Directory.Exists(path))
            return FileOpResult.NotFound;
        if (Directory.EnumerateFileSystemEntries(path).Any())
            return FileOpResult.Conflict;
        try
        {
            Directory.Delete(path);
            return FileOpResult.Ok;
        }
        catch (Exception e) when (e is IOException or UnauthorizedAccessException)
        {
            return FileOpResult.Failed;
        }
    }

    /// <summary>Hay sitio para <paramref name="bytes"/> en la unidad de la carpeta (si se puede saber).</summary>
    public static bool HasSpace(string directory, long bytes)
    {
        try
        {
            string? root = Path.GetPathRoot(Path.GetFullPath(directory));
            return root is null || new DriveInfo(root).AvailableFreeSpace >= bytes;
        }
        catch (Exception e) when (e is IOException or ArgumentException or UnauthorizedAccessException)
        {
            return true;
        }
    }

    /// <summary>Guarda un flujo como fichero nuevo (o sustituye el existente si se permite).</summary>
    public static async Task<FileOpResult> SaveAsync(string directory, string name, Stream content, bool overwrite, CancellationToken ct, bool allowMacMetadata = false)
    {
        if (!IsCreatableName(name) && !(allowMacMetadata && IsMacMetadataName(name)))
            return FileOpResult.InvalidName;
        string target = Path.Combine(directory, name);
        if (Directory.Exists(target))
            return FileOpResult.Exists;
        if (File.Exists(target) && !overwrite)
            return FileOpResult.Exists;
        string temp = Path.Combine(directory, TempPrefix + Guid.NewGuid().ToString("N") + ".tmp");
        try
        {
            await using (var file = new FileStream(temp, FileMode.CreateNew, FileAccess.Write, FileShare.None, 65536, useAsync: true))
                await content.CopyToAsync(file, 65536, ct);
            return Commit(temp, target, overwrite);
        }
        catch
        {
            TryDelete(temp);
            throw;
        }
    }

    /// <summary>
    /// Escribe un trozo de una subida por partes (PUT con Content-Range). El temporal se identifica
    /// por el id que manda el cliente; cada trozo debe empezar donde acabo el anterior. Con el
    /// ultimo trozo el fichero se renombra a su sitio.
    /// </summary>
    public static async Task<(FileOpResult Result, bool Completed)> WritePartAsync(
        string directory, string name, string uploadId, long start, long total, Stream content, bool overwrite, CancellationToken ct)
    {
        if (!IsCreatableName(name))
            return (FileOpResult.InvalidName, false);
        string target = Path.Combine(directory, name);
        if (Directory.Exists(target) || (File.Exists(target) && !overwrite))
            return (FileOpResult.Exists, false);
        string temp = Path.Combine(directory, TempPrefix + uploadId + ".part");
        long length;
        await using (var file = new FileStream(temp, start == 0 ? FileMode.Create : FileMode.Open, FileAccess.Write, FileShare.None, 65536, useAsync: true))
        {
            if (file.Length != start)
                return (FileOpResult.Conflict, false);
            file.Position = start;
            await content.CopyToAsync(file, 65536, ct);
            length = file.Length;
        }
        if (length > total)
        {
            TryDelete(temp);
            return (FileOpResult.Conflict, false);
        }
        if (length < total)
            return (FileOpResult.Ok, false);
        return (Commit(temp, target, overwrite), true);
    }

    private static FileOpResult Commit(string temp, string target, bool overwrite)
    {
        try
        {
            File.Move(temp, target, overwrite);
            return FileOpResult.Ok;
        }
        catch (IOException) when (File.Exists(target) && !overwrite)
        {
            TryDelete(temp);
            return FileOpResult.Exists;
        }
    }

    /// <summary>
    /// El AppleDouble ("._nombre") de un fichero de Mac acompana siempre a su fichero: se mueve y
    /// se borra con el.
    /// </summary>
    private static string CompanionOf(string path) =>
        Path.Combine(Path.GetDirectoryName(path) ?? "", "._" + Path.GetFileName(path));

    private static void MoveCompanion(string source, string target)
    {
        if (Path.GetFileName(source).StartsWith("._", StringComparison.Ordinal))
            return;
        string from = CompanionOf(source);
        if (File.Exists(from))
        {
            try { File.Move(from, CompanionOf(target), overwrite: true); }
            catch (Exception e) when (e is IOException or UnauthorizedAccessException) { }
        }
    }

    private static void DeleteCompanion(string path)
    {
        if (!Path.GetFileName(path).StartsWith("._", StringComparison.Ordinal))
            TryDelete(CompanionOf(path));
    }

    private static void TryDelete(string path)
    {
        try { File.Delete(path); }
        catch (Exception e) when (e is IOException or UnauthorizedAccessException) { }
    }
}
