Bridge.Database.AfterMigration("sky_phone", function()

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 Billing UUID.")
    end
    return rows[1].id
end

local function trimmed(value, maximum_length)
    if type(value) ~= "string" then
        return nil
    end
    local result = value:match("^%s*(.-)%s*$")
    if result == "" or #result > maximum_length then
        return nil
    end
    return result
end

local function valid_amount(value)
    local amount = tonumber(value)
    if not amount or amount ~= math.floor(amount) then
        return nil
    end
    if amount < Config.Billing.MinimumAmount or amount > Config.Billing.MaximumAmount then
        return nil
    end
    return amount
end

local function valid_invoice_id(value)
    return type(value) == "string" and #value == 36 and value:match("^[0-9a-fA-F%-]+$") ~= nil
end

local function require_billing_session(source)
    if not Config.Billing.Enabled then
        return nil, { success = false, error = "billing_unavailable" }
    end
    local session, error_response = SkyPhone.RequireSession(source)
    if not session then
        return nil, error_response
    end
    local identifier = Bridge.Framework.GetIdentifier(source)
    if type(identifier) ~= "string" or identifier == "" then
        return nil, { success = false, error = "billing_unavailable" }
    end
    return identifier
end

local function invoice_dto(row, identifier)
    local due_at = tonumber(row.due_at_unix)
    local paid_at = tonumber(row.paid_at_unix)
    local issued_at = tonumber(row.issued_at_unix) or 0
    local direction = row.recipient_identifier == identifier and "inbox" or "sent"
    local status = row.status or "open"
    return {
        id = row.id,
        amount = tonumber(row.amount) or 0,
        currency = row.currency or Config.Billing.Currency,
        description = row.description or "",
        direction = direction,
        dueAt = due_at and due_at * 1000 or nil,
        issuedAt = issued_at * 1000,
        issuerAccount = row.issuer_account or "",
        issuerLabel = row.issuer_label or "",
        isOverdue = status == "open" and due_at ~= nil and due_at < os.time(),
        isUnread = direction == "inbox" and row.read_at == nil,
        paidAt = paid_at and paid_at * 1000 or nil,
        paymentReference = row.payment_reference or "",
        status = status,
        title = row.title or "",
        canPay = direction == "inbox" and status == "open",
        canDispute = direction == "inbox" and status == "open" and Config.Billing.AllowDisputes,
    }
end

local function invoice_select(where_sql, parameters)
    local rows = Bridge.Database.Query(([=[
        SELECT `id`, `recipient_identifier`, `issuer_identifier`, `issuer_account`, `issuer_label`,
            `title`, `description`, `amount`, `currency`, `status`, `read_at`, `payment_reference`,
            UNIX_TIMESTAMP(`issued_at`) AS `issued_at_unix`,
            UNIX_TIMESTAMP(`due_at`) AS `due_at_unix`,
            UNIX_TIMESTAMP(`paid_at`) AS `paid_at_unix`
        FROM `sky_phone_billing_invoices`
        WHERE %s
    ]=]):format(where_sql), parameters)
    return rows
end

local function find_owned_invoice(id, identifier)
    if not valid_invoice_id(id) then
        return nil
    end
    return invoice_select("`id` = ? AND (`recipient_identifier` = ? OR `issuer_identifier` = ?) LIMIT 1", {
        id, identifier, identifier,
    })[1]
end

local function unread_count(identifier)
    local rows = Bridge.Database.Query([[
        SELECT COUNT(*) AS `count`
        FROM `sky_phone_billing_invoices`
        WHERE `recipient_identifier` = ? AND `read_at` IS NULL
    ]], { identifier })
    return tonumber(rows[1] and rows[1].count) or 0
end

local function notify_identifier(identifier, event_name, data)
    for _, player_source in ipairs(Bridge.Framework.GetPlayers()) do
        local target = tonumber(player_source) or player_source
        if Bridge.Framework.GetIdentifier(target) == identifier then
            TriggerClientEvent(event_name, target, data or {})
        end
    end
end

local function create_invoice(data)
    if not Config.Billing.Enabled or type(data) ~= "table" then
        return nil, "billing_unavailable"
    end
    local recipient_identifier = trimmed(data.recipientIdentifier, 80)
    if not recipient_identifier and tonumber(data.recipientSource) then
        recipient_identifier = Bridge.Framework.GetIdentifier(tonumber(data.recipientSource))
    end
    local issuer_identifier = trimmed(data.issuerIdentifier, 80) or ""
    if issuer_identifier == "" and tonumber(data.issuerSource) then
        issuer_identifier = Bridge.Framework.GetIdentifier(tonumber(data.issuerSource)) or ""
    end
    local issuer_account = trimmed(data.issuerAccount, 80)
    local issuer_label = trimmed(data.issuerLabel, 80)
    local title = trimmed(data.title, Config.Billing.MaximumTitleLength)
    local description = type(data.description) == "string" and data.description:match("^%s*(.-)%s*$") or ""
    local amount = valid_amount(data.amount)
    if not recipient_identifier or not issuer_account or not issuer_label or not title or not amount
        or #description > Config.Billing.MaximumDescriptionLength then
        return nil, "invalid_request"
    end
    local due_days = math.max(0, math.min(365, math.floor(tonumber(data.dueDays) or Config.Billing.DefaultDueDays)))
    local id = uuid()
    local result = Bridge.Database.Query([[
        INSERT INTO `sky_phone_billing_invoices`
            (`id`, `recipient_identifier`, `issuer_identifier`, `issuer_account`, `issuer_label`,
             `title`, `description`, `amount`, `currency`, `due_at`)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL ? DAY))
    ]], {
        id, recipient_identifier, issuer_identifier, issuer_account, issuer_label,
        title, description, amount, Config.Billing.Currency, due_days,
    })
    if affected_rows(result) ~= 1 then
        return nil, "request_failed"
    end
    Bridge.Database.Query([[
        INSERT INTO `sky_phone_billing_events` (`invoice_id`, `event`, `actor_identifier`)
        VALUES (?, 'created', ?)
    ]], { id, issuer_identifier })
    notify_identifier(recipient_identifier, "sky_phone:billing:new", {
        amount = amount,
        issuer = issuer_label,
    })
    return id
