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

SkyPhoneCalls = {}

local calls = {}
local active_by_source = {}
local active_by_sim = {}
local dial_locks = {}
local dialing_by_sim = {}
local next_voice_channel = 10000
local reroute_company_call

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

local function is_uuid(value)
    return type(value) == "string"
        and value:match(
            "^%x%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$"
        ) ~= nil
end

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

local function is_player_source(value)
    return type(value) == "number"
        and value == value
        and value ~= math.huge
        and value ~= -math.huge
        and value >= 1
        and value % 1 == 0
end

local function normalize_contact_email(value)
    local email = trim(value)
    if not email or email == "" then
        return ""
    end
    email = email:lower()
    local local_part = email
    if email:find("@", 1, true) then
        local_part = email:match("^([^@]+)@" .. Config.Mail.Domain:gsub("%.", "%%.") .. "$")
    end
    if not local_part
        or #local_part < Config.Mail.LocalPartMinLength
        or #local_part > Config.Mail.LocalPartMaxLength
        or not local_part:match("^[a-z0-9][a-z0-9._-]*[a-z0-9]$")
        or local_part:find("..", 1, true)
    then
        return nil
    end
    return local_part .. "@" .. Config.Mail.Domain
end

local function scope_for_device(device)
    if device.account_id then
        return tonumber(device.account_id), nil
    end
    return nil, device.imei
end

local function current_scope(source)
    local session, error_response = SkyPhone.RequireSession(source)
    if not session then
        return nil, error_response
    end
    local device = SkyPhone.LoadDevice(session.imei)
    if not device then
        return nil, { success = false, error = "device_not_found" }
    end
    local account_id, device_imei = scope_for_device(device)
    return {
        account_id = account_id,
        device = device,
        device_imei = device_imei,
        session = session,
    }
end

local function scope_condition(scope, alias)
    local prefix = alias and (alias .. ".") or ""
    if scope.account_id then
        return prefix .. "`account_id` = ?", { scope.account_id }
    end
    return prefix .. "`device_imei` = ?", { scope.device_imei }
end

local function find_device_holder(imei)
    for _, player_source in ipairs(Bridge.Framework.GetPlayers()) do
        local source = tonumber(player_source) or player_source
        if SkyPhone.FindDeviceSlots(source, imei)[1] then
            return source
        end
    end
    return nil
end

local function airplane_mode(imei)
    local rows = Bridge.Database.Query([[
        SELECT `payload` FROM `sky_phone_device_data`
        WHERE `device_imei` = ? AND `namespace` = 'settings' LIMIT 1
    ]], { imei })
    if not rows[1] then
        return false
    end
    local payload = json.decode(rows[1].payload)
    return payload and payload.settings and payload.settings.airplaneMode == true
end

local function add_call_entry(call_id, device, direction, status, other_number)
    local account_id, device_imei = scope_for_device(device)
    Bridge.Database.Query([[
        INSERT INTO `sky_phone_call_entries`
            (`call_id`, `account_id`, `device_imei`, `direction`, `status`, `other_number`)
        VALUES (?, ?, ?, ?, ?, ?)
    ]], { call_id, account_id, device_imei, direction, status, other_number })
end

local function call_payload(call, source, state, channel)
    local outgoing = source == call.caller_source
    local speaker_supported = Bridge.Speaker.IsEnabled() and (
        call.voice_provider == "yaca"
        or call.voice_provider == "saltychat"
        or (not call.voice_provider and Bridge.Calls.SupportsSpeaker())
    )
    local mute_supported = call.voice_provider == "yaca"
        or (not call.voice_provider and Bridge.Calls.SupportsMute())
    local payload = {
        id = call.id,
        state = state,
        direction = outgoing and "outgoing" or "incoming",
        otherNumber = outgoing and call.callee_number or call.caller_number,
        startedAt = call.started_at,
        answeredAt = call.answered_at,
        channel = channel,
        speakerEnabled = call.speakers and call.speakers[source] == true or false,
        speakerSupported = speaker_supported,
        muted = call.muted and call.muted[source] == true or false,
        muteSupported = mute_supported,
    }
    if call.payphone and outgoing then
        payload.elapsedSeconds = call.payphone.elapsed_seconds or 0
        payload.totalCost = call.payphone.total_cost or 0
    end
    return payload, outgoing
end

local function call_snapshot(call, source)
    local state = call.answered_at and "connected" or "ringing"
    local channel = state == "connected" and call.channel or nil
    local payload = call_payload(call, source, state, channel)
    payload.anonymous = false
    payload.caller = {
        number = call.caller_number,
        source = call.caller_source,
    }
    payload.callee = {
        number = call.callee_number,
        source = call.callee_source,
    }
    payload.companyId = call.company_id
    payload.payphone = call.payphone ~= nil
    payload.video = false
    return payload
end

local function send_state(call, source, state, channel)
    local payload, outgoing = call_payload(call, source, state, channel)
    if call.payphone and outgoing then
        TriggerClientEvent("sky_phone:payphone:state", source, payload)
        return
    end
    TriggerClientEvent("sky_phone:call:state", source, payload)
end

local function notify_recents(device, source)
    if device.account_id then
        SkyPhone.NotifyAccount(device.account_id, "sky_phone:calls:changed", {})
    elseif source then
        TriggerClientEvent("sky_phone:calls:changed", source)
    end
end

local function settle_payphone_call(call, duration)
    local payphone = call.payphone
    local elapsed_seconds = math.max(0, math.floor(tonumber(duration) or 0))
    local price_per_second = math.max(0, math.floor(tonumber(payphone.price_per_second) or 0))
    local billable_seconds = elapsed_seconds
    local total_cost = billable_seconds * price_per_second

    if total_cost > 0 then
        local available_money = tonumber(Bridge.Framework.GetMoney(call.caller_source, Config.Payphones.PaymentAccount))
        if not available_money then
            Bridge.Debug(
                "error",
                "[sky_phone] Payphone settlement could not read the balance for source %s.",
                tostring(call.caller_source)
            )
            billable_seconds = 0
            total_cost = 0
        elseif available_money < total_cost then
            billable_seconds = math.min(elapsed_seconds, math.floor(math.max(0, available_money) / price_per_second))
            total_cost = billable_seconds * price_per_second
        end
    end

    local charged = total_cost == 0
    if total_cost > 0 then
        local success, result = pcall(
            Bridge.Framework.RemoveMoney,
            call.caller_source,
            Config.Payphones.PaymentAccount,
            total_cost
        )
        charged = success and result and true or false
        if not success then
            Bridge.Debug(
                "error",
                "[sky_phone] Payphone settlement failed for source %s: %s",
                tostring(call.caller_source),
                tostring(result)
            )
        elseif not result then
            Bridge.Debug(
                "warn",
                "[sky_phone] Payphone settlement was rejected for source %s.",
                tostring(call.caller_source)
            )
        end
    end

    payphone.elapsed_seconds = elapsed_seconds
    payphone.total_cost = charged and total_cost or 0
    return charged and billable_seconds == elapsed_seconds
end

