using System.IO;
using System.Text.Json;

namespace TeamsISO.App.Services;

/// <summary>
/// Persistent named snapshots of which participants should have ISOs enabled and
/// what their custom output names are. Useful for recurring shows: an operator
/// can save the assignment they spent 5 minutes setting up, and on the next
/// meeting load the same preset and auto-enable everyone whose display name
/// matches.
///
/// Persisted as JSON at <c>%LOCALAPPDATA%\TeamsISO\presets.json</c>. We key by
/// participant <see cref="DisplayName"/> rather than <see cref="Guid"/> Id
/// because the Id is freshly generated for every meeting (Teams' NDI source
/// identity isn't stable across sessions); display name is the operator's
/// natural identifier and is what they see in the UI anyway.
/// </summary>
public static class OperatorPresetStore
{
    /// <summary>
    /// Test-only override for the presets file path. Tests set this to a temp
    /// path so they don't pollute the operator's real %LOCALAPPDATA% store.
    /// Null in production. <see cref="System.Runtime.CompilerServices.InternalsVisibleToAttribute"/>
    /// in the project file grants the test assembly access.
    /// </summary>
    internal static string? PathOverride { get; set; }

    private static string PresetsPath =>
        PathOverride ?? Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "TeamsISO",
            "presets.json");

    /// <summary>
    /// One operator preset: a name, when it was saved, and a list of
    /// per-participant assignments keyed by display name.
    /// </summary>
    public sealed record Preset(
        string Name,
        DateTimeOffset SavedAt,
        IReadOnlyList<Assignment> Assignments);

    /// <summary>
    /// Single participant's assignment within a preset. Both fields are stable
    /// across meetings; <see cref="DisplayName"/> is the join key when applying.
    /// </summary>
    public sealed record Assignment(
        string DisplayName,
        string? CustomOutputName,
        bool Enabled);

    /// <summary>
    /// On-disk shape: a list of presets indexed by name. Wrapped in an object so
    /// we can grow the schema (versioning, defaults, last-used) without breaking
    /// existing files. <see cref="LastAppliedName"/> + <see cref="AutoApplyOnStartup"/>
    /// drive the "auto-apply on startup" feature; reading older files (which lack
    /// these fields) falls back to default values via the records' default ctor.
    /// </summary>
    private sealed record File(
        int Version,
        IReadOnlyList<Preset> Presets,
        string? LastAppliedName = null,
        bool AutoApplyOnStartup = false);

    /// <summary>
    /// Operator-level preferences that travel inside the same JSON envelope as the
    /// presets themselves. Currently used for the "auto-apply last preset on launch"
    /// feature so the host can decide on startup whether to silently re-apply the
    /// most recent preset and which one to apply.
    /// </summary>
    public sealed record StartupPreference(string? LastAppliedName, bool AutoApplyOnStartup);

    /// <summary>Returns all stored presets, oldest first. Empty list if no file exists.</summary>
    public static IReadOnlyList<Preset> LoadAll() => LoadFile().Presets ?? Array.Empty<Preset>();

    /// <summary>
    /// Returns the operator's startup preference (which preset, if any, should be
    /// auto-applied on launch). Defaults to <c>(null, false)</c> when no file exists
    /// or the file predates the field — older preset.json files deserialize cleanly
    /// because both fields are optional with default values.
    /// </summary>
    public static StartupPreference GetStartupPreference()
    {
        var file = LoadFile();
        return new StartupPreference(file.LastAppliedName, file.AutoApplyOnStartup);
    }

    /// <summary>
    /// Records that <paramref name="name"/> was just successfully applied. Combined
    /// with <see cref="SetAutoApplyOnStartup"/>, drives the auto-apply-on-launch flow.
    /// Preserves the rest of the file (presets, AutoApplyOnStartup flag) intact.
    /// </summary>
    public static void MarkApplied(string name)
    {
        var file = LoadFile();
        WriteFile(file with { LastAppliedName = name });
    }

    /// <summary>
    /// Toggles whether the host should auto-apply <see cref="StartupPreference.LastAppliedName"/>
    /// on next launch. Independent of <see cref="MarkApplied"/> so the operator can flip
    /// the toggle without losing the most-recent name.
    /// </summary>
    public static void SetAutoApplyOnStartup(bool enabled)
    {
        var file = LoadFile();
        WriteFile(file with { AutoApplyOnStartup = enabled });
    }

    /// <summary>
    /// Adds (or replaces) a preset by name. Atomic write: writes to a temp file
    /// then File.Replace so a crash mid-write doesn't corrupt the existing file.
    /// Preserves <see cref="StartupPreference"/> across writes.
    /// </summary>
    public static void Save(Preset preset)
    {
        var file = LoadFile();
        var presets = (file.Presets ?? Array.Empty<Preset>())
            .Where(p => !string.Equals(p.Name, preset.Name, StringComparison.OrdinalIgnoreCase))
            .Append(preset)
            .OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
            .ToArray();
        WriteFile(file with { Presets = presets });
    }

    /// <summary>
    /// Removes a preset by name. No-op if not present. If the deleted preset was
    /// the last-applied one, clears that field so we don't try to re-apply a missing
    /// preset on next launch.
    /// </summary>
    public static void Delete(string name)
    {
        var file = LoadFile();
        var existing = file.Presets ?? Array.Empty<Preset>();
        var remaining = existing
            .Where(p => !string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase))
            .ToArray();
        if (remaining.Length == existing.Count) return; // not present

        var clearedLastApplied =
            string.Equals(file.LastAppliedName, name, StringComparison.OrdinalIgnoreCase)
                ? null
                : file.LastAppliedName;

        WriteFile(file with { Presets = remaining, LastAppliedName = clearedLastApplied });
    }

    /// <summary>Looks up a preset by name (case-insensitive). Null if not present.</summary>
    public static Preset? Find(string name) =>
        LoadAll().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));

    /// <summary>
    /// Bundle format for the export/import surface. Wraps the preset list with
    /// a version stamp + an export timestamp so a future-format-aware importer
    /// can migrate the data. We deliberately export a flat preset list — not
    /// the full <see cref="File"/> envelope — because StartupPreference is
    /// machine-local (operator A's "auto-apply Friday Show" shouldn't follow
    /// the bundle to operator B's machine).
    /// </summary>
    public sealed record Bundle(
        string Schema,
        DateTimeOffset ExportedAt,
        IReadOnlyList<Preset> Presets)
    {
        public const string CurrentSchema = "teamsiso-presets-bundle/v1";
    }

    /// <summary>
    /// Serialize every preset to a JSON string suitable for writing to disk.
    /// The shape is human-readable (WriteIndented) so an operator can diff
    /// two bundles in their editor.
    /// </summary>
    public static string ExportAllAsJson()
    {
        var bundle = new Bundle(
            Schema: Bundle.CurrentSchema,
            ExportedAt: DateTimeOffset.Now,
            Presets: LoadAll());
        return JsonSerializer.Serialize(bundle, new JsonSerializerOptions { WriteIndented = true });
    }

    /// <summary>
    /// Result of an import attempt — counts so the UI can toast a clear summary.
    /// </summary>
    public sealed record ImportResult(int Added, int Overwritten, int Skipped, string? Error)
    {
        public static ImportResult Failed(string error) => new(0, 0, 0, error);
    }

    /// <summary>
    /// Import a bundle JSON. Per-preset name collision policy is determined by
    /// <paramref name="overwrite"/>: when true, identically-named presets in the
    /// bundle replace local ones; when false they're skipped. Returns counts
    /// so the caller can toast a "added X, overwrote Y, skipped Z" summary.
    /// </summary>
    public static ImportResult ImportBundle(string json, bool overwrite)
    {
        Bundle? bundle;
        try
        {
            bundle = JsonSerializer.Deserialize<Bundle>(json);
        }
        catch (Exception ex)
        {
            return ImportResult.Failed("Could not parse bundle: " + ex.Message);
        }
        if (bundle is null || bundle.Presets is null)
            return ImportResult.Failed("Bundle was empty or malformed.");

        var existingNames = new HashSet<string>(
            LoadAll().Select(p => p.Name),
            StringComparer.OrdinalIgnoreCase);

        var added = 0;
        var overwritten = 0;
        var skipped = 0;

        foreach (var p in bundle.Presets)
        {
            if (existingNames.Contains(p.Name))
            {
                if (!overwrite) { skipped++; continue; }
                overwritten++;
            }
            else
            {
                added++;
            }
            try { Save(p); }
            catch
            {
                // One bad preset shouldn't abort the rest. Count as skipped so
                // the user knows their import wasn't 100% clean.
                if (overwrite && existingNames.Contains(p.Name)) overwritten--;
                else added--;
                skipped++;
            }
        }

        return new ImportResult(added, overwritten, skipped, null);
    }

    private static File LoadFile()
    {
        try
        {
            if (!System.IO.File.Exists(PresetsPath))
                return new File(1, Array.Empty<Preset>());
            var json = System.IO.File.ReadAllText(PresetsPath);
            return JsonSerializer.Deserialize<File>(json) ?? new File(1, Array.Empty<Preset>());
        }
        catch
        {
            return new File(1, Array.Empty<Preset>());
        }
    }

    private static void WriteFile(File file)
    {
        var dir = Path.GetDirectoryName(PresetsPath);
        if (!string.IsNullOrEmpty(dir))
            Directory.CreateDirectory(dir);

        var json = JsonSerializer.Serialize(file, new JsonSerializerOptions { WriteIndented = true });
        var temp = PresetsPath + ".tmp";
        System.IO.File.WriteAllText(temp, json);
        if (System.IO.File.Exists(PresetsPath))
            System.IO.File.Replace(temp, PresetsPath, destinationBackupFileName: null);
        else
            System.IO.File.Move(temp, PresetsPath);
    }
}
