using JobTrackerApi.Data;
using Microsoft.EntityFrameworkCore;

namespace JobTrackerApi.Services;

public sealed record AccountOwnedFile(string Category, string ExportPath, string? SourcePath, byte[]? InlineBytes);
public sealed record AccountOwnedFileInventoryResult(IReadOnlyList<AccountOwnedFile> Files, IReadOnlyList<string> Warnings);

public sealed class AccountOwnedFileInventory(JobTrackerContext db, AppPaths paths, IAttachmentStorage attachmentStorage)
{
    public async Task<AccountOwnedFileInventoryResult> BuildAsync(string ownerUserId, CancellationToken cancellationToken, bool includeAccountExports = false)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(ownerUserId);
        var files = new List<AccountOwnedFile>();
        var warnings = new List<string>();
        var applicationIds = await db.JobApplications.IgnoreQueryFilters().AsNoTracking()
            .Where(item => item.OwnerUserId == ownerUserId)
            .Select(item => item.Id)
            .ToListAsync(cancellationToken);
        var attachments = await db.Attachments.IgnoreQueryFilters().AsNoTracking()
            .Where(item => applicationIds.Contains(item.JobApplicationId))
            .OrderBy(item => item.Id)
            .ToListAsync(cancellationToken);
        foreach (var attachment in attachments)
        {
            AddPath(files, warnings, attachment.FilePath, $"files/attachments/{attachment.Id}/{SafeSegment(attachment.FileName)}", "attachment", attachmentStorage.IsManagedPath);
        }

        var artifacts = await db.CvUploadArtifacts.IgnoreQueryFilters().AsNoTracking()
            .Where(item => item.OwnerUserId == ownerUserId)
            .OrderBy(item => item.Id)
            .ToListAsync(cancellationToken);
        foreach (var artifact in artifacts)
        {
            AddPath(files, warnings, artifact.StoragePath, $"files/cv-artifacts/{artifact.Id}/{SafeSegment(artifact.OriginalFileName)}", "cv-artifact", candidate => IsManagedPath(paths.CvArtifactsRoot, candidate));
        }

        var avatar = await db.Users.AsNoTracking().Where(item => item.Id == ownerUserId).Select(item => item.AvatarImageDataUrl).FirstOrDefaultAsync(cancellationToken);
        AddAvatar(files, warnings, ownerUserId, avatar);
        AddDirectory(files, warnings, paths.GetOwnerCvExportsRoot(ownerUserId), "files/generated-cv", "generated-cv");
        AddDirectory(files, warnings, paths.GetOwnerDailyExportsRoot(null, ownerUserId), "files/daily-exports", "daily-export");
        if (includeAccountExports)
            AddDirectory(files, warnings, paths.GetOwnerAccountExportsRoot(ownerUserId), "files/account-exports", "account-export");
        return new AccountOwnedFileInventoryResult(files, warnings);
    }

    private void AddAvatar(ICollection<AccountOwnedFile> files, ICollection<string> warnings, string ownerUserId, string? storedAvatar)
    {
        if (string.IsNullOrWhiteSpace(storedAvatar)) return;
        if (storedAvatar.StartsWith("file:", StringComparison.Ordinal))
        {
            var path = storedAvatar[5..];
            var root = Path.Combine(paths.DataRoot, "Avatars", AppPaths.GetOwnerStorageKey(ownerUserId));
            AddPath(files, warnings, path, $"files/avatar/{SafeSegment(Path.GetFileName(path))}", "avatar", candidate => IsManagedPath(root, candidate));
            return;
        }
        if (storedAvatar.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
        {
            var comma = storedAvatar.IndexOf(',');
            if (comma > 0 && storedAvatar[..comma].Contains(";base64", StringComparison.OrdinalIgnoreCase))
            {
                try
                {
                    files.Add(new AccountOwnedFile("avatar", "files/avatar/avatar", null, Convert.FromBase64String(storedAvatar[(comma + 1)..])));
                    return;
                }
                catch (FormatException) { }
            }
        }
        warnings.Add("The profile avatar was stored in an unsupported format and could not be included.");
    }

    private static void AddPath(ICollection<AccountOwnedFile> files, ICollection<string> warnings, string sourcePath, string exportPath, string category, Func<string, bool> isManaged)
    {
        if (!isManaged(sourcePath))
        {
            warnings.Add($"Excluded unsafe {category} path for {exportPath}.");
            return;
        }
        if (!File.Exists(sourcePath))
        {
            warnings.Add($"Owned {category} file was unavailable: {exportPath}.");
            return;
        }
        files.Add(new AccountOwnedFile(category, exportPath, Path.GetFullPath(sourcePath), null));
    }

    private static void AddDirectory(ICollection<AccountOwnedFile> files, ICollection<string> warnings, string root, string exportRoot, string category)
    {
        if (!Directory.Exists(root)) return;
        foreach (var path in Directory.EnumerateFiles(root, "*", new EnumerationOptions
                 {
                     RecurseSubdirectories = true,
                     IgnoreInaccessible = true,
                     AttributesToSkip = FileAttributes.ReparsePoint,
                 }))
        {
            var relative = Path.GetRelativePath(root, path);
            if (relative.StartsWith("..", StringComparison.Ordinal) || !IsManagedPath(root, path))
            {
                warnings.Add($"Excluded unsafe {category} path.");
                continue;
            }
            var exportPath = $"{exportRoot}/{string.Join('/', relative.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Select(SafeSegment))}";
            files.Add(new AccountOwnedFile(category, exportPath, Path.GetFullPath(path), null));
        }
    }

    public static bool IsManagedPath(string root, string path)
    {
        if (string.IsNullOrWhiteSpace(path)) return false;
        try
        {
            var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
            var fullRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
            var fullPath = Path.GetFullPath(path);
            if (!fullPath.StartsWith(fullRoot + Path.DirectorySeparatorChar, comparison)) return false;
            var current = Path.GetDirectoryName(fullPath);
            while (!string.IsNullOrWhiteSpace(current) && !string.Equals(current, fullRoot, comparison))
            {
                if (Directory.Exists(current) && (File.GetAttributes(current) & FileAttributes.ReparsePoint) != 0) return false;
                current = Path.GetDirectoryName(current);
            }
            return !File.Exists(fullPath) || (File.GetAttributes(fullPath) & FileAttributes.ReparsePoint) == 0;
        }
        catch
        {
            return false;
        }
    }

    private static string SafeSegment(string? value)
    {
        var candidate = Path.GetFileName(value ?? string.Empty).Trim();
        foreach (var invalid in Path.GetInvalidFileNameChars()) candidate = candidate.Replace(invalid, '_');
        if (candidate.Length > 120) candidate = candidate[..120];
        return string.IsNullOrWhiteSpace(candidate) || candidate is "." or ".." ? "file" : candidate;
    }
}
