using JobTrackerApi.Data;
using Microsoft.EntityFrameworkCore;

namespace JobTrackerApi.Services;

public sealed record AttachmentReconciliationResult(int Promoted, int Restored, int Purged, int Missing, int UnknownOrphans, int UnsafePaths, int Failures);

public interface IAttachmentStorage
{
    string CreateFinalPath(int jobId, string storedFileName);
    string StagePath(string finalPath);
    string DeletePath(string finalPath);
    bool IsManagedPath(string path);
    Task StageAsync(IFormFile file, string stagePath, CancellationToken cancellationToken);
    void Promote(string stagePath, string finalPath);
    void Quarantine(string finalPath, string deletePath);
    void Restore(string deletePath, string finalPath);
    void Purge(string path);
    Task<AttachmentReconciliationResult> ReconcileAsync(JobTrackerContext db, CancellationToken cancellationToken);
}

public sealed class AttachmentStorage : IAttachmentStorage
{
    private const string UploadSuffix = ".uploading";
    private const string DeleteSuffix = ".deleting";
    private readonly string _root;
    private readonly StringComparison _comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
    private readonly StringComparer _comparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;

    public AttachmentStorage(AppPaths paths)
    {
        _root = Path.GetFullPath(paths.AttachmentsRoot).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
    }

    public string CreateFinalPath(int jobId, string storedFileName)
    {
        var folder = Path.Combine(_root, jobId.ToString(System.Globalization.CultureInfo.InvariantCulture));
        Directory.CreateDirectory(folder);
        EnsureManagedPath(folder, allowMissingLeaf: false);
        return EnsureManagedPath(Path.Combine(folder, Path.GetFileName(storedFileName)), allowMissingLeaf: true);
    }

    public string StagePath(string finalPath) => EnsureManagedPath(finalPath, true) + UploadSuffix;
    public string DeletePath(string finalPath) => EnsureManagedPath(finalPath, true) + DeleteSuffix;

    public bool IsManagedPath(string path)
    {
        try
        {
            EnsureManagedPath(path, allowMissingLeaf: true);
            return true;
        }
        catch
        {
            return false;
        }
    }

    public async Task StageAsync(IFormFile file, string stagePath, CancellationToken cancellationToken)
    {
        var safeStage = EnsureManagedPath(stagePath, allowMissingLeaf: true);
        var created = false;
        try
        {
            await using var stream = new FileStream(safeStage, FileMode.CreateNew, FileAccess.Write, FileShare.None);
            created = true;
            await file.CopyToAsync(stream, cancellationToken);
            await stream.FlushAsync(cancellationToken);
        }
        catch
        {
            if (created)
                try { File.Delete(safeStage); } catch { }
            throw;
        }
    }

    public void Promote(string stagePath, string finalPath) =>
        File.Move(EnsureManagedPath(stagePath, false), EnsureManagedPath(finalPath, true), overwrite: false);

    public void Quarantine(string finalPath, string deletePath) =>
        File.Move(EnsureManagedPath(finalPath, false), EnsureManagedPath(deletePath, true), overwrite: false);

    public void Restore(string deletePath, string finalPath) =>
        File.Move(EnsureManagedPath(deletePath, false), EnsureManagedPath(finalPath, true), overwrite: false);

    public void Purge(string path)
    {
        var safePath = EnsureManagedPath(path, allowMissingLeaf: true);
        if (File.Exists(safePath)) File.Delete(safePath);
    }

    public async Task<AttachmentReconciliationResult> ReconcileAsync(JobTrackerContext db, CancellationToken cancellationToken)
    {
        var storedPaths = await db.Attachments.IgnoreQueryFilters().AsNoTracking()
            .Select(x => x.FilePath)
            .ToListAsync(cancellationToken);
        var known = new HashSet<string>(storedPaths.Where(IsManagedPath).Select(Path.GetFullPath), _comparer);
        var unsafePaths = storedPaths.Count(path => !IsManagedPath(path));
        var promoted = 0;
        var restored = 0;
        var purged = 0;
        var failures = 0;

        foreach (var staged in EnumerateStateFiles(UploadSuffix).ToList())
        {
            try
            {
                var finalPath = staged[..^UploadSuffix.Length];
                if (known.Contains(finalPath) && !File.Exists(finalPath))
                {
                    Promote(staged, finalPath);
                    promoted++;
                }
                else
                {
                    Purge(staged);
                    purged++;
                }
            }
            catch
            {
                failures++;
            }
        }

        foreach (var deleting in EnumerateStateFiles(DeleteSuffix).ToList())
        {
            try
            {
                var finalPath = deleting[..^DeleteSuffix.Length];
                if (known.Contains(finalPath) && !File.Exists(finalPath))
                {
                    Restore(deleting, finalPath);
                    restored++;
                }
                else
                {
                    Purge(deleting);
                    purged++;
                }
            }
            catch
            {
                failures++;
            }
        }

        var missing = known.Count(path => !File.Exists(path));
        var unknownOrphans = EnumerateFiles()
            .Count(path => !path.EndsWith(UploadSuffix, _comparison)
                && !path.EndsWith(DeleteSuffix, _comparison)
                && !known.Contains(path));
        return new AttachmentReconciliationResult(promoted, restored, purged, missing, unknownOrphans, unsafePaths, failures);
    }

    private IEnumerable<string> EnumerateStateFiles(string suffix) =>
        EnumerateFiles().Where(path => path.EndsWith(suffix, _comparison));

    private IEnumerable<string> EnumerateFiles()
    {
        if (!Directory.Exists(_root)) return Array.Empty<string>();
        return Directory.EnumerateFiles(_root, "*", new EnumerationOptions
        {
            RecurseSubdirectories = true,
            IgnoreInaccessible = true,
            AttributesToSkip = FileAttributes.ReparsePoint,
        }).Select(Path.GetFullPath);
    }

    private string EnsureManagedPath(string path, bool allowMissingLeaf)
    {
        if (string.IsNullOrWhiteSpace(path)) throw new InvalidOperationException("Attachment path is empty.");
        var fullPath = Path.GetFullPath(path);
        var rootPrefix = _root + Path.DirectorySeparatorChar;
        if (!fullPath.StartsWith(rootPrefix, _comparison)) throw new InvalidOperationException("Attachment path is outside the configured storage root.");

        var current = Directory.Exists(fullPath) ? fullPath : Path.GetDirectoryName(fullPath);
        while (!string.IsNullOrWhiteSpace(current) && !string.Equals(current, _root, _comparison))
        {
            if (Directory.Exists(current) && (File.GetAttributes(current) & FileAttributes.ReparsePoint) != 0)
                throw new InvalidOperationException("Attachment path crosses a symbolic link or junction.");
            current = Path.GetDirectoryName(current);
        }

        if (!allowMissingLeaf && !File.Exists(fullPath) && !Directory.Exists(fullPath))
            throw new FileNotFoundException("Attachment storage path does not exist.");
        if (File.Exists(fullPath) && (File.GetAttributes(fullPath) & FileAttributes.ReparsePoint) != 0)
            throw new InvalidOperationException("Attachment file is a symbolic link.");
        return fullPath;
    }
}
