Bridge.Database.AfterMigration("sky_phone", function()
local genders = { woman = true, man = true, nonbinary = true }
local interests = { woman = true, man = true, nonbinary = true, everyone = true }
local looking_for = { longTerm = true, dates = true, friends = true }
local message_types = { text = true, image = true, gif = true, video = true, share = true }

local function allowed_gif_url(value)
    if type(value) ~= "string" or #value == 0 or #value > Config.Media.UrlMaxLength then
        return false
    end
    local host = value:lower():match("^https://([^/:?#]+)")
    if not host then
        return false
    end
    for _, allowed_host in ipairs(Config.Media.AllowedGifHosts) do
        local suffix = "." .. allowed_host
        if host == allowed_host or host:sub(-#suffix) == suffix then
            return true
        end
    end
    return false
end

local function is_enabled(value)
    return value == true or tonumber(value) == 1
end

local function text_length(value)
    return type(value) == "string" and utf8.len(value) or nil
end

local function trim(value)
    if type(value) ~= "string" then
        return nil
    end
    return value:match("^%s*(.-)%s*$")
end

local function affected_rows(result)
    if type(result) == "number" then
        return result
    end
    return type(result) == "table" and tonumber(result.affectedRows) or 0
end

local function uuid()
    local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {})
    if not rows[1] or type(rows[1].id) ~= "string" then
        error("[sky_phone] Database did not generate a Flare UUID.")
    end
    return rows[1].id
end

local function decode_interests(value)
    if type(value) ~= "string" or value == "" then
        return {}
    end
    local decoded = json.decode(value)
    if type(decoded) ~= "table" then
        error("[sky_phone] Invalid Flare interests JSON.")
    end
    local result = {}
    for _, interest in ipairs(decoded) do
        if type(interest) == "string" then
            result[#result + 1] = interest
        end
    end
    return result
end

local function attach_profile_photos(rows, id_field)
    id_field = id_field or "id"
    local profile_ids = {}
    local seen = {}
    for _, row in ipairs(rows) do
        local profile_id = tonumber(row[id_field])
        if profile_id and not seen[profile_id] then
            seen[profile_id] = true
            profile_ids[#profile_ids + 1] = profile_id
        end
    end
    if #profile_ids == 0 then
        return rows
    end

    local placeholders = {}
    for index = 1, #profile_ids do
        placeholders[index] = "?"
    end
    local photo_rows = Bridge.Database.Query(([[
        SELECT photo.`profile_id`, photo.`media_id`, media.`url`
        FROM `sky_phone_flare_profile_photos` photo
        JOIN `sky_phone_flare_profiles` profile ON profile.`id` = photo.`profile_id`
        JOIN `sky_phone_media` media
            ON media.`id` = photo.`media_id`
            AND media.`account_id` = profile.`account_id`
            AND media.`media_type` = 'photo'
        WHERE photo.`profile_id` IN (%s)
            AND media.`url` LIKE 'https://%%'
        ORDER BY photo.`profile_id`, photo.`sort_order`, photo.`id`
    ]]):format(table.concat(placeholders, ", ")), profile_ids)
    local photos_by_profile = {}
    for _, photo in ipairs(photo_rows) do
        local profile_id = tonumber(photo.profile_id)
        if profile_id and type(photo.url) == "string" then
            local bucket = photos_by_profile[profile_id]
            if not bucket then
                bucket = { media_ids = {}, urls = {} }
                photos_by_profile[profile_id] = bucket
            end
            bucket.media_ids[#bucket.media_ids + 1] = tonumber(photo.media_id)
            bucket.urls[#bucket.urls + 1] = photo.url
        end
    end
    for _, row in ipairs(rows) do
        local bucket = photos_by_profile[tonumber(row[id_field])] or { media_ids = {}, urls = {} }
        row.photo_media_ids = bucket.media_ids
        row.photo_urls = bucket.urls
    end
    return rows
end

local function profile_payload(row, include_preferences)
    local payload = {
        id = tonumber(row.id),
        name = row.name,
        age = tonumber(row.age),
        bio = row.bio,
        gender = row.gender,
        avatar = tonumber(row.avatar),
        interests = decode_interests(row.interests),
        lookingFor = row.looking_for,
        photoUrls = row.photo_urls or {},
    }
    if include_preferences then
        payload.discoverable = is_enabled(row.discoverable)
        payload.interestedIn = row.interested_in
        payload.minAge = tonumber(row.min_age)
        payload.maxAge = tonumber(row.max_age)
        payload.photoMediaIds = row.photo_media_ids or {}
    end
    return payload
end

local function load_profile(account_id)
    local rows = Bridge.Database.Query([[
        SELECT `id`, `name`, `age`, `bio`, `gender`, `interested_in`, `min_age`, `max_age`,
            `discoverable`,
            `avatar`, `interests`, `looking_for`
        FROM `sky_phone_flare_profiles`
        WHERE `account_id` = ? LIMIT 1
    ]], { account_id })
    attach_profile_photos(rows)
    return rows[1]
end

local function list_suggestions(account_id, profile)
    if not profile
        or not is_enabled(profile.discoverable)
        or type(profile.photo_urls) ~= "table"
        or #profile.photo_urls < 1
    then
        return {}
    end
    local rows = Bridge.Database.Query([[
        SELECT target.`id`, target.`name`, target.`age`, target.`bio`, target.`gender`,
            target.`avatar`, target.`interests`, target.`looking_for`
        FROM `sky_phone_flare_profiles` mine
        JOIN `sky_phone_flare_profiles` target ON target.`account_id` <> mine.`account_id`
        LEFT JOIN `sky_phone_flare_swipes` swipe
            ON swipe.`swiper_account_id` = mine.`account_id`
            AND swipe.`target_account_id` = target.`account_id`
        LEFT JOIN `sky_phone_flare_matches` matched
            ON (matched.`account_a_id` = mine.`account_id` AND matched.`account_b_id` = target.`account_id`)
            OR (matched.`account_b_id` = mine.`account_id` AND matched.`account_a_id` = target.`account_id`)
        WHERE mine.`account_id` = ?
            AND mine.`discoverable` = 1
            AND target.`discoverable` = 1
            AND swipe.`id` IS NULL
            AND matched.`id` IS NULL
            AND target.`age` BETWEEN mine.`min_age` AND mine.`max_age`
            AND mine.`age` BETWEEN target.`min_age` AND target.`max_age`
            AND (mine.`interested_in` = 'everyone' OR mine.`interested_in` = target.`gender`)
            AND (target.`interested_in` = 'everyone' OR target.`interested_in` = mine.`gender`)
            AND EXISTS (
                SELECT 1
                FROM `sky_phone_flare_profile_photos` target_photo
                JOIN `sky_phone_media` target_media
                    ON target_media.`id` = target_photo.`media_id`
                    AND target_media.`account_id` = target.`account_id`
                    AND target_media.`media_type` = 'photo'
                WHERE target_photo.`profile_id` = target.`id`
                    AND target_media.`url` LIKE 'https://%'
            )
        ORDER BY target.`updated_at` DESC, target.`id`
        LIMIT 30
    ]], { account_id })
    attach_profile_photos(rows)
    local suggestions = {}
    for _, row in ipairs(rows) do
        suggestions[#suggestions + 1] = profile_payload(row, false)
    end
    return suggestions
end

local function list_likes(account_id)
    local rows = Bridge.Database.Query([[
        SELECT profile.`id`, profile.`name`, profile.`age`, profile.`bio`, profile.`gender`,
            profile.`avatar`, profile.`interests`, profile.`looking_for`, incoming.`choice`
        FROM `sky_phone_flare_swipes` incoming
        JOIN `sky_phone_flare_profiles` profile
            ON profile.`account_id` = incoming.`swiper_account_id`
        LEFT JOIN `sky_phone_flare_swipes` response
            ON response.`swiper_account_id` = incoming.`target_account_id`
            AND response.`target_account_id` = incoming.`swiper_account_id`
        LEFT JOIN `sky_phone_flare_matches` matched
            ON (matched.`account_a_id` = incoming.`swiper_account_id`
                AND matched.`account_b_id` = incoming.`target_account_id`)
            OR (matched.`account_b_id` = incoming.`swiper_account_id`
                AND matched.`account_a_id` = incoming.`target_account_id`)
        WHERE incoming.`target_account_id` = ?
            AND incoming.`choice` IN ('like', 'superlike')
            AND response.`id` IS NULL
            AND matched.`id` IS NULL
        ORDER BY incoming.`choice` = 'superlike' DESC, incoming.`updated_at` DESC
        LIMIT 100
    ]], { account_id })
    attach_profile_photos(rows)
    local likes = {}
    for _, row in ipairs(rows) do
        local payload = profile_payload(row, false)
        payload.superLiked = row.choice == "superlike"
        likes[#likes + 1] = payload
    end
    return likes
end

local function list_matches(account_id)
    local rows = Bridge.Database.Query([[
        SELECT match_row.`id`, match_row.`created_at`,
            profile.`id` AS `profile_id`, profile.`name`, profile.`age`, profile.`bio`,
            profile.`gender`, profile.`avatar`, profile.`interests`, profile.`looking_for`,
            latest.`body` AS `last_message`, latest.`message_type` AS `last_message_type`,
            UNIX_TIMESTAMP(latest.`created_at`) * 1000 AS `last_message_at`,
            (SELECT COUNT(*) FROM `sky_phone_flare_messages` unread
                WHERE unread.`match_id` = match_row.`id`
                    AND unread.`sender_account_id` <> ?
                    AND unread.`read_at` IS NULL) AS `unread`
        FROM `sky_phone_flare_matches` match_row
        JOIN `sky_phone_flare_profiles` profile ON profile.`account_id` =
            IF(match_row.`account_a_id` = ?, match_row.`account_b_id`, match_row.`account_a_id`)
        LEFT JOIN `sky_phone_flare_messages` latest ON latest.`id` = (
            SELECT message.`id` FROM `sky_phone_flare_messages` message
            WHERE message.`match_id` = match_row.`id`
            ORDER BY message.`created_at` DESC, message.`id` DESC LIMIT 1
        )
        WHERE match_row.`account_a_id` = ? OR match_row.`account_b_id` = ?
        ORDER BY COALESCE(latest.`created_at`, match_row.`created_at`) DESC
        LIMIT 100
    ]], { account_id, account_id, account_id, account_id })
    attach_profile_photos(rows, "profile_id")
    local matches = {}
    for _, row in ipairs(rows) do
        matches[#matches + 1] = {
            id = row.id,
            profile = profile_payload({
                id = row.profile_id,
                name = row.name,
                age = row.age,
                bio = row.bio,
                gender = row.gender,
                avatar = row.avatar,
                interests = row.interests,
                looking_for = row.looking_for,
                photo_urls = row.photo_urls,
            }, false),
            lastMessage = row.last_message or "",
            lastMessageAt = tonumber(row.last_message_at),
            lastMessageType = row.last_message_type,
            unread = tonumber(row.unread) or 0,
        }
    end
    return matches
end

local function bootstrap(account_id)
    local profile = load_profile(account_id)
    return {
        profile = profile and profile_payload(profile, true) or nil,
        suggestions = list_suggestions(account_id, profile),
        likes = list_likes(account_id),
        matches = list_matches(account_id),
    }
end

local function validate_profile(source, data)
    if type(data) ~= "table" then
        return nil
    end
    local name = trim(data.name)
    local bio = trim(data.bio) or ""
    local name_length = text_length(name)
    local bio_length = text_length(bio)
    local age = math.floor(tonumber(data.age) or 0)
    local min_age = math.floor(tonumber(data.minAge) or 0)
    local max_age = math.floor(tonumber(data.maxAge) or 0)
    local avatar = math.floor(tonumber(data.avatar) or -1)
    if not name_length or name_length < 2 or name_length > 32
        or not bio_length or bio_length > 300
        or age < 18 or age > 99
        or min_age < 18 or min_age > 99
        or max_age < min_age or max_age > 99
        or avatar < 0 or avatar > 5
        or not genders[data.gender]
        or not interests[data.interestedIn]
        or not looking_for[data.lookingFor]
    then
        return nil
    end
    local clean_interests = {}
    if type(data.interests) == "table" then
        for _, value in ipairs(data.interests) do
            local interest = trim(value)
            local length = text_length(interest)
            if length and length > 0 and length <= 24 and #clean_interests < 5 then
                clean_interests[#clean_interests + 1] = interest
            end
        end
    end
    if type(data.photoMediaIds) ~= "table"
        or #data.photoMediaIds < 1
        or #data.photoMediaIds > 6
    then
        return nil, "invalid_profile_photos"
    end
    local photo_media_ids = {}
    local seen_media = {}
    for key in pairs(data.photoMediaIds) do
        if type(key) ~= "number" or key < 1 or key > #data.photoMediaIds
            or key ~= math.floor(key)
        then
            return nil, "invalid_profile_photos"
        end
    end
    for _, value in ipairs(data.photoMediaIds) do
        local media_id = tonumber(value)
        if not media_id or media_id < 1 or media_id ~= math.floor(media_id)
            or seen_media[media_id]
            or not SkyPhoneMedia.ResolveOwnedMedia(source, media_id, "photo")
        then
            return nil, "invalid_profile_photos"
        end
        seen_media[media_id] = true
        photo_media_ids[#photo_media_ids + 1] = media_id
    end
    return {
        name = name,
        age = age,
        bio = bio,
        gender = data.gender,
        interested_in = data.interestedIn,
        min_age = min_age,
        max_age = max_age,
        avatar = avatar,
        interests = json.encode(clean_interests),
        looking_for = data.lookingFor,
        photo_media_ids = photo_media_ids,
        replace_photos = true,
    }, nil
end

local function load_match(account_id, match_id)
    if type(match_id) ~= "string" or #match_id ~= 36 then
        return nil
    end
    local rows = Bridge.Database.Query([[
        SELECT `id`, `account_a_id`, `account_b_id`
        FROM `sky_phone_flare_matches`
        WHERE `id` = ? AND (`account_a_id` = ? OR `account_b_id` = ?)
        LIMIT 1
    ]], { match_id, account_id, account_id })
    return rows[1]
end

local function validate_message(source, data)
    if type(data) ~= "table" then
        return nil, "invalid_message"
    end
    local message_type = data.messageType or "text"
    if not message_types[message_type] then
        return nil, "invalid_message"
    end
    if message_type == "text" then
        local body = trim(data.body)
        local length = text_length(body)
        if not length or length < 1 or length > 1000 then
            return nil, "invalid_message"
        end
        return { body = body, message_type = message_type }
    end

    if message_type == "share" then
        local share, share_error, encoded = SkyPhoneEasyShare.SanitizeChatPayload(source, data.sharePayload)
        if not share then
            return nil, share_error or "invalid_payload"
        end
        local body = trim(data.body) or ""
        if #body > 1000 then
            return nil, "invalid_message"
        end
        return {
            body = body ~= "" and body or share.title,
            message_type = message_type,
            share_payload = encoded,
        }
    end

    if type(data.mediaAssetId) ~= "string" then
        return nil, "invalid_attachment"
    end
    local media_url
    if message_type == "gif" then
        if not allowed_gif_url(data.mediaAssetId) then
            return nil, "invalid_attachment"
        end
        media_url = data.mediaAssetId
    else
        local media_id = tonumber(data.mediaAssetId)
        local expected_type = message_type == "image" and "photo" or "video"
        if not media_id or media_id < 1 or media_id ~= math.floor(media_id) then
            return nil, "invalid_attachment"
        end
        media_url = SkyPhoneMedia.ResolveOwnedMedia(source, media_id, expected_type)
        if type(media_url) ~= "string" or #media_url > Config.Media.UrlMaxLength
            or not media_url:match("^https://")
        then
            Bridge.Debug("warn", ("[sky_phone] Rejected unowned Flare media from source %s."):format(tostring(source)))
            return nil, "invalid_attachment"
        end
    end

    local duration = nil
    if message_type == "video" and data.mediaDurationMs ~= nil then
        duration = tonumber(data.mediaDurationMs)
        if not duration or duration < 1000 or duration > Config.Messages.VideoMaxDurationMs then
            return nil, "invalid_attachment"
        end
        duration = math.floor(duration)
    end
    return {
        body = "",
        media_duration_ms = duration,
        media_url = media_url,
        message_type = message_type,
    }
end

local function message_payload(row, account_id)
    local share_payload = row.message_type == "share" and json.decode(row.share_payload or "") or nil
    if row.message_type == "share" and type(share_payload) ~= "table" then
        error(("[sky_phone] Flare message %s has an invalid share payload."):format(tostring(row.id)))
    end
    return {
        id = row.id,
        direction = tonumber(row.sender_account_id) == tonumber(account_id) and "sent" or "received",
        body = row.body or "",
        createdAt = tonumber(row.created_at_ms),
        messageType = row.message_type or "text",
        mediaUrl = row.media_url,
        mediaDurationMs = tonumber(row.media_duration_ms),
        sharePayload = share_payload,
    }
end

Bridge.Callbacks.Register("sky_phone:flare:bootstrap", function(source)
    local account, error_response = SkyPhone.RequireAccount(source)
    if not account then
        return error_response
    end
    return { success = true, data = bootstrap(account.id) }
end)

Bridge.Callbacks.Register("sky_phone:flare:delete-profile", function(source)
    if not SkyPhone.AllowOperation(source, "flare_profile_delete", 3, 60) then
        return { success = false, error = "rate_limited" }
    end
    local account, error_response = SkyPhone.RequireAccount(source)
    if not account then
        return error_response
    end
    if not load_profile(account.id) then
        return { success = false, error = "profile_not_found" }
    end
    if not Bridge.Database.Transaction({
        {
            query = [[
                DELETE FROM `sky_phone_flare_matches`
                WHERE `account_a_id` = ? OR `account_b_id` = ?
            ]],
            params = { account.id, account.id },
        },
        {
            query = [[
                DELETE FROM `sky_phone_flare_swipes`
                WHERE `swiper_account_id` = ? OR `target_account_id` = ?
            ]],
            params = { account.id, account.id },
        },
        {
            query = "DELETE FROM `sky_phone_flare_profiles` WHERE `account_id` = ?",
            params = { account.id },
        },
    }) then
        return { success = false, error = "request_failed" }
    end
    return { success = true }
end)

Bridge.Callbacks.Register("sky_phone:flare:save-profile", function(source, data)
    if not SkyPhone.AllowOperation(source, "flare_profile", 12, 60) then
        return { success = false, error = "rate_limited" }
    end
    local account, error_response = SkyPhone.RequireAccount(source)
    if not account then
        return error_response
    end
    local profile, validation_error = validate_profile(source, data)
    if not profile then
        return { success = false, error = validation_error or "invalid_profile" }
    end
    local statements = {{
        query = [[
            INSERT INTO `sky_phone_flare_profiles`
                (`account_id`, `name`, `age`, `bio`, `gender`, `interested_in`, `min_age`, `max_age`,
                    `avatar`, `interests`, `looking_for`)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE
                `name` = VALUES(`name`), `age` = VALUES(`age`), `bio` = VALUES(`bio`),
                `gender` = VALUES(`gender`), `interested_in` = VALUES(`interested_in`),
                `min_age` = VALUES(`min_age`), `max_age` = VALUES(`max_age`),
                `avatar` = VALUES(`avatar`), `interests` = VALUES(`interests`),
                `looking_for` = VALUES(`looking_for`)
        ]],
        params = {
            account.id, profile.name, profile.age, profile.bio, profile.gender,
            profile.interested_in, profile.min_age, profile.max_age, profile.avatar,
            profile.interests, profile.looking_for,
        },
    }}
    if profile.replace_photos then
        statements[#statements + 1] = {
            query = [[
                DELETE photo FROM `sky_phone_flare_profile_photos` photo
                JOIN `sky_phone_flare_profiles` profile ON profile.`id` = photo.`profile_id`
                WHERE profile.`account_id` = ?
            ]],
            params = { account.id },
        }
        for index, media_id in ipairs(profile.photo_media_ids) do
            statements[#statements + 1] = {
                query = [[
                    INSERT INTO `sky_phone_flare_profile_photos`
                        (`profile_id`, `media_id`, `sort_order`)
                    SELECT `id`, ?, ? FROM `sky_phone_flare_profiles` WHERE `account_id` = ?
                ]],
                params = { media_id, index, account.id },
            }
        end
    end
    if not Bridge.Database.Transaction(statements) then
        return { success = false, error = "request_failed" }
    end
    return { success = true, data = bootstrap(account.id) }
end)

Bridge.Callbacks.Register("sky_phone:flare:set-discovery", function(source, data)
    if not SkyPhone.AllowOperation(source, "flare_discovery", 20, 60) then
        return { success = false, error = "rate_limited" }
    end
    local account, error_response = SkyPhone.RequireAccount(source)
    if not account then
        return error_response
    end
    if type(data) ~= "table" or type(data.enabled) ~= "boolean" then
        return { success = false, error = "invalid_discovery" }
    end
    if not load_profile(account.id) then
        return { success = false, error = "invalid_profile" }
    end
    Bridge.Database.Query([[
        UPDATE `sky_phone_flare_profiles` SET `discoverable` = ? WHERE `account_id` = ?
    ]], { data.enabled and 1 or 0, account.id })
    return { success = true, data = bootstrap(account.id) }
end)

Bridge.Callbacks.Register("sky_phone:flare:swipe", function(source, data)
    if not SkyPhone.AllowOperation(source, "flare_swipe", 120, 60) then
        return { success = false, error = "rate_limited" }
    end
    local account, error_response = SkyPhone.RequireAccount(source)
    if not account then
        return error_response
    end
    local own_profile = load_profile(account.id)
    if not own_profile then
        return { success = false, error = "invalid_profile" }
    end
    if not is_enabled(own_profile.discoverable) then
        return { success = false, error = "discovery_disabled" }
    end
    local target_id = math.floor(tonumber(data and data.targetId) or 0)
    local choice = data and data.choice
    if target_id <= 0 or (choice ~= "like" and choice ~= "pass" and choice ~= "superlike") then
        return { success = false, error = "invalid_choice" }
    end
    local targets = Bridge.Database.Query([[
        SELECT target.`account_id`
        FROM `sky_phone_flare_profiles` mine
        JOIN `sky_phone_flare_profiles` target ON target.`id` = ?
        WHERE mine.`account_id` = ?
            AND target.`account_id` <> mine.`account_id`
            AND mine.`discoverable` = 1
            AND target.`discoverable` = 1
            AND target.`age` BETWEEN mine.`min_age` AND mine.`max_age`
            AND mine.`age` BETWEEN target.`min_age` AND target.`max_age`
            AND (mine.`interested_in` = 'everyone' OR mine.`interested_in` = target.`gender`)
            AND (target.`interested_in` = 'everyone' OR target.`interested_in` = mine.`gender`)
        LIMIT 1
    ]], { target_id, account.id })
    local target_account_id = targets[1] and tonumber(targets[1].account_id)
    if not target_account_id then
        return { success = false, error = "invalid_target" }
    end
    Bridge.Database.Query([[
        INSERT INTO `sky_phone_flare_swipes` (`swiper_account_id`, `target_account_id`, `choice`)
        VALUES (?, ?, ?)
        ON DUPLICATE KEY UPDATE `choice` = VALUES(`choice`), `updated_at` = CURRENT_TIMESTAMP
    ]], { account.id, target_account_id, choice })
    if choice == "pass" then
        return { success = true, data = { match = nil } }
    end
    local reciprocal = Bridge.Database.Query([[
        SELECT `id` FROM `sky_phone_flare_swipes`
        WHERE `swiper_account_id` = ? AND `target_account_id` = ?
            AND `choice` IN ('like', 'superlike')
        LIMIT 1
    ]], { target_account_id, account.id })
    if not reciprocal[1] then
        return { success = true, data = { match = nil } }
    end
    local account_a = math.min(account.id, target_account_id)
    local account_b = math.max(account.id, target_account_id)
    local insert_result = Bridge.Database.Query([[
        INSERT IGNORE INTO `sky_phone_flare_matches` (`id`, `account_a_id`, `account_b_id`)
        VALUES (?, ?, ?)
    ]], { uuid(), account_a, account_b })
    local match_created = affected_rows(insert_result) > 0
    local matches = list_matches(account.id)
    local created_match = nil
    for _, match in ipairs(matches) do
        if tonumber(match.profile.id) == target_id then
            created_match = match
            break
        end
    end
    if created_match and match_created then
        SkyPhone.NotifyAccountDevices(target_account_id, "sky_phone:flare:match", {
            matchId = created_match.id,
            sender = own_profile.name,
        })
    end
    return { success = true, data = { match = created_match } }
end)

Bridge.Callbacks.Register("sky_phone:flare:rewind", function(source)
    if not SkyPhone.AllowOperation(source, "flare_rewind", 20, 60) then
        return { success = false, error = "rate_limited" }
    end
    local account, error_response = SkyPhone.RequireAccount(source)
    if not account then
        return error_response
    end
    local rows = Bridge.Database.Query([[
        SELECT `id`, `target_account_id`
        FROM `sky_phone_flare_swipes`
        WHERE `swiper_account_id` = ?
        ORDER BY `updated_at` DESC, `id` DESC
        LIMIT 1
    ]], { account.id })
    local swipe = rows[1]
    if not swipe then
        return { success = false, error = "nothing_to_rewind" }
    end
    local target_account_id = tonumber(swipe.target_account_id)
    local matched = Bridge.Database.Query([[
        SELECT `id` FROM `sky_phone_flare_matches`
        WHERE (`account_a_id` = ? AND `account_b_id` = ?)
            OR (`account_a_id` = ? AND `account_b_id` = ?)
        LIMIT 1
    ]], { account.id, target_account_id, target_account_id, account.id })
    if matched[1] then
        return { success = false, error = "cannot_rewind_match" }
    end
    Bridge.Database.Query([[
        DELETE FROM `sky_phone_flare_swipes` WHERE `id` = ? AND `swiper_account_id` = ?
    ]], { swipe.id, account.id })
    return { success = true, data = bootstrap(account.id) }
end)

Bridge.Callbacks.Register("sky_phone:flare:unmatch", function(source, data)
    if not SkyPhone.AllowOperation(source, "flare_unmatch", 20, 60) then
        return { success = false, error = "rate_limited" }
    end
    local account, error_response = SkyPhone.RequireAccount(source)
    if not account then
        return error_response
    end
    local match = load_match(account.id, data and data.matchId)
    if not match then
        return { success = false, error = "match_not_found" }
    end
    Bridge.Database.Query([[
        DELETE FROM `sky_phone_flare_matches`
        WHERE `id` = ? AND (`account_a_id` = ? OR `account_b_id` = ?)
    ]], { match.id, account.id, account.id })
    return { success = true, data = { matches = list_matches(account.id) } }
end)

Bridge.Callbacks.Register("sky_phone:flare:thread", function(source, data)
    local account, error_response = SkyPhone.RequireAccount(source)
    if not account then
        return error_response
    end
    local match = load_match(account.id, data and data.matchId)
    if not match then
        return { success = false, error = "match_not_found" }
    end
    Bridge.Database.Query([[
        UPDATE `sky_phone_flare_messages` SET `read_at` = CURRENT_TIMESTAMP
        WHERE `match_id` = ? AND `sender_account_id` <> ? AND `read_at` IS NULL
    ]], { match.id, account.id })
    local rows = Bridge.Database.Query([[
        SELECT `id`, `sender_account_id`, `body`, `message_type`, `media_url`, `share_payload`,
            `media_duration_ms`, UNIX_TIMESTAMP(`created_at`) * 1000 AS `created_at_ms`
        FROM `sky_phone_flare_messages`
        WHERE `match_id` = ?
        ORDER BY `created_at`, `id`
        LIMIT 500
    ]], { match.id })
    local messages = {}
    for _, row in ipairs(rows) do
        messages[#messages + 1] = message_payload(row, account.id)
    end
    return { success = true, data = { messages = messages } }
end)

Bridge.Callbacks.Register("sky_phone:flare:send", function(source, data)
    if not SkyPhone.AllowOperation(source, "flare_message", 60, 60) then
        return { success = false, error = "rate_limited" }
    end
    local account, error_response = SkyPhone.RequireAccount(source)
    if not account then
        return error_response
    end
    local match = load_match(account.id, data and data.matchId)
    if not match then
        return { success = false, error = "match_not_found" }
    end
    local message, validation_error = validate_message(source, data)
    if not message then
        return { success = false, error = validation_error }
    end
    local id = uuid()
    local own_profile = load_profile(account.id)
    Bridge.Database.Query([[
        INSERT INTO `sky_phone_flare_messages`
            (`id`, `match_id`, `sender_account_id`, `body`, `message_type`, `media_url`,
                `media_duration_ms`, `share_payload`)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    ]], {
        id, match.id, account.id, message.body, message.message_type, message.media_url,
        message.media_duration_ms, message.share_payload,
    })
    local recipient_account_id = tonumber(match.account_a_id) == tonumber(account.id)
        and tonumber(match.account_b_id) or tonumber(match.account_a_id)
    SkyPhone.NotifyAccountDevices(recipient_account_id, "sky_phone:flare:message", {
        matchId = match.id,
        body = message.body,
        sender = own_profile and own_profile.name or "",
    })
    return {
        success = true,
        data = message_payload({
            id = id,
            sender_account_id = account.id,
            body = message.body,
            created_at_ms = os.time() * 1000,
            message_type = message.message_type,
            media_url = message.media_url,
            media_duration_ms = message.media_duration_ms,
            share_payload = message.share_payload,
        }, account.id),
    }
end)
end)
