Bridge.Database.AfterMigration("sky_phone", function()
local config = Config.CityWarn

if type(config) ~= "table" then
    error("[sky_phone] Config.CityWarn must be configured.")
end

local categories = {
    public_safety = true,
    police = true,
    fire = true,
    medical = true,
    infrastructure = true,
    evacuation = true,
}
local severity_rank = {
    information = 1,
    warning = 2,
    danger = 3,
    extreme = 4,
}

local function require_integer_config(name, minimum, maximum)
    local value = config[name]
    if type(value) ~= "number" or value ~= math.floor(value) or value < minimum or value > maximum then
        error(("[sky_phone] Config.CityWarn.%s must be an integer between %d and %d."):format(name, minimum, maximum))
    end
end

if type(config.Enabled) ~= "boolean" or type(config.RequireDuty) ~= "boolean" then
    error("[sky_phone] CityWarn Enabled and RequireDuty must be booleans.")
end
require_integer_config("PageSize", 1, 100)
require_integer_config("MaximumActiveAlerts", 1, 1000)
require_integer_config("TitleMaxLength", 1, 120)
require_integer_config("BodyMaxLength", 1, 2000)
require_integer_config("InstructionsMaxLength", 1, 2000)
require_integer_config("UpdateMaxLength", 1, 2000)
require_integer_config("AreaLabelMaxLength", 1, 120)
require_integer_config("MinimumRadius", 1, 10000)
require_integer_config("MaximumRadius", config.MinimumRadius, 50000)
require_integer_config("DefaultDurationMinutes", 1, 1440)
require_integer_config("MaximumDurationMinutes", config.DefaultDurationMinutes, 10080)

if type(config.RateLimits) ~= "table" or type(config.Publishers) ~= "table" then
    error("[sky_phone] CityWarn RateLimits and Publishers must be tables.")
end
for _, name in ipairs({ "Read", "Write" }) do
    local value = config.RateLimits[name]
    if type(value) ~= "number" or value ~= math.floor(value) or value < 1 or value > 10000 then
        error(("[sky_phone] Invalid CityWarn rate limit '%s'."):format(name))
    end
end
for job_name, publisher in pairs(config.Publishers) do
    if type(job_name) ~= "string" or not job_name:match("^[%w_-]+$") or type(publisher) ~= "table"
        or type(publisher.MinimumGrade) ~= "number" or publisher.MinimumGrade < 0
        or publisher.MinimumGrade ~= math.floor(publisher.MinimumGrade)
        or not severity_rank[publisher.MaximumSeverity] or type(publisher.CityWide) ~= "boolean"
        or type(publisher.Categories) ~= "table"
    then
        error(("[sky_phone] Invalid CityWarn publisher '%s'."):format(tostring(job_name)))
    end
    local seen = {}
    for _, category in ipairs(publisher.Categories) do
        if not categories[category] or seen[category] then
            error(("[sky_phone] Invalid CityWarn category '%s' for publisher '%s'."):format(tostring(category), job_name))
        end
        seen[category] = true
    end
end

local alert_columns = [[
    alert.`id`, alert.`title`, alert.`body`, alert.`instructions`, alert.`category`,
    alert.`severity`, alert.`status`, alert.`area_type`, alert.`area_label`,
    alert.`center_x`, alert.`center_y`, alert.`radius`, alert.`source_label`,
    alert.`author_name`, alert.`revision`,
    UNIX_TIMESTAMP(alert.`starts_at`) AS `starts_at_unix`,
    UNIX_TIMESTAMP(alert.`expires_at`) AS `expires_at_unix`,
    UNIX_TIMESTAMP(alert.`created_at`) AS `created_at_unix`,
    UNIX_TIMESTAMP(alert.`updated_at`) AS `updated_at_unix`
]]

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

local function text_length(value)
    if type(value) ~= "string" then
        return nil
    end
    local success, length = pcall(utf8.len, value)
    return success and length or nil
end

local function valid_text(value, maximum, allow_empty)
    local normalized = trim(value)
    local length = text_length(normalized)
    if not length or normalized:find("%z") or length > maximum or (not allow_empty and length < 1) then
        return nil
    end
    return normalized
end

local function valid_number(value, minimum, maximum)
    local number = tonumber(value)
    if not number or number ~= number or number < minimum or number > maximum then
        return nil
    end
    return number
end

local function valid_integer(value, minimum, maximum)
    local number = valid_number(value, minimum, maximum)
    return number and number == math.floor(number) and number or nil
end

local function valid_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 affected_rows(result)
    if type(result) == "number" then
        return result
    end
    return type(result) == "table" and tonumber(result.affectedRows) or 0
end

local function new_uuid()
    local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {})
    local id = rows[1] and rows[1].id
    if not valid_uuid(id) then
        error("[sky_phone] Database did not generate a valid CityWarn UUID.")
    end
    return id
