using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.VisualBasic.FileIO;

namespace AutoIngest.Engine
{
    /// <summary>
    /// Photo-folder retention: sends dated import folders
    /// (<c>~/Pictures/YYYY-MM-DD/</c>) that are older than the configured threshold to the
    /// Recycle Bin, EXCEPT when they've been accessed recently. The filesystem timestamps are
    /// the access record — no separate skip-list is maintained, because any access resets the
    /// clock automatically and the next sweep re-evaluates from scratch.
    ///
    /// "Age" of a folder is the most recent activity across all of its files:
    ///   - If access-time tracking is on  → max(LastAccessTime) over files
    ///   - Otherwise (Windows default)    → max(LastWriteTime) over files (import/creation time)
    /// Using the max means "any file in the folder was touched → the whole folder counts as
    /// recently used," matching the spec's "skip the whole folder" semantics.
    ///
    /// Safety rails (never deleted):
    ///   - Today's folder.
    ///   - Any folder whose name isn't a parseable YYYY-MM-DD date (e.g. _Archive, user folders).
    ///   - Any folder newer than the threshold (by definition).
    /// Everything goes to the Recycle Bin, so a mistaken setting is recoverable.
    /// </summary>
    public static class FolderRetentionPolicy
    {
        /// <summary>
        /// Enumerates AutoIngest's own dated folders under <paramref name="picturesBase"/>,
        /// computing per-folder access time and size. Folders whose names don't parse as
        /// YYYY-MM-DD are skipped (they're not ours to manage).
        /// </summary>
        public static IEnumerable<DatedFolder> EnumerateDatedFolders(string picturesBase, bool preferAccessTime)
        {
            string[] dirs;
            try { dirs = Directory.GetDirectories(picturesBase); }
            catch (UnauthorizedAccessException) { yield break; }
            catch (IOException) { yield break; }

            foreach (var dir in dirs)
            {
                string name = Path.GetFileName(dir);
                if (!DateTime.TryParseExact(name, "yyyy-MM-dd",
                        CultureInfo.InvariantCulture, DateTimeStyles.None, out var folderDate))
                    continue;

                var (accessTime, fileCount, sizeBytes) = SummarizeFolder(dir, preferAccessTime);
                if (fileCount == 0) continue; // empty folder — leave it alone

                yield return new DatedFolder
                {
                    Path = dir,
                    FolderDate = folderDate,
                    AccessTime = accessTime,
                    FileCount = fileCount,
                    SizeBytes = sizeBytes
                };
            }
        }

