local policies_by_id = {}
local policy_ids_by_adapter = {}
local policy_ids_by_owner = {}
local current_resource = GetCurrentResourceName()

local function validate_owner_resource(owner_resource, allow_stopping)
    if type(owner_resource) ~= "string"
        or #owner_resource == 0
        or #owner_resource > 64
        or not owner_resource:match("^[%w][%w._-]*$")
    then
        return false, "invalid_owner_resource"
    end

    local state = GetResourceState(owner_resource)
    if state == "started" or state == "starting" or (allow_stopping and state == "stopping") then
        return true
    end

    return false, "owner_resource_not_running"
end

local function add_owner_index(index, resource_name, app_id)
    local app_ids = index[resource_name]
    if not app_ids then
        app_ids = {}
        index[resource_name] = app_ids
    end
    app_ids[app_id] = true
end

local function remove_owner_index(index, resource_name, app_id)
    local app_ids = index[resource_name]
    if not app_ids then
        return
    end

    app_ids[app_id] = nil
    if not next(app_ids) then
        index[resource_name] = nil
    end
end

local function build_permission_set(permissions)
    local permission_set = {}
    for index = 1, #permissions do
        permission_set[permissions[index]] = true
    end
    return permission_set
end

local function register_policy(policy)
    local app_id = policy.id
    local existing = policies_by_id[app_id]
    if existing then
        Bridge.Debug(
            "warn",
            "[sky_phone] Rejected duplicate custom app policy '%s' from '%s'; it is already owned by '%s'.",
            app_id,
            policy.ownerResource,
            existing.ownerResource
        )
        return false, "duplicate_app_id"
    end

    policies_by_id[app_id] = policy
    add_owner_index(policy_ids_by_owner, policy.ownerResource, app_id)
    if policy.adapterResource then
        add_owner_index(policy_ids_by_adapter, policy.adapterResource, app_id)
    end
    return true
end

local function remove_policy(app_id)
    local policy = policies_by_id[app_id]
    if not policy then
        return false, "app_not_found"
    end

    policies_by_id[app_id] = nil
    remove_owner_index(policy_ids_by_owner, policy.ownerResource, app_id)
    if policy.adapterResource then
        remove_owner_index(policy_ids_by_adapter, policy.adapterResource, app_id)
    end
    return true
end

local function get_direct_owner()
    local owner_resource = GetInvokingResource()
    if not owner_resource then
        return nil, "missing_invoking_resource"
    end

    local valid_owner, owner_error = validate_owner_resource(owner_resource, false)
    if not valid_owner then
        return nil, owner_error
    end

    return owner_resource
end

local function get_trusted_adapter()
    local adapter_resource = GetInvokingResource()
    if not adapter_resource or not Config.CustomApps.TrustedAdapters[adapter_resource] then
        return nil, "untrusted_adapter"
    end

    local valid_adapter, adapter_error = validate_owner_resource(adapter_resource, false)
    if not valid_adapter then
        return nil, adapter_error
    end

    return adapter_resource
end

local function normalize_policy(owner_resource, adapter_resource, definition)
    if type(definition) ~= "table" then
        return nil, "invalid_definition"
    end
    if definition.schemaVersion ~= nil and definition.schemaVersion ~= SkyPhoneApps.ProtocolVersion then
        return nil, "unsupported_schema_version"
    end

    local valid_id, id_error = SkyPhoneApps.ValidateAppId(definition.id)
    if not valid_id then
        return nil, id_error
    end
    if SkyPhoneApps.ReservedAppIds[definition.id] then
        return nil, "reserved_app_id"
    end

    local permissions, permissions_error = SkyPhoneApps.ValidatePermissions(definition.permissions)
    if not permissions then
        return nil, permissions_error
    end

    return {
        adapterResource = adapter_resource,
        bundled = false,
        id = definition.id,
        ownerResource = owner_resource,
        permissions = build_permission_set(permissions),
    }
end

local function add_custom_app_policy(definition)
    if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then
        return false, "external_apps_disabled"
    end

    local owner_resource, owner_error = get_direct_owner()
    if not owner_resource then
        return false, owner_error
    end

    local policy, policy_error = normalize_policy(owner_resource, nil, definition)
    if not policy then
        return false, policy_error
    end
    return register_policy(policy)
end

local function add_custom_app_policy_from_adapter(owner_resource, definition)
    if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then
        return false, "external_apps_disabled"
    end

    local adapter_resource, adapter_error = get_trusted_adapter()
    if not adapter_resource then
        return false, adapter_error
    end
    local valid_owner, owner_error = validate_owner_resource(owner_resource, false)
    if not valid_owner then
        return false, owner_error
    end

    local policy, policy_error = normalize_policy(owner_resource, adapter_resource, definition)
    if not policy then
        return false, policy_error
    end
    return register_policy(policy)
