using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;

namespace AutoIngest.Core
{
    public class AppConfig
    {
        public int JpgQuality { get; set; } = 92;
        public bool AutoImport { get; set; } = true;

        /// <summary>
        /// Devices the user has explicitly approved for import. Empty by default —
        /// no device imports until it has been registered here (or the user picks
        /// "Import once" for a session). This is the opt-in registry: a friend's
        /// USB stick or phone never matches and is left alone.
        /// </summary>
        public List<RegisteredDevice> Devices { get; set; } = new();

        /// <summary>
        /// When true (default), an unregistered device raises a prompt the first time
        /// it's seen each session. When false, unregistered devices are silently skipped.
        /// </summary>
        public bool PromptOnUnknown { get; set; } = true;

        /// <summary>
        /// When true, dated import folders older than <see cref="RetentionDays"/> (by last
        /// access, or last write if access tracking is off) are sent to the Recycle Bin on a
        /// daily sweep. Default false — retention is strictly opt-in.
        /// </summary>
        public bool RetentionEnabled { get; set; } = false;

        /// <summary>
        /// Age threshold in days above which a dated folder is eligible for recycling. Default
        /// ~6 months (180). The UI offers fixed presets (1 week through 7 years) that map to this.
        /// </summary>
        public int RetentionDays { get; set; } = 180;

        /// <summary>
        /// When true, photos imported from a phone (MTP) are deleted from the phone after the
        /// local copy is verified — mirroring the "move, not copy" behavior SD/USB have always had.
        /// When false (default), phones are copy-and-leave: the user curates phone storage
        /// themselves. This affects phones ONLY; SD/USB always moved/converted as before.
        /// </summary>
        public bool DeleteFromPhoneAfterImport { get; set; } = false;

        /// <summary>
        /// When true, converted JPGs are auto-oriented using their EXIF orientation tag before
        /// write. Phones and some cameras set the tag without rotating the pixels, so the image
        /// appears sideways in browsers that don't honor the tag. Default on — pure win.
        /// </summary>
        public bool AutoOrient { get; set; } = true;

        /// <summary>
        /// When true, EXIF metadata (including GPS coordinates) is stripped from converted JPGs.
        /// Phone photos can leak the seller's location into listing images. Default on.
        /// Applies to converted files; JPGs that are moved directly are untouched (see remarks
        /// in ImageConverter for why).
        /// </summary>
        public bool StripExif { get; set; } = true;

        /// <summary>
        /// Whether the first-run welcome wizard has been completed. Default true so existing
        /// users (whose config predates this flag) are never prompted. A brand-new install has
        /// no config file at all, which is what MainForm actually keys the wizard on.
        /// </summary>
        public bool SetupCompleted { get; set; } = true;

        /// <summary>
        /// Branding settings for converted photos: visible text/logo watermarks and optional
        /// EXIF copyright fields. Off by default; the user opts in via the welcome wizard or
        /// tray Options ▸ Branding. See <see cref="BrandingConfig"/> for details.
        /// </summary>
        public BrandingConfig Branding { get; set; } = new();
    }

    /// <summary>
    /// Corner/edge anchors for overlay positioning, expressed generically so the same enum
    /// serves both the text watermark and the logo watermark.
    /// </summary>
    public enum WatermarkPosition
    {
        BottomRight,
        BottomLeft,
        TopRight,
        TopLeft,
        Center
    }

    /// <summary>
    /// Branding applied to converted photos. Everything is off by default — the user opts in
    /// via the welcome wizard or tray Options ▸ Branding ▸ Enabled. Three independent features:
    /// a visible text watermark (store name/URL/handle), a visible logo watermark (PNG), and
    /// EXIF copyright fields. Watermarks are burned into pixels; EXIF copyright is written
    /// AFTER metadata stripping so the seller's info survives while camera/GPS tags are wiped.
    /// </summary>
    public class BrandingConfig
    {
        /// <summary>Master switch. When false, none of the branding features run.</summary>
        public bool Enabled { get; set; } = false;

        // --- Store identity (drives the text watermark content) ---
        public string StoreName { get; set; } = "";
        public string StoreUrl { get; set; } = "";
        public string SellerHandle { get; set; } = "";

        // --- Text watermark ---
        public bool TextWatermarkEnabled { get; set; } = false;
        public WatermarkPosition TextPosition { get; set; } = WatermarkPosition.BottomRight;
        /// <summary>0–100. How opaque the text is.</summary>
        public int TextOpacity { get; set; } = 60;

