//Source https://github.com/OrchardCMS/OrchardCore/tree/dev/src/OrchardCore

using Grand.Business.Core.Interfaces.Storage;
using Microsoft.Extensions.FileProviders.Physical;

namespace Grand.Business.Storage.Services;

public class FileSystemStore : IFileStore
{
    private readonly string _fileSystemPath;

    public FileSystemStore(string fileSystemPath)
    {
        _fileSystemPath = Path.GetFullPath(fileSystemPath);
    }

    public Task<IFileStoreEntry> GetFileInfo(string path)
    {
        var physicalPath = GetPhysicalPath(path);

        var fileInfo = new PhysicalFileInfo(new FileInfo(physicalPath));

        return Task.FromResult<IFileStoreEntry>(fileInfo.Exists ? new FileSystemStoreEntry(path, fileInfo) : null);
    }

    public IFileStoreEntry GetDirectoryInfo(string path)
    {
        var physicalPath = GetPhysicalPath(path);

        var directoryInfo = new PhysicalDirectoryInfo(new DirectoryInfo(physicalPath));

        return directoryInfo.Exists ? new FileSystemStoreEntry(path, directoryInfo) : null;
    }

    public Task<PhysicalDirectoryInfo> GetPhysicalDirectoryInfo(string directoryPath)
    {
        var physicalPath = GetPhysicalPath(directoryPath);

        var directoryInfo = new PhysicalDirectoryInfo(new DirectoryInfo(physicalPath));

        return directoryInfo.Exists ? Task.FromResult(directoryInfo) : Task.FromResult<PhysicalDirectoryInfo>(null);
    }

