using Microsoft.Data.Sqlite;
using Scry.Core.Models;

namespace Scry.Core.Data;

/// <summary>
/// SQLite database for storing card data and perceptual hashes.
/// Schema mirrors Scryfall's data model: oracles (game cards), sets, and cards (printings).
/// </summary>
public class CardDatabase : IDisposable
{
    private readonly SqliteConnection _connection;
    private readonly string _dbPath;
    
    public CardDatabase(string dbPath)
    {
        _dbPath = dbPath;
        _connection = new SqliteConnection($"Data Source={dbPath}");
        _connection.Open();
        Initialize();
    }
    
    private void Initialize()
    {
        using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            -- Abstract game cards (oracle)
            CREATE TABLE IF NOT EXISTS oracles (
                id TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                mana_cost TEXT,
                cmc REAL,
                type_line TEXT,
                oracle_text TEXT,
                colors TEXT,
                color_identity TEXT,
                keywords TEXT,
                reserved INTEGER DEFAULT 0,
                legalities TEXT,
                power TEXT,
                toughness TEXT
            );
            
            CREATE INDEX IF NOT EXISTS idx_oracles_name ON oracles(name);
            
            -- MTG sets
            CREATE TABLE IF NOT EXISTS sets (
                id TEXT PRIMARY KEY,
                code TEXT NOT NULL UNIQUE,
                name TEXT NOT NULL,
                set_type TEXT,
                released_at TEXT,
                card_count INTEGER,
                icon_svg_uri TEXT,
                digital INTEGER DEFAULT 0,
                parent_set_code TEXT,
                block TEXT
            );
            
            CREATE INDEX IF NOT EXISTS idx_sets_code ON sets(code);
            
            -- Card printings with hashes
            CREATE TABLE IF NOT EXISTS cards (
                id TEXT PRIMARY KEY,
                oracle_id TEXT NOT NULL,
                set_id TEXT NOT NULL,
                set_code TEXT,
                name TEXT NOT NULL,
                collector_number TEXT,
                rarity TEXT,
                artist TEXT,
                illustration_id TEXT,
                image_uri TEXT,
                hash BLOB,
                lang TEXT DEFAULT 'en',
                prices_usd REAL,
                prices_usd_foil REAL,
                FOREIGN KEY (oracle_id) REFERENCES oracles(id),
                FOREIGN KEY (set_id) REFERENCES sets(id)
            );
            
            CREATE INDEX IF NOT EXISTS idx_cards_oracle_id ON cards(oracle_id);
            CREATE INDEX IF NOT EXISTS idx_cards_set_id ON cards(set_id);
            CREATE INDEX IF NOT EXISTS idx_cards_name ON cards(name);
            CREATE INDEX IF NOT EXISTS idx_cards_set_code ON cards(set_code);
            
            -- Metadata for tracking sync state
            CREATE TABLE IF NOT EXISTS metadata (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL
            );
        """;
        cmd.ExecuteNonQuery();
    }
    
    #region Metadata
    
    public async Task<string?> GetMetadataAsync(string key, CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = "SELECT value FROM metadata WHERE key = $key";
        cmd.Parameters.AddWithValue("$key", key);
        
        var result = await cmd.ExecuteScalarAsync(ct);
        return result as string;
    }
    
    public async Task SetMetadataAsync(string key, string value, CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            INSERT OR REPLACE INTO metadata (key, value) VALUES ($key, $value)
        """;
        cmd.Parameters.AddWithValue("$key", key);
        cmd.Parameters.AddWithValue("$value", value);
        await cmd.ExecuteNonQueryAsync(ct);
    }
    
    #endregion
    
    #region Oracles
    