        // --- Logo watermark ---
        public bool LogoWatermarkEnabled { get; set; } = false;
        public string LogoPath { get; set; } = "";
        public WatermarkPosition LogoPosition { get; set; } = WatermarkPosition.BottomRight;
        /// <summary>0–100. How opaque the logo is.</summary>
        public int LogoOpacity { get; set; } = 50;
        /// <summary>Logo scale as a percentage of the photo's width. 0 = use the logo's native size.</summary>
        public int LogoScalePercent { get; set; } = 20;

        // --- EXIF copyright (written after Strip, so only this survives) ---
        public bool ExifCopyrightEnabled { get; set; } = false;
        public string ExifArtist { get; set; } = "";
        public string ExifCopyright { get; set; } = "";
        public string ExifDescription { get; set; } = "";
    }

    /// <summary>
    /// A device the user has trusted for import. <see cref="Id"/> is a stable per-device
    /// fingerprint (volume serial for SD/USB, PTP serial for phones) so the same physical
    /// device is recognized across reboots and drive-letter changes. <see cref="Name"/> and
    /// <see cref="Kind"/> are human-facing labels.
    /// </summary>
    public class RegisteredDevice
    {
        /// <summary>Stable device fingerprint. See <see cref="DeviceIdentity.Id"/> for format.</summary>
        public string Id { get; set; } = "";

        /// <summary>Friendly name as shown in the UI (volume label or phone name).</summary>
        public string Name { get; set; } = "";

        /// <summary>"SD", "USB", "USB-weak", or "Phone".</summary>
        public string Kind { get; set; } = "";

        /// <summary>When the device was added to the registry (UTC).</summary>
        public DateTime AddedUtc { get; set; } = DateTime.UtcNow;
    }

    public static class ConfigManager
    {
        static readonly string CONFIG_PATH = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
            ".autoingest_config.json");

        // Backup of the previous config, written atomically by every SaveConfig via File.Replace.
        // LoadConfig falls back to this when the main file is corrupt (partial write from a crash,
        // AV quarantine, disk error) so a corruption event no longer silently wipes the device
        // registry and all settings back to defaults.
        static readonly string CONFIG_BAK_PATH = CONFIG_PATH + ".bak";

        static readonly JsonSerializerOptions JSON_OPTS = new() { WriteIndented = true };

        /// <summary>
        /// True if a config file already exists on disk. Used to detect a brand-new install
        /// (first launch ever) so the welcome wizard can run exactly once. Existing users
        /// upgrading will have a config file and won't be prompted.
        /// </summary>
        public static bool ConfigExists => File.Exists(CONFIG_PATH);

        public static AppConfig LoadConfig()
        {
            // Try the main file first; on a JsonException (corrupt/partial), fall back to the
            // .bak before giving up and returning defaults. This closes the silent-wipe path.
            if (TryLoad(CONFIG_PATH, out var config)) return config;
            if (TryLoad(CONFIG_BAK_PATH, out config)) return config;
            return new AppConfig();
        }

        static bool TryLoad(string path, out AppConfig config)
        {
            config = new AppConfig();
            try
            {
                if (File.Exists(path))
                {
                    config = JsonSerializer.Deserialize<AppConfig>(File.ReadAllText(path)) ?? new AppConfig();
                    return true;
                }
            }
            catch (UnauthorizedAccessException) { }
            catch (IOException) { }
            catch (JsonException) { }
            return false;
        }

        public static void SaveConfig(AppConfig config)
        {
            // Atomic write: serialize to a temp file in the same directory, then File.Replace it
            // over the real config. On NTFS, File.Replace is atomic — a crash mid-write leaves the
            // previous config intact and the new one discarded, not a truncated file. The Replace
            // also produces the .bak that LoadConfig falls back to. The first-ever save (no
            // destination yet) uses the temp-rename path since File.Replace requires an existing
            // destination.
            string json = JsonSerializer.Serialize(config, JSON_OPTS);
            string tmp = CONFIG_PATH + ".tmp";
            try
            {
                Directory.CreateDirectory(Path.GetDirectoryName(CONFIG_PATH)!);
                File.WriteAllText(tmp, json);

                if (File.Exists(CONFIG_PATH))
                    File.Replace(tmp, CONFIG_PATH, CONFIG_BAK_PATH);
                else
                    File.Move(tmp, CONFIG_PATH);
            }
            catch (UnauthorizedAccessException) { }
            catch (IOException) { }
            finally
            {
                // Clean up a stray temp file if Replace/Move threw before consuming it.
                try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
            }
        }
    }
}