end

local function verify_owned_policy(owner_resource, app_id, adapter_resource)
    local valid_id, id_error = SkyPhoneApps.ValidateAppId(app_id)
    if not valid_id then
        return nil, id_error
    end

    local policy = policies_by_id[app_id]
    if not policy then
        return nil, "app_not_found"
    end
    if policy.ownerResource ~= owner_resource then
        return nil, "app_owner_mismatch"
    end
    if adapter_resource and policy.adapterResource ~= adapter_resource then
        return nil, "app_adapter_mismatch"
    end

    return policy
end

local function replace_policy(policy, adapter_resource)
    local existing, policy_error = verify_owned_policy(
        policy.ownerResource,
        policy.id,
        adapter_resource
    )
    if not existing then
        return false, policy_error
    end
    if existing.bundled then
        return false, "bundled_app"
    end

    policies_by_id[policy.id] = policy
    return true
end

local function update_custom_app_policy(definition)
    if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then
        return false, "external_apps_disabled"
    end

    local owner_resource, owner_error = get_direct_owner()
    if not owner_resource then
        return false, owner_error
    end

    local policy, policy_error = normalize_policy(owner_resource, nil, definition)
    if not policy then
        return false, policy_error
    end
    return replace_policy(policy, nil)
end

local function update_custom_app_policy_from_adapter(owner_resource, definition)
    if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then
        return false, "external_apps_disabled"
    end

    local adapter_resource, adapter_error = get_trusted_adapter()
    if not adapter_resource then
        return false, adapter_error
    end
    local valid_owner, owner_error = validate_owner_resource(owner_resource, false)
    if not valid_owner then
        return false, owner_error
    end

    local policy, policy_error = normalize_policy(owner_resource, adapter_resource, definition)
    if not policy then
        return false, policy_error
    end
    return replace_policy(policy, adapter_resource)
end

local function remove_custom_app_policy(app_id)
    local owner_resource, owner_error = get_direct_owner()
    if not owner_resource then
        return false, owner_error
    end

    local policy, policy_error = verify_owned_policy(owner_resource, app_id)
    if not policy then
        return false, policy_error
    end
    if policy.bundled then
        return false, "bundled_app"
    end

    return remove_policy(app_id)
end

local function remove_custom_app_policy_from_adapter(owner_resource, app_id)
    local adapter_resource, adapter_error = get_trusted_adapter()
    if not adapter_resource then
        return false, adapter_error
    end

    local policy, policy_error = verify_owned_policy(owner_resource, app_id, adapter_resource)
    if not policy then
        return false, policy_error
    end
    return remove_policy(app_id)
end

local function has_custom_app_permission(app_id, permission)
    local valid_id = SkyPhoneApps.ValidateAppId(app_id)
    if not valid_id or type(permission) ~= "string" or not SkyPhoneApps.AllowedPermissions[permission] then
        return false
    end

    local policy = policies_by_id[app_id]
    return policy ~= nil and policy.permissions[permission] == true
end