    public async Task InsertOracleAsync(Oracle oracle, CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            INSERT OR REPLACE INTO oracles 
            (id, name, mana_cost, cmc, type_line, oracle_text, colors, color_identity, keywords, reserved, legalities, power, toughness)
            VALUES ($id, $name, $mana_cost, $cmc, $type_line, $oracle_text, $colors, $color_identity, $keywords, $reserved, $legalities, $power, $toughness)
        """;
        cmd.Parameters.AddWithValue("$id", oracle.Id);
        cmd.Parameters.AddWithValue("$name", oracle.Name);
        cmd.Parameters.AddWithValue("$mana_cost", oracle.ManaCost ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$cmc", oracle.Cmc ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$type_line", oracle.TypeLine ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$oracle_text", oracle.OracleText ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$colors", oracle.Colors ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$color_identity", oracle.ColorIdentity ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$keywords", oracle.Keywords ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$reserved", oracle.Reserved ? 1 : 0);
        cmd.Parameters.AddWithValue("$legalities", oracle.Legalities ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$power", oracle.Power ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$toughness", oracle.Toughness ?? (object)DBNull.Value);
        
        await cmd.ExecuteNonQueryAsync(ct);
    }
    
    public async Task InsertOracleBatchAsync(IEnumerable<Oracle> oracles, CancellationToken ct = default)
    {
        await using var transaction = await _connection.BeginTransactionAsync(ct);
        
        try
        {
            await using var cmd = _connection.CreateCommand();
            cmd.CommandText = """
                INSERT OR REPLACE INTO oracles 
                (id, name, mana_cost, cmc, type_line, oracle_text, colors, color_identity, keywords, reserved, legalities, power, toughness)
                VALUES ($id, $name, $mana_cost, $cmc, $type_line, $oracle_text, $colors, $color_identity, $keywords, $reserved, $legalities, $power, $toughness)
            """;
            
            var idParam = cmd.Parameters.Add("$id", SqliteType.Text);
            var nameParam = cmd.Parameters.Add("$name", SqliteType.Text);
            var manaCostParam = cmd.Parameters.Add("$mana_cost", SqliteType.Text);
            var cmcParam = cmd.Parameters.Add("$cmc", SqliteType.Real);
            var typeLineParam = cmd.Parameters.Add("$type_line", SqliteType.Text);
            var oracleTextParam = cmd.Parameters.Add("$oracle_text", SqliteType.Text);
            var colorsParam = cmd.Parameters.Add("$colors", SqliteType.Text);
            var colorIdentityParam = cmd.Parameters.Add("$color_identity", SqliteType.Text);
            var keywordsParam = cmd.Parameters.Add("$keywords", SqliteType.Text);
            var reservedParam = cmd.Parameters.Add("$reserved", SqliteType.Integer);
            var legalitiesParam = cmd.Parameters.Add("$legalities", SqliteType.Text);
            var powerParam = cmd.Parameters.Add("$power", SqliteType.Text);
            var toughnessParam = cmd.Parameters.Add("$toughness", SqliteType.Text);
            
            foreach (var oracle in oracles)
            {
                ct.ThrowIfCancellationRequested();
                
                idParam.Value = oracle.Id;
                nameParam.Value = oracle.Name;
                manaCostParam.Value = oracle.ManaCost ?? (object)DBNull.Value;
                cmcParam.Value = oracle.Cmc ?? (object)DBNull.Value;
                typeLineParam.Value = oracle.TypeLine ?? (object)DBNull.Value;
                oracleTextParam.Value = oracle.OracleText ?? (object)DBNull.Value;
                colorsParam.Value = oracle.Colors ?? (object)DBNull.Value;
                colorIdentityParam.Value = oracle.ColorIdentity ?? (object)DBNull.Value;
                keywordsParam.Value = oracle.Keywords ?? (object)DBNull.Value;
                reservedParam.Value = oracle.Reserved ? 1 : 0;
                legalitiesParam.Value = oracle.Legalities ?? (object)DBNull.Value;
                powerParam.Value = oracle.Power ?? (object)DBNull.Value;
                toughnessParam.Value = oracle.Toughness ?? (object)DBNull.Value;
                
                await cmd.ExecuteNonQueryAsync(ct);
            }
            
            await transaction.CommitAsync(ct);
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }
    
    public async Task<Oracle?> GetOracleByIdAsync(string id, CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            SELECT id, name, mana_cost, cmc, type_line, oracle_text, colors, color_identity, keywords, reserved, legalities, power, toughness
            FROM oracles WHERE id = $id
        """;
        cmd.Parameters.AddWithValue("$id", id);
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        if (await reader.ReadAsync(ct))
        {
            return ReadOracle(reader);
        }
        return null;
    }
    
