using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using AutoIngest.Core;

namespace AutoIngest.Engine
{
    /// <summary>
    /// Result the UI returns when asked about an unknown device.
    /// </summary>
    public enum UnknownDeviceResolution
    {
        /// <summary>Register the device and import now (and from now on).</summary>
        RegisterAndImport,
        /// <summary>Import this one time only; do not persist to the registry.</summary>
        ImportOnce,
        /// <summary>Skip this device for the rest of the session.</summary>
        Ignore,
        /// <summary>User dismissed the prompt; treat like Ignore for this tick but ask again next session.</summary>
        Dismissed
    }

    /// <summary>
    /// Owns the polling thread. Every tick it asks each registered <see cref="IDeviceSource"/>
    /// for connected devices and, for each device that passes the <see cref="DeviceRegistry"/>
    /// filter (or that the user approves via <see cref="ResolveUnknownDevice"/>), runs it through
    /// the shared <see cref="DeviceImporter"/>. This is the direct successor to SDCardMonitor's
    /// old RunLoop: same 4-second cadence, same single-import-at-a-time lock, but now source-
    /// agnostic and gated by the opt-in registry.
    /// </summary>
    public class ImportCoordinator : IDisposable
    {
        const int CHECK_INTERVAL_MS = 4000;

        // Retention sweep cadence: once on startup, then at most this often while running.
        static readonly TimeSpan RETENTION_INTERVAL = TimeSpan.FromHours(24);
        // Delay the very first sweep so it doesn't race startup imports or disk activity.
        static readonly TimeSpan RETENTION_STARTUP_DELAY = TimeSpan.FromSeconds(30);

        readonly DeviceImporter _importer;
        readonly DeviceRegistry _registry;
        readonly List<IDeviceSource> _sources;
        readonly AppConfig _config;

        Thread? _thread;
        volatile bool _running;
        volatile bool _importing;

        // Devices we've already handled this session, keyed by stable DeviceIdentity.Id so the
        // same physical card isn't re-imported when it stays inserted, and survives letter changes.
        readonly HashSet<string> _handledThisSession = new();
        // Devices explicitly ignored by the user this session (not persisted).
        readonly HashSet<string> _ignoredThisSession = new();
        readonly object _lock = new();

        // Retention scheduling. _lastSweep starts at MinValue so the first loop tick after the
        // startup delay satisfies the interval check and fires one sweep; thereafter it's gated
        // by the 24h interval. NOTE: the on-demand sweep (RunRetentionSweepNow, from the Settings
        // UI) does NOT coordinate with this via a lock; a double-run is harmless because Sweep is
        // idempotent (the second pass finds the already-recycled folder gone). If Sweep ever
        // becomes non-idempotent, add a dedicated sweep lock here.
        DateTime _startTime = DateTime.UtcNow;
        DateTime _lastSweep = DateTime.MinValue;
        bool _accessTimeTrackingEnabled;

        public ImportCoordinator(DeviceImporter importer, DeviceRegistry registry, AppConfig config, IEnumerable<IDeviceSource> sources)
        {
            _importer = importer;
            _registry = registry;
            _config = config;
            _sources = sources.ToList();

            // Forward the importer's events through the coordinator's own, so the UI wires a
            // single set of handlers against the coordinator regardless of which source raised.
            // (ImportDone is owned and raised by the coordinator itself, based on Import's result.)
            _importer.DriveFound     += (a, b) => DriveFound?.Invoke(a, b);
            _importer.ImportProgress += (a, b, c) => ImportProgress?.Invoke(a, b, c);
            _importer.ImportError    += m => ImportError?.Invoke(m);
            _importer.LogMessage     += (m, c) => LogMessage?.Invoke(m, c);
        }

        /// <summary>
        /// Raised when a connected device isn't in the registry. The UI must resolve it
        /// synchronously and return the user's choice. Raised on the coordinator thread, so the
        /// handler must marshal to its own UI thread (e.g. Form.Invoke) before blocking on input.
        /// </summary>
        public event Func<DeviceIdentity, UnknownDeviceResolution>? ResolveUnknownDevice;

        // The coordinator surfaces its own event fields and forwards the importer's raises through
        // them. (C# events can only be invoked from the declaring type, so we can't simply re-expose
        // the importer's events via add/remove that tries to invoke them.)
        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;