local function send_payphone_visual(call, action, delay_ms)
    if not call.payphone then
        return
    end
    local coords = call.payphone.coords
    local payload = {
        id = call.id,
        callerSource = call.caller_source,
        model = call.payphone.model,
        coords = { x = coords.x, y = coords.y, z = coords.z },
    }
    local event_name = ("sky_phone:payphone:visual:%s"):format(action)
    local routing_bucket = call.payphone.routing_bucket
    local targets = {}
    if action == "stop" and call.payphone.visual_targets then
        for _, target in ipairs(call.payphone.visual_targets) do
            targets[#targets + 1] = target
        end
    else
        for _, player_source in ipairs(Bridge.Framework.GetPlayers()) do
            local target = tonumber(player_source) or player_source
            if GetPlayerRoutingBucket(target) == routing_bucket then
                targets[#targets + 1] = target
            end
        end
        if action == "start" then
            call.payphone.visual_targets = targets
        end
    end
    local function dispatch()
        for _, target in ipairs(targets) do
            TriggerClientEvent(event_name, target, payload)
        end
    end
    if delay_ms and delay_ms > 0 then
        SetTimeout(delay_ms, dispatch)
    else
        dispatch()
    end
end

local function finish_call(call, status)
    if not call or call.ended then
        return
    end
    call.ended = true
    if call.voice_started then
        local player_handles = { call.caller_source, call.callee_source }
        for _, player_source in ipairs(player_handles) do
            if call.speakers and call.speakers[player_source] then
                Bridge.Calls.SetSpeaker(player_source, false, call.voice_provider)
            end
        end
        Bridge.Calls.Stop(call.id, player_handles, call.voice_provider)
        call.speakers = {}
        call.muted = {}
        call.voice_started = false
    end
    local ended_at = os.time()
    local duration = call.answered_at and math.max(0, ended_at - call.answered_at) or 0
    if call.payphone and call.answered_at and not settle_payphone_call(call, duration)
        and status ~= "disconnected"
    then
        status = "insufficient_funds"
    end
    local callee_status = status
    if status == "no_answer" or status == "cancelled" then
        callee_status = "missed"
    end
    if call.payphone and call.answered_at and status == "insufficient_funds" then
        callee_status = "completed"
    end
    if not call.payphone then
        Bridge.Database.Transaction({
            {
                query = [[
                    UPDATE `sky_phone_calls`
                    SET `status` = ?, `ended_at` = CURRENT_TIMESTAMP, `duration_seconds` = ?
                    WHERE `id` = ?
                ]],
                params = { status, duration, call.id },
            },
            {
                query = [[
                    UPDATE `sky_phone_call_entries` SET `status` = ?
                    WHERE `call_id` = ? AND `direction` = 'outgoing' AND `status` IN ('ringing', 'connected')
                ]],
                params = { status, call.id },
            },
            {
                query = [[
                    UPDATE `sky_phone_call_entries` SET `status` = ?
                    WHERE `call_id` = ? AND `direction` = 'incoming' AND `status` IN ('ringing', 'connected')
                ]],
                params = { callee_status, call.id },
            },
        })
    end
    active_by_source[call.caller_source] = nil
    if call.caller_sim_id then
        active_by_sim[call.caller_sim_id] = nil
    end
    if call.callee_source then
        active_by_source[call.callee_source] = nil
    end
    if call.callee_sim_id then
        active_by_sim[call.callee_sim_id] = nil
    end
    send_state(call, call.caller_source, status)
    if call.callee_source then
        send_state(call, call.callee_source, callee_status)
    end
    if call.caller_device then
        notify_recents(call.caller_device, call.caller_source)
    end
    if call.callee_device and not call.payphone then
        notify_recents(call.callee_device, call.callee_source)
    end
    if call.payphone then
        local hangup_duration = math.max(
            250,
            math.floor(tonumber(Config.Payphones.Animation.HangupDurationMs) or 2000)
        )
        send_payphone_visual(call, "stop", hangup_duration)
    end
    calls[call.id] = nil
end

local function active_call_for_source(source)
    local call_id = active_by_source[source]
    local call = call_id and calls[call_id] or nil
    if not call or call.ended
        or (call.caller_source ~= source and call.callee_source ~= source)
    then
        return nil
    end
    return call
end

local function end_active_call(call, source)
    if call.company_service_call and not call.answered_at and call.callee_source == source then
        if call.rerouting then
            return false, "call_not_found"
        end
        if not reroute_company_call(call, "declined") then
            finish_call(call, "declined")
        end
        return true
    end

    finish_call(call, call.answered_at and "completed" or "cancelled")
    return true
end

function SkyPhoneCalls.GetForSource(source)
    if not is_player_source(source) then
        Bridge.Debug("error", "[sky_phone] Rejected invalid player source for call lookup.")
        return nil, "invalid_source"
    end

    local call = active_call_for_source(source)
    if not call then
        return nil, "call_not_found"
    end

    return call_snapshot(call, source)
end

function SkyPhoneCalls.GetById(call_id)
    if not is_uuid(call_id) then
        Bridge.Debug("error", "[sky_phone] Rejected invalid call ID for call lookup.")
        return nil, "invalid_call_id"
    end

    local call = calls[call_id]
    if not call or call.ended then
        return nil, "call_not_found"
    end

    return call_snapshot(call, call.caller_source)
end

function SkyPhoneCalls.IsActiveForSource(source)
    if not is_player_source(source) then
        return false
    end
    return active_call_for_source(source) ~= nil
end

function SkyPhoneCalls.EndForSource(source)
    if not is_player_source(source) then
        Bridge.Debug("error", "[sky_phone] Rejected invalid player source for call termination.")
        return false, "invalid_source"
    end

    local call = active_call_for_source(source)
    if not call then
        return false, "call_not_found"
    end
    return end_active_call(call, source)
end

function SkyPhoneCalls.TerminateForSource(source)
    if not is_player_source(source) then
        Bridge.Debug("error", "[sky_phone] Rejected invalid player source for forced call termination.")
        return false, "invalid_source"
    end

    local call = active_call_for_source(source)
    if not call then
        return false, "call_not_found"
    end
    finish_call(call, call.answered_at and "completed" or "cancelled")
    return true
end

function SkyPhoneCalls.EndForSim(sim_id, reason)
    local call_id = active_by_sim[sim_id]
    local call = call_id and calls[call_id] or nil
    if not call then
        return
    end
    if call.callee_sim_id == sim_id and call.company_service_call and not call.answered_at then
        if not reroute_company_call(call, "missed") then
            finish_call(call, "unavailable")
        end
        return
    end
    finish_call(call, reason or "ended")
end

function SkyPhoneCalls.LinkAccountData(account_id, imei)
    local counts = Bridge.Database.Query([[
        SELECT
            (SELECT COUNT(*) FROM `sky_phone_contacts` WHERE `account_id` = ?) AS `contacts`,
            (SELECT COUNT(*) FROM `sky_phone_call_entries` WHERE `account_id` = ?) AS `recents`
    ]], { account_id, account_id })
    local has_cloud_data = counts[1] and ((tonumber(counts[1].contacts) or 0) > 0 or (tonumber(counts[1].recents) or 0) > 0)
    if has_cloud_data then
        return Bridge.Database.Transaction({
            { query = "DELETE FROM `sky_phone_contacts` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } },
            { query = "DELETE FROM `sky_phone_call_entries` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } },
        })
    end
    return Bridge.Database.Transaction({
        {
            query = "UPDATE `sky_phone_contacts` SET `account_id` = ?, `device_imei` = NULL WHERE `device_imei` = ? AND `account_id` IS NULL",
            params = { account_id, imei },
        },
        {
            query = "UPDATE `sky_phone_call_entries` SET `account_id` = ?, `device_imei` = NULL WHERE `device_imei` = ? AND `account_id` IS NULL",
            params = { account_id, imei },
        },
    })
end

function SkyPhoneCalls.CopyCloudToDevice(account_id, imei)
    return Bridge.Database.Transaction({
        { query = "DELETE FROM `sky_phone_contacts` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } },
        { query = "DELETE FROM `sky_phone_call_entries` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } },
        {
            query = [[
                INSERT INTO `sky_phone_contacts`
                    (`id`, `contact_id`, `device_imei`, `name`, `notes`, `organization`, `email`, `phone_number`, `avatar_media_id`, `favorite`, `created_at`, `updated_at`)
                SELECT UUID(), `contact_id`, ?, `name`, `notes`, `organization`, `email`, `phone_number`, NULL, `favorite`, `created_at`, `updated_at`
                FROM `sky_phone_contacts` WHERE `account_id` = ?
            ]],
            params = { imei, account_id },
        },
        {
            query = [[
                INSERT INTO `sky_phone_call_entries`
                    (`call_id`, `device_imei`, `direction`, `status`, `other_number`, `created_at`)
                SELECT `call_id`, ?, `direction`, `status`, `other_number`, `created_at`
                FROM `sky_phone_call_entries` WHERE `account_id` = ?
            ]],
            params = { imei, account_id },
        },
    })
end

Bridge.Callbacks.Register("sky_phone:contacts:list", function(source)
    local scope, error_response = current_scope(source)
    if not scope then
        return error_response
    end
    local condition, params = scope_condition(scope)
    local rows = Bridge.Database.Query(([[
        SELECT `contact_id` AS `id`, `name`, `notes`, `organization`, `email`, `phone_number`, `avatar_media_id`, `favorite`,
            (SELECT media.`url` FROM `sky_phone_media` media WHERE media.`id` = `avatar_media_id`) AS `avatar_url`,
            `created_at`, `updated_at`
        FROM `sky_phone_contacts` WHERE %s ORDER BY LOWER(`name`), `phone_number`
    ]]):format(condition), params)
    local system_contacts = SkyPhoneCompanies.GetSystemContacts()
    local system_ids = {}
    local contacts = {}
    for _, contact in ipairs(system_contacts) do
        system_ids[contact.id] = true
        contacts[#contacts + 1] = contact
    end
    for _, contact in ipairs(rows) do
        if not system_ids[contact.id] and not SkyPhoneCompanies.IsSystemContactNumber(contact.phone_number) then
            contacts[#contacts + 1] = contact
        end
    end
    table.sort(contacts, function(left, right)
        local left_name = tostring(left.name):lower()
        local right_name = tostring(right.name):lower()
        if left_name == right_name then
            return tostring(left.phone_number) < tostring(right.phone_number)
        end
        return left_name < right_name
    end)
    return { success = true, data = contacts }
end)

Bridge.Callbacks.Register("sky_phone:contacts:save", function(source, data)
    if not SkyPhone.AllowOperation(source, "contact_save", 30, 60) or type(data) ~= "table" then
        return { success = false, error = "invalid_request" }
    end
    local scope, error_response = current_scope(source)
    if not scope then
        return error_response
    end
    local name = trim(data.name)
    local notes = trim(data.notes) or ""
    local organization = trim(data.organization) or ""
    local email = normalize_contact_email(data.email)
    local number = SkyPhoneSimNumber.Normalize(data.phoneNumber, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
    local avatar_media_id = tonumber(data.avatarMediaId) or 0
    if not name or name == "" or #name > Config.Calls.ContactNameMaxLength
        or #notes > Config.Calls.ContactNotesMaxLength
        or #organization > Config.Calls.ContactNameMaxLength
        or email == nil or not number
    then
        return { success = false, error = "invalid_contact" }
    end
    if (type(data.id) == "string" and data.id:sub(1, 8) == "company:")
        or SkyPhoneCompanies.IsSystemContactNumber(number)
    then
        Bridge.Debug(
            "warn",
            "[sky_phone] Source %s attempted to modify a read-only company contact.",
            tostring(source)
        )
        return { success = false, error = "readonly_contact" }
    end
    if avatar_media_id < 0 or avatar_media_id ~= math.floor(avatar_media_id) then
        return { success = false, error = "invalid_contact" }
    end
    local avatar_url
    if avatar_media_id > 0 then
        local media_error
        avatar_url, media_error = SkyPhoneMedia.ResolveOwnedMedia(source, tostring(avatar_media_id), "photo")
        if not avatar_url then
            return { success = false, error = media_error }
        end
    end
    local condition, condition_params = scope_condition(scope)
    local id = type(data.id) == "string" and data.id or uuid()
    if data.id then
        local owned_params = { id }
        for _, value in ipairs(condition_params) do
            owned_params[#owned_params + 1] = value
        end
        local owned = Bridge.Database.Query(("SELECT `id` FROM `sky_phone_contacts` WHERE `contact_id` = ? AND %s LIMIT 1"):format(condition), owned_params)
        if not owned[1] then
            return { success = false, error = "contact_not_found" }
        end
        local params = { name, notes, organization, email, number, avatar_media_id, id }
        for _, value in ipairs(condition_params) do
            params[#params + 1] = value
        end
        Bridge.Database.Query(([[
            UPDATE `sky_phone_contacts` SET `name` = ?, `notes` = NULLIF(?, ''), `organization` = NULLIF(?, ''), `email` = NULLIF(?, ''), `phone_number` = ?, `avatar_media_id` = NULLIF(?, 0)
            WHERE `contact_id` = ? AND %s
        ]]):format(condition), params)
    else
        Bridge.Database.Query([[
            INSERT INTO `sky_phone_contacts` (`id`, `contact_id`, `account_id`, `device_imei`, `name`, `notes`, `organization`, `email`, `phone_number`, `avatar_media_id`)
            VALUES (?, ?, ?, ?, ?, NULLIF(?, ''), NULLIF(?, ''), NULLIF(?, ''), ?, NULLIF(?, 0))
        ]], { uuid(), id, scope.account_id, scope.device_imei, name, notes, organization, email, number, avatar_media_id })
    end
    if scope.account_id then
        SkyPhone.NotifyAccount(scope.account_id, "sky_phone:contacts:changed", {})
    end
    return {
        success = true,
        data = {
            id = id,
            name = name,
            notes = notes ~= "" and notes or nil,
            organization = organization ~= "" and organization or nil,
            email = email ~= "" and email or nil,
            phone_number = number,
            avatar_media_id = avatar_media_id > 0 and avatar_media_id or nil,
            avatar_url = avatar_url,
        },
    }
end)

Bridge.Callbacks.Register("sky_phone:contacts:favorite", function(source, data)
    if not SkyPhone.AllowOperation(source, "contact_favorite", 60, 60) or type(data) ~= "table" or type(data.id) ~= "string" or type(data.favorite) ~= "boolean" then
        return { success = false, error = "invalid_request" }
    end
    local scope, error_response = current_scope(source)
    if not scope then
        return error_response
    end
    local condition, condition_params = scope_condition(scope)
    local owned_params = { data.id }
    for _, value in ipairs(condition_params) do
        owned_params[#owned_params + 1] = value
    end
    local owned = Bridge.Database.Query(("SELECT `id` FROM `sky_phone_contacts` WHERE `contact_id` = ? AND %s LIMIT 1"):format(condition), owned_params)
    if not owned[1] then
        return { success = false, error = "contact_not_found" }
    end
    local params = { data.favorite and 1 or 0, data.id }
    for _, value in ipairs(condition_params) do
        params[#params + 1] = value
    end
    Bridge.Database.Query(("UPDATE `sky_phone_contacts` SET `favorite` = ? WHERE `contact_id` = ? AND %s"):format(condition), params)
    if scope.account_id then
        SkyPhone.NotifyAccount(scope.account_id, "sky_phone:contacts:changed", {})
    end
    return { success = true, data = { id = data.id, favorite = data.favorite } }
end)

Bridge.Callbacks.Register("sky_phone:contacts:delete", function(source, data)
    if type(data) ~= "table" or type(data.id) ~= "string" then
        return { success = false, error = "invalid_request" }
    end
    local scope, error_response = current_scope(source)
    if not scope then
        return error_response
    end
    if data.id:sub(1, 8) == "company:" then
        Bridge.Debug(
            "warn",
            "[sky_phone] Source %s attempted to delete a read-only company contact.",
            tostring(source)
        )
        return { success = false, error = "readonly_contact" }
    end
    local condition, values = scope_condition(scope)
    local params = { data.id }
    for _, value in ipairs(values) do
        params[#params + 1] = value
    end
    Bridge.Database.Query(("DELETE FROM `sky_phone_contacts` WHERE `contact_id` = ? AND %s"):format(condition), params)
    if scope.account_id then
        SkyPhone.NotifyAccount(scope.account_id, "sky_phone:contacts:changed", {})
    end
    return { success = true }
end)

Bridge.Callbacks.Register("sky_phone:calls:recents", function(source)
    local scope, error_response = current_scope(source)
    if not scope then
        return error_response
    end
    local condition, params = scope_condition(scope, "e")
    params[#params + 1] = Config.Calls.RecentPageSize
    local rows = Bridge.Database.Query(([[
        SELECT e.`id`, e.`call_id`, e.`direction`, e.`status`, e.`other_number`, e.`created_at`,
            c.`duration_seconds`
        FROM `sky_phone_call_entries` e
        JOIN `sky_phone_calls` c ON c.`id` = e.`call_id`
        WHERE %s ORDER BY e.`created_at` DESC, e.`id` DESC LIMIT ?
    ]]):format(condition), params)
    return { success = true, data = rows }
end)

local function create_terminal_call(scope, number, target_sim, status, caller_number)
    local id = uuid()
    Bridge.Database.Query([[
        INSERT INTO `sky_phone_calls`
            (`id`, `caller_sim_id`, `callee_sim_id`, `caller_number`, `callee_number`, `status`, `ended_at`)
        VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
    ]], {
        id,
        scope.device.sim_id,
        target_sim and target_sim.id or nil,
        caller_number or scope.device.phone_number,
        number,
        status,
    })
    add_call_entry(id, scope.device, "outgoing", status, number)
    notify_recents(scope.device, nil)
    return {
        id = id,
        state = status,
        direction = "outgoing",
        otherNumber = number,
        startedAt = os.time(),
    }
end

local function company_call_target(company_id, caller_source, caller_sim_id, excluded_sim_ids)
    local has_busy_target = false
    for _, candidate in ipairs(SkyPhoneCompanies.GetCallTargets(company_id)) do
        local candidate_source = tonumber(candidate.source)
        local candidate_sim_id = candidate.simId
        local candidate_imei = candidate.imei
        if candidate_source and type(candidate_sim_id) == "string" and type(candidate_imei) == "string"
            and candidate_source ~= caller_source and candidate_sim_id ~= caller_sim_id
            and (not excluded_sim_ids or not excluded_sim_ids[candidate_sim_id])
        then
            if active_by_source[candidate_source] or active_by_sim[candidate_sim_id]
                or dialing_by_sim[candidate_sim_id]
            then
                has_busy_target = true
            else
                dialing_by_sim[candidate_sim_id] = true
                local device = SkyPhone.LoadDevice(candidate_imei)
                local holder = device and find_device_holder(candidate_imei) or nil
                if not device or device.sim_id ~= candidate_sim_id or holder ~= candidate_source then
                    dialing_by_sim[candidate_sim_id] = nil
                elseif SkyPhoneCompanies.IsServiceNumber(device.phone_number) then
                    dialing_by_sim[candidate_sim_id] = nil
                    Bridge.Debug(
                        "error",
                        "[sky_phone] Company call target %s uses reserved service number %s.",
                        tostring(candidate_source),
                        tostring(device.phone_number)
                    )
                elseif airplane_mode(candidate_imei) then
                    dialing_by_sim[candidate_sim_id] = nil
                else
                    return {
                        device = device,
                        sim_id = candidate_sim_id,
                        source = candidate_source,
                    }
                end
            end
        end
    end
    return nil, has_busy_target and "busy" or "unavailable"
end

local handle_no_answer

local function ring_callee(call)
    SkyPhone.OpenDeviceForCall(call.callee_source, call.callee_device.imei)
    TriggerClientEvent("sky_phone:call:incoming", call.callee_source, {
        id = call.id,
        state = "ringing",
        direction = "incoming",
        otherNumber = call.caller_number,
        startedAt = call.started_at,
        device = {
            imei = call.callee_device.imei,
            name = call.callee_device.device_name,
        },
    })
end

local function schedule_no_answer(call)
    call.ring_attempt = (call.ring_attempt or 0) + 1
    local ring_attempt = call.ring_attempt
    SetTimeout(call.ring_seconds * 1000, function()
        local active_call = calls[call.id]
        if active_call and not active_call.answered_at and active_call.ring_attempt == ring_attempt then
            handle_no_answer(active_call)
        end
    end)
end

local function start_ringing_call(call, ring_seconds)
    if not call.payphone then
        Bridge.Database.Query([[
            INSERT INTO `sky_phone_calls`
                (`id`, `caller_sim_id`, `callee_sim_id`, `caller_number`, `callee_number`, `status`)
            VALUES (?, ?, ?, ?, ?, 'ringing')
        ]], { call.id, call.caller_sim_id, call.callee_sim_id, call.caller_number, call.callee_number })
        add_call_entry(call.id, call.caller_device, "outgoing", "ringing", call.callee_number)
        add_call_entry(call.id, call.callee_device, "incoming", "ringing", call.caller_number)
    end

    calls[call.id] = call
    active_by_source[call.caller_source] = call.id
    active_by_source[call.callee_source] = call.id
    if call.caller_sim_id then
        active_by_sim[call.caller_sim_id] = call.id
        dialing_by_sim[call.caller_sim_id] = nil
    end
    active_by_sim[call.callee_sim_id] = call.id
    dialing_by_sim[call.callee_sim_id] = nil
    dial_locks[call.caller_source] = nil

    send_state(call, call.caller_source, "ringing")
    if call.payphone then
        send_payphone_visual(call, "start")
    end
    call.ring_seconds = math.max(1, math.floor(tonumber(ring_seconds) or Config.Calls.RingSeconds))
    ring_callee(call)
    schedule_no_answer(call)

    local result = {
        id = call.id,
        state = "ringing",
        direction = "outgoing",
        otherNumber = call.callee_number,
        startedAt = call.started_at,
    }
    if call.payphone then
        result.elapsedSeconds = 0
        result.totalCost = 0
    end
    return result
end

reroute_company_call = function(call, previous_status)
    if not call.company_service_call or call.company_attempts_remaining <= 0 then
        return false
    end
    if call.rerouting then
        return true
    end
    call.rerouting = true
    local next_target = company_call_target(
        call.company_id,
        call.caller_source,
        call.caller_sim_id,
        call.company_attempted_sims
    )
    if not next_target then
        call.rerouting = nil
        return false
    end
    if call.ended or calls[call.id] ~= call then
        dialing_by_sim[next_target.sim_id] = nil
        call.rerouting = nil
        return true
    end

    local previous_source = call.callee_source
    local previous_sim_id = call.callee_sim_id
    local previous_device = call.callee_device
    if not call.payphone then
        local next_account_id, next_device_imei = scope_for_device(next_target.device)
        local updated = Bridge.Database.Transaction({
            {
                query = "UPDATE `sky_phone_calls` SET `callee_sim_id` = ? WHERE `id` = ? AND `status` = 'ringing'",
                params = { next_target.sim_id, call.id },
            },
            {
                query = [[
                    UPDATE `sky_phone_call_entries` SET `status` = ?
                    WHERE `call_id` = ? AND `direction` = 'incoming' AND `status` = 'ringing'
                ]],
                params = { previous_status, call.id },
            },
            {
                query = [[
                    INSERT INTO `sky_phone_call_entries`
                        (`call_id`, `account_id`, `device_imei`, `direction`, `status`, `other_number`)
                    SELECT `id`, ?, ?, 'incoming', 'ringing', ?
                    FROM `sky_phone_calls`
                    WHERE `id` = ? AND `status` = 'ringing'
                ]],
                params = { next_account_id, next_device_imei, call.caller_number, call.id },
            },
        })
        if not updated then
            dialing_by_sim[next_target.sim_id] = nil
            call.rerouting = nil
            Bridge.Debug(
                "error",
                "[sky_phone] Could not reroute company call %s to the next target.",
                tostring(call.id)
            )
            return false
        end
        if call.ended or calls[call.id] ~= call then
            dialing_by_sim[next_target.sim_id] = nil
            call.rerouting = nil
            return true
        end
    end

    send_state(call, previous_source, previous_status)
    if active_by_source[previous_source] == call.id then
        active_by_source[previous_source] = nil
    end
    if active_by_sim[previous_sim_id] == call.id then
        active_by_sim[previous_sim_id] = nil
    end
    if not call.payphone then
        notify_recents(previous_device, previous_source)
    end
    if call.ended or calls[call.id] ~= call then
        dialing_by_sim[next_target.sim_id] = nil
        call.rerouting = nil
        return true
    end

    call.callee_source = next_target.source
    call.callee_sim_id = next_target.sim_id
    call.callee_device = next_target.device
    call.company_attempted_sims[next_target.sim_id] = true
    call.company_attempts_remaining = call.company_attempts_remaining - 1
    active_by_source[call.callee_source] = call.id
    active_by_sim[call.callee_sim_id] = call.id
    dialing_by_sim[call.callee_sim_id] = nil
    call.rerouting = nil
    ring_callee(call)
    schedule_no_answer(call)
    return true
end

handle_no_answer = function(call)
    if not reroute_company_call(call, "missed") then
        finish_call(call, "no_answer")
    end
end

local function lock_direct_target(caller_source, target, caller_sim_id)
    if not target or not target.imei then
        return nil, "unavailable"
    end
    if caller_sim_id then
        local blocks = Bridge.Database.Query([[
            SELECT 1 FROM `sky_phone_call_blocks`
            WHERE `blocker_sim_id` = ? AND `blocked_sim_id` = ? LIMIT 1
        ]], { target.id, caller_sim_id })
        if blocks[1] then
            return nil, "declined"
        end
    end
    local callee_source = find_device_holder(target.imei)
    if not callee_source or callee_source == caller_source then
        return nil, "unavailable"
    end
    if active_by_source[callee_source] or active_by_sim[target.id] or dialing_by_sim[target.id] then
        return nil, "busy"
    end
    dialing_by_sim[target.id] = true
    if airplane_mode(target.imei) then
        dialing_by_sim[target.id] = nil
        return nil, "unavailable"
    end
    return callee_source
end

function SkyPhoneCalls.StartCompanyCall(source, company_id, customer_number)
    source = tonumber(source)
    if not source or type(company_id) ~= "string" then
        return { success = false, error = "invalid_request" }
    end
    if not SkyPhoneCompanies.CanPlaceCompanyCall(source, company_id) then
        return { success = false, error = "not_authorized" }
    end
    if not SkyPhone.AllowOperation(source, "company_call_customer", 15, 60) then
        return { success = false, error = "rate_limited" }
    end
    if dial_locks[source] then
        return { success = false, error = "busy" }
    end

    local service_line = SkyPhoneCompanies.GetServiceLineForCompany(company_id)
    if not service_line or not service_line.canCall then
        return { success = false, error = "company_unavailable" }
    end
    local scope, error_response = current_scope(source)
    if not scope then
        return error_response
    end
    if not scope.device.sim_id then
        return { success = false, error = "no_sim" }
    end
    if SkyPhoneCompanies.IsServiceNumber(scope.device.phone_number) then
        Bridge.Debug(
            "error",
            "[sky_phone] Source %s attempted to call from a SIM using reserved service number %s.",
            tostring(source),
            tostring(scope.device.phone_number)
        )
        return { success = false, error = "invalid_sim" }
    end
    if airplane_mode(scope.device.imei) then
        return { success = false, error = "airplane_mode" }
    end

    local number = SkyPhoneSimNumber.Normalize(customer_number, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
    if not number or SkyPhoneCompanies.IsServiceNumber(number) then
        return { success = false, error = "invalid_number" }
    end
    if number == scope.device.phone_number then
        return { success = false, error = "self_call" }
    end
    if active_by_source[source] or active_by_sim[scope.device.sim_id] or dialing_by_sim[scope.device.sim_id] then
        return { success = false, error = "busy" }
    end

    dial_locks[source] = true
    dialing_by_sim[scope.device.sim_id] = true
    local targets = Bridge.Database.Query([[
        SELECT s.`id`, s.`phone_number`, d.`imei`, d.`account_id`, d.`device_name`
        FROM `sky_phone_sims` s LEFT JOIN `sky_phone_devices` d ON d.`sim_id` = s.`id`
        WHERE s.`phone_number` = ? LIMIT 1
    ]], { number })
    local target = targets[1]
    local callee_source, target_status = lock_direct_target(source, target, scope.device.sim_id)
    if not callee_source then
        local terminal = create_terminal_call(scope, number, target, target_status, service_line.number)
        dialing_by_sim[scope.device.sim_id] = nil
        dial_locks[source] = nil
        return { success = true, data = terminal }
    end

    return {
        success = true,
        data = start_ringing_call({
            id = uuid(),
            caller_source = source,
            caller_sim_id = scope.device.sim_id,
            caller_number = service_line.number,
            caller_device = scope.device,
            callee_source = callee_source,
            callee_sim_id = target.id,
            callee_number = number,
            callee_device = target,
            company_id = company_id,
            started_at = os.time(),
        }, Config.Calls.RingSeconds),
    }
end

Bridge.Callbacks.Register("sky_phone:calls:dial", function(source, data)
    if not SkyPhone.AllowOperation(source, "call_dial", 15, 60) then
        return { success = false, error = "rate_limited" }
    end
    if type(data) ~= "table" then
        return { success = false, error = "invalid_request" }
    end
    if dial_locks[source] then
        return { success = false, error = "busy" }
    end
    dial_locks[source] = true
    local scope, error_response = current_scope(source)
    if not scope then
        dial_locks[source] = nil
        return error_response
    end
    if not scope.device.sim_id then
        dial_locks[source] = nil
        return { success = false, error = "no_sim" }
    end
    if SkyPhoneCompanies.IsServiceNumber(scope.device.phone_number) then
        Bridge.Debug(
            "error",
            "[sky_phone] Source %s attempted to call from a SIM using reserved service number %s.",
            tostring(source),
            tostring(scope.device.phone_number)
        )
        dial_locks[source] = nil
        return { success = false, error = "invalid_sim" }
    end
    if airplane_mode(scope.device.imei) then
        dial_locks[source] = nil
        return { success = false, error = "airplane_mode" }
    end
    local service_line = type(data.company) == "string"
        and SkyPhoneCompanies.GetServiceLineForCompany(data.company)
        or SkyPhoneCompanies.GetServiceLine(data.phoneNumber)
    local number = service_line and service_line.number
        or SkyPhoneSimNumber.Normalize(data.phoneNumber, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
    if not number then
        dial_locks[source] = nil
        return { success = false, error = "invalid_number" }
    end
    if number == scope.device.phone_number then
        dial_locks[source] = nil
        return { success = false, error = "self_call" }
    end
    if active_by_source[source] or active_by_sim[scope.device.sim_id] or dialing_by_sim[scope.device.sim_id] then
        dial_locks[source] = nil
        return { success = false, error = "busy" }
    end
    dialing_by_sim[scope.device.sim_id] = true
    if service_line then
        if not service_line.canCall then
            local terminal = create_terminal_call(scope, number, nil, "unavailable")
            dialing_by_sim[scope.device.sim_id] = nil
            dial_locks[source] = nil
            return { success = true, data = terminal }
        end
        local company_target, target_status = company_call_target(
            service_line.companyId,
            source,
            scope.device.sim_id
        )
        if not company_target then
            local terminal = create_terminal_call(scope, number, nil, target_status)
            dialing_by_sim[scope.device.sim_id] = nil
            dial_locks[source] = nil
            return { success = true, data = terminal }
        end
        return {
            success = true,
            data = start_ringing_call({
                id = uuid(),
                caller_source = source,
                caller_sim_id = scope.device.sim_id,
                caller_number = scope.device.phone_number,
                caller_device = scope.device,
                callee_source = company_target.source,
                callee_sim_id = company_target.sim_id,
                callee_number = service_line.number,
                callee_device = company_target.device,
                company_id = service_line.companyId,
                company_service_call = service_line.routing == "round_robin",
                company_attempted_sims = { [company_target.sim_id] = true },
                company_attempts_remaining = math.max(
                    0,
                    math.floor(tonumber(Config.Companies.CallRouting.MaxAttempts) or 1) - 1
                ),
                started_at = os.time(),
            }, Config.Companies.CallRouting.RingSeconds),
        }
    end

    local targets = Bridge.Database.Query([[
        SELECT s.`id`, s.`phone_number`, d.`imei`, d.`account_id`, d.`device_name`
        FROM `sky_phone_sims` s LEFT JOIN `sky_phone_devices` d ON d.`sim_id` = s.`id`
        WHERE s.`phone_number` = ? LIMIT 1
    ]], { number })
    local target = targets[1]
    if not target then
        dialing_by_sim[scope.device.sim_id] = nil
        dial_locks[source] = nil
        return { success = false, error = "recipient_not_found" }
    end
    local callee_source, target_status = lock_direct_target(source, target, scope.device.sim_id)
    if not callee_source then
        local terminal = create_terminal_call(scope, number, target, target_status)
        dialing_by_sim[scope.device.sim_id] = nil
        dial_locks[source] = nil
        return { success = true, data = terminal }
    end

    return {
        success = true,
        data = start_ringing_call({
            id = uuid(),
            caller_source = source,
            caller_sim_id = scope.device.sim_id,
            caller_number = scope.device.phone_number,
            caller_device = scope.device,
            callee_source = callee_source,
            callee_sim_id = target.id,
            callee_number = number,
            callee_device = target,
            started_at = os.time(),
        }, Config.Calls.RingSeconds),
    }
end)

local payphone_models = {}

local function refresh_payphone_models()
    payphone_models = {}
    for _, model_name in ipairs(Config.Payphones.Props or {}) do
        if type(model_name) == "string" then
            payphone_models[model_name] = true
        end
    end
end

refresh_payphone_models()

if Config.Payphones.Enabled and not next(payphone_models) then
    Bridge.Debug(
        "error",
        "[sky_phone] Payphones are enabled, but Config.Payphones.Props contains no valid models; payphone calls will be rejected.",
        { always = true }
    )
end

AddEventHandler("sky_phone:configurator:serverUpdated", function()
    refresh_payphone_models()
end)

local function valid_payphone_position(source, detected_booth)
    local ped = GetPlayerPed(source)
    if not ped or ped == 0 then
        return nil
    end
    local location = SkyPhonePayphones.ValidateDetected(
        detected_booth,
        payphone_models,
        GetEntityCoords(ped),
        Config.Payphones.ServerValidationDistance
    )
    if not location then
        return nil
    end
    return vector3(location.coords.x, location.coords.y, location.coords.z), location.model
end

local function payphone_terminal(number, state)
    return {
        id = ("payphone-terminal-%s-%s"):format(os.time(), math.random(100000, 999999)),
        state = state,
        direction = "outgoing",
        otherNumber = number,
        startedAt = os.time(),
        elapsedSeconds = 0,
        totalCost = 0,
    }
end

Bridge.Callbacks.Register("sky_phone:payphone:dial", function(source, data)
    if type(data) ~= "table" then
        return { success = false, error = "invalid_request" }
    end
    if not Config.Payphones.Enabled or not SkyPhone.AllowOperation(source, "payphone_dial", 15, 60) then
        return { success = false, error = "rate_limited" }
    end
    local booth_coords, booth_model = valid_payphone_position(source, data)
    if not booth_coords then
        return { success = false, error = "invalid_payphone" }
    end
    if active_by_source[source] or dial_locks[source] then
        return { success = false, error = "busy" }
    end

    local service_line = SkyPhoneCompanies.GetServiceLine(data.phoneNumber)
    local number = service_line and service_line.number
        or SkyPhoneSimNumber.Normalize(data.phoneNumber, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
    if not number then
        return { success = false, error = "invalid_number" }
    end
    local price_per_second = math.max(0, math.floor(tonumber(Config.Payphones.PricePerSecond) or 0))
    local available_money = Bridge.Framework.GetMoney(source, Config.Payphones.PaymentAccount)
    if price_per_second > 0 and (not available_money or available_money < price_per_second) then
        return { success = false, error = "insufficient_funds" }
    end

    dial_locks[source] = true
    if service_line then
        if not service_line.canCall then
            dial_locks[source] = nil
            return { success = true, data = payphone_terminal(number, "unavailable") }
        end
        local company_target, target_status = company_call_target(service_line.companyId, source, nil)
        if not company_target then
            dial_locks[source] = nil
            return { success = true, data = payphone_terminal(number, target_status) }
        end
        return {
            success = true,
            data = start_ringing_call({
                id = uuid(),
                caller_source = source,
                caller_number = Config.Payphones.CallerNumber,
                callee_source = company_target.source,
                callee_sim_id = company_target.sim_id,
                callee_number = service_line.number,
                callee_device = company_target.device,
                company_id = service_line.companyId,
                company_service_call = service_line.routing == "round_robin",
                company_attempted_sims = { [company_target.sim_id] = true },
                company_attempts_remaining = math.max(
                    0,
                    math.floor(tonumber(Config.Companies.CallRouting.MaxAttempts) or 1) - 1
                ),
                started_at = os.time(),
                payphone = {
                    elapsed_seconds = 0,
                    total_cost = 0,
                    coords = booth_coords,
                    model = booth_model,
                    price_per_second = price_per_second,
                    routing_bucket = GetPlayerRoutingBucket(source),
                },
            }, Config.Companies.CallRouting.RingSeconds),
        }
    end

    local targets = Bridge.Database.Query([[
        SELECT s.`id`, s.`phone_number`, d.`imei`, d.`account_id`, d.`device_name`
        FROM `sky_phone_sims` s LEFT JOIN `sky_phone_devices` d ON d.`sim_id` = s.`id`
        WHERE s.`phone_number` = ? LIMIT 1
    ]], { number })
    local target = targets[1]
    local callee_source, target_status = lock_direct_target(source, target)
    if not callee_source then
        dial_locks[source] = nil
        return { success = true, data = payphone_terminal(number, target_status) }
    end

    return {
        success = true,
        data = start_ringing_call({
            id = uuid(),
            caller_source = source,
            caller_number = Config.Payphones.CallerNumber,
            callee_source = callee_source,
            callee_sim_id = target.id,
            callee_number = number,
            callee_device = target,
            started_at = os.time(),
            payphone = {
                elapsed_seconds = 0,
                total_cost = 0,
                coords = booth_coords,
                model = booth_model,
                price_per_second = price_per_second,
                routing_bucket = GetPlayerRoutingBucket(source),
            },
        }, Config.Payphones.NoAnswerTimeoutSeconds),
    }
end)

Bridge.Callbacks.Register("sky_phone:calls:answer", function(source, data)
    local call = type(data) == "table" and calls[data.id] or nil
    if not call or call.callee_source ~= source or call.answered_at or call.rerouting then
        return { success = false, error = "call_not_found" }
    end
    if call.company_service_call and not SkyPhoneCompanies.CanAnswerCompanyCall(
        source,
        call.company_id,
        call.callee_device.imei,
        call.callee_sim_id
    ) then
        if not reroute_company_call(call, "missed") then
            finish_call(call, "unavailable")
        end
        return { success = false, error = "call_not_found" }
    end
    if not SkyPhone.FindDeviceSlots(source, call.callee_device.imei)[1] then
        finish_call(call, "unavailable")
        return { success = false, error = "phone_not_owned" }
    end
    if not Bridge.Calls.IsAvailable() then
        return { success = false, error = "voice_unavailable" }
    end
    local voice_started, voice_provider = Bridge.Calls.Start(call.id, {
        call.caller_source,
        call.callee_source,
    })
    if not voice_started then
        return { success = false, error = "voice_unavailable" }
    end
    call.voice_provider = voice_provider
    call.voice_started = true
    call.speakers = {}
    call.muted = {}
    call.answered_at = os.time()
    call.channel = next_voice_channel
    next_voice_channel = next_voice_channel + 1
    if not call.payphone then
        Bridge.Database.Query([[
            UPDATE `sky_phone_calls` SET `status` = 'connected', `answered_at` = CURRENT_TIMESTAMP
            WHERE `id` = ? AND `status` = 'ringing'
        ]], { call.id })
        Bridge.Database.Query([[
            UPDATE `sky_phone_call_entries` SET `status` = 'connected'
            WHERE `call_id` = ? AND `status` = 'ringing'
        ]], { call.id })
    end
    if call.ended or calls[call.id] ~= call then
        return { success = false, error = "call_not_found" }
    end
    send_state(call, call.caller_source, "connected", call.channel)
    send_state(call, call.callee_source, "connected", call.channel)
    return { success = true }
end)

Bridge.Callbacks.Register("sky_phone:calls:set-speaker", function(source, data)
    if type(data) ~= "table" or type(data.id) ~= "string" or type(data.enabled) ~= "boolean" then
        return { success = false, error = "invalid_request" }
    end
    if not SkyPhone.AllowOperation(source, "call_speaker", 30, 60) then
        return { success = false, error = "rate_limited" }
    end

    local call_id = active_by_source[source]
    local call = call_id and calls[call_id] or nil
    if not call or call.id ~= data.id or not call.answered_at or call.ended or not call.voice_started then
        return { success = false, error = "call_not_found" }
    end
    if call.voice_provider ~= "yaca" and call.voice_provider ~= "saltychat" then
        return { success = false, error = "speaker_unsupported" }
    end
    if not Bridge.Speaker.IsEnabled() then
        return { success = false, error = "speaker_unsupported" }
    end
    if not Bridge.Calls.SetSpeaker(source, data.enabled, call.voice_provider) then
        return { success = false, error = "voice_unavailable" }
    end

    call.speakers[source] = data.enabled
    send_state(call, source, "connected", call.channel)
    return {
        success = true,
        data = {
            speakerEnabled = data.enabled,
            speakerSupported = true,
        },
    }
end)

Bridge.Callbacks.Register("sky_phone:calls:set-muted", function(source, data)
    if type(data) ~= "table" or type(data.id) ~= "string" or type(data.enabled) ~= "boolean" then
        return { success = false, error = "invalid_request" }
    end
    if not SkyPhone.AllowOperation(source, "call_mute", 30, 60) then
        return { success = false, error = "rate_limited" }
    end

    local call_id = active_by_source[source]
    local call = call_id and calls[call_id] or nil
    if not call or call.id ~= data.id or not call.answered_at or call.ended or not call.voice_started then
        return { success = false, error = "call_not_found" }
    end
    if call.voice_provider ~= "yaca" then
        return { success = false, error = "mute_unsupported" }
    end
    if not Bridge.Calls.SetMuted(source, data.enabled, call.voice_provider) then
        return { success = false, error = "voice_unavailable" }
    end

    call.muted[source] = data.enabled
    send_state(call, source, "connected", call.channel)
    return {
        success = true,
        data = {
            muted = data.enabled,
            muteSupported = true,
        },
    }
end)

Bridge.Callbacks.Register("sky_phone:calls:decline", function(source, data)
    local call = type(data) == "table" and calls[data.id] or nil
    if not call or call.callee_source ~= source or call.answered_at or call.rerouting then
        return { success = false, error = "call_not_found" }
    end
    if not reroute_company_call(call, "declined") then
        finish_call(call, "declined")
    end
    return { success = true }
end)

Bridge.Callbacks.Register("sky_phone:calls:hangup", function(source, data)
    local call_id = active_by_source[source]
    local call = call_id and calls[call_id] or nil
    if not call or (call.payphone and call.caller_source == source)
        or (type(data) == "table" and data.id and data.id ~= call.id)
    then
        return { success = false, error = "call_not_found" }
    end
    local success, call_error = end_active_call(call, source)
    return { success = success, error = call_error }
end)

Bridge.Callbacks.Register("sky_phone:calls:terminate", function(source, data)
    local call_id = active_by_source[source]
    local call = call_id and calls[call_id] or nil
    if not call
        or type(data) ~= "table"
        or data.id ~= call.id
    then
        return { success = false, error = "call_not_found" }
    end
    finish_call(call, call.answered_at and "completed" or "cancelled")
    return { success = true }
end)

Bridge.Callbacks.Register("sky_phone:calls:block", function(source, data)
    if not SkyPhone.AllowOperation(source, "call_block", 10, 60) then
        return { success = false, error = "rate_limited" }
    end
    local scope, error_response = current_scope(source)
    if not scope then
        return error_response
    end
    if not scope.device.sim_id then
        return { success = false, error = "no_sim" }
    end
    local number = type(data) == "table" and SkyPhoneSimNumber.Normalize(
        data.phoneNumber,
        Config.Sim.NumberLength,
        Config.Sim.NumberPrefix
    ) or nil
    if not number then
        return { success = false, error = "invalid_number" }
    end
    if number == scope.device.phone_number then
        return { success = false, error = "self_call" }
    end
    local rows = Bridge.Database.Query(
        "SELECT `id` FROM `sky_phone_sims` WHERE `phone_number` = ? LIMIT 1",
        { number }
    )
    if not rows[1] then
        return { success = false, error = "recipient_not_found" }
    end
    Bridge.Database.Query([[
        INSERT IGNORE INTO `sky_phone_call_blocks` (`blocker_sim_id`, `blocked_sim_id`)
        VALUES (?, ?)
    ]], { scope.device.sim_id, rows[1].id })

    local call_id = active_by_source[source]
    local call = call_id and calls[call_id] or nil
    if call then
        local other_number = source == call.caller_source and call.callee_number or call.caller_number
        if other_number == number then
            finish_call(call, call.answered_at and "completed" or "declined")
        end
    end
    return { success = true }
end)

Bridge.Callbacks.Register("sky_phone:payphone:hangup", function(source, data)
    local call_id = active_by_source[source]
    local call = call_id and calls[call_id] or nil
    if not call or not call.payphone or call.caller_source ~= source
        or (type(data) == "table" and data.id and data.id ~= call.id)
    then
        return { success = false, error = "call_not_found" }
    end
    finish_call(call, call.answered_at and "completed" or "cancelled")
    return { success = true }
end)

CreateThread(function()
    while true do
        Wait(1000)
        local calls_to_finish = {}
        for call_id, call in pairs(calls) do
            local caller_valid = true
            if call.payphone then
                local caller_ped = GetPlayerPed(call.caller_source)
                caller_valid = caller_ped and caller_ped ~= 0
                if caller_valid then
                    caller_valid = #(GetEntityCoords(caller_ped) - call.payphone.coords)
                        <= Config.Payphones.MaximumCallDistance
                end
            else
                caller_valid = SkyPhone.FindDeviceSlots(call.caller_source, call.caller_device.imei)[1] ~= nil
            end
            local callee_valid = not call.callee_source
                or SkyPhone.FindDeviceSlots(call.callee_source, call.callee_device.imei)[1] ~= nil
            if not caller_valid then
                calls_to_finish[call_id] = "disconnected"
            elseif not callee_valid then
                if call.company_service_call and not call.answered_at then
                    if not reroute_company_call(call, "missed") then
                        calls_to_finish[call_id] = "unavailable"
                    end
                else
                    calls_to_finish[call_id] = "disconnected"
                end
            elseif call.payphone and call.answered_at and not call.ended then
                local elapsed_seconds = math.max(0, os.time() - call.answered_at)
                local total_cost = elapsed_seconds * call.payphone.price_per_second
                local available_money = tonumber(
                    Bridge.Framework.GetMoney(call.caller_source, Config.Payphones.PaymentAccount)
                )
                if total_cost > 0 and (not available_money or available_money < total_cost) then
                    calls_to_finish[call_id] = "insufficient_funds"
                elseif call.payphone.elapsed_seconds ~= elapsed_seconds then
                    call.payphone.elapsed_seconds = elapsed_seconds
                    call.payphone.total_cost = total_cost
                    send_state(call, call.caller_source, "connected", call.channel)
                end
            end
        end
        for call_id, status in pairs(calls_to_finish) do
            finish_call(calls[call_id], status)
        end
    end
end)

AddEventHandler("playerDropped", function()
    local call_id = active_by_source[source]
    local call = call_id and calls[call_id] or nil
    if not call then
        return
    end
    if call.callee_source == source and call.company_service_call and not call.answered_at then
        if not reroute_company_call(call, "missed") then
            finish_call(call, "unavailable")
        end
        return
    end
    finish_call(call, "disconnected")
end)

AddEventHandler("onResourceStop", function(resource_name)
    if resource_name ~= GetCurrentResourceName() then
        return
    end
    local call_ids = {}
    for call_id in pairs(calls) do
        call_ids[#call_ids + 1] = call_id
    end
    for _, call_id in ipairs(call_ids) do
        finish_call(calls[call_id], "disconnected")
    end
end)
end)
