Bridge.Database.AfterMigration("sky_phone", function()
SkyPhoneNotes = {}

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

local function note_owner(account_id, imei)
    if account_id then
        return "`account_id` = ?", { account_id }
    end
    return "`account_id` IS NULL AND `device_imei` = ?", { imei }
end

local function note_dto(row)
    return {
        id = row.id,
        title = row.title,
        body = row.body,
        pinned = tonumber(row.pinned) == 1,
        revision = tonumber(row.revision) or 1,
        createdAt = (tonumber(row.created_at_unix) or 0) * 1000,
        updatedAt = (tonumber(row.updated_at_unix) or 0) * 1000,
    }
end

local function notify_owner(account_id, imei)
    if account_id then
        SkyPhone.RefreshAccount(account_id)
    else
        SkyPhone.RefreshDevice(imei)
    end
end

function SkyPhoneNotes.List(account_id, imei)
    local condition, params = note_owner(account_id, imei)
    local rows = Bridge.Database.Query(([[
        SELECT `id`, `title`, `body`, `pinned`, `revision`,
            UNIX_TIMESTAMP(`created_at`) AS `created_at_unix`,
            UNIX_TIMESTAMP(`updated_at`) AS `updated_at_unix`
        FROM `sky_phone_notes`
        WHERE %s
        ORDER BY `pinned` DESC, `updated_at` DESC
    ]]):format(condition), params)
    local notes = {}
    for _, row in ipairs(rows) do
        notes[#notes + 1] = note_dto(row)
    end
    return notes
end

local function session_owner(source)
    local session, error_response = SkyPhone.RequireSession(source)
    if not session then
        return nil, nil, error_response
    end
    local account = SkyPhone.RequireAccount(source)
    return account and account.id or nil, session.imei
end

Bridge.Callbacks.Register("sky_phone:notes:list", function(source)
    local account_id, imei, error_response = session_owner(source)
    if not imei then
        return error_response
    end
    return { success = true, data = SkyPhoneNotes.List(account_id, imei) }
end)
end)

Bridge.Callbacks.Register("sky_phone:notes:create", function(source, data)
    if not SkyPhone.AllowOperation(source, "notes_write", 120, 60) then
        return { success = false, error = "rate_limited" }
    end
    local account_id, imei, error_response = session_owner(source)
    if not imei then
        return error_response
    end
    if type(data) ~= "table"
        or type(data.id) ~= "string"
        or #data.id < 8
        or #data.id > 64
        or not data.id:match("^[%w%-]+$")
        or not text_length(data.title)
        or text_length(data.title) > 120
        or not text_length(data.body)
        or text_length(data.body) > 20000
        or type(data.pinned) ~= "boolean"
    then
        return { success = false, error = "invalid_note" }
    end

    local result
    if account_id then
        result = Bridge.Database.Query([[
            INSERT IGNORE INTO `sky_phone_notes`
                (`id`, `account_id`, `device_imei`, `title`, `body`, `pinned`)
            VALUES (?, ?, NULL, ?, ?, ?)
        ]], { data.id, account_id, data.title, data.body, data.pinned and 1 or 0 })
    else
        result = Bridge.Database.Query([[
            INSERT IGNORE INTO `sky_phone_notes`
                (`id`, `account_id`, `device_imei`, `title`, `body`, `pinned`)
            VALUES (?, NULL, ?, ?, ?, ?)
        ]], { data.id, imei, data.title, data.body, data.pinned and 1 or 0 })
    end
    local affected = type(result) == "number" and result
        or (type(result) == "table" and tonumber(result.affectedRows) or 0)
    if affected <= 0 then
        return { success = false, error = "note_exists" }
    end

    notify_owner(account_id, imei)
    return { success = true, data = SkyPhoneNotes.List(account_id, imei) }
end)

Bridge.Callbacks.Register("sky_phone:notes:update", function(source, data)
    if not SkyPhone.AllowOperation(source, "notes_write", 120, 60) then
        return { success = false, error = "rate_limited" }
    end
    local account_id, imei, error_response = session_owner(source)
    if not imei then
        return error_response
    end
    if type(data) ~= "table"
        or type(data.id) ~= "string"
        or not text_length(data.title)
        or text_length(data.title) > 120
        or not text_length(data.body)
        or text_length(data.body) > 20000
        or type(data.pinned) ~= "boolean"
    then
        return { success = false, error = "invalid_note" }
    end

    local revision = math.max(1, math.floor(tonumber(data.revision) or 0))
    local condition, owner_params = note_owner(account_id, imei)
    local params = { data.title, data.body, data.pinned and 1 or 0, data.id }
    for _, value in ipairs(owner_params) do
        params[#params + 1] = value
    end
    params[#params + 1] = revision
    local result = Bridge.Database.Query(([[
        UPDATE `sky_phone_notes`
        SET `title` = ?, `body` = ?, `pinned` = ?, `revision` = `revision` + 1
        WHERE `id` = ? AND %s AND `revision` = ?
    ]]):format(condition), params)
    local affected = type(result) == "number" and result
        or (type(result) == "table" and tonumber(result.affectedRows) or 0)
    if affected ~= 1 then
        return { success = false, error = "conflict", data = SkyPhoneNotes.List(account_id, imei) }
    end

    notify_owner(account_id, imei)
    return { success = true, data = SkyPhoneNotes.List(account_id, imei) }
end)

Bridge.Callbacks.Register("sky_phone:notes:delete", function(source, data)
    if not SkyPhone.AllowOperation(source, "notes_write", 120, 60) then
        return { success = false, error = "rate_limited" }
    end
    local account_id, imei, error_response = session_owner(source)
    if not imei then
        return error_response
    end
    if type(data) ~= "table" or type(data.id) ~= "string" then
        return { success = false, error = "invalid_note" }
    end

    local condition, owner_params = note_owner(account_id, imei)
    local params = { data.id }
    for _, value in ipairs(owner_params) do
        params[#params + 1] = value
    end
    Bridge.Database.Query(("DELETE FROM `sky_phone_notes` WHERE `id` = ? AND %s"):format(condition), params)
    notify_owner(account_id, imei)
    return { success = true, data = SkyPhoneNotes.List(account_id, imei) }
end)