        /// <summary>Raised after a retention sweep with (foldersRecycled, bytesReclaimed).</summary>
        public event Action<int, long>? RetentionSweepDone;

        public void Start()
        {
            _running = true;
            _startTime = DateTime.UtcNow;
            // Probe access-time tracking once at startup (cheap, used to pick the age signal).
            // Failures default to false, which means retention falls back to LastWriteTime.
            try { _accessTimeTrackingEnabled = AccessTimeTracker.IsEnabled(); }
            catch { _accessTimeTrackingEnabled = false; }

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

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

        /// <summary>Clears both the handled-this-session and ignored-this-session sets.</summary>
        public void ResetMemory()
        {
            lock (_lock)
            {
                _handledThisSession.Clear();
                _ignoredThisSession.Clear();
            }
        }

        /// <summary>The set of currently-connected devices across all sources (for the UI's
        /// "add currently connected device" feature).</summary>
        public List<DeviceIdentity> GetCurrentlyConnected()
        {
            var list = new List<DeviceIdentity>();
            foreach (var src in _sources)
            {
                try { list.AddRange(src.GetConnectedDevices()); }
                catch (Exception ex) { LogMessage?.Invoke($"{src.SourceName} scan failed: {ex.Message}", "#f85149"); }
            }
            return list;
        }

        /// <summary>
        /// Runs a retention sweep immediately, ignoring the 24h throttle. Called from the
        /// Settings dialog's "Run sweep now" button. Runs synchronously on the caller's thread
        /// (the UI thread) — sweeps are I/O-bound but fast for typical folder counts, and doing
        /// it synchronously lets the UI report the result. Returns (recycled, bytesReclaimed).
        /// </summary>
        public (int recycled, long bytesReclaimed) RunRetentionSweepNow()
        {
            return DoRetentionSweep(forceLog: true);
        }

        /// <summary>Whether NTFS access-time tracking is on (affects which timestamp retention
        /// uses). Probed once at Start; safe to re-query from the UI.</summary>
        public bool IsAccessTimeTrackingEnabled => _accessTimeTrackingEnabled;

        (int recycled, long bytesReclaimed) DoRetentionSweep(bool forceLog)
        {
            if (!_config.RetentionEnabled)
            {
                if (forceLog) LogMessage?.Invoke("Retention is off — sweep skipped.", "#8b949e");
                return (0, 0);
            }

            var picturesBase = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyPictures);
            int retentionDays = _config.RetentionDays;
            bool preferAccess = _accessTimeTrackingEnabled;

            if (forceLog)
                LogMessage?.Invoke(
                    $"Retention sweep starting (>{retentionDays} days, {(preferAccess ? "access time" : "import date")}): {picturesBase}",
                    "#d29922");

            try
            {
                var (recycled, reclaimed) = FolderRetentionPolicy.Sweep(
                    picturesBase, retentionDays, preferAccess,
                    (msg, color) => LogMessage?.Invoke(msg, color));

                RetentionSweepDone?.Invoke(recycled, reclaimed);
                return (recycled, reclaimed);
            }
            catch (Exception ex)
            {
                LogMessage?.Invoke($"Retention sweep failed: {ex.Message}", "#f85149");
                return (0, 0);
            }
        }

        void RunLoop()
        {
            var photosBase = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyPictures);
            LogMessage?.Invoke("Auto-import monitor active. Insert a registered SD card, USB drive, or phone.", "#58a6ff");
            LogMessage?.Invoke($"Destination: {photosBase}\\<today>\\", "#58a6ff");

            while (_running)
            {
                try { PollOnce(); }
                catch (Exception ex)
                {
                    LogMessage?.Invoke(ex.Message, "#f85149");
                    ImportError?.Invoke(ex.Message);
                    _importing = false;
                }

                TryScheduleRetention();

                Thread.Sleep(CHECK_INTERVAL_MS);
            }
        }

