using System.IO;
using System.Security.Cryptography;
using Microsoft.Data.Sqlite;
using ytLive.Models;

namespace ytLive.Services;

public class LayoutStore : IDisposable
{
    public static LayoutStore? Instance { get; private set; }

    private readonly SqliteConnection _connection;
    public string ActivePath { get; }

    /// <summary>The app-wide webcam identity loaded with the last Load() (null = never picked).</summary>
    public Webcam? Webcam { get; private set; }

    /// <summary>The app-wide social bar config loaded with the last Load() (null = never defined).</summary>
    public SocialsConfig? Socials { get; private set; }

    /// <summary>The app-wide background music (TASK 9 TRAX) loaded with the last
    /// Load() (null = no track chosen).</summary>
    public Music? Music { get; private set; }

    public LayoutStore(string path)
    {
        ActivePath = path;
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        _connection = new SqliteConnection($"Data Source={path}");
        _connection.Open();
        using (var pragma = _connection.CreateCommand())
        {
            pragma.CommandText = "PRAGMA foreign_keys = ON;";
            pragma.ExecuteNonQuery();
        }
        EnsureSchema();
        Instance = this;
    }

    private void EnsureSchema()
    {
        string[] statements =
        {
            """
            CREATE TABLE IF NOT EXISTS Scene (
                Id TEXT PRIMARY KEY,
                Name TEXT NOT NULL,
                IsHidden INTEGER NOT NULL DEFAULT 0,
                IsChatScene INTEGER NOT NULL DEFAULT 0,
                HasBackdrop INTEGER NOT NULL DEFAULT 1,
                SortOrder INTEGER NOT NULL DEFAULT 0
            );
            """,
            """
            CREATE TABLE IF NOT EXISTS Asset (
                Id TEXT PRIMARY KEY,
                Hash TEXT NOT NULL UNIQUE,
                Data BLOB NOT NULL,
                PixelWidth INTEGER NOT NULL DEFAULT 0,
                PixelHeight INTEGER NOT NULL DEFAULT 0
            );
            """,
            """
            CREATE TABLE IF NOT EXISTS Source (
                Id TEXT PRIMARY KEY,
                SceneId TEXT NOT NULL REFERENCES Scene(Id) ON DELETE CASCADE,
                AssetId TEXT REFERENCES Asset(Id),
                Type TEXT NOT NULL,
                Name TEXT NOT NULL,
                IsEnabled INTEGER NOT NULL DEFAULT 1,
                X REAL NOT NULL DEFAULT 0,
                Y REAL NOT NULL DEFAULT 0,
                Width REAL NOT NULL DEFAULT 0,
                Height REAL NOT NULL DEFAULT 0,
                Opacity REAL NOT NULL DEFAULT 1,
                MonitorIndex INTEGER,
                DeviceId TEXT,
                ClipShape TEXT NOT NULL DEFAULT 'Traditional',
                IsMirrored INTEGER NOT NULL DEFAULT 0,
                IsBackdrop INTEGER NOT NULL DEFAULT 0,
                CaptureKey TEXT,
                SortOrder INTEGER NOT NULL DEFAULT 0
            );
            """,
            """
            CREATE TABLE IF NOT EXISTS Webcam (
                Id TEXT PRIMARY KEY,
                DeviceId TEXT NOT NULL UNIQUE,
                Name TEXT NOT NULL
            );
            """,
            """
            CREATE TABLE IF NOT EXISTS WebcamSceneConfig (
                SceneId TEXT NOT NULL REFERENCES Scene(Id) ON DELETE CASCADE,
                WebcamId TEXT NOT NULL REFERENCES Webcam(Id) ON DELETE CASCADE,
                IsVisible INTEGER NOT NULL DEFAULT 1,
                X REAL NOT NULL DEFAULT 0,
                Y REAL NOT NULL DEFAULT 0,
                Width REAL NOT NULL DEFAULT 0,
                Height REAL NOT NULL DEFAULT 0,
                Opacity REAL NOT NULL DEFAULT 1,
                ClipShape TEXT NOT NULL DEFAULT 'Traditional',
                IsMirrored INTEGER NOT NULL DEFAULT 0,
                RectWidth REAL,
                RectHeight REAL,
                BorderColor TEXT,
                BorderOpacity REAL NOT NULL DEFAULT 1,
                BorderWidth INTEGER NOT NULL DEFAULT 0,
                BorderAnimation TEXT NOT NULL DEFAULT 'None',
                SortOrder INTEGER NOT NULL DEFAULT 0,
                PRIMARY KEY (SceneId, WebcamId)
            );
            """,
            """
            CREATE TABLE IF NOT EXISTS Socials (
                Id TEXT PRIMARY KEY,
                BarPosition TEXT NOT NULL DEFAULT 'Bottom',
                BarJustify TEXT NOT NULL DEFAULT 'Center'
            );
            """,
            """
            CREATE TABLE IF NOT EXISTS SocialEntry (
                Id TEXT PRIMARY KEY,
                SocialsId TEXT NOT NULL REFERENCES Socials(Id) ON DELETE CASCADE,
                Service TEXT NOT NULL,
                Handle TEXT NOT NULL,
                ProfileUrl TEXT NOT NULL,
                SortOrder INTEGER NOT NULL DEFAULT 0
            );
            """,
            """
            CREATE TABLE IF NOT EXISTS Music (
                Id TEXT PRIMARY KEY,
                TrackPath TEXT NOT NULL,
                IsEnabled INTEGER NOT NULL DEFAULT 0
            );
            """,
            """
            CREATE TABLE IF NOT EXISTS Settings (
                Key TEXT PRIMARY KEY,
                Value TEXT NOT NULL
            );
            """,
        };
        foreach (var sql in statements)
        {
            using var cmd = _connection.CreateCommand();
            cmd.CommandText = sql;
            cmd.ExecuteNonQuery();
        }
        MigrateSourceTable();
        MigrateWebcamConfigTable();
        MigrateSceneTable();
        MigrateSceneSocialBarColumn();
        MigrateSocialsTable();
        MigrateSocialEntryTable();
        if (GetUserVersion() < 3)
            MigrateToV3();
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "PRAGMA user_version = 9;";
            cmd.ExecuteNonQuery();
        }
    }

    private int GetUserVersion()
    {
        using var cmd = _connection.CreateCommand();
        cmd.CommandText = "PRAGMA user_version;";
        return Convert.ToInt32(cmd.ExecuteScalar());
    }

    // v1 → v2: Source gains ClipShape + IsMirrored (webcam clip/mirror prefs).
    // CREATE TABLE IF NOT EXISTS doesn't touch existing tables, so pre-v2 DBs
    // get the columns here instead.
    private void MigrateSourceTable()
    {
        var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "PRAGMA table_info(Source);";
            using var reader = cmd.ExecuteReader();
            while (reader.Read())
                columns.Add(reader.GetString(1));
        }

        if (!columns.Contains("ClipShape"))
        {
            using var cmd = _connection.CreateCommand();
            cmd.CommandText = "ALTER TABLE Source ADD COLUMN ClipShape TEXT NOT NULL DEFAULT 'Traditional';";
            cmd.ExecuteNonQuery();
        }

        if (!columns.Contains("IsMirrored"))
        {
            using var cmd = _connection.CreateCommand();
            cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsMirrored INTEGER NOT NULL DEFAULT 0;";
            cmd.ExecuteNonQuery();
        }

        if (!columns.Contains("IsBackdrop"))
        {
            using var cmd = _connection.CreateCommand();
            cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsBackdrop INTEGER NOT NULL DEFAULT 0;";
            cmd.ExecuteNonQuery();
        }

        if (!columns.Contains("CaptureKey"))
        {
            using var cmd = _connection.CreateCommand();
            cmd.CommandText = "ALTER TABLE Source ADD COLUMN CaptureKey TEXT;";
            cmd.ExecuteNonQuery();
        }
    }

    // v3 → v4: WebcamSceneConfig gains RectWidth/RectHeight (the pre-Round rect,
    // so a reloaded Round webcam restores its aspect on toggle-back).
    private void MigrateWebcamConfigTable()
    {
        var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "PRAGMA table_info(WebcamSceneConfig);";
            using var reader = cmd.ExecuteReader();
            while (reader.Read())
                columns.Add(reader.GetString(1));
        }

        if (!columns.Contains("RectWidth"))
        {
            using var cmd = _connection.CreateCommand();
            cmd.CommandText = "ALTER TABLE WebcamSceneConfig ADD COLUMN RectWidth REAL;";
            cmd.ExecuteNonQuery();
        }

        if (!columns.Contains("RectHeight"))
        {
            using var cmd = _connection.CreateCommand();
            cmd.CommandText = "ALTER TABLE WebcamSceneConfig ADD COLUMN RectHeight REAL;";
            cmd.ExecuteNonQuery();
        }
    }

    // v2 → v3: the webcam leaves the Source table. Any webcam Source rows become
    // one Webcam identity (first row — one camera app-wide) + a WebcamSceneConfig
    // per scene that had one, then the webcam rows are deleted. No backfill:
    // scenes without the webcam stay webcam-free.
    private void MigrateToV3()
    {
        var rows = new List<(string Id, string SceneId, string Name, double X, double Y, double W, double H, double Opacity, string DeviceId, string ClipShape, bool IsMirrored)>();
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = """
                SELECT Id, SceneId, Name, X, Y, Width, Height, Opacity, DeviceId, ClipShape, IsMirrored
                FROM Source WHERE Type = 'Webcam' ORDER BY SortOrder;
                """;
            using var reader = cmd.ExecuteReader();
            while (reader.Read())
            {
                rows.Add((
                    reader.GetString(0),
                    reader.GetString(1),
                    reader.GetString(2),
                    reader.GetDouble(3),
                    reader.GetDouble(4),
                    reader.GetDouble(5),
                    reader.GetDouble(6),
                    reader.GetDouble(7),
                    reader.IsDBNull(8) ? string.Empty : reader.GetString(8),
                    reader.GetString(9),
                    reader.GetInt32(10) != 0));
            }
        }
        if (rows.Count == 0) return;

        using var tx = _connection.BeginTransaction();
        var webcamId = Guid.NewGuid().ToString();

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "INSERT INTO Webcam (Id, DeviceId, Name) VALUES ($id, $device, $name);";
            cmd.Transaction = tx;
            cmd.Parameters.AddWithValue("$id", webcamId);
            cmd.Parameters.AddWithValue("$device", rows[0].DeviceId);
            cmd.Parameters.AddWithValue("$name", rows[0].Name);
            cmd.ExecuteNonQuery();
        }

        foreach (var row in rows)
        {
            using var cmd = _connection.CreateCommand();
            cmd.CommandText = """
                INSERT INTO WebcamSceneConfig
                    (SceneId, WebcamId, IsVisible, X, Y, Width, Height, Opacity,
                     ClipShape, IsMirrored, BorderColor, BorderOpacity, BorderWidth, BorderAnimation, SortOrder)
                VALUES ($sceneId, $webcamId, 1, $x, $y, $w, $h, $opacity,
                        $clip, $mirrored, NULL, 1, 0, 'None', 0);
                """;
            cmd.Transaction = tx;
            cmd.Parameters.AddWithValue("$sceneId", row.SceneId);
            cmd.Parameters.AddWithValue("$webcamId", webcamId);
            cmd.Parameters.AddWithValue("$x", row.X);
            cmd.Parameters.AddWithValue("$y", row.Y);
            cmd.Parameters.AddWithValue("$w", row.W);
            cmd.Parameters.AddWithValue("$h", row.H);
            cmd.Parameters.AddWithValue("$opacity", row.Opacity);
            cmd.Parameters.AddWithValue("$clip", row.ClipShape);
            cmd.Parameters.AddWithValue("$mirrored", row.IsMirrored ? 1 : 0);
            cmd.ExecuteNonQuery();
        }

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM Source WHERE Type = 'Webcam';";
            cmd.Transaction = tx;
            cmd.ExecuteNonQuery();
        }
        tx.Commit();
    }

    // v5 → v6: Scene gains HasBackdrop. Added columns default to true; the
    // ONE-TIME backfill (runs only when the column is first added) turns every
    // non-Live canonical scene off and drops their backdrop sources — the fix for
    // DBs saved before the Live-only policy. MainViewModel.EnforceBackdropPolicy
    // re-runs the same normalization on every load for DBs that miss the backfill.
    private void MigrateSceneTable()
    {
        var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "PRAGMA table_info(Scene);";
            using var reader = cmd.ExecuteReader();
            while (reader.Read())
                columns.Add(reader.GetString(1));
        }

        if (columns.Contains("HasBackdrop")) return;

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "ALTER TABLE Scene ADD COLUMN HasBackdrop INTEGER NOT NULL DEFAULT 1;";
            cmd.ExecuteNonQuery();
        }

        const string noBackdrop = "('starting', 'brb', 'chat', 'ending')";
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = $"UPDATE Scene SET HasBackdrop = 0 WHERE LOWER(TRIM(Name)) IN {noBackdrop};";
            cmd.ExecuteNonQuery();
        }
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = $"""
                DELETE FROM Source
                WHERE IsBackdrop = 1 AND SceneId IN
                    (SELECT Id FROM Scene WHERE LOWER(TRIM(Name)) IN {noBackdrop});
                """;
            cmd.ExecuteNonQuery();
        }
    }

    // v6 → v7: Scene gains HasSocialBar (the per-scene social-bar toggle).
    // Defaults to 0 (off) — the creator adds it explicitly via the footer [+].
    private void MigrateSceneSocialBarColumn()
    {
        var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "PRAGMA table_info(Scene);";
            using var reader = cmd.ExecuteReader();
            while (reader.Read())
                columns.Add(reader.GetString(1));
        }

        if (columns.Contains("HasSocialBar")) return;

        using var alter = _connection.CreateCommand();
        alter.CommandText = "ALTER TABLE Scene ADD COLUMN HasSocialBar INTEGER NOT NULL DEFAULT 0;";
        alter.ExecuteNonQuery();
    }

    // v7 → v8: Socials gains BarEnabled (the dialog's show/hide switch). The old
    // BarJustify column stays for back-compat but is no longer read or written.
    private void MigrateSocialsTable()
    {
        var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "PRAGMA table_info(Socials);";
            using var reader = cmd.ExecuteReader();
            while (reader.Read())
                columns.Add(reader.GetString(1));
        }

        if (columns.Contains("BarEnabled")) return;

        using var alter = _connection.CreateCommand();
        alter.CommandText = "ALTER TABLE Socials ADD COLUMN BarEnabled INTEGER NOT NULL DEFAULT 1;";
        alter.ExecuteNonQuery();
    }

    // v8 → v9: SocialEntry gains Software (the fediverse instance's nodeinfo
    // software name) so a reloaded entry still shows the right service logo.
    private void MigrateSocialEntryTable()
    {
        var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "PRAGMA table_info(SocialEntry);";
            using var reader = cmd.ExecuteReader();
            while (reader.Read())
                columns.Add(reader.GetString(1));
        }

        if (columns.Contains("Software")) return;

        using var alter = _connection.CreateCommand();
        alter.CommandText = "ALTER TABLE SocialEntry ADD COLUMN Software TEXT;";
        alter.ExecuteNonQuery();
    }

    public List<Scene> Load()
    {
        Webcam = null;
        Socials = null;
        Music = null;
        var scenes = new List<Scene>();
        var sourcesByScene = new Dictionary<string, List<Source>>();
        var configsByScene = new Dictionary<string, List<WebcamSceneConfig>>();

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "SELECT Id, Name, IsHidden, IsChatScene, HasBackdrop, HasSocialBar FROM Scene ORDER BY SortOrder";
            using var reader = cmd.ExecuteReader();
            while (reader.Read())
            {
                scenes.Add(new Scene
                {
                    Id = reader.GetString(0),
                    Name = reader.GetString(1),
                    IsHidden = reader.GetInt32(2) != 0,
                    IsChatScene = reader.GetInt32(3) != 0,
                    HasBackdrop = reader.GetInt32(4) != 0,
                    HasSocialBar = reader.FieldCount > 5 && !reader.IsDBNull(5) && reader.GetInt32(5) != 0,
                });
            }
        }

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "SELECT Id, DeviceId, Name FROM Webcam;";
            using var reader = cmd.ExecuteReader();
            if (reader.Read())
            {
                Webcam = new Webcam
                {
                    Id = reader.GetString(0),
                    DeviceId = reader.GetString(1),
                    Name = reader.GetString(2),
                };
            }
        }

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "SELECT Id, BarPosition, BarEnabled FROM Socials LIMIT 1;";
            using var reader = cmd.ExecuteReader();
            if (reader.Read())
            {
                Socials = new SocialsConfig
                {
                    BarPosition = Enum.TryParse<SocialBarPosition>(reader.GetString(1), out var pos) ? pos : SocialBarPosition.Bottom,
                    BarEnabled = reader.FieldCount > 2 && !reader.IsDBNull(2) && reader.GetInt32(2) != 0,
                };
                var socialsId = reader.GetString(0);

                using var entryCmd = _connection.CreateCommand();
                entryCmd.CommandText = "SELECT Service, Handle, ProfileUrl, Software FROM SocialEntry WHERE SocialsId = $id ORDER BY SortOrder;";
                entryCmd.Parameters.AddWithValue("$id", socialsId);
                using var entryReader = entryCmd.ExecuteReader();
                while (entryReader.Read())
                {
                    Socials.Entries.Add(new SocialEntry
                    {
                        Service = Enum.TryParse<SocialService>(entryReader.GetString(0), out var svc) ? svc : SocialService.Link,
                        Handle = entryReader.GetString(1),
                        ProfileUrl = entryReader.GetString(2),
                        FediverseSoftware = entryReader.FieldCount > 3 && !entryReader.IsDBNull(3) ? entryReader.GetString(3) : null,
                    });
                }
            }
        }

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "SELECT TrackPath, IsEnabled FROM Music LIMIT 1;";
            using var reader = cmd.ExecuteReader();
            if (reader.Read())
            {
                Music = new Music
                {
                    TrackPath = reader.GetString(0),
                    IsEnabled = reader.GetInt32(1) != 0,
                };
            }
        }

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = """
                SELECT Id, SceneId, AssetId, Type, Name, IsEnabled,
                       X, Y, Width, Height, Opacity, MonitorIndex, ClipShape, IsMirrored,
                       IsBackdrop, CaptureKey
                FROM Source ORDER BY SortOrder
                """;
            using var reader = cmd.ExecuteReader();
            while (reader.Read())
            {
                var sceneId = reader.GetString(1);
                var source = new Source
                {
                    Id = reader.GetString(0),
                    AssetId = reader.IsDBNull(2) ? null : reader.GetString(2),
                    Type = Enum.Parse<SourceType>(reader.GetString(3)),
                    Name = reader.GetString(4),
                    IsEnabled = reader.GetInt32(5) != 0,
                    X = reader.GetDouble(6),
                    Y = reader.GetDouble(7),
                    Width = reader.GetDouble(8),
                    Height = reader.GetDouble(9),
                    Opacity = reader.GetDouble(10),
                    MonitorIndex = reader.IsDBNull(11) ? null : reader.GetInt32(11),
                    ClipShape = Enum.TryParse<ClipShape>(reader.GetString(12), out var clip) ? clip : ClipShape.Traditional,
                    IsMirrored = reader.GetInt32(13) != 0,
                    IsBackdrop = reader.GetInt32(14) != 0,
                    CaptureKey = reader.IsDBNull(15) ? null : reader.GetString(15),
                };
                if (!sourcesByScene.TryGetValue(sceneId, out var list))
                    sourcesByScene[sceneId] = list = new List<Source>();
                list.Add(source);
            }
        }

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = """
                SELECT SceneId, WebcamId, IsVisible, X, Y, Width, Height, Opacity,
                       ClipShape, IsMirrored, BorderColor, BorderOpacity, BorderWidth, BorderAnimation,
                       RectWidth, RectHeight
                FROM WebcamSceneConfig ORDER BY SortOrder
                """;
            using var reader = cmd.ExecuteReader();
            while (reader.Read())
            {
                var sceneId = reader.GetString(0);
                var config = new WebcamSceneConfig
                {
                    WebcamId = reader.GetString(1),
                    Name = Webcam?.Name ?? "Webcam",
                    IsVisible = reader.GetInt32(2) != 0,
                    X = reader.GetDouble(3),
                    Y = reader.GetDouble(4),
                    Width = reader.GetDouble(5),
                    Height = reader.GetDouble(6),
                    Opacity = reader.GetDouble(7),
                    ClipShape = Enum.TryParse<ClipShape>(reader.GetString(8), out var clip) ? clip : ClipShape.Traditional,
                    IsMirrored = reader.GetInt32(9) != 0,
                    BorderColor = reader.IsDBNull(10) ? string.Empty : reader.GetString(10),
                    BorderOpacity = reader.IsDBNull(11) ? 1.0 : reader.GetDouble(11),
                    BorderWidth = reader.IsDBNull(12) ? 0 : reader.GetInt32(12),
                    BorderAnimation = Enum.TryParse<BorderAnimation>(reader.GetString(13), out var anim) ? anim : BorderAnimation.None,
                    RectWidth = reader.IsDBNull(14) ? null : reader.GetDouble(14),
                    RectHeight = reader.IsDBNull(15) ? null : reader.GetDouble(15),
                };
                if (!configsByScene.TryGetValue(sceneId, out var list))
                    configsByScene[sceneId] = list = new List<WebcamSceneConfig>();
                list.Add(config);
            }
        }

        foreach (var scene in scenes)
        {
            if (sourcesByScene.TryGetValue(scene.Id, out var sources))
                foreach (var source in sources)
                    scene.Elements.Add(source);
            if (configsByScene.TryGetValue(scene.Id, out var configs))
                foreach (var config in configs)
                    scene.Elements.Add(config);
        }

        return scenes;
    }

    /// <summary>The last picked mic source name, or null if never chosen. Restored
    /// at startup so the sound bar reconnects to the same, already-vetted device.</summary>
    public string? LoadMicSourceName()
    {
        using var cmd = _connection.CreateCommand();
        cmd.CommandText = "SELECT Value FROM Settings WHERE Key = 'MicSourceName';";
        var value = cmd.ExecuteScalar() as string;
        return string.IsNullOrWhiteSpace(value) ? null : value;
    }

    public void SaveMicSourceName(string? name)
    {
        using var cmd = _connection.CreateCommand();
        cmd.CommandText = "DELETE FROM Settings WHERE Key = 'MicSourceName';";
        cmd.ExecuteNonQuery();
        if (string.IsNullOrWhiteSpace(name))
            return;

        using var insert = _connection.CreateCommand();
        insert.CommandText = "INSERT INTO Settings (Key, Value) VALUES ('MicSourceName', $name);";
        insert.Parameters.AddWithValue("$name", name);
        insert.ExecuteNonQuery();
    }

    public void Save(IEnumerable<Scene> scenes, Webcam? webcam, SocialsConfig? socials, Music? music = null)
    {
        using var tx = _connection.BeginTransaction();
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM WebcamSceneConfig;";
            cmd.Transaction = tx;
            cmd.ExecuteNonQuery();
        }
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM Source;";
            cmd.Transaction = tx;
            cmd.ExecuteNonQuery();
        }
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM Webcam;";
            cmd.Transaction = tx;
            cmd.ExecuteNonQuery();
        }
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM SocialEntry;";
            cmd.Transaction = tx;
            cmd.ExecuteNonQuery();
        }
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM Socials;";
            cmd.Transaction = tx;
            cmd.ExecuteNonQuery();
        }
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM Music;";
            cmd.Transaction = tx;
            cmd.ExecuteNonQuery();
        }
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM Scene;";
            cmd.Transaction = tx;
            cmd.ExecuteNonQuery();
        }

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = """
                INSERT INTO Scene (Id, Name, IsHidden, IsChatScene, HasBackdrop, HasSocialBar, SortOrder)
                VALUES ($id, $name, $isHidden, $isChat, $hasBackdrop, $hasSocialBar, $sort)
                """;
            cmd.Transaction = tx;
            var idP = cmd.Parameters.Add("$id", SqliteType.Text);
            var nameP = cmd.Parameters.Add("$name", SqliteType.Text);
            var hiddenP = cmd.Parameters.Add("$isHidden", SqliteType.Integer);
            var chatP = cmd.Parameters.Add("$isChat", SqliteType.Integer);
            var backdropP = cmd.Parameters.Add("$hasBackdrop", SqliteType.Integer);
            var socialBarP = cmd.Parameters.Add("$hasSocialBar", SqliteType.Integer);
            var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);

            var sort = 0;
            foreach (var scene in scenes)
            {
                idP.Value = scene.Id;
                nameP.Value = scene.Name;
                hiddenP.Value = scene.IsHidden ? 1 : 0;
                chatP.Value = scene.IsChatScene ? 1 : 0;
                backdropP.Value = scene.HasBackdrop ? 1 : 0;
                socialBarP.Value = scene.HasSocialBar ? 1 : 0;
                sortP.Value = sort++;
                cmd.ExecuteNonQuery();
            }
        }

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = """
                INSERT INTO Source (Id, SceneId, AssetId, Type, Name, IsEnabled,
                                    X, Y, Width, Height, Opacity, MonitorIndex,
                                    ClipShape, IsMirrored, IsBackdrop, CaptureKey, SortOrder)
                VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled,
                        $x, $y, $w, $h, $opacity, $monitor,
                        $clip, $mirrored, $isBackdrop, $captureKey, $sort)
                """;
            cmd.Transaction = tx;
            var idP = cmd.Parameters.Add("$id", SqliteType.Text);
            var sceneIdP = cmd.Parameters.Add("$sceneId", SqliteType.Text);
            var assetIdP = cmd.Parameters.Add("$assetId", SqliteType.Text);
            var typeP = cmd.Parameters.Add("$type", SqliteType.Text);
            var nameP = cmd.Parameters.Add("$name", SqliteType.Text);
            var enabledP = cmd.Parameters.Add("$isEnabled", SqliteType.Integer);
            var xP = cmd.Parameters.Add("$x", SqliteType.Real);
            var yP = cmd.Parameters.Add("$y", SqliteType.Real);
            var wP = cmd.Parameters.Add("$w", SqliteType.Real);
            var hP = cmd.Parameters.Add("$h", SqliteType.Real);
            var opacityP = cmd.Parameters.Add("$opacity", SqliteType.Real);
            var monitorP = cmd.Parameters.Add("$monitor", SqliteType.Integer);
            var clipP = cmd.Parameters.Add("$clip", SqliteType.Text);
            var mirroredP = cmd.Parameters.Add("$mirrored", SqliteType.Integer);
            var isBackdropP = cmd.Parameters.Add("$isBackdrop", SqliteType.Integer);
            var captureKeyP = cmd.Parameters.Add("$captureKey", SqliteType.Text);
            var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);

            foreach (var scene in scenes)
            {
                var sort = 0;
                foreach (var element in scene.Elements)
                {
                    if (element is not Source source) continue;
                    idP.Value = source.Id;
                    sceneIdP.Value = scene.Id;
                    assetIdP.Value = (object?)source.AssetId ?? DBNull.Value;
                    typeP.Value = source.Type.ToString();
                    nameP.Value = source.Name;
                    enabledP.Value = source.IsEnabled ? 1 : 0;
                    xP.Value = source.X;
                    yP.Value = source.Y;
                    wP.Value = source.Width;
                    hP.Value = source.Height;
                    opacityP.Value = source.Opacity;
                    monitorP.Value = (object?)source.MonitorIndex ?? DBNull.Value;
                    clipP.Value = source.ClipShape.ToString();
                    mirroredP.Value = source.IsMirrored ? 1 : 0;
                    isBackdropP.Value = source.IsBackdrop ? 1 : 0;
                    captureKeyP.Value = (object?)source.CaptureKey ?? DBNull.Value;
                    sortP.Value = sort++;
                    cmd.ExecuteNonQuery();
                }
            }
        }

        if (webcam != null)
        {
            using (var cmd = _connection.CreateCommand())
            {
                cmd.CommandText = "INSERT INTO Webcam (Id, DeviceId, Name) VALUES ($id, $device, $name);";
                cmd.Transaction = tx;
                cmd.Parameters.AddWithValue("$id", webcam.Id);
                cmd.Parameters.AddWithValue("$device", webcam.DeviceId);
                cmd.Parameters.AddWithValue("$name", webcam.Name);
                cmd.ExecuteNonQuery();
            }

            using (var cmd = _connection.CreateCommand())
            {
                cmd.CommandText = """
                    INSERT INTO WebcamSceneConfig
                        (SceneId, WebcamId, IsVisible, X, Y, Width, Height, Opacity,
                         ClipShape, IsMirrored, BorderColor, BorderOpacity, BorderWidth, BorderAnimation,
                         RectWidth, RectHeight, SortOrder)
                    VALUES ($sceneId, $webcamId, $isVisible, $x, $y, $w, $h, $opacity,
                            $clip, $mirrored, $borderColor, $borderOpacity, $borderWidth, $borderAnimation,
                            $rectWidth, $rectHeight, $sort)
                    """;
                cmd.Transaction = tx;
                var sceneIdP = cmd.Parameters.Add("$sceneId", SqliteType.Text);
                var webcamIdP = cmd.Parameters.Add("$webcamId", SqliteType.Text);
                var visibleP = cmd.Parameters.Add("$isVisible", SqliteType.Integer);
                var xP = cmd.Parameters.Add("$x", SqliteType.Real);
                var yP = cmd.Parameters.Add("$y", SqliteType.Real);
                var wP = cmd.Parameters.Add("$w", SqliteType.Real);
                var hP = cmd.Parameters.Add("$h", SqliteType.Real);
                var opacityP = cmd.Parameters.Add("$opacity", SqliteType.Real);
                var clipP = cmd.Parameters.Add("$clip", SqliteType.Text);
                var mirroredP = cmd.Parameters.Add("$mirrored", SqliteType.Integer);
                var colorP = cmd.Parameters.Add("$borderColor", SqliteType.Text);
                var borderOpacityP = cmd.Parameters.Add("$borderOpacity", SqliteType.Real);
                var borderWidthP = cmd.Parameters.Add("$borderWidth", SqliteType.Integer);
                var animationP = cmd.Parameters.Add("$borderAnimation", SqliteType.Text);
                var rectWidthP = cmd.Parameters.Add("$rectWidth", SqliteType.Real);
                var rectHeightP = cmd.Parameters.Add("$rectHeight", SqliteType.Real);
                var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);

                foreach (var scene in scenes)
                {
                    var sort = 0;
                    foreach (var config in scene.Elements.OfType<WebcamSceneConfig>())
                    {
                        sceneIdP.Value = scene.Id;
                        webcamIdP.Value = config.WebcamId;
                        visibleP.Value = config.IsVisible ? 1 : 0;
                        xP.Value = config.X;
                        yP.Value = config.Y;
                        wP.Value = config.Width;
                        hP.Value = config.Height;
                        opacityP.Value = config.Opacity;
                        clipP.Value = config.ClipShape.ToString();
                        mirroredP.Value = config.IsMirrored ? 1 : 0;
                        colorP.Value = (object?)(string.IsNullOrEmpty(config.BorderColor) ? null : config.BorderColor) ?? DBNull.Value;
                        borderOpacityP.Value = config.BorderOpacity;
                        borderWidthP.Value = config.BorderWidth;
                        animationP.Value = config.BorderAnimation.ToString();
                        rectWidthP.Value = (object?)config.RectWidth ?? DBNull.Value;
                        rectHeightP.Value = (object?)config.RectHeight ?? DBNull.Value;
                        sortP.Value = sort++;
                        cmd.ExecuteNonQuery();
                    }
                }
            }
        }

        if (socials != null && socials.Entries.Count > 0)
        {
            var socialsId = Guid.NewGuid().ToString();
            using (var cmd = _connection.CreateCommand())
            {
                cmd.CommandText = "INSERT INTO Socials (Id, BarPosition, BarEnabled) VALUES ($id, $pos, $enabled);";
                cmd.Transaction = tx;
                cmd.Parameters.AddWithValue("$id", socialsId);
                cmd.Parameters.AddWithValue("$pos", socials.BarPosition.ToString());
                cmd.Parameters.AddWithValue("$enabled", socials.BarEnabled ? 1 : 0);
                cmd.ExecuteNonQuery();
            }

            using var entryCmd = _connection.CreateCommand();
            entryCmd.CommandText = """
                INSERT INTO SocialEntry (Id, SocialsId, Service, Handle, ProfileUrl, Software, SortOrder)
                VALUES ($id, $socialsId, $service, $handle, $url, $software, $sort)
                """;
            entryCmd.Transaction = tx;
            var eId = entryCmd.Parameters.Add("$id", SqliteType.Text);
            var eSocialsId = entryCmd.Parameters.Add("$socialsId", SqliteType.Text);
            var eService = entryCmd.Parameters.Add("$service", SqliteType.Text);
            var eHandle = entryCmd.Parameters.Add("$handle", SqliteType.Text);
            var eUrl = entryCmd.Parameters.Add("$url", SqliteType.Text);
            var eSoftware = entryCmd.Parameters.Add("$software", SqliteType.Text);
            var eSort = entryCmd.Parameters.Add("$sort", SqliteType.Integer);

            var entrySort = 0;
            foreach (var entry in socials.Entries)
            {
                eId.Value = Guid.NewGuid().ToString();
                eSocialsId.Value = socialsId;
                eService.Value = entry.Service.ToString();
                eHandle.Value = entry.Handle;
                eUrl.Value = entry.ProfileUrl;
                eSoftware.Value = (object?)entry.FediverseSoftware ?? DBNull.Value;
                eSort.Value = entrySort++;
                entryCmd.ExecuteNonQuery();
            }
        }

        if (music != null && !string.IsNullOrWhiteSpace(music.TrackPath))
        {
            using var cmd = _connection.CreateCommand();
            cmd.CommandText = "INSERT INTO Music (Id, TrackPath, IsEnabled) VALUES ($id, $path, $enabled);";
            cmd.Transaction = tx;
            cmd.Parameters.AddWithValue("$id", Guid.NewGuid().ToString());
            cmd.Parameters.AddWithValue("$path", music.TrackPath);
            cmd.Parameters.AddWithValue("$enabled", music.IsEnabled ? 1 : 0);
            cmd.ExecuteNonQuery();
        }

        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM Asset WHERE Id NOT IN (SELECT AssetId FROM Source WHERE AssetId IS NOT NULL);";
            cmd.Transaction = tx;
            cmd.ExecuteNonQuery();
        }

        tx.Commit();
    }

    public byte[]? GetAssetBytes(string assetId)
    {
        using var cmd = _connection.CreateCommand();
        cmd.CommandText = "SELECT Data FROM Asset WHERE Id = $id";
        cmd.Parameters.AddWithValue("$id", assetId);
        return cmd.ExecuteScalar() as byte[];
    }

    public string UpsertAsset(byte[] data, int pixelWidth, int pixelHeight)
    {
        var hash = Convert.ToHexString(SHA256.HashData(data));
        var id = Guid.NewGuid().ToString();

        using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            INSERT INTO Asset (Id, Hash, Data, PixelWidth, PixelHeight)
            VALUES ($id, $hash, $data, $w, $h)
            ON CONFLICT(Hash) DO NOTHING;
            """;
        cmd.Parameters.AddWithValue("$id", id);
        cmd.Parameters.AddWithValue("$hash", hash);
        cmd.Parameters.AddWithValue("$data", data);
        cmd.Parameters.AddWithValue("$w", pixelWidth);
        cmd.Parameters.AddWithValue("$h", pixelHeight);
        cmd.ExecuteNonQuery();

        cmd.CommandText = "SELECT Id FROM Asset WHERE Hash = $hash;";
        return (string)cmd.ExecuteScalar()!;
    }

    public void Dispose()
    {
        _connection.Dispose();
    }
}
