using System;
using System.IO;

namespace SharpCompress.Common;

internal static class DirectoryManagement
{
    internal const string CreateDirectoryOutsideDestinationMessage =
        "Entry is trying to create a directory outside of the destination directory.";
    internal const string WriteFileOutsideDestinationMessage =
        "Entry is trying to write a file outside of the destination directory.";
    internal const string LinkTargetOutsideDestinationMessage =
        "Entry is trying to create a symbolic link whose target is outside of the destination directory.";
    internal const string ReparsePointInDestinationMessage =
        "Entry is trying to extract through a symbolic link or reparse point.";

    internal static string GetFullDestinationDirectoryPath(string destinationDirectory)
    {
        var fullDestinationDirectoryPath = Path.GetFullPath(destinationDirectory);

        // Keep the trailing separator so prefix checks cannot match sibling directories.
        if (
            !IsDirectorySeparator(
                fullDestinationDirectoryPath[fullDestinationDirectoryPath.Length - 1]
            )
        )
        {
            fullDestinationDirectoryPath += Path.DirectorySeparatorChar;
        }

        if (!Directory.Exists(fullDestinationDirectoryPath))
        {
            throw new ExtractionException(
                $"Directory does not exist to extract to: {fullDestinationDirectoryPath}"
            );
        }

        return fullDestinationDirectoryPath;
    }

    internal static void EnsurePathInDestinationDirectory(
        string destinationPath,
        string fullDestinationDirectoryPath,
        string exceptionMessage
    )
    {
        if (destinationPath.StartsWith(fullDestinationDirectoryPath, Utility.PathComparison))
        {
            return;
        }

        if (
            string.Equals(
                destinationPath,
                TrimTrailingDirectorySeparators(fullDestinationDirectoryPath),
                Utility.PathComparison
            )
        )
        {
            return;
        }

        throw new ExtractionException(exceptionMessage);
    }

    internal static void EnsureNoReparsePointInDestinationDirectory(
        string destinationPath,
        string fullDestinationDirectoryPath
    )
    {
        var destinationDirectoryPath = TrimTrailingDirectorySeparators(
            fullDestinationDirectoryPath
        );
        EnsurePathIsNotReparsePoint(destinationDirectoryPath);

        if (string.Equals(destinationPath, destinationDirectoryPath, Utility.PathComparison))
        {
            return;
        }

        var relativeDestinationPath = destinationPath.Substring(
            fullDestinationDirectoryPath.Length
        );
        var path = destinationDirectoryPath;

        foreach (
            var pathPart in relativeDestinationPath.Split(
                new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar },
                StringSplitOptions.RemoveEmptyEntries
            )
        )
        {
            path = Path.Combine(path, pathPart);

            if (!PathExistsAndIsNotReparsePoint(path))
            {
                return;
            }
        }
    }

    internal static void CreateDirectory(
        string destinationPath,
        string fullDestinationDirectoryPath
    )
    {
        EnsureNoReparsePointInDestinationDirectory(destinationPath, fullDestinationDirectoryPath);

        if (!Directory.Exists(destinationPath))
        {
            Directory.CreateDirectory(destinationPath);
        }

        EnsureNoReparsePointInDestinationDirectory(destinationPath, fullDestinationDirectoryPath);
    }

    internal static void EnsureLinkTargetInDestinationDirectory(
        string destinationFileName,
        string linkTarget,
        string fullDestinationDirectoryPath
    )
    {
        var destinationDirectory = Path.GetDirectoryName(destinationFileName)
            .NotNull("Destination directory is null");
        var fullLinkTargetPath = Path.GetFullPath(Path.Combine(destinationDirectory, linkTarget));

        EnsurePathInDestinationDirectory(
            fullLinkTargetPath,
            fullDestinationDirectoryPath,
            LinkTargetOutsideDestinationMessage
        );
    }

    internal static void EnsurePathIsNotReparsePoint(string path)
    {
        var destinationDirectory = Path.GetDirectoryName(path);
        if (destinationDirectory is not null)
        {
            PathExistsAndIsNotReparsePoint(destinationDirectory);
        }
        PathExistsAndIsNotReparsePoint(path);
    }

    private static bool PathExistsAndIsNotReparsePoint(string path)
    {
        try
        {
            if ((File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0)
            {
                throw new ExtractionException(ReparsePointInDestinationMessage);
            }

            return true;
        }
        catch (FileNotFoundException)
        {
            return false;
        }
        catch (DirectoryNotFoundException)
        {
            return false;
        }
        catch (UnauthorizedAccessException exception)
        {
            throw new ExtractionException(
                "Unable to verify the extraction path for symbolic links or reparse points.",
                exception
            );
        }
        catch (IOException exception)
        {
            throw new ExtractionException(
                "Unable to verify the extraction path for symbolic links or reparse points.",
                exception
            );
        }
    }

    private static bool IsDirectorySeparator(char value) =>
        value == Path.DirectorySeparatorChar || value == Path.AltDirectorySeparatorChar;

    private static string TrimTrailingDirectorySeparators(string path)
    {
        var root = Path.GetPathRoot(path);
        var rootLength = root?.Length ?? 0;
        var end = path.Length;

        while (end > rootLength && IsDirectorySeparator(path[end - 1]))
        {
            end--;
        }

        return end == path.Length ? path : path.Substring(0, end);
    }
}