        /// <summary>
        /// One polling tick: snapshot connected devices, handle the first actionable one, prune
        /// disconnected devices from session memory. All under _lock so ResetMemory (UI thread)
        /// can't race with an in-flight tick. Flattened from the original nested loop so the
        /// per-device decision lives in its own single-level method.
        /// </summary>
        void PollOnce()
        {
            var connected = GetCurrentlyConnected();
            var connectedIds = new HashSet<string>(connected.Select(d => d.Id));

            lock (_lock)
            {
                if (_importing) return;

                foreach (var device in connected)
                {
                    if (_handledThisSession.Contains(device.Id)) continue;
                    if (_ignoredThisSession.Contains(device.Id)) continue;
                    if (HandleDevice(device)) break; // at most one import per tick
                }

                // Forget handled devices that have since disconnected so they re-import next plug.
                // Ignored devices stay ignored for the session — that's a user choice.
                _handledThisSession.ExceptWith(
                    _handledThisSession.Where(id => !connectedIds.Contains(id)).ToList());
            }
        }

        /// <summary>
        /// Resolves one connected device: import it if registered, else ask the UI and dispatch on
        /// its answer. Returns true if an import was kicked off (caller stops iterating for this
        /// tick — only one import at a time). Single-level: no nesting beyond the switch.
        /// </summary>
        bool HandleDevice(DeviceIdentity device)
        {
            // Registered → import directly, no prompt.
            if (_registry.IsRegistered(device.Id))
            {
                _handledThisSession.Add(device.Id);
                _importing = true;
                ImportDevice(device);
                return true;
            }

            // Unregistered → ask the UI synchronously (it marshals to the UI thread). If no handler
            // is wired, default to Ignore so a misconfigured host never auto-imports unknowns.
            var resolution = ResolveUnknownDevice?.Invoke(device) ?? UnknownDeviceResolution.Ignore;
            switch (resolution)
            {
                case UnknownDeviceResolution.RegisterAndImport:
                    _registry.Register(device.Id, device.DisplayName, device.Kind);
                    _registry.Save();
                    _handledThisSession.Add(device.Id);
                    _importing = true;
                    ImportDevice(device);
                    LogMessage?.Invoke($"Registered '{device.DisplayName}' ({device.Kind}).", "#3fb950");
                    return true;

                case UnknownDeviceResolution.ImportOnce:
                    _handledThisSession.Add(device.Id);
                    _importing = true;
                    ImportDevice(device);
                    return true;

                case UnknownDeviceResolution.Ignore:
                case UnknownDeviceResolution.Dismissed:
                default:
                    _ignoredThisSession.Add(device.Id);
                    LogMessage?.Invoke($"Ignoring '{device.DisplayName}' for this session.", "#8b949e");
                    return false;
            }
        }

        /// <summary>
        /// Retention sweep scheduler: one pass shortly after startup, then at most every 24h.
        /// Runs the sweep off the import lock on a background thread so a slow disk sweep never
        /// holds up the 4s import poll. The on-demand UI path uses RunRetentionSweepNow() instead.
        /// A theoretical double-run is harmless — Sweep is idempotent (a just-recycled folder is
        /// not returned by the next EnumerateDatedFolders).
        /// </summary>
        void TryScheduleRetention()
        {
            try
            {
                if (!_config.RetentionEnabled) return;
                var now = DateTime.UtcNow;
                bool startupElapsed = (now - _startTime) >= RETENTION_STARTUP_DELAY;
                bool intervalElapsed = (now - _lastSweep) >= RETENTION_INTERVAL;
                if (!startupElapsed || !intervalElapsed || _importing) return;

                _lastSweep = now;
                System.Threading.Tasks.Task.Run(() => DoRetentionSweep(forceLog: false));
            }
            catch (Exception ex)
            {
                LogMessage?.Invoke($"Retention scheduler error: {ex.Message}", "#f85149");
            }
        }

        void ImportDevice(DeviceIdentity device)
        {
            try
            {
                var result = _importer.Import(device);
                if (result.total > 0)
                    ImportDone?.Invoke(result.moved, result.converted, result.sizeStr, result.destFolder);
            }
            catch (Exception ex)
            {
                LogMessage?.Invoke($"Import failed for {device.DisplayName}: {ex.Message}", "#f85149");
                ImportError?.Invoke(ex.Message);
            }
            finally
            {
                // Release any per-import resources the provider holds (notably the MTP staging
                // temp directory, which must not leak across the 4s poll ticks).
                if (device.Photos is IDisposable disposable) disposable.Dispose();
            }
        }

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