    public IList<IFileStoreEntry> GetDirectoryContent(string path = null, bool includeSubDirectories = false,
        bool listDirectories = true, bool listFiles = true)
    {
        var physicalPath = GetPhysicalPath(path);
        var results = new List<IFileStoreEntry>();

        if (!Directory.Exists(physicalPath)) return results.ToList();

        // Add directories.
        if (listDirectories)
            results.AddRange(
                Directory
                    .GetDirectories(physicalPath, "*",
                        includeSubDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
                    .Select(f =>
                    {
                        var fileSystemInfo = new PhysicalDirectoryInfo(new DirectoryInfo(f));
                        var fileRelativePath = f[_fileSystemPath.Length..];
                        var filePath = this.NormalizePath(fileRelativePath);
                        return new FileSystemStoreEntry(filePath, fileSystemInfo);
                    }));

        // Add files.
        if (listFiles)
            results.AddRange(
                Directory
                    .GetFiles(physicalPath, "*",
                        includeSubDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
                    .Select(f =>
                    {
                        var fileSystemInfo = new PhysicalFileInfo(new FileInfo(f));
                        var fileRelativePath = f[_fileSystemPath.Length..];
                        var filePath = this.NormalizePath(fileRelativePath);
                        return new FileSystemStoreEntry(filePath, fileSystemInfo);
                    }));

        return results.ToList();
    }

    public bool TryCreateDirectory(string path)
    {
        var physicalPath = GetPhysicalPath(path);

        if (File.Exists(physicalPath))
            throw new Exception($"Cannot create directory because the path '{path}' already exists and is a file.");

        if (Directory.Exists(physicalPath)) return false;

        Directory.CreateDirectory(physicalPath);

        return true;
    }

    public Task<bool> TryRenameDirectory(string path, string newName)
    {
        var physicalPath = GetPhysicalPath(path);

        if (File.Exists(physicalPath))
            throw new Exception($"Cannot create directory because the path '{path}' already exists and is a file.");

        if (!Directory.Exists(physicalPath)) return Task.FromResult(false);
        var directoryInfo = new DirectoryInfo(physicalPath);

        var newphysicalPath = GetPhysicalPath(Path.Combine(directoryInfo.Parent.FullName, newName));

        if (Directory.Exists(newphysicalPath)) return Task.FromResult(false);
        directoryInfo.MoveTo(newphysicalPath);

        return Task.FromResult(true);
    }

    public Task<bool> TryDeleteFile(string path)
    {
        var physicalPath = GetPhysicalPath(path);

        if (!File.Exists(physicalPath)) return Task.FromResult(false);

        File.Delete(physicalPath);

        return Task.FromResult(true);
    }

    public Task<bool> TryDeleteDirectory(string path)
    {
        var physicalPath = GetPhysicalPath(path);

        if (!Directory.Exists(physicalPath)) return Task.FromResult(false);

        Directory.Delete(physicalPath, true);

        return Task.FromResult(true);
    }

    public Task MoveFile(string oldPath, string newPath)
    {
        var physicalOldPath = GetPhysicalPath(oldPath);

        if (!File.Exists(physicalOldPath))
            throw new Exception($"Cannot move file '{oldPath}' because it does not exist.");

        var physicalNewPath = GetPhysicalPath(newPath);

        if (File.Exists(physicalNewPath) || Directory.Exists(physicalNewPath))
            throw new Exception($"Cannot move file because the new path '{newPath}' already exists.");

        File.Move(physicalOldPath, physicalNewPath);

        return Task.CompletedTask;
    }

    public async Task CopyFile(string srcPath, string dstPath)
    {
        var physicalSrcPath = GetPhysicalPath(srcPath);

        if (!File.Exists(physicalSrcPath)) throw new Exception($"The file '{srcPath}' does not exist.");
        var file = await GetFileInfo(srcPath);
        var physicalDstPath = GetPhysicalPath(Path.Combine(dstPath));

        if (File.Exists(physicalDstPath) || Directory.Exists(physicalDstPath))
            throw new Exception($"Cannot copy file because the destination path '{dstPath}' already exists.");

        File.Copy(physicalSrcPath, physicalDstPath);
    }

    public async Task RenameFile(string file, string newName)
    {
        var physicalSrcFile = GetPhysicalPath(file);

        if (!File.Exists(physicalSrcFile)) throw new Exception($"The file '{file}' does not exist.");

        var physicalFile = await GetFileInfo(file);

        var newphysicalFile = GetPhysicalPath(Path.Combine(physicalFile.DirectoryPath, newName));
        if (File.Exists(newphysicalFile)) throw new Exception($"The file '{newName}' does exist.");
        File.Move(physicalSrcFile, newphysicalFile);
    }

    public Task<Stream> GetFileStream(string path)
    {
        var physicalPath = GetPhysicalPath(path);

        if (!File.Exists(physicalPath))
            throw new Exception($"Cannot get file stream because the file '{path}' does not exist.");

        var stream = File.OpenRead(physicalPath);

        return Task.FromResult<Stream>(stream);
    }

    public Task<Stream> GetFileStream(IFileStoreEntry fileStoreEntry)
    {
        var physicalPath = GetPhysicalPath(fileStoreEntry.Path);
        if (!File.Exists(physicalPath))
            throw new Exception($"Cannot get file stream because the file '{fileStoreEntry.Path}' does not exist.");

        var stream = File.OpenRead(physicalPath);

        return Task.FromResult<Stream>(stream);
    }

    public async Task<string> CreateFileFromStream(string path, Stream inputStream, bool overwrite = false)
    {
        var physicalPath = GetPhysicalPath(path);

        if (!overwrite && File.Exists(physicalPath))
            throw new Exception($"Cannot create file '{path}' because it already exists.");

        if (Directory.Exists(physicalPath))
            throw new Exception($"Cannot create file '{path}' because it already exists as a directory.");

        // Create directory path if it doesn't exist.
        var physicalDirectoryPath = Path.GetDirectoryName(physicalPath);
        Directory.CreateDirectory(physicalDirectoryPath);

        var fileInfo = new FileInfo(physicalPath);
        await using var outputStream = fileInfo.Create();
        await inputStream.CopyToAsync(outputStream);

        return path;
    }

    public Task<string> ReadAllText(string path)
    {
        var physicalPath = GetPhysicalPath(path);

        if (!File.Exists(physicalPath)) throw new Exception("File not exists.");
        return File.ReadAllTextAsync(physicalPath);
    }

    public Task WriteAllText(string path, string text)
    {
        var physicalPath = GetPhysicalPath(path);

        if (!File.Exists(physicalPath))
            File.Create(physicalPath).Close();

        return File.WriteAllTextAsync(physicalPath, text, Encoding.UTF8);
    }


    /// <summary>
    ///     Translates a relative path in the virtual file store to a physical path in the underlying file system.
    /// </summary>
    /// <param name="path">The relative path within the file store.</param>
    /// <returns></returns>
    /// <remarks>The resulting physical path is verified to be inside designated root file system path.</remarks>
    private string GetPhysicalPath(string path)
    {
        path = this.NormalizePath(path);

        var physicalPath = string.IsNullOrEmpty(path) ? _fileSystemPath : Path.Combine(_fileSystemPath, path);

        // Verify that the resulting path is inside the root file system path.
        var pathIsAllowed = Path.GetFullPath(physicalPath)
            .StartsWith(_fileSystemPath, StringComparison.OrdinalIgnoreCase);
        if (!pathIsAllowed)
            throw new Exception($"The path '{path}' resolves to a physical path outside the file system store root.");

        return physicalPath;
    }
}