using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Threading;

namespace AutoIngest.Engine
{
    public enum DriveType : uint
    {
        DRIVE_UNKNOWN = 0,
        DRIVE_NO_ROOT_DIR = 1,
        DRIVE_REMOVABLE = 2,
        DRIVE_FIXED = 3,
        DRIVE_REMOTE = 4,
        DRIVE_CDROM = 5,
        DRIVE_RAMDISK = 6
    }

    public static class Win32
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern DriveType GetDriveType(string lpRootPathName);

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern bool GetVolumeInformationW(
            string rootPathName,
            System.Text.StringBuilder volumeNameBuffer,
            int volumeNameSize,
            out uint volumeSerialNumber,
            out uint maximumComponentLength,
            out uint fileSystemFlags,
            System.Text.StringBuilder fileSystemNameBuffer,
            int fileSystemNameSize);
    }

    public class SDCardMonitor : IDisposable
    {
        const int CHECK_INTERVAL_MS = 4000;
        const int MAX_COLLISION_SUFFIX = 9999;

        static readonly HashSet<char> IGNORE_DRIVES = new() { 'C' };

        static readonly string[] SD_KEYWORDS = {
            "SD", "CARD", "CAMERA", "NO NAME", "UNTITLED",
            "CANON", "NIKON", "SONY", "FUJI", "GOPRO",
            "EOS", "LUMIX", "CYBERSHOT", "ALPHA", "PENTAX",
            "OLYMPUS", "LEICA", "HUAWEI", "SAMSUNG", "PIXEL",
            "BLACKMAGIC", "DJI", "INSTA360", "MEMORY", "FLASH"
        };

        static readonly string[] DCIM_PROBES = { "DCIM", "Pictures", "photos", "PICTURES" };

        static readonly HashSet<string> SKIP_FOLDERS = new(StringComparer.OrdinalIgnoreCase) {
            "System Volume Information", "$RECYCLE.BIN",
            "Windows", "Program Files", "Program Files (x86)",
            "ProgramData", "AppData", "Recovery"
        };

        static readonly HashSet<string> SKIP_PREFIXES = new() { ".", "$", "~" };

        public static readonly HashSet<string> IMAGE_EXTENSIONS = new(StringComparer.OrdinalIgnoreCase) {
            ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".webp",
            ".raw", ".cr2", ".cr3", ".nef", ".arw", ".dng", ".orf", ".rw2",
            ".gpr", ".pef", ".srw", ".srf", ".sr2", ".raf",
            ".heic", ".heif",
            ".ico", ".ppm", ".pgm", ".pbm", ".svg"
        };

        public event Action<string, string>? DriveFound;
        public event Action<int, int, string>? ImportProgress;
        public event Action<int, int, string, string>? ImportDone;
        public event Action<string>? ImportError;
        public event Action<string, string>? LogMessage;

        private Thread? _thread;
        private volatile bool _running;
        private volatile bool _importing;
        private readonly HashSet<string> _processedDrives = new();
        private readonly object _lock = new();
        private readonly ImageConverter _converter;
        private readonly int _jpgQuality;

        public SDCardMonitor(ImageConverter converter, int jpgQuality = 92)
        {
            _converter = converter;
            _jpgQuality = jpgQuality;
        }

        public void Start()
        {
            _running = true;
            _thread = new Thread(RunLoop) { IsBackground = true };
            _thread.Start();
        }

        public void Stop()
        {
            _running = false;
            _thread?.Join(5000);
        }

        public void ResetMemory()
        {
            lock (_lock) _processedDrives.Clear();
        }

        void RunLoop()
        {
            // Import into the user's Pictures library (~/Pictures), under a dated subfolder
            // for today's imports: Pictures/<yyyy-MM-dd>/.
            var photosBase = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);

            Log("Auto-import monitor active. Insert SD card or USB drive.", "#58a6ff");
            Log($"Destination: {photosBase}\\<today>\\", "#58a6ff");

            while (_running)
            {
                try
                {
                    lock (_lock)
                    {
                        if (!_importing)
                        {
                            foreach (var ch in GetDriveLetters())
                            {
                                string did = $"DRV:{ch}";
                                if (_processedDrives.Contains(did)) continue;

                                _importing = true;
                                _processedDrives.Add(did);

                                var result = ImportFromDrive(ch, photosBase);
                                if (result.total > 0)
                                    ImportDone?.Invoke(result.moved, result.converted, result.sizeStr, result.destFolder);
                                break;
                            }

                            var currentIds = new HashSet<string>(
                                GetDriveLetters().Select(c => $"DRV:{c}"));
                            _processedDrives.ExceptWith(currentIds);

                            _importing = false;
                        }
                    }
                }
                catch (Exception ex)
                {
                    Log(ex.Message, "#f85149");
                    ImportError?.Invoke(ex.Message);
                    _importing = false;
                }

                Thread.Sleep(CHECK_INTERVAL_MS);
            }
        }

        static IEnumerable<char> GetDriveLetters()
        {
            return Enumerable.Range(0, 26)
                .Select(i => (char)('A' + i))
                .Where(ch => !IGNORE_DRIVES.Contains(ch))
                .Where(ch =>
                {
                    string root = $"{ch}:\\";
                    if (!Directory.Exists(root)) return false;
                    try
                    {
                        var dt = Win32.GetDriveType(root);
                        if (dt == DriveType.DRIVE_REMOVABLE) return true;
                        if (dt == DriveType.DRIVE_FIXED) return LooksLikeSDCard(ch);
                    }
                    catch (UnauthorizedAccessException) { }
                    catch (IOException) { }
                    return false;
                });
        }

        static bool LooksLikeSDCard(char ch)
        {
            string root = $"{ch}:\\";

            if (DCIM_PROBES.Any(p => Directory.Exists(Path.Combine(root, p))))
                return true;

            try
            {
                var volName = new System.Text.StringBuilder(1024);
                var fsName = new System.Text.StringBuilder(1024);
                if (Win32.GetVolumeInformationW(
                    root, volName, 1024, out _, out _, out _, fsName, 1024))
                {
                    string label = volName.ToString().ToUpperInvariant();
                    return SD_KEYWORDS.Any(kw => label.Contains(kw));
                }
            }
            catch (UnauthorizedAccessException) { }
            catch (IOException) { }

            return false;
        }

        List<string> FindPhotos(char ch)
        {
            string root = $"{ch}:\\";
            var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            var dcimResults = DCIM_PROBES
                .Select(p => Path.Combine(root, p))
                .Where(Directory.Exists)
                .SelectMany(p => ScanFolder(p, 10))
                .Where(fp => seen.Add(fp))
                .ToList();
            if (dcimResults.Count > 0) return dcimResults;

            List<string> rootResults = new();
            try
            {
                rootResults = Directory.EnumerateFiles(root)
                    .Where(f => seen.Add(f) && IsImageFile(f))
                    .ToList();
            }
            catch (UnauthorizedAccessException) { }
            catch (IOException) { }
            if (rootResults.Count > 0) return rootResults;

            return ScanFolder(root, 3).Where(fp => seen.Add(fp)).ToList();
        }

        static List<string> ScanFolder(string folder, int maxDepth)
        {
            var results = new List<string>();
            try
            {
                ScanRecursive(folder, maxDepth, 0, results);
            }
            catch (UnauthorizedAccessException) { }
            catch (IOException) { }
            return results;
        }

        static void ScanRecursive(string dir, int maxDepth, int depth, List<string> results)
        {
            if (depth > maxDepth) return;

            string[] dirs;
            string[] files;
            try
            {
                dirs = Directory.GetDirectories(dir);
                files = Directory.GetFiles(dir);
            }
            catch (UnauthorizedAccessException) { return; }
            catch (IOException) { return; }

            dirs = dirs.Where(d =>
            {
                string name = Path.GetFileName(d);
                return !SKIP_FOLDERS.Contains(name) &&
                       !SKIP_PREFIXES.Any(p => name.StartsWith(p));
            }).ToArray();

            results.AddRange(files.Where(f =>
            {
                string name = Path.GetFileName(f);
                return !SKIP_PREFIXES.Any(p => name.StartsWith(p)) && IsImageFile(f);
            }));

            foreach (var d in dirs)
                ScanRecursive(d, maxDepth, depth + 1, results);
        }

        (int moved, int converted, int total, string sizeStr, string destFolder) ImportFromDrive(char ch, string photosBase)
        {
            var photos = FindPhotos(ch);
            if (photos.Count == 0)
                return (0, 0, 0, "", "");

            string today = DateTime.Now.ToString("yyyy-MM-dd");
            string destFolder = Path.Combine(photosBase, today);
            Directory.CreateDirectory(destFolder);

            DriveFound?.Invoke(ch.ToString(), $"{photos.Count} photos found on {ch}:\\");

            int moved = 0, converted = 0, skipped = 0;
            long totalBytes = 0;

            foreach (var (src, idx) in photos.Select((f, i) => (f, i)))
            {
                string filename = Path.GetFileName(src);
                ImportProgress?.Invoke(idx + 1, photos.Count, filename);

                try
                {
                    string ext = Path.GetExtension(src).ToLowerInvariant();

                    if (ext is ".jpg" or ".jpeg")
                    {
                        // Resolve the destination, comparing against a same-named file in
                        // TODAY'S folder only: same size + same hash => genuine dupe (skip);
                        // otherwise rename with a suffix and move.
                        string destPath = ResolveDestPath(destFolder, filename, src);
                        if (destPath == "")
                        {
                            skipped++;
                            Log($"Skipped (duplicate of file already imported today): {filename}", "#8b949e");
                            continue;
                        }

                        long srcLen = new FileInfo(src).Length;
                        File.Move(src, destPath);

                        // Verify the move landed correctly before counting it.
                        if (!File.Exists(destPath) || new FileInfo(destPath).Length != srcLen)
                            throw new IOException("move verification failed (size mismatch)");

                        moved++;
                        totalBytes += srcLen;
                        Log(string.Equals(destPath, Path.Combine(destFolder, filename), StringComparison.OrdinalIgnoreCase)
                            ? $"Moved: {filename}"
                            : $"Moved: {filename} -> {Path.GetFileName(destPath)} (name in use today)",
                            "#3fb950");
                    }
                    else
                    {
                        // Convert to JPG, then delete the original from the device. The output
                        // name is resolved against today's folder (two RAWs may share a stem;
                        // only an identical converted output is a real dupe).
                        string destName = Path.GetFileNameWithoutExtension(src) + ".jpg";
                        string destPath = ResolveDestPath(destFolder, destName, src);
                        if (destPath == "")
                        {
                            skipped++;
                            Log($"Skipped (duplicate output already imported today): {filename}", "#8b949e");
                            continue;
                        }

                        long srcLen = new FileInfo(src).Length;
                        _converter.ConvertToJpg(src, destPath, _jpgQuality);

                        // Verify destination exists with non-zero size before removing the source.
                        long size = new FileInfo(destPath).Length;
                        if (size <= 0)
                            throw new IOException("conversion produced an empty file");

                        File.Delete(src);
                        converted++;
                        totalBytes += size;
                        Log($"Converted: {filename} -> JPG ({size / 1024.0:F1} KB) [src {srcLen / 1024.0:F0} KB removed]", "#58a6ff");
                    }
                }
                catch (Exception ex)
                {
                    Log($"{filename}: {ex.Message}", "#f85149");
                    ImportError?.Invoke($"{filename}: {ex.Message}");
                }
            }

            if (skipped > 0)
                Log($"Skipped {skipped} duplicate file(s) already imported today.", "#8b949e");

            string sizeStr = FormatSize(totalBytes);
            return (moved, converted, moved + converted, sizeStr, destFolder);
        }

        /// <summary>
        /// Resolves a destination path inside <paramref name="destFolder"/> for a source
        /// file that wants to be named <paramref name="desiredName"/>.
        ///
        /// Dupe rules (scoped to TODAY'S folder only — we are not a cross-day dedupe tool):
        ///   - No existing file with the desired name  -> use the desired name.
        ///   - Existing file, different size           -> not a dupe; rename with a suffix.
        ///   - Existing file, same size, same hash     -> genuine duplicate; return "" (skip).
        ///   - Existing file, same size, different hash-> not a dupe; rename with a suffix.
        ///
        /// Returns the resolved destination path, or "" if the source is a byte-for-byte
        /// duplicate of what's already there and should be skipped.
        /// </summary>
        static string ResolveDestPath(string destFolder, string desiredName, string sourceFile)
        {
            string desired = Path.Combine(destFolder, desiredName);
            if (!File.Exists(desired))
                return desired;

            // Name collision in today's folder. Compare sizes first (cheap) before hashing.
            long srcLen = new FileInfo(sourceFile).Length;
            long dstLen = new FileInfo(desired).Length;

            if (srcLen != dstLen)
                return MakeUniquePath(destFolder, desiredName);

            // Same size: hash both to decide. Same hash => genuine dupe (skip).
            if (FilesEqual(sourceFile, desired))
                return "";

            // Same size, different content: rename with a suffix.
            return MakeUniquePath(destFolder, desiredName);
        }

        /// <summary>True if the two files are byte-for-byte identical (SHA256 match).</summary>
        static bool FilesEqual(string a, string b)
        {
            try
            {
                using var ha = SHA256.Create();
                using var hb = SHA256.Create();
                using var fa = File.OpenRead(a);
                using var fb = File.OpenRead(b);
                return ha.ComputeHash(fa).SequenceEqual(hb.ComputeHash(fb));
            }
            catch (UnauthorizedAccessException) { return false; }
            catch (IOException) { return false; }
        }

        /// <summary>
        /// Returns a non-colliding path in <paramref name="folder"/> by appending
        /// _1, _2, ... to the stem until a free name is found.
        /// </summary>
        static string MakeUniquePath(string folder, string desiredName)
        {
            string path = Path.Combine(folder, desiredName);
            if (!File.Exists(path)) return path;

            string stem = Path.GetFileNameWithoutExtension(desiredName);
            string ext = Path.GetExtension(desiredName);

            foreach (int n in Enumerable.Range(1, MAX_COLLISION_SUFFIX))
            {
                path = Path.Combine(folder, $"{stem}_{n}{ext}");
                if (!File.Exists(path)) return path;
            }

            return Path.Combine(folder, $"{stem}_{Guid.NewGuid():N}{ext}");
        }

        static bool IsImageFile(string path) => IMAGE_EXTENSIONS.Contains(Path.GetExtension(path));

        static string FormatSize(long bytes)
        {
            if (bytes < 1024) return $"{bytes} B";
            if (bytes < 1048576) return $"{bytes / 1024.0:F1} KB";
            return $"{bytes / 1048576.0:F1} MB";
        }

        void Log(string msg, string color) => LogMessage?.Invoke(msg, color);

        public void Dispose()
        {
            Stop();
            GC.SuppressFinalize(this);
        }
    }
}
