namespace NeonSprawl.Server.Game.Mastery;

/// <summary>PostgreSQL-backed perk state keyed by normalized player id (NEO-47).</summary>
public sealed class PostgresPlayerPerkStateStore(Npgsql.NpgsqlDataSource dataSource) : IPlayerPerkStateStore
{
    /// <inheritdoc />
    public bool CanWritePlayer(string playerId)
    {
        var norm = NormalizePlayerId(playerId);
        if (norm.Length == 0)
        {
            return false;
        }

        PostgresPerkStateBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        using var cmd = new Npgsql.NpgsqlCommand(
            "SELECT 1 FROM player_position WHERE player_id = @pid LIMIT 1;",
            conn);
        cmd.Parameters.AddWithValue("pid", norm);
        return cmd.ExecuteScalar() is not null;
    }

    /// <inheritdoc />
    public PerkStateSnapshot GetSnapshot(string playerId)
    {
        var norm = NormalizePlayerId(playerId);
        if (norm.Length == 0)
        {
            return PerkStateSnapshot.Empty;
        }

        PostgresPerkStateBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();

        var branchPicks = new Dictionary<string, Dictionary<int, string>>(StringComparer.Ordinal);
        using (var pickCmd = new Npgsql.NpgsqlCommand(
                   """
                   SELECT skill_id, tier_index, branch_id
                   FROM player_mastery_branch_pick
                   WHERE player_id = @pid;
                   """,
                   conn))
        {
            pickCmd.Parameters.AddWithValue("pid", norm);
            using var reader = pickCmd.ExecuteReader();
            while (reader.Read())
            {
                var skillId = reader.GetString(0);
                var tierIndex = reader.GetInt32(1);
                var branchId = reader.GetString(2);
                if (!branchPicks.TryGetValue(skillId, out var tiers))
                {
                    tiers = new Dictionary<int, string>();
                    branchPicks[skillId] = tiers;
                }

                tiers[tierIndex] = branchId;
            }
        }

        var bySkill = new Dictionary<string, IReadOnlyDictionary<int, string>>(StringComparer.Ordinal);
        foreach (var (skillId, tiers) in branchPicks)
        {
            bySkill[skillId] = tiers;
        }

        var unlocked = new HashSet<string>(StringComparer.Ordinal);
        using (var perkCmd = new Npgsql.NpgsqlCommand(
                   """
                   SELECT perk_id
                   FROM player_unlocked_perk
                   WHERE player_id = @pid;
                   """,
                   conn))
        {
            perkCmd.Parameters.AddWithValue("pid", norm);
            using var reader = perkCmd.ExecuteReader();
            while (reader.Read())
            {
                unlocked.Add(reader.GetString(0));
            }
        }

        return new PerkStateSnapshot(bySkill, unlocked);
    }

    /// <inheritdoc />
    public bool TrySetBranchPick(string playerId, string skillId, int tierIndex, string branchId)
    {
        var norm = NormalizePlayerId(playerId);
        var sid = skillId.Trim();
        var bid = branchId.Trim();
        if (norm.Length == 0 || sid.Length == 0 || bid.Length == 0 || tierIndex < 1)
        {
            return false;
        }

        PostgresPerkStateBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        using var tx = conn.BeginTransaction();
        if (!PlayerExists(conn, norm, tx))
        {
            tx.Rollback();
            return false;
        }

        using (var exists = new Npgsql.NpgsqlCommand(
                   """
                   SELECT 1
                   FROM player_mastery_branch_pick
                   WHERE player_id = @pid AND skill_id = @sid AND tier_index = @tier
                   LIMIT 1;
                   """,
                   conn,
                   tx))
        {
            exists.Parameters.AddWithValue("pid", norm);
            exists.Parameters.AddWithValue("sid", sid);
            exists.Parameters.AddWithValue("tier", tierIndex);
            if (exists.ExecuteScalar() is not null)
            {
                tx.Rollback();
                return false;
            }
        }

        using var insert = new Npgsql.NpgsqlCommand(
            """
            INSERT INTO player_mastery_branch_pick (player_id, skill_id, tier_index, branch_id, updated_at)
            VALUES (@pid, @sid, @tier, @bid, now());
            """,
            conn,
            tx);
        insert.Parameters.AddWithValue("pid", norm);
        insert.Parameters.AddWithValue("sid", sid);
        insert.Parameters.AddWithValue("tier", tierIndex);
        insert.Parameters.AddWithValue("bid", bid);
        insert.ExecuteNonQuery();
        tx.Commit();
        return true;
    }

