using Microsoft.Data.Sqlite;
using YMhut.Box.Core.App;

namespace YMhut.Box.Core.Plugins;

public interface IPluginStateStore
{
    string DatabasePath { get; }

    Task<PluginRuntimeState> GetStateAsync(string pluginId, CancellationToken cancellationToken = default);

    Task SetEnabledAsync(string pluginId, bool enabled, CancellationToken cancellationToken = default);

    Task SetPermissionAsync(string pluginId, PluginPermission permission, bool granted, CancellationToken cancellationToken = default);

    Task SetSurfaceMountedAsync(string pluginId, string surfaceId, bool mounted, CancellationToken cancellationToken = default);

    Task MarkRunAsync(string pluginId, CancellationToken cancellationToken = default);

    Task<string?> GetValueAsync(string pluginId, string key, CancellationToken cancellationToken = default);

    Task SetValueAsync(string pluginId, string key, string value, CancellationToken cancellationToken = default);

    Task RemoveValueAsync(string pluginId, string key, CancellationToken cancellationToken = default);

    Task<IReadOnlyDictionary<string, string>> ListValuesAsync(string pluginId, CancellationToken cancellationToken = default);
}

public sealed class PluginStateStore : IPluginStateStore
{
    private readonly SemaphoreSlim _gate = new(1, 1);
    private bool _initialized;

    public PluginStateStore(AppPaths paths)
    {
        paths.EnsureCreated();
        DatabasePath = Path.Combine(paths.Data, "plugins.db");
    }

    public string DatabasePath { get; }