end

local function require_phone(source, operation, maximum)
    if not config.Enabled then
        return nil, { success = false, error = "feature_disabled" }
    end
    local session, error_response = SkyPhone.RequireSession(source)
    if not session then
        return nil, error_response
    end
    if not SkyPhone.AllowOperation(source, "citywarn_" .. operation, maximum, 60) then
        return nil, { success = false, error = "rate_limited" }
    end
    return session
end

local function publisher_access(source)
    local job = Bridge.Framework.GetJob(source)
    local publisher = config.Publishers[job.name]
    local grade = tonumber(job.grade) or 0
    local on_duty = job.onDuty == true
    if not publisher or grade < publisher.MinimumGrade or (config.RequireDuty and not on_duty) then
        return nil, job, publisher
    end
    local allowed_categories = {}
    local allowed_lookup = {}
    for _, category in ipairs(publisher.Categories) do
        allowed_categories[#allowed_categories + 1] = category
        allowed_lookup[category] = true
    end
    return {
        allowed_categories = allowed_categories,
        allowed_lookup = allowed_lookup,
        can_city_wide = publisher.CityWide,
        job_name = job.name,
        job_label = type(job.label) == "string" and job.label or job.name,
        grade_label = type(job.gradeLabel) == "string" and job.gradeLabel or "",
        maximum_severity = publisher.MaximumSeverity,
        on_duty = on_duty,
    }, job, publisher
end

local function context_dto(source)
    local access, job, publisher = publisher_access(source)
    local allowed_categories = {}
    if publisher and type(publisher.Categories) == "table" then
        for _, category in ipairs(publisher.Categories) do
            allowed_categories[#allowed_categories + 1] = category
        end
    end
    return {
        canPublish = access ~= nil,
        jobLabel = publisher and (job.label or job.name) or nil,
        gradeLabel = publisher and (job.gradeLabel or "") or nil,
        onDuty = job.onDuty == true,
        requiresDuty = config.RequireDuty,
        allowedCategories = allowed_categories,
        maximumSeverity = publisher and publisher.MaximumSeverity or nil,
        canCityWide = publisher and publisher.CityWide == true or false,
    }
end

local function require_publisher(source)
    local access = publisher_access(source)
    if not access then
        return nil, { success = false, error = "not_authorized" }
    end
    return access
end

local function actor_identity(source)
    local identifier = Bridge.Framework.GetIdentifier(source)
    if type(identifier) ~= "string" or #identifier < 1 or #identifier > 80 then
        return nil
    end
    local first_name = trim(Bridge.Framework.GetFirstname(source)) or ""
    local last_name = trim(Bridge.Framework.GetLastname(source)) or ""
    local name = trim(first_name .. " " .. last_name)
    if not name or name == "" then
        name = trim(GetPlayerName(source)) or "CityWarn"
    end
    if #name > 120 then
        name = name:sub(1, 120)
    end
    return { identifier = identifier, name = name }
end

local function alert_dto(row)
    return {
        id = row.id,
        title = row.title,
        body = row.body,
        instructions = row.instructions,
        category = row.category,
        severity = row.severity,
        status = row.status,
        area = {
            type = row.area_type,
            label = row.area_label,
            centerX = row.center_x and tonumber(row.center_x) or nil,
            centerY = row.center_y and tonumber(row.center_y) or nil,
            radius = row.radius and tonumber(row.radius) or nil,
        },
        sourceLabel = row.source_label,
        authorName = row.author_name,
        revision = tonumber(row.revision) or 1,
        startsAt = tonumber(row.starts_at_unix) * 1000,
        expiresAt = tonumber(row.expires_at_unix) * 1000,
        createdAt = tonumber(row.created_at_unix) * 1000,
        updatedAt = tonumber(row.updated_at_unix) * 1000,
        updates = {},
    }
end

local function attach_updates(alerts)
    if #alerts < 1 then
        return alerts
    end
    local ids, placeholders, by_id = {}, {}, {}
    for index, alert in ipairs(alerts) do
        ids[index] = alert.id
        placeholders[index] = "?"
        by_id[alert.id] = alert
    end
    local rows = Bridge.Database.Query(([[
        SELECT `id`, `alert_id`, `kind`, `message`, `actor_name`,
            UNIX_TIMESTAMP(`created_at`) AS `created_at_unix`
        FROM `sky_phone_citywarn_updates`
        WHERE `alert_id` IN (%s)
        ORDER BY `created_at` DESC, `id` DESC
    ]]):format(table.concat(placeholders, ",")), ids)
    for _, row in ipairs(rows) do
        local alert = by_id[row.alert_id]
        if alert then
            alert.updates[#alert.updates + 1] = {
                id = row.id,
                kind = row.kind,
                message = row.message,
                actorName = row.actor_name,
                createdAt = tonumber(row.created_at_unix) * 1000,
            }
        end
    end
    return alerts
end

local function expire_alerts()
    Bridge.Database.Query([[
        UPDATE `sky_phone_citywarn_alerts`
        SET `status` = 'expired', `revision` = `revision` + 1
        WHERE `status` = 'active' AND `expires_at` <= NOW()
    ]], {})
end

local function query_alerts(status)
    local where = status == "active"
        and "alert.`status` = 'active' AND alert.`expires_at` > NOW()"
        or "(alert.`status` <> 'active' OR alert.`expires_at` <= NOW())"
    local rows = Bridge.Database.Query(([[
        SELECT %s
        FROM `sky_phone_citywarn_alerts` alert
        WHERE %s
        ORDER BY alert.`created_at` DESC, alert.`id` DESC
        LIMIT ?
    ]]):format(alert_columns, where), { config.PageSize })
    local alerts = {}
    for _, row in ipairs(rows) do
        alerts[#alerts + 1] = alert_dto(row)
    end
    return attach_updates(alerts)
end

local function load_alert(id)
    local rows = Bridge.Database.Query(([[
        SELECT %s FROM `sky_phone_citywarn_alerts` alert WHERE alert.`id` = ? LIMIT 1
    ]]):format(alert_columns), { id })
    local alert = rows[1] and alert_dto(rows[1]) or nil
    return alert and attach_updates({ alert })[1] or nil
end

local function validate_area(data, access)
    if type(data) ~= "table" or (data.type ~= "radius" and data.type ~= "district" and data.type ~= "city") then
        return nil
    end
    local label = valid_text(data.label, config.AreaLabelMaxLength, false)
    if not label or (data.type == "city" and not access.can_city_wide) then
        return nil
    end
    local center_x, center_y, radius
    if data.type == "radius" then
        center_x = valid_number(data.centerX, -10000, 10000)
        center_y = valid_number(data.centerY, -10000, 10000)
        radius = valid_integer(data.radius, config.MinimumRadius, config.MaximumRadius)
        if not center_x or not center_y or not radius then
            return nil
        end
    elseif data.centerX ~= nil or data.centerY ~= nil then
        center_x = valid_number(data.centerX, -10000, 10000)
        center_y = valid_number(data.centerY, -10000, 10000)
        if not center_x or not center_y then
            return nil
        end
    end
    return {
        type = data.type,
        label = label,
        center_x = center_x,
        center_y = center_y,
        radius = radius,
    }
end

local function broadcast(kind, alert)
    TriggerClientEvent("sky_phone:citywarn:changed", -1, {
        alert = alert,
        alertId = alert.id,
        kind = kind,
        severity = alert.severity,
        sourceLabel = alert.sourceLabel,
    })
end

Bridge.Callbacks.Register("sky_phone:citywarn:bootstrap", function(source)
    local _, error_response = require_phone(source, "read", config.RateLimits.Read)
    if error_response then
        return error_response
    end
    expire_alerts()
    return {
        success = true,
        data = {
            active = query_alerts("active"),
            archive = query_alerts("archive"),
            context = context_dto(source),
            onlinePlayers = #Bridge.Framework.GetPlayers(),
        },
    }
end)

Bridge.Callbacks.Register("sky_phone:citywarn:publish", function(source, data)
    local _, error_response = require_phone(source, "write", config.RateLimits.Write)
    if error_response then
        return error_response
    end
    local access, access_error = require_publisher(source)
    if access_error then
        return access_error
    end
    local actor = actor_identity(source)
    local title = type(data) == "table" and valid_text(data.title, config.TitleMaxLength, false) or nil
    local body = type(data) == "table" and valid_text(data.body, config.BodyMaxLength, false) or nil
    local instructions = type(data) == "table" and valid_text(data.instructions, config.InstructionsMaxLength, true) or nil
    local duration = type(data) == "table" and valid_integer(data.durationMinutes, 1, config.MaximumDurationMinutes) or nil
    local area = type(data) == "table" and validate_area(data.area, access) or nil
    if not actor or not title or not body or instructions == nil or not duration or not area
        or not access.allowed_lookup[data.category]
        or not severity_rank[data.severity]
        or severity_rank[data.severity] > severity_rank[access.maximum_severity]
    then
        return { success = false, error = "invalid_warning" }
    end
    local counts = Bridge.Database.Query([[
        SELECT COUNT(*) AS `count` FROM `sky_phone_citywarn_alerts`
        WHERE `status` = 'active' AND `expires_at` > NOW()
    ]], {})
    if tonumber(counts[1] and counts[1].count) >= config.MaximumActiveAlerts then
        return { success = false, error = "active_limit" }
    end

    local id = new_uuid()
    local result = Bridge.Database.Query([[
        INSERT INTO `sky_phone_citywarn_alerts`
            (`id`, `title`, `body`, `instructions`, `category`, `severity`, `area_type`, `area_label`,
             `center_x`, `center_y`, `radius`, `source_job`, `source_label`, `author_identifier`,
             `author_name`, `expires_at`)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL ? MINUTE))
    ]], {
        id, title, body, instructions, data.category, data.severity, area.type, area.label,
        area.center_x, area.center_y, area.radius, access.job_name, access.job_label,
        actor.identifier, actor.name, duration,
    })
    if affected_rows(result) ~= 1 then
        return { success = false, error = "request_failed" }
    end
    Bridge.Database.Query([[
        INSERT INTO `sky_phone_citywarn_updates`
            (`id`, `alert_id`, `kind`, `message`, `actor_identifier`, `actor_name`)
        VALUES (?, ?, 'published', ?, ?, ?)
    ]], { new_uuid(), id, title, actor.identifier, actor.name })
    local alert = load_alert(id)
    broadcast("published", alert)
    return {
        success = true,
        data = { alert = alert, recipients = #Bridge.Framework.GetPlayers() },
    }
end)

Bridge.Callbacks.Register("sky_phone:citywarn:update", function(source, data)
    local _, error_response = require_phone(source, "write", config.RateLimits.Write)
    if error_response then
        return error_response
    end
    local _, access_error = require_publisher(source)
    if access_error then
        return access_error
    end
    local actor = actor_identity(source)
    local message = type(data) == "table" and valid_text(data.message, config.UpdateMaxLength, false) or nil
    local revision = type(data) == "table" and valid_integer(data.revision, 1, 4294967295) or nil
    if not actor or not message or not revision or not valid_uuid(data.id) then
        return { success = false, error = "invalid_update" }
    end
    local result = Bridge.Database.Query([[
        UPDATE `sky_phone_citywarn_alerts`
        SET `revision` = `revision` + 1
        WHERE `id` = ? AND `status` = 'active' AND `expires_at` > NOW() AND `revision` = ?
    ]], { data.id, revision })
    if affected_rows(result) ~= 1 then
        return { success = false, error = "revision_conflict" }
    end
    Bridge.Database.Query([[
        INSERT INTO `sky_phone_citywarn_updates`
            (`id`, `alert_id`, `kind`, `message`, `actor_identifier`, `actor_name`)
        VALUES (?, ?, 'update', ?, ?, ?)
    ]], { new_uuid(), data.id, message, actor.identifier, actor.name })
    local alert = load_alert(data.id)
    broadcast("update", alert)
    return { success = true, data = { alert = alert } }
end)

Bridge.Callbacks.Register("sky_phone:citywarn:resolve", function(source, data)
    local _, error_response = require_phone(source, "write", config.RateLimits.Write)
    if error_response then
        return error_response
    end
    local _, access_error = require_publisher(source)
    if access_error then
        return access_error
    end
    local actor = actor_identity(source)
    local message = type(data) == "table" and valid_text(data.message, config.UpdateMaxLength, false) or nil
    local revision = type(data) == "table" and valid_integer(data.revision, 1, 4294967295) or nil
    if not actor or not message or not revision or not valid_uuid(data.id) then
        return { success = false, error = "invalid_update" }
    end
    local result = Bridge.Database.Query([[
        UPDATE `sky_phone_citywarn_alerts`
        SET `status` = 'resolved', `resolved_at` = NOW(), `revision` = `revision` + 1
        WHERE `id` = ? AND `status` = 'active' AND `revision` = ?
    ]], { data.id, revision })
    if affected_rows(result) ~= 1 then
        return { success = false, error = "revision_conflict" }
    end
    Bridge.Database.Query([[
        INSERT INTO `sky_phone_citywarn_updates`
            (`id`, `alert_id`, `kind`, `message`, `actor_identifier`, `actor_name`)
        VALUES (?, ?, 'resolved', ?, ?, ?)
    ]], { new_uuid(), data.id, message, actor.identifier, actor.name })
    local alert = load_alert(data.id)
    broadcast("resolved", alert)
    return { success = true, data = { alert = alert } }
end)
end)