local function get_custom_app_policy(app_id)
    local valid_id = SkyPhoneApps.ValidateAppId(app_id)
    if not valid_id then
        return nil
    end

    local policy = policies_by_id[app_id]
    if not policy then
        return nil
    end

    local permissions = {}
    for permission in pairs(policy.permissions) do
        permissions[#permissions + 1] = permission
    end
    table.sort(permissions)
    return {
        bundled = policy.bundled,
        id = policy.id,
        ownerResource = policy.ownerResource,
        permissions = permissions,
    }
end

local function get_custom_app_capabilities()
    return {
        abiVersion = 1,
        enabled = Config.CustomApps.Enabled == true,
        exports = {
            "AddCustomAppPolicy",
            "GetCustomAppCapabilities",
            "GetCustomAppPolicy",
            "HasCustomAppPermission",
            "RemoveCustomAppPolicy",
            "SendAppMessage",
            "SendCustomAppMessage",
            "SendCustomAppNotification",
            "UpdateCustomAppPolicy",
        },
        externalApps = Config.CustomApps.Enabled == true
            and Config.CustomApps.ExternalApps == true,
        messageDispatch = true,
        notificationDispatch = true,
        policyRegistration = true,
        policyUpdate = true,
        protocolVersion = SkyPhoneApps.ProtocolVersion,
    }
end

local function normalize_player_source(player_source)
    if type(player_source) ~= "number"
        or player_source ~= player_source
        or player_source <= 0
        or player_source >= math.huge
        or player_source ~= math.floor(player_source)
    then
        return nil
    end
    return player_source
end

local function normalize_message_payload(payload)
    local encoded_success, encoded = pcall(json.encode, payload)
    if not encoded_success or type(encoded) ~= "string" then
        return nil, "invalid_payload"
    end
    if #encoded > Config.CustomApps.MaximumMessageBytes then
        return nil, "payload_too_large"
    end

    local decoded_success, decoded = pcall(json.decode, encoded)
    if not decoded_success then
        return nil, "invalid_payload"
    end
    return decoded
end

local function send_custom_app_message(player_source, app_id, payload)
    local owner_resource, owner_error = get_direct_owner()
    if not owner_resource then
        return false, owner_error
    end

    local policy, policy_error = verify_owned_policy(owner_resource, app_id)
    if not policy then
        return false, policy_error
    end

    player_source = normalize_player_source(player_source)
    if not player_source then
        return false, "invalid_source"
    end
    if not Bridge.Framework.GetIdentifier(player_source) then
        return false, "player_unavailable"
    end

    local normalized_payload, payload_error = normalize_message_payload(payload)
    if payload_error then
        return false, payload_error
    end

    TriggerClientEvent(
        "sky_phone:custom-app:message",
        player_source,
        owner_resource,
        app_id,
        normalized_payload
    )
    return true
end

local function send_custom_app_notification(player_source, app_id, notification)
    local owner_resource, owner_error = get_direct_owner()
    if not owner_resource then
        return false, owner_error
    end

    local policy, policy_error = verify_owned_policy(owner_resource, app_id)
    if not policy then
        return false, policy_error
    end
    if not policy.permissions.notifications then
        return false, "permission_denied"
    end

    player_source = normalize_player_source(player_source)
    if not player_source then
        return false, "invalid_source"
    end
    if type(notification) ~= "table" then
        return false, "invalid_notification"
    end
    if notification.appId ~= nil and notification.appId ~= app_id then
        return false, "invalid_app_id"
    end
    if notification.app_id ~= nil and notification.app_id ~= app_id then
        return false, "invalid_app_id"
    end

    local notifications = SkyPhoneNotifications
    if type(notifications) ~= "table" or type(notifications.Send) ~= "function" then
        return false, "api_not_ready"
    end

    local result, notification_error = notifications.Send(
        { kind = "source", value = player_source },
        {
            appId = app_id,
            text = notification.text or notification.content,
            title = notification.title,
        }
    )
    if not result then
        return false, notification_error
    end
    if result.delivered ~= 1 then
        return false, "device_not_equipped"
    end
    return true, result
end

SkyPhoneApps.GetPolicy = get_custom_app_policy
SkyPhoneApps.HasPermission = has_custom_app_permission
SkyPhoneApps.ServerPublicApi = {
    AddCustomAppPolicy = add_custom_app_policy,
    AddCustomAppPolicyFromAdapter = add_custom_app_policy_from_adapter,
    GetCustomAppCapabilities = get_custom_app_capabilities,
    GetCustomAppPolicy = get_custom_app_policy,
    HasCustomAppPermission = has_custom_app_permission,
    RemoveCustomAppPolicy = remove_custom_app_policy,
    RemoveCustomAppPolicyFromAdapter = remove_custom_app_policy_from_adapter,
    SendAppMessage = send_custom_app_message,
    SendCustomAppMessage = send_custom_app_message,
    SendCustomAppNotification = send_custom_app_notification,
    UpdateCustomAppPolicy = update_custom_app_policy,
    UpdateCustomAppPolicyFromAdapter = update_custom_app_policy_from_adapter,
}

local function refresh_bundled_policies()
    local removals = {}
    for app_id, policy in pairs(policies_by_id) do
        if policy.bundled then
            removals[#removals + 1] = app_id
        end
    end
    for _, app_id in ipairs(removals) do
        remove_policy(app_id)
    end

    if Config.CustomApps.Enabled and Config.CustomApps.BundledApps then
        local bundled = SkyPhoneApps.GetBundledManifests()
        for index = 1, #bundled do
            local manifest = bundled[index]
            local registered, register_error = register_policy({
                adapterResource = nil,
                bundled = true,
                id = manifest.id,
                ownerResource = current_resource,
                permissions = build_permission_set(manifest.permissions),
            })
            if not registered then
                error(("[sky_phone] Could not register bundled custom app policy '%s': %s"):format(
                    manifest.id,
                    register_error
                ))
            end
        end
    end
end

refresh_bundled_policies()

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

AddEventHandler("onResourceStop", function(resource_name)
    if resource_name == current_resource then
        return
    end

    local app_ids = {}
    local owner_policies = policy_ids_by_owner[resource_name]
    if owner_policies then
        for app_id in pairs(owner_policies) do
            app_ids[app_id] = true
        end
    end

    local adapter_policies = policy_ids_by_adapter[resource_name]
    if adapter_policies then
        for app_id in pairs(adapter_policies) do
            app_ids[app_id] = true
        end
    end

    for app_id in pairs(app_ids) do
        remove_policy(app_id)
    end
end)