end

Bridge.Callbacks.Register("sky_phone:billing:overview", function(source, data)
    local identifier, error_response = require_billing_session(source)
    if not identifier then
        return error_response
    end
    local direction = data and data.direction == "sent" and "sent" or "inbox"
    local owner_column = direction == "sent" and "issuer_identifier" or "recipient_identifier"
    local rows = Bridge.Database.Query(([=[
        SELECT COUNT(CASE WHEN `status` = 'open' THEN 1 END) AS `open_count`,
            COALESCE(SUM(CASE WHEN `status` = 'open' THEN `amount` ELSE 0 END), 0) AS `open_total`,
            COUNT(CASE WHEN `status` = 'open' AND `due_at` < NOW() THEN 1 END) AS `overdue_count`
        FROM `sky_phone_billing_invoices`
        WHERE `%s` = ?
    ]=]):format(owner_column), { identifier })
    local summary = rows[1] or {}
    local urgent_rows = invoice_select(("`%s` = ? AND `status` = 'open' ORDER BY (`due_at` IS NULL), `due_at`, `id` DESC LIMIT ?"):format(owner_column), {
        identifier, Config.Billing.UrgentLimit,
    })
    local urgent = {}
    for _, row in ipairs(urgent_rows) do
        urgent[#urgent + 1] = invoice_dto(row, identifier)
    end
    return {
        success = true,
        data = {
            currency = Config.Billing.Currency,
            openCount = tonumber(summary.open_count) or 0,
            openTotal = tonumber(summary.open_total) or 0,
            overdueCount = tonumber(summary.overdue_count) or 0,
            supportsDisputes = Config.Billing.AllowDisputes,
            supportsSent = true,
            unreadCount = unread_count(identifier),
            urgentInvoices = urgent,
        },
    }
end)

Bridge.Callbacks.Register("sky_phone:billing:list", function(source, data)
    local identifier, error_response = require_billing_session(source)
    if not identifier then
        return error_response
    end
    local direction = data and data.direction == "sent" and "sent" or "inbox"
    local filter = type(data and data.filter) == "string" and data.filter or "all"
    if filter ~= "all" and filter ~= "open" and filter ~= "overdue" and filter ~= "paid" then
        return { success = false, error = "invalid_request" }
    end
    local offset = math.max(0, math.floor(tonumber(data and data.offset) or 0))
    local search = type(data and data.search) == "string" and data.search:sub(1, 80) or ""
    local owner_column = direction == "sent" and "issuer_identifier" or "recipient_identifier"
    local where = { ("`%s` = ?"):format(owner_column) }
    local parameters = { identifier }
    if filter == "open" then
        where[#where + 1] = "`status` = 'open'"
    elseif filter == "overdue" then
        where[#where + 1] = "`status` = 'open' AND `due_at` < NOW()"
    elseif filter == "paid" then
        where[#where + 1] = "`status` IN ('paid', 'disputed', 'cancelled', 'refunded')"
    end
    if search ~= "" then
        where[#where + 1] = "(`title` LIKE ? OR `issuer_label` LIKE ? OR `description` LIKE ?)"
        local pattern = "%" .. search .. "%"
        parameters[#parameters + 1] = pattern
        parameters[#parameters + 1] = pattern
        parameters[#parameters + 1] = pattern
    end
    parameters[#parameters + 1] = Config.Billing.PageSize + 1
    parameters[#parameters + 1] = offset
    local rows = invoice_select(table.concat(where, " AND ") .. " ORDER BY `issued_at` DESC, `id` DESC LIMIT ? OFFSET ?", parameters)
    local has_more = #rows > Config.Billing.PageSize
    if has_more then
        rows[#rows] = nil
    end
    local invoices = {}
    for _, row in ipairs(rows) do
        invoices[#invoices + 1] = invoice_dto(row, identifier)
    end
    return {
        success = true,
        data = { invoices = invoices, hasMore = has_more, nextOffset = offset + #invoices },
    }
end)

Bridge.Callbacks.Register("sky_phone:billing:detail", function(source, data)
    local identifier, error_response = require_billing_session(source)
    if not identifier then
        return error_response
    end
    local row = find_owned_invoice(data and data.id, identifier)
    if not row then
        return { success = false, error = "invoice_not_found" }
    end
    return { success = true, data = invoice_dto(row, identifier) }
end)

Bridge.Callbacks.Register("sky_phone:billing:markRead", function(source, data)
    local identifier, error_response = require_billing_session(source)
    if not identifier then
        return error_response
    end
    if not valid_invoice_id(data and data.id) then
        return { success = false, error = "invoice_not_found" }
    end
    Bridge.Database.Query([[
        UPDATE `sky_phone_billing_invoices` SET `read_at` = COALESCE(`read_at`, NOW())
        WHERE `id` = ? AND `recipient_identifier` = ?
    ]], { data and data.id, identifier })
    return { success = true, data = { unreadCount = unread_count(identifier) } }
end)

Bridge.Callbacks.Register("sky_phone:billing:dispute", function(source, data)
    if not Config.Billing.AllowDisputes then
        return { success = false, error = "dispute_unavailable" }
    end
    local identifier, error_response = require_billing_session(source)
    if not identifier then
        return error_response
    end
    if not valid_invoice_id(data and data.id) then
        return { success = false, error = "invoice_not_found" }
    end
    local result = Bridge.Database.Query([[
        UPDATE `sky_phone_billing_invoices` SET `status` = 'disputed', `read_at` = COALESCE(`read_at`, NOW())
        WHERE `id` = ? AND `recipient_identifier` = ? AND `status` = 'open'
    ]], { data and data.id, identifier })
    if affected_rows(result) ~= 1 then
        return { success = false, error = "dispute_unavailable" }
    end
    Bridge.Database.Query([[
        INSERT INTO `sky_phone_billing_events` (`invoice_id`, `event`, `actor_identifier`)
        VALUES (?, 'disputed', ?)
    ]], { data.id, identifier })
    notify_identifier(identifier, "sky_phone:billing:changed")
    local row = find_owned_invoice(data.id, identifier)
    return { success = true, data = invoice_dto(row, identifier) }
end)

Bridge.Callbacks.Register("sky_phone:billing:pay", function(source, data)
    if not SkyPhone.AllowOperation(source, "billing_payment", Config.Billing.ActionsPerMinute, 60) then
        return { success = false, error = "rate_limited" }
    end
    local identifier, error_response = require_billing_session(source)
    if not identifier then
        return error_response
    end
    local invoice_id = data and data.id
    if not valid_invoice_id(invoice_id) then
        return { success = false, error = "invoice_not_found" }
    end
    local rows = invoice_select("`id` = ? AND `recipient_identifier` = ? LIMIT 1", { invoice_id, identifier })
    local row = rows[1]
    if not row then
        return { success = false, error = "invoice_not_found" }
    end
    if row.status == "paid" then
        return { success = false, error = "invoice_already_paid" }
    end
    if row.status == "processing" then
        return { success = false, error = "payment_in_progress" }
    end
    if row.status ~= "open" then
        return { success = false, error = "invoice_not_payable" }
    end
    local claim = Bridge.Database.Query([[
        UPDATE `sky_phone_billing_invoices` SET `status` = 'processing'
        WHERE `id` = ? AND `recipient_identifier` = ? AND `status` = 'open'
    ]], { invoice_id, identifier })
    if affected_rows(claim) ~= 1 then
        return { success = false, error = "payment_in_progress" }
    end
    local payment_id = uuid()
    local amount = tonumber(row.amount) or 0
    Bridge.Database.Query([[
        INSERT INTO `sky_phone_billing_payments` (`id`, `invoice_id`, `recipient_identifier`, `amount`)
        VALUES (?, ?, ?, ?)
    ]], { payment_id, invoice_id, identifier, amount })
    if not Bridge.Framework.RemoveMoney(source, Config.Billing.PaymentAccount, amount) then
        Bridge.Database.Transaction({
            { query = "UPDATE `sky_phone_billing_invoices` SET `status` = 'open' WHERE `id` = ? AND `status` = 'processing'", params = { invoice_id } },
            { query = "UPDATE `sky_phone_billing_payments` SET `status` = 'failed', `error_code` = 'insufficient_funds' WHERE `id` = ?", params = { payment_id } },
        })
        return { success = false, error = "insufficient_funds" }
    end
    local completed = Bridge.Database.Transaction({
        { query = "UPDATE `sky_phone_billing_invoices` SET `status` = 'paid', `paid_at` = NOW(), `read_at` = COALESCE(`read_at`, NOW()), `payment_reference` = ? WHERE `id` = ? AND `status` = 'processing'", params = { payment_id, invoice_id } },
        { query = "UPDATE `sky_phone_billing_payments` SET `status` = 'paid' WHERE `id` = ?", params = { payment_id } },
        { query = "INSERT INTO `sky_phone_billing_events` (`invoice_id`, `event`, `actor_identifier`) VALUES (?, 'paid', ?)", params = { invoice_id, identifier } },
        { query = "INSERT INTO `sky_phone_billing_accounts` (`account_key`, `balance`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `balance` = `balance` + VALUES(`balance`)", params = { row.issuer_account, amount } },
        { query = "INSERT INTO `sky_phone_bank_transactions` (`owner_identifier`, `kind`, `amount`, `label`, `reference`) VALUES (?, 'withdrawal', ?, ?, ?)", params = { identifier, amount, row.issuer_label, payment_id } },
    })
    if not completed then
        Bridge.Framework.AddMoney(source, Config.Billing.PaymentAccount, amount)
        Bridge.Database.Transaction({
            { query = "UPDATE `sky_phone_billing_invoices` SET `status` = 'open' WHERE `id` = ? AND `status` = 'processing'", params = { invoice_id } },
            { query = "UPDATE `sky_phone_billing_payments` SET `status` = 'failed', `error_code` = 'payment_failed' WHERE `id` = ?", params = { payment_id } },
        })
        Bridge.Debug("error", "[sky_phone] Billing payment transaction failed for invoice %s; payer was refunded.", tostring(invoice_id))
        return { success = false, error = "payment_failed" }
    end
    TriggerClientEvent("sky_phone:banking:changed", source)
    notify_identifier(identifier, "sky_phone:billing:changed")
    if row.issuer_identifier ~= "" then
        notify_identifier(row.issuer_identifier, "sky_phone:billing:changed")
    end
    local paid_row = find_owned_invoice(invoice_id, identifier)
    return { success = true, data = invoice_dto(paid_row, identifier) }
end)

exports("CreateInvoice", function(data)
    return create_invoice(data)
end)

exports("CancelInvoice", function(invoice_id, actor_identifier)
    if not valid_invoice_id(invoice_id) then
        return false
    end
    local result = Bridge.Database.Query([[
        UPDATE `sky_phone_billing_invoices` SET `status` = 'cancelled'
        WHERE `id` = ? AND `status` = 'open'
    ]], { invoice_id })
    if affected_rows(result) ~= 1 then
        return false
    end
    Bridge.Database.Query([[
        INSERT INTO `sky_phone_billing_events` (`invoice_id`, `event`, `actor_identifier`)
        VALUES (?, 'cancelled', ?)
    ]], { invoice_id, type(actor_identifier) == "string" and actor_identifier or "" })
    local rows = Bridge.Database.Query("SELECT `recipient_identifier`, `issuer_identifier` FROM `sky_phone_billing_invoices` WHERE `id` = ? LIMIT 1", { invoice_id })
    if rows[1] then
        notify_identifier(rows[1].recipient_identifier, "sky_phone:billing:changed")
        if rows[1].issuer_identifier ~= "" then
            notify_identifier(rows[1].issuer_identifier, "sky_phone:billing:changed")
        end
    end
    return true
end)

exports("GetBillingAccountBalance", function(account_key)
    local key = trimmed(account_key, 80)
    if not key then
        return nil
    end
    local rows = Bridge.Database.Query("SELECT `balance` FROM `sky_phone_billing_accounts` WHERE `account_key` = ? LIMIT 1", { key })
    return tonumber(rows[1] and rows[1].balance) or 0
end)

exports("RemoveBillingAccountBalance", function(account_key, value)
    local key = trimmed(account_key, 80)
    local amount = valid_amount(value)
    if not key or not amount then
        return false
    end
    local result = Bridge.Database.Query([[
        UPDATE `sky_phone_billing_accounts` SET `balance` = `balance` - ?
        WHERE `account_key` = ? AND `balance` >= ?
    ]], { amount, key, amount })
    return affected_rows(result) == 1
end)

end)
