using Butter.Types;
using Lactose.Models;
using Lactose.Repositories;
using System.Data;
using System.Diagnostics;
using static System.String;

namespace Lactose.Services;

public class FileSystemScannerService(
    ILogger<FileSystemScannerService> logger,
    IAssetRepository assetRepository,
    IFolderRepository folderRepository,
    ISettingsRepository settingsRepository
) : BackgroundService {
    public bool     IsRunning    { get; private set; } = false;
    public bool     IsInitialized { get; private set; } = false;
    public TimeSpan   ScanInterval { get; private set; }
    List<Folder>        folders = [];
    readonly List<Task> tasks   = [];

    // ReSharper disable ArrangeObjectCreationWhenTypeEvident
    static readonly MimeTypeMap[] ImageMime = [
        new("image/avif", [".avif", ".avifs"]),
        new("image/bmp}", [".bmp"]),
        new("image/cgm", [".cgm"]),
        new("image/g3fax", [".g3"]),
        new("image/gif", [".gif"]),
        new("image/heic", [".heif", ".heic"]),
        new("image/ief", [".ief"]),
        new("image/jpeg", [".jpe", ".jpeg", ".jpg", ".pjpg", ".jfif", ".jfif-tbnl", ".jif"]),
        new("image/pjpeg", [".jpe", ".jpeg", ".jpg", ".pjpg", ".jfi", ".jfif", ".jfif-tbnl", ".jif"]),
        new("image/png", [".png"]),
        new("image/prs.btif", [".btif"]),
        new("image/svg+xml", [".svg", ".svgz"]),
        new("image/tiff", [".tif", ".tiff"]),
        new("image/vnd.adobe.photoshop", [".psd"]),
        new("image/vnd.djvu", [".djv", ".djvu"]),
        new("image/vnd.dwg", [".dwg"]),
        new("image/vnd.dxf", [".dxf"]),
        new("image/vnd.fastbidsheet", [".fbs"]),
        new("image/vnd.fpx", [".fpx"]),
        new("image/vnd.fst", [".fst"]),
        new("image/vnd.fujixerox.edmics-mmr", [".mmr"]),
        new("image/vnd.fujixerox.edmics-rlc", [".rlc"]),
        new("image/vnd.ms-modi", [".mdi"]),
        new("image/vnd.net-fpx", [".npx"]),
        new("image/vnd.wap.wbmp", [".wbmp"]),
        new("image/vnd.xiff", [".xif"]),
        new("image/webp", [".webp"]),
        new("image/x-adobe-dng", [".dng"]),
        new("image/x-canon-cr2", [".cr2"]),
        new("image/x-canon-crw", [".crw"]),
        new("image/x-cmu-raster", [".ras"]),
        new("image/x-cmx", [".cmx"]),
        new("image/x-epson-erf", [".erf"]),
        new("image/x-freehand", [".fh", ".fh4", ".fh5", ".fh7", ".fhc"]),
        new("image/x-fuji-raf", [".raf"]),
        new("image/x-icns", [".icns"]),
        new("image/x-icon", [".ico"]),
        new("image/x-kodak-dcr", [".dcr"]),
        new("image/x-kodak-k25", [".k25"]),
        new("image/x-kodak-kdc", [".kdc"]),
        new("image/x-minolta-mrw", [".mrw"]),
        new("image/x-nikon-nef", [".nef"]),
        new("image/x-olympus-orf", [".orf"]),
        new("image/x-panasonic-raw", [".raw", ".rw2", ".rwl"]),
        new("image/x-pcx", [".pcx"]),
        new("image/x-pentax-pef", [".pef", ".ptx"]),
        new("image/x-pict", [".pct", ".pic"]),
        new("image/x-portable-anymap", [".pnm"]),
        new("image/x-portable-bitmap", [".pbm"]),
        new("image/x-portable-graymap", [".pgm"]),
        new("image/x-portable-pixmap", [".ppm"]),
        new("image/x-rgb", [".rgb"]),
        new("image/x-sigma-x3f", [".x3f"]),
        new("image/x-sony-arw", [".arw"]),
        new("image/x-sony-sr2", [".sr2"]),
        new("image/x-sony-srf", [".srf"]),
        new("image/x-xbitmap", [".xbm"]),
        new("image/x-xpixmap", [".xpm"]),
        new("image/x-xwindowdump", [".xwd"])
    ];

    static readonly MimeTypeMap[] VideoMime = [
        new("video/3gpp", [".3gp"]),
        new("video/3gpp2", [".3g2"]),
        new("video/h261", [".h261"]),
        new("video/h263", [".h263"]),
        new("video/h264", [".h264"]),
        new("video/jpeg", [".jpgv"]),
        new("video/jpm", [".jpgm, .jpm"]),
        new("video/mj2", [".mj2, .mjp2"]),
        new("video/mp2t", [".ts"]),
        new("video/mp4", [".mp4, .mp4v, .mpg4"]),
        new("video/mpeg", [".m1v, .m2v, .mpa, .mpe, .mpeg, .mpg"]),
        new("video/ogg", [".ogv"]),
        new("video/quicktime", [".mov", ".qt"]),
        new("video/vnd.fvt", [".fvt"]),
        new("video/vnd.mpegurl", [".m4u", ".mxu"]),
        new("video/vnd.ms-playready.media.pyv", [".pyv"]),
        new("video/vnd.vivo", [".viv"]),
        new("video/webm", [".webm"]),
        new("video/x-f4v", [".f4v"]),
        new("video/x-fli", [".fli"]),
        new("video/x-flv", [".flv"]),
        new("video/x-m4v", [".m4v"]),
        new("video/x-matroska", [".mkv"]),
        new("video/x-ms-asf", [".asf", ".asx"]),
        new("video/x-ms-wm", [".wm"]),
        new("video/x-ms-wmv", [".wmv"]),
        new("video/x-ms-wmx", [".wmx"]),
        new("video/x-ms-wvx", [".wvx"]),
        new("video/x-msvideo", [".avi"]),
        new("video/x-sgi-movie", [".movie"])
    ];

    protected override Task ExecuteAsync(CancellationToken stoppingToken) {
        if(settingsRepository.Get(Settings.FolderScanEnabled.AsString())?.Value == "false") {
            logger.LogInformation("Folder scanning is disabled. Service will not start.");
            return Task.CompletedTask;
        }
        
        var service = new Task(() => ServiceLogic(stoppingToken));
        service.Start();
        return service;
    }

    //TODO: This really needs a decent name.
    void ServiceLogic(CancellationToken stoppingToken) {
        PeriodicTimer scanTimer;
        logger.LogInformation("Filesystem Scan Service starting...");
        
        var value = settingsRepository.Get(Settings.FolderScanInterval.AsString())?.Value;
        if (value != null && !int.TryParse(value, out var interval)) {
            logger.LogWarning("Invalid scan interval setting. Defaulting to 30 minutes.");
            interval = 30; // Default to 30 minutes if parsing fails
        } else interval = int.Parse(value!);

        ScanInterval = TimeSpan.FromMinutes(interval);
        logger.LogInformation($"Service will scan every {ScanInterval.TotalMinutes} minutes.");
        scanTimer    = new (ScanInterval);
        IsInitialized = true;
        
        while (!stoppingToken.IsCancellationRequested) {
            logger.LogInformation("Retrieving folder paths to scan...");
            folders = folderRepository.GetAll().ToList();
            // Wait for all tasks to complete before starting a new scan
            if(tasks.Count > 0) {
                logger.LogInformation("Waiting for {count} tasks to complete...", tasks.Count);
                Task.WaitAll(tasks.ToArray(), stoppingToken);
                IsRunning = false;
            }
            
            if (folders.Count != 0) {
                logger.LogInformation("Found {count} folders to scan.", folders.Count);
                logger.LogInformation("Starting Tasks for scanning folders...");
                // Launch a folder scan for each folder, passing the stopping token to each task
                folders.ForEach(folder => { tasks.Add(Task.Run(() => ScanFolder(folder.BasePath), stoppingToken)); });
                IsRunning = true;
            } else {
                logger.LogWarning("No folders to scan. Waiting for next scan interval...");
                scanTimer.WaitForNextTickAsync(stoppingToken);
            }
        }
    }

    void ScanFolder(string path) {
        List<string> filePaths   = [];
        List<string> folderPaths = [];

        // Get all files and folders in the folder
        try {
            filePaths.AddRange(Directory.GetFiles(path));
            folderPaths.AddRange(Directory.GetDirectories(path));
        } catch (Exception e) { logger.LogError(e, "Error scanning folder {folder}", path); }

        // Launch a folder scan for each subfolder
        folderPaths.ForEach(folderPath => { tasks.Add(Task.Run(() => ScanFolder(folderPath))); });

        // Process all files
        filePaths.ForEach(
            filePath => {
                // Check if the file is already in the database
                if (assetRepository.FindByPath(filePath) != null) return;

                // Get the file info
                var finfo = new FileInfo(filePath);

                // Determine the type of the file
                string      ext  = finfo.Extension.ToLower();
                EAssetType? type;

                // ReSharper disable ArrangeObjectCreationWhenTypeNotEvident
                var mimeImg = ImageMime.FirstOrDefault(mime => mime.Extensions.Contains(ext), new(Empty, []));
                var mimeVid = VideoMime.FirstOrDefault(mime => mime.Extensions.Contains(ext), new(Empty, []));

                if (mimeImg.MimeType != Empty) type = EAssetType.Image;
                else if (mimeVid.MimeType != Empty) type = EAssetType.Video;
                else return;

                // Create a new asset
                var asset = new Asset() {
                    OriginalPath = finfo.FullName,
                    OriginalFilename = finfo.Name,
                    Type = type.Value,
                    IsPubliclyShared = false,
                    CreatedAt = finfo.CreationTime,
                    UpdatedAt = finfo.LastWriteTime,
                    MimeType = type.Value switch {
                        EAssetType.Image => mimeImg.MimeType,
                        EAssetType.Video => mimeVid.MimeType,
                        _                => throw new ArgumentOutOfRangeException()
                    },
                    FileSize = finfo.Length,
                    Hash = []
                };
                
                //TODO: check if the file is already in the database
                // Add the asset to the database
                try { assetRepository.Insert(asset); }
                catch (DuplicateNameException e) {
                    logger.LogError(e, $"Duplicate asset name \"{finfo.FullName}\", skipped."); }
            }
        );
    }
}

public struct MimeTypeMap(string mimeType, string[] extensions) {
    public string   MimeType   { get; set; } = mimeType;
    public string[] Extensions { get; set; } = extensions;
}