    public async Task<PluginRuntimeState> GetStateAsync(string pluginId, CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();

            var enabled = false;
            DateTimeOffset? lastRun = null;
            await using (var command = connection.CreateCommand())
            {
                command.CommandText = "SELECT enabled, last_run_at FROM plugin_states WHERE plugin_id = $plugin_id;";
                command.Parameters.AddWithValue("$plugin_id", pluginId);
                await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
                if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
                {
                    enabled = reader.GetInt32(0) != 0;
                    lastRun = reader.IsDBNull(1) ? null : DateTimeOffset.Parse(reader.GetString(1));
                }
            }

            var permissions = await ReadStringSetAsync(connection, "plugin_permissions", "permission", pluginId, cancellationToken).ConfigureAwait(false);
            var surfaces = await ReadStringSetAsync(connection, "plugin_surfaces", "surface_id", pluginId, cancellationToken).ConfigureAwait(false);
            return new PluginRuntimeState(
                pluginId,
                enabled,
                permissions.Select(Enum.Parse<PluginPermission>).ToHashSet(),
                surfaces.ToHashSet(StringComparer.OrdinalIgnoreCase),
                lastRun);
        }
        finally
        {
            _gate.Release();
        }
    }

    public Task SetEnabledAsync(string pluginId, bool enabled, CancellationToken cancellationToken = default)
        => UpsertStateAsync(pluginId, enabled: enabled, markRun: false, cancellationToken);

    public async Task SetPermissionAsync(string pluginId, PluginPermission permission, bool granted, CancellationToken cancellationToken = default)
    {
        await SetStringFlagAsync("plugin_permissions", "permission", pluginId, permission.ToString(), granted, cancellationToken).ConfigureAwait(false);
    }

    public async Task SetSurfaceMountedAsync(string pluginId, string surfaceId, bool mounted, CancellationToken cancellationToken = default)
    {
        await SetStringFlagAsync("plugin_surfaces", "surface_id", pluginId, surfaceId, mounted, cancellationToken).ConfigureAwait(false);
    }

    public Task MarkRunAsync(string pluginId, CancellationToken cancellationToken = default)
        => UpsertStateAsync(pluginId, enabled: null, markRun: true, cancellationToken);

    public async Task<string?> GetValueAsync(string pluginId, string key, CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = "SELECT value FROM plugin_kv WHERE plugin_id = $plugin_id AND key = $key;";
            command.Parameters.AddWithValue("$plugin_id", pluginId);
            command.Parameters.AddWithValue("$key", key);
            var value = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
            return value as string;
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task SetValueAsync(string pluginId, string key, string value, CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = """
                INSERT INTO plugin_kv(plugin_id, key, value)
                VALUES ($plugin_id, $key, $value)
                ON CONFLICT(plugin_id, key) DO UPDATE SET value = excluded.value;
                """;
            command.Parameters.AddWithValue("$plugin_id", pluginId);
            command.Parameters.AddWithValue("$key", key);
            command.Parameters.AddWithValue("$value", value);
            await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task RemoveValueAsync(string pluginId, string key, CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = "DELETE FROM plugin_kv WHERE plugin_id = $plugin_id AND key = $key;";
            command.Parameters.AddWithValue("$plugin_id", pluginId);
            command.Parameters.AddWithValue("$key", key);
            await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task<IReadOnlyDictionary<string, string>> ListValuesAsync(string pluginId, CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = "SELECT key, value FROM plugin_kv WHERE plugin_id = $plugin_id ORDER BY key;";
            command.Parameters.AddWithValue("$plugin_id", pluginId);
            var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
            while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                values[reader.GetString(0)] = reader.GetString(1);
            }

            return values;
        }
        finally
        {
            _gate.Release();
        }
    }

    private async Task UpsertStateAsync(string pluginId, bool? enabled, bool markRun, CancellationToken cancellationToken)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = """
                INSERT INTO plugin_states(plugin_id, enabled, last_run_at)
                VALUES ($plugin_id, $enabled, $last_run_at)
                ON CONFLICT(plugin_id) DO UPDATE SET
                    enabled = CASE WHEN $enabled_set = 1 THEN excluded.enabled ELSE plugin_states.enabled END,
                    last_run_at = CASE WHEN $mark_run = 1 THEN excluded.last_run_at ELSE plugin_states.last_run_at END;
                """;
            command.Parameters.AddWithValue("$plugin_id", pluginId);
            command.Parameters.AddWithValue("$enabled", enabled == true ? 1 : 0);
            command.Parameters.AddWithValue("$enabled_set", enabled is null ? 0 : 1);
            command.Parameters.AddWithValue("$mark_run", markRun ? 1 : 0);
            command.Parameters.AddWithValue("$last_run_at", DateTimeOffset.Now.ToString("O"));
            await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    private async Task SetStringFlagAsync(string table, string column, string pluginId, string value, bool enabled, CancellationToken cancellationToken)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = enabled
                ? $"INSERT OR IGNORE INTO {table}(plugin_id, {column}) VALUES ($plugin_id, $value);"
                : $"DELETE FROM {table} WHERE plugin_id = $plugin_id AND {column} = $value;";
            command.Parameters.AddWithValue("$plugin_id", pluginId);
            command.Parameters.AddWithValue("$value", value);
            await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    private static async Task<IReadOnlyList<string>> ReadStringSetAsync(SqliteConnection connection, string table, string column, string pluginId, CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = $"SELECT {column} FROM {table} WHERE plugin_id = $plugin_id;";
        command.Parameters.AddWithValue("$plugin_id", pluginId);
        var values = new List<string>();
        await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
        {
            values.Add(reader.GetString(0));
        }

        return values;
    }

    private async Task EnsureInitializedAsync(CancellationToken cancellationToken)
    {
        if (_initialized)
        {
            return;
        }

        Directory.CreateDirectory(Path.GetDirectoryName(DatabasePath)!);
        await using var connection = OpenConnection();
        await using var command = connection.CreateCommand();
        command.CommandText = """
            CREATE TABLE IF NOT EXISTS plugin_states (
                plugin_id TEXT PRIMARY KEY,
                enabled INTEGER NOT NULL DEFAULT 0,
                last_run_at TEXT NULL
            );
            CREATE TABLE IF NOT EXISTS plugin_permissions (
                plugin_id TEXT NOT NULL,
                permission TEXT NOT NULL,
                PRIMARY KEY(plugin_id, permission)
            );
            CREATE TABLE IF NOT EXISTS plugin_surfaces (
                plugin_id TEXT NOT NULL,
                surface_id TEXT NOT NULL,
                PRIMARY KEY(plugin_id, surface_id)
            );
            CREATE TABLE IF NOT EXISTS plugin_kv (
                plugin_id TEXT NOT NULL,
                key TEXT NOT NULL,
                value TEXT NOT NULL,
                PRIMARY KEY(plugin_id, key)
            );
            """;
        await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
        _initialized = true;
    }

    private SqliteConnection OpenConnection()
    {
        var connection = new SqliteConnection($"Data Source={DatabasePath}");
        connection.Open();
        return connection;
    }
}