        /// <summary>
        /// Runs one retention pass. Returns the count of folders recycled and total bytes
        /// reclaimed. Every decision (recycle or skip) is reported via <paramref name="log"/>
        /// as (message, hexColor) pairs so the UI log shows the decision trail. Never throws —
        /// any per-folder error is logged and the sweep continues.
        /// </summary>
        public static (int recycled, long bytesReclaimed) Sweep(
            string picturesBase,
            int retentionDays,
            bool preferAccessTime,
            Action<string, string> log)
        {
            if (retentionDays <= 0)
            {
                log("Retention sweep skipped: threshold is zero/negative.", "#8b949e");
                return (0, 0);
            }

            string todayName = DateTime.Now.ToString("yyyy-MM-dd");
            var now = DateTime.Now;
            // Freshness guard: never recycle a folder whose most recent activity is within this
            // window. This is a safety net INDEPENDENT of the today-folder-name check — it catches
            // midnight rollover (an import that started at 23:58 writes to yesterday's folder,
            // which wouldn't match today's name but is clearly still active) and clock skew. 10
            // minutes is far shorter than any retention threshold but long enough to cover any
            // in-flight import at the moment the sweep fires.
            var freshnessWindow = TimeSpan.FromMinutes(10);
            int recycled = 0;
            long reclaimed = 0;

            foreach (var folder in EnumerateDatedFolders(picturesBase, preferAccessTime))
            {
                // Never touch today's folder by name — it's actively being imported into.
                if (Path.GetFileName(folder.Path) == todayName)
                {
                    log($"Skip (today):       {Path.GetFileName(folder.Path)}", "#8b949e");
                    continue;
                }

                var age = now - folder.AccessTime;

                // Freshness safety net: a folder written within the last few minutes is active
                // regardless of its name or nominal age. Protects rollover/skew edge cases.
                if (age < freshnessWindow)
                {
                    log($"Skip (active):      {Path.GetFileName(folder.Path)} — written {FormatAge(age)} ago (freshness guard)",
                        "#8b949e");
                    continue;
                }

                if (age.TotalDays < retentionDays)
                {
                    log($"Skip (in use):      {Path.GetFileName(folder.Path)} — last activity {FormatAge(age)} ago",
                        "#8b949e");
                    continue;
                }

                // Eligible. Log the intent first so the trail exists even if the recycle call
                // then fails for some reason.
                log($"Recycling:          {Path.GetFileName(folder.Path)} — {folder.FileCount} files, {FileSize.Format(folder.SizeBytes)} (last activity {FormatAge(age)} ago)",
                    "#d29922");

                try
                {
                    // SendToRecycleBin is the recoverable path. OnlyErrorDialogs means the user
                    // only sees a dialog if something genuinely goes wrong (no progress noise).
                    FileSystem.DeleteDirectory(folder.Path,
                        UIOption.OnlyErrorDialogs,
                        RecycleOption.SendToRecycleBin);
                    recycled++;
                    reclaimed += folder.SizeBytes;
                }
                catch (OperationCanceledException) { log($"  canceled by user: {Path.GetFileName(folder.Path)}", "#f85149"); }
                catch (Exception ex) { log($"  failed: {ex.Message}", "#f85149"); }
            }

            if (recycled > 0)
                log($"Retention sweep complete: recycled {recycled} folder(s), reclaimed {FileSize.Format(reclaimed)}.",
                    "#3fb950");
            else
                log("Retention sweep complete: nothing eligible.", "#58a6ff");

            return (recycled, reclaimed);
        }

        /// <summary>
        /// Computes (latest activity time, file count, total bytes) for a folder. When
        /// <paramref name="preferAccessTime"/> is true we take the max of LastAccessTime across
        /// files; otherwise (or if access times look stale/zero) we fall back to LastWriteTime.
        /// We also take the max of the two as a defensive measure so a folder is never treated
        /// as older than its most recent write just because access tracking is off.
        /// </summary>
        static (DateTime accessTime, int fileCount, long sizeBytes) SummarizeFolder(
            string folder, bool preferAccessTime)
        {
            DateTime latest = DateTime.MinValue;
            int count = 0;
            long bytes = 0;

            IEnumerable<string> files;
            try { files = Directory.EnumerateFiles(folder, "*", System.IO.SearchOption.AllDirectories); }
            catch (UnauthorizedAccessException) { return (DateTime.MinValue, 0, 0); }
            catch (IOException) { return (DateTime.MinValue, 0, 0); }

            foreach (var f in files)
            {
                FileInfo fi;
                try { fi = new FileInfo(f); }
                catch { continue; }

                count++;
                bytes += fi.Length;

                // Take the most recent of access/write. If access tracking is off, LastAccessTime
                // is stale and the write time dominates — which is what we want.
                DateTime candidate = preferAccessTime
                    ? (fi.LastAccessTime > fi.LastWriteTime ? fi.LastAccessTime : fi.LastWriteTime)
                    : fi.LastWriteTime;

                if (candidate > latest) latest = candidate;
            }

            return (latest, count, bytes);
        }

        static string FormatAge(TimeSpan age)
        {
            if (age.TotalDays < 1) return $"{(int)age.TotalHours}h";
            if (age.TotalDays < 60) return $"{(int)age.TotalDays}d";
            return $"{(int)(age.TotalDays / 30.44):F0}mo";
        }
    }

    /// <summary>Summary of one dated folder for retention decisions and UI display.</summary>
    public class DatedFolder
    {
        public string Path { get; set; } = "";
        public DateTime FolderDate { get; set; }
        /// <summary>Latest activity time across the folder's files (access or write).</summary>
        public DateTime AccessTime { get; set; }
        public int FileCount { get; set; }
        public long SizeBytes { get; set; }
    }
}