    /// <inheritdoc />
    public bool TryAddUnlockedPerks(string playerId, IEnumerable<string> perkIds)
    {
        var norm = NormalizePlayerId(playerId);
        if (norm.Length == 0)
        {
            return false;
        }

        var toAdd = new List<string>();
        foreach (var raw in perkIds)
        {
            var pid = raw.Trim();
            if (pid.Length > 0)
            {
                toAdd.Add(pid);
            }
        }

        if (toAdd.Count == 0)
        {
            return true;
        }

        PostgresPerkStateBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        using var tx = conn.BeginTransaction();
        if (!PlayerExists(conn, norm, tx))
        {
            tx.Rollback();
            return false;
        }

        foreach (var perkId in toAdd)
        {
            using var upsert = new Npgsql.NpgsqlCommand(
                """
                INSERT INTO player_unlocked_perk (player_id, perk_id, unlocked_at)
                VALUES (@pid, @perk, now())
                ON CONFLICT (player_id, perk_id) DO NOTHING;
                """,
                conn,
                tx);
            upsert.Parameters.AddWithValue("pid", norm);
            upsert.Parameters.AddWithValue("perk", perkId);
            upsert.ExecuteNonQuery();
        }

        tx.Commit();
        return true;
    }

    /// <inheritdoc />
    public bool TryRemoveBranchPick(string playerId, string skillId, int tierIndex)
    {
        var norm = NormalizePlayerId(playerId);
        var sid = skillId.Trim();
        if (norm.Length == 0 || sid.Length == 0 || tierIndex < 1)
        {
            return false;
        }

        PostgresPerkStateBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        using var cmd = new Npgsql.NpgsqlCommand(
            """
            DELETE FROM player_mastery_branch_pick
            WHERE player_id = @pid AND skill_id = @sid AND tier_index = @tier;
            """,
            conn);
        cmd.Parameters.AddWithValue("pid", norm);
        cmd.Parameters.AddWithValue("sid", sid);
        cmd.Parameters.AddWithValue("tier", tierIndex);
        return cmd.ExecuteNonQuery() > 0;
    }

    /// <inheritdoc />
    public bool TryResetPerkState(string playerId)
    {
        var norm = NormalizePlayerId(playerId);
        if (norm.Length == 0)
        {
            return false;
        }

        PostgresPerkStateBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        using var tx = conn.BeginTransaction();
        if (!PlayerExists(conn, norm, tx))
        {
            tx.Rollback();
            return false;
        }

        using (var picks = new Npgsql.NpgsqlCommand(
                   "DELETE FROM player_mastery_branch_pick WHERE player_id = @pid;",
                   conn,
                   tx))
        {
            picks.Parameters.AddWithValue("pid", norm);
            picks.ExecuteNonQuery();
        }

        using (var perks = new Npgsql.NpgsqlCommand(
                   "DELETE FROM player_unlocked_perk WHERE player_id = @pid;",
                   conn,
                   tx))
        {
            perks.Parameters.AddWithValue("pid", norm);
            perks.ExecuteNonQuery();
        }

        tx.Commit();
        return true;
    }

    private static bool PlayerExists(Npgsql.NpgsqlConnection conn, string playerIdNormalized, Npgsql.NpgsqlTransaction tx)
    {
        using var cmd = new Npgsql.NpgsqlCommand(
            "SELECT 1 FROM player_position WHERE player_id = @pid LIMIT 1;",
            conn,
            tx);
        cmd.Parameters.AddWithValue("pid", playerIdNormalized);
        return cmd.ExecuteScalar() is not null;
    }

    private static string NormalizePlayerId(string? playerId)
    {
        var t = playerId?.Trim();
        if (string.IsNullOrEmpty(t))
        {
            return string.Empty;
        }

        return t.ToLowerInvariant();
    }
}