    public async Task<Oracle?> GetOracleByNameAsync(string name, CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            SELECT id, name, mana_cost, cmc, type_line, oracle_text, colors, color_identity, keywords, reserved, legalities, power, toughness
            FROM oracles WHERE name = $name COLLATE NOCASE
        """;
        cmd.Parameters.AddWithValue("$name", name);
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        if (await reader.ReadAsync(ct))
        {
            return ReadOracle(reader);
        }
        return null;
    }
    
    public async Task<HashSet<string>> GetExistingOracleIdsAsync(CancellationToken ct = default)
    {
        var ids = new HashSet<string>();
        
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = "SELECT id FROM oracles";
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
        {
            ids.Add(reader.GetString(0));
        }
        
        return ids;
    }
    
    public async Task<int> GetOracleCountAsync(CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = "SELECT COUNT(*) FROM oracles";
        var result = await cmd.ExecuteScalarAsync(ct);
        return Convert.ToInt32(result);
    }
    
    private static Oracle ReadOracle(SqliteDataReader reader) => new()
    {
        Id = reader.GetString(0),
        Name = reader.GetString(1),
        ManaCost = reader.IsDBNull(2) ? null : reader.GetString(2),
        Cmc = reader.IsDBNull(3) ? null : reader.GetDouble(3),
        TypeLine = reader.IsDBNull(4) ? null : reader.GetString(4),
        OracleText = reader.IsDBNull(5) ? null : reader.GetString(5),
        Colors = reader.IsDBNull(6) ? null : reader.GetString(6),
        ColorIdentity = reader.IsDBNull(7) ? null : reader.GetString(7),
        Keywords = reader.IsDBNull(8) ? null : reader.GetString(8),
        Reserved = reader.GetInt32(9) != 0,
        Legalities = reader.IsDBNull(10) ? null : reader.GetString(10),
        Power = reader.IsDBNull(11) ? null : reader.GetString(11),
        Toughness = reader.IsDBNull(12) ? null : reader.GetString(12),
    };
    
    #endregion
    
    #region Sets
    
    public async Task InsertSetAsync(Set set, CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            INSERT OR REPLACE INTO sets 
            (id, code, name, set_type, released_at, card_count, icon_svg_uri, digital, parent_set_code, block)
            VALUES ($id, $code, $name, $set_type, $released_at, $card_count, $icon_svg_uri, $digital, $parent_set_code, $block)
        """;
        cmd.Parameters.AddWithValue("$id", set.Id);
        cmd.Parameters.AddWithValue("$code", set.Code);
        cmd.Parameters.AddWithValue("$name", set.Name);
        cmd.Parameters.AddWithValue("$set_type", set.SetType ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$released_at", set.ReleasedAt ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$card_count", set.CardCount ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$icon_svg_uri", set.IconSvgUri ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$digital", set.Digital ? 1 : 0);
        cmd.Parameters.AddWithValue("$parent_set_code", set.ParentSetCode ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$block", set.Block ?? (object)DBNull.Value);
        
        await cmd.ExecuteNonQueryAsync(ct);
    }
    
    public async Task InsertSetBatchAsync(IEnumerable<Set> sets, CancellationToken ct = default)
    {
        await using var transaction = await _connection.BeginTransactionAsync(ct);
        
        try
        {
            await using var cmd = _connection.CreateCommand();
            cmd.CommandText = """
                INSERT OR REPLACE INTO sets 
                (id, code, name, set_type, released_at, card_count, icon_svg_uri, digital, parent_set_code, block)
                VALUES ($id, $code, $name, $set_type, $released_at, $card_count, $icon_svg_uri, $digital, $parent_set_code, $block)
            """;
            
            var idParam = cmd.Parameters.Add("$id", SqliteType.Text);
            var codeParam = cmd.Parameters.Add("$code", SqliteType.Text);
            var nameParam = cmd.Parameters.Add("$name", SqliteType.Text);
            var setTypeParam = cmd.Parameters.Add("$set_type", SqliteType.Text);
            var releasedAtParam = cmd.Parameters.Add("$released_at", SqliteType.Text);
            var cardCountParam = cmd.Parameters.Add("$card_count", SqliteType.Integer);
            var iconSvgUriParam = cmd.Parameters.Add("$icon_svg_uri", SqliteType.Text);
            var digitalParam = cmd.Parameters.Add("$digital", SqliteType.Integer);
            var parentSetCodeParam = cmd.Parameters.Add("$parent_set_code", SqliteType.Text);
            var blockParam = cmd.Parameters.Add("$block", SqliteType.Text);
            
            foreach (var set in sets)
            {
                ct.ThrowIfCancellationRequested();
                
                idParam.Value = set.Id;
                codeParam.Value = set.Code;
                nameParam.Value = set.Name;
                setTypeParam.Value = set.SetType ?? (object)DBNull.Value;
                releasedAtParam.Value = set.ReleasedAt ?? (object)DBNull.Value;
                cardCountParam.Value = set.CardCount ?? (object)DBNull.Value;
                iconSvgUriParam.Value = set.IconSvgUri ?? (object)DBNull.Value;
                digitalParam.Value = set.Digital ? 1 : 0;
                parentSetCodeParam.Value = set.ParentSetCode ?? (object)DBNull.Value;
                blockParam.Value = set.Block ?? (object)DBNull.Value;
                
                await cmd.ExecuteNonQueryAsync(ct);
            }
            
            await transaction.CommitAsync(ct);
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }
    
    public async Task<Set?> GetSetByIdAsync(string id, CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            SELECT id, code, name, set_type, released_at, card_count, icon_svg_uri, digital, parent_set_code, block
            FROM sets WHERE id = $id
        """;
        cmd.Parameters.AddWithValue("$id", id);
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        if (await reader.ReadAsync(ct))
        {
            return ReadSet(reader);
        }
        return null;
    }
    
    public async Task<Set?> GetSetByCodeAsync(string code, CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            SELECT id, code, name, set_type, released_at, card_count, icon_svg_uri, digital, parent_set_code, block
            FROM sets WHERE code = $code COLLATE NOCASE
        """;
        cmd.Parameters.AddWithValue("$code", code);
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        if (await reader.ReadAsync(ct))
        {
            return ReadSet(reader);
        }
        return null;
    }
    
    public async Task<HashSet<string>> GetExistingSetIdsAsync(CancellationToken ct = default)
    {
        var ids = new HashSet<string>();
        
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = "SELECT id FROM sets";
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
        {
            ids.Add(reader.GetString(0));
        }
        
        return ids;
    }
    
    public async Task<int> GetSetCountAsync(CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = "SELECT COUNT(*) FROM sets";
        var result = await cmd.ExecuteScalarAsync(ct);
        return Convert.ToInt32(result);
    }
    
    private static Set ReadSet(SqliteDataReader reader) => new()
    {
        Id = reader.GetString(0),
        Code = reader.GetString(1),
        Name = reader.GetString(2),
        SetType = reader.IsDBNull(3) ? null : reader.GetString(3),
        ReleasedAt = reader.IsDBNull(4) ? null : reader.GetString(4),
        CardCount = reader.IsDBNull(5) ? null : reader.GetInt32(5),
        IconSvgUri = reader.IsDBNull(6) ? null : reader.GetString(6),
        Digital = reader.GetInt32(7) != 0,
        ParentSetCode = reader.IsDBNull(8) ? null : reader.GetString(8),
        Block = reader.IsDBNull(9) ? null : reader.GetString(9),
    };
    
    #endregion
    
    #region Cards (Printings)
    
    public async Task InsertCardAsync(Card card, CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            INSERT OR REPLACE INTO cards 
            (id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil)
            VALUES ($id, $oracle_id, $set_id, $set_code, $name, $collector_number, $rarity, $artist, $illustration_id, $image_uri, $hash, $lang, $prices_usd, $prices_usd_foil)
        """;
        cmd.Parameters.AddWithValue("$id", card.Id);
        cmd.Parameters.AddWithValue("$oracle_id", card.OracleId);
        cmd.Parameters.AddWithValue("$set_id", card.SetId);
        cmd.Parameters.AddWithValue("$set_code", card.SetCode ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$name", card.Name);
        cmd.Parameters.AddWithValue("$collector_number", card.CollectorNumber ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$rarity", card.Rarity ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$artist", card.Artist ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$illustration_id", card.IllustrationId ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$image_uri", card.ImageUri ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$hash", card.Hash ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$lang", card.Lang ?? "en");
        cmd.Parameters.AddWithValue("$prices_usd", card.PricesUsd ?? (object)DBNull.Value);
        cmd.Parameters.AddWithValue("$prices_usd_foil", card.PricesUsdFoil ?? (object)DBNull.Value);
        
        await cmd.ExecuteNonQueryAsync(ct);
    }
    
    public async Task InsertCardBatchAsync(IEnumerable<Card> cards, CancellationToken ct = default)
    {
        await using var transaction = await _connection.BeginTransactionAsync(ct);
        
        try
        {
            await using var cmd = _connection.CreateCommand();
            cmd.CommandText = """
                INSERT OR REPLACE INTO cards 
                (id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil)
                VALUES ($id, $oracle_id, $set_id, $set_code, $name, $collector_number, $rarity, $artist, $illustration_id, $image_uri, $hash, $lang, $prices_usd, $prices_usd_foil)
            """;
            
            var idParam = cmd.Parameters.Add("$id", SqliteType.Text);
            var oracleIdParam = cmd.Parameters.Add("$oracle_id", SqliteType.Text);
            var setIdParam = cmd.Parameters.Add("$set_id", SqliteType.Text);
            var setCodeParam = cmd.Parameters.Add("$set_code", SqliteType.Text);
            var nameParam = cmd.Parameters.Add("$name", SqliteType.Text);
            var collectorNumberParam = cmd.Parameters.Add("$collector_number", SqliteType.Text);
            var rarityParam = cmd.Parameters.Add("$rarity", SqliteType.Text);
            var artistParam = cmd.Parameters.Add("$artist", SqliteType.Text);
            var illustrationIdParam = cmd.Parameters.Add("$illustration_id", SqliteType.Text);
            var imageUriParam = cmd.Parameters.Add("$image_uri", SqliteType.Text);
            var hashParam = cmd.Parameters.Add("$hash", SqliteType.Blob);
            var langParam = cmd.Parameters.Add("$lang", SqliteType.Text);
            var pricesUsdParam = cmd.Parameters.Add("$prices_usd", SqliteType.Real);
            var pricesUsdFoilParam = cmd.Parameters.Add("$prices_usd_foil", SqliteType.Real);
            
            foreach (var card in cards)
            {
                ct.ThrowIfCancellationRequested();
                
                idParam.Value = card.Id;
                oracleIdParam.Value = card.OracleId;
                setIdParam.Value = card.SetId;
                setCodeParam.Value = card.SetCode ?? (object)DBNull.Value;
                nameParam.Value = card.Name;
                collectorNumberParam.Value = card.CollectorNumber ?? (object)DBNull.Value;
                rarityParam.Value = card.Rarity ?? (object)DBNull.Value;
                artistParam.Value = card.Artist ?? (object)DBNull.Value;
                illustrationIdParam.Value = card.IllustrationId ?? (object)DBNull.Value;
                imageUriParam.Value = card.ImageUri ?? (object)DBNull.Value;
                hashParam.Value = card.Hash ?? (object)DBNull.Value;
                langParam.Value = card.Lang ?? "en";
                pricesUsdParam.Value = card.PricesUsd ?? (object)DBNull.Value;
                pricesUsdFoilParam.Value = card.PricesUsdFoil ?? (object)DBNull.Value;
                
                await cmd.ExecuteNonQueryAsync(ct);
            }
            
            await transaction.CommitAsync(ct);
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }
    
    public async Task<Card?> GetCardByIdAsync(string id, CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            SELECT id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil
            FROM cards WHERE id = $id
        """;
        cmd.Parameters.AddWithValue("$id", id);
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        if (await reader.ReadAsync(ct))
        {
            return ReadCard(reader);
        }
        return null;
    }
    
    public async Task<List<Card>> GetCardsByOracleIdAsync(string oracleId, CancellationToken ct = default)
    {
        var cards = new List<Card>();
        
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            SELECT id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil
            FROM cards WHERE oracle_id = $oracle_id
        """;
        cmd.Parameters.AddWithValue("$oracle_id", oracleId);
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
        {
            cards.Add(ReadCard(reader));
        }
        
        return cards;
    }
    
    public async Task<List<Card>> GetCardsByNameAsync(string name, CancellationToken ct = default)
    {
        var cards = new List<Card>();
        
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            SELECT id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil
            FROM cards WHERE name = $name COLLATE NOCASE
        """;
        cmd.Parameters.AddWithValue("$name", name);
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
        {
            cards.Add(ReadCard(reader));
        }
        
        return cards;
    }
    
    public async Task<List<Card>> GetAllCardsAsync(CancellationToken ct = default)
    {
        var cards = new List<Card>();
        
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            SELECT id, oracle_id, set_id, set_code, name, collector_number, rarity, artist, illustration_id, image_uri, hash, lang, prices_usd, prices_usd_foil
            FROM cards
        """;
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
        {
            cards.Add(ReadCard(reader));
        }
        
        return cards;
    }
    
    public async Task<List<Card>> GetCardsWithHashAsync(CancellationToken ct = default)
    {
        var cards = new List<Card>();
        
        await using var cmd = _connection.CreateCommand();
        // Join with oracles and sets to get denormalized data
        cmd.CommandText = """
            SELECT c.id, c.oracle_id, c.set_id, c.set_code, c.name, c.collector_number, c.rarity, c.artist, 
                   c.illustration_id, c.image_uri, c.hash, c.lang, c.prices_usd, c.prices_usd_foil,
                   o.mana_cost, o.type_line, o.oracle_text, o.power, o.toughness,
                   s.name as set_name
            FROM cards c
            LEFT JOIN oracles o ON c.oracle_id = o.id
            LEFT JOIN sets s ON c.set_id = s.id
            WHERE c.hash IS NOT NULL
        """;
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
        {
            cards.Add(ReadCardWithOracle(reader));
        }
        
        return cards;
    }
    
    public async Task<HashSet<string>> GetExistingCardIdsAsync(CancellationToken ct = default)
    {
        var ids = new HashSet<string>();
        
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = "SELECT id FROM cards";
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
        {
            ids.Add(reader.GetString(0));
        }
        
        return ids;
    }
    
    public async Task<HashSet<string>> GetExistingCardNamesAsync(CancellationToken ct = default)
    {
        var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = "SELECT DISTINCT name FROM cards";
        
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
        {
            names.Add(reader.GetString(0));
        }
        
        return names;
    }
    
    public async Task<int> GetCardCountAsync(CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = "SELECT COUNT(*) FROM cards";
        var result = await cmd.ExecuteScalarAsync(ct);
        return Convert.ToInt32(result);
    }
    
    public async Task DeleteCardByIdAsync(string id, CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = "DELETE FROM cards WHERE id = $id";
        cmd.Parameters.AddWithValue("$id", id);
        await cmd.ExecuteNonQueryAsync(ct);
    }
    
    public async Task ClearCardsAsync(CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = "DELETE FROM cards";
        await cmd.ExecuteNonQueryAsync(ct);
    }
    
    public async Task ClearAllAsync(CancellationToken ct = default)
    {
        await using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            DELETE FROM cards;
            DELETE FROM oracles;
            DELETE FROM sets;
            DELETE FROM metadata;
        """;
        await cmd.ExecuteNonQueryAsync(ct);
    }
    
    private static Card ReadCard(SqliteDataReader reader) => new()
    {
        Id = reader.GetString(0),
        OracleId = reader.GetString(1),
        SetId = reader.GetString(2),
        SetCode = reader.IsDBNull(3) ? null : reader.GetString(3),
        Name = reader.GetString(4),
        CollectorNumber = reader.IsDBNull(5) ? null : reader.GetString(5),
        Rarity = reader.IsDBNull(6) ? null : reader.GetString(6),
        Artist = reader.IsDBNull(7) ? null : reader.GetString(7),
        IllustrationId = reader.IsDBNull(8) ? null : reader.GetString(8),
        ImageUri = reader.IsDBNull(9) ? null : reader.GetString(9),
        Hash = reader.IsDBNull(10) ? null : (byte[])reader.GetValue(10),
        Lang = reader.IsDBNull(11) ? null : reader.GetString(11),
        PricesUsd = reader.IsDBNull(12) ? null : (decimal)reader.GetDouble(12),
        PricesUsdFoil = reader.IsDBNull(13) ? null : (decimal)reader.GetDouble(13),
    };
    
    /// <summary>
    /// Reads a card with joined Oracle and Set data (columns 14-19).
    /// </summary>
    private static Card ReadCardWithOracle(SqliteDataReader reader) => new()
    {
        Id = reader.GetString(0),
        OracleId = reader.GetString(1),
        SetId = reader.GetString(2),
        SetCode = reader.IsDBNull(3) ? null : reader.GetString(3),
        Name = reader.GetString(4),
        CollectorNumber = reader.IsDBNull(5) ? null : reader.GetString(5),
        Rarity = reader.IsDBNull(6) ? null : reader.GetString(6),
        Artist = reader.IsDBNull(7) ? null : reader.GetString(7),
        IllustrationId = reader.IsDBNull(8) ? null : reader.GetString(8),
        ImageUri = reader.IsDBNull(9) ? null : reader.GetString(9),
        Hash = reader.IsDBNull(10) ? null : (byte[])reader.GetValue(10),
        Lang = reader.IsDBNull(11) ? null : reader.GetString(11),
        PricesUsd = reader.IsDBNull(12) ? null : (decimal)reader.GetDouble(12),
        PricesUsdFoil = reader.IsDBNull(13) ? null : (decimal)reader.GetDouble(13),
        // Denormalized Oracle fields (from JOIN)
        ManaCost = reader.IsDBNull(14) ? null : reader.GetString(14),
        TypeLine = reader.IsDBNull(15) ? null : reader.GetString(15),
        OracleText = reader.IsDBNull(16) ? null : reader.GetString(16),
        Power = reader.IsDBNull(17) ? null : reader.GetString(17),
        Toughness = reader.IsDBNull(18) ? null : reader.GetString(18),
        SetName = reader.IsDBNull(19) ? null : reader.GetString(19),
    };
    
    #endregion
    
    public void Dispose()
    {
        _connection.Dispose();
    }
}
