local MOD_NAME = "SurrounDeadOnline-Phase9A-FixedNoon"
local dataPath = [[__DATA_PATH__]]

local triggerPath =
    dataPath .. "\\phase9a.fixed_noon_once.txt"

local disablePath =
    dataPath .. "\\phase9a.disable_fixed_noon.txt"

local loadedPath =
    dataPath .. "\\lua_phase9a_fixed_noon_loaded.txt"

local probePath =
    dataPath .. "\\lua_phase9a_fixed_noon_probe.txt"

local resultPath =
    dataPath .. "\\lua_phase9a_fixed_noon_result.txt"

local errorPath =
    dataPath .. "\\lua_phase9a_fixed_noon_error.txt"

local localStatePath =
    dataPath .. "\\local_state.tsv"

local UEHelpers = require("UEHelpers")

local TARGET_TIME_OF_DAY = 1200.0
local LOOP_INTERVAL_MS = 500
local READY_TICKS_REQUIRED = 40

local attempted = false
local scheduled = false
local readyTicks = 0

local timePropertyCandidates = {
    "Time Of Day",
    "Time of Day",
    "TimeOfDay"
}

local animatePropertyCandidates = {
    "Animate Time of Day",
    "Animate Time Of Day",
    "AnimateTimeOfDay"
}

local systemTimePropertyCandidates = {
    "Use System Time",
    "UseSystemTime"
}

local udsLinkCandidates = {
    "UDS",
    "Ultra Dynamic Sky",
    "Ultra_Dynamic_Sky",
    "UltraDynamicSky",
    "Sky",
    "Sky Actor",
    "Time Of Day System",
    "Time of Day System"
}

local function log(message)
    print(
        string.format(
            "[%s] %s\n",
            MOD_NAME,
            tostring(message)
        )
    )
end

local function fileExists(path)
    local file = io.open(path, "r")

    if file then
        file:close()
        return true
    end

    return false
end

local function sanitize(value)
    local text = tostring(value or "")
    text = text:gsub("[\r\n\t]", " ")

    if #text > 700 then
        text = text:sub(1, 700)
    end

    return text
end

local function atomicWrite(path, contents)
    local tempPath = path .. ".tmp"
    local file, errorMessage = io.open(tempPath, "w")

    if not file then
        return false, errorMessage
    end

    file:write(contents)
    file:flush()
    file:close()

    os.remove(path)

    local ok, renameError =
        os.rename(tempPath, path)

    if not ok then
        return false, renameError
    end

    return true, nil
end

local function isValidObject(value)
    if value == nil then
        return false
    end

    local ok, valid =
        pcall(function()
            return value:IsValid()
        end)

    return ok and valid == true
end

local function objectName(value)
    if not isValidObject(value) then
        return "<invalid>"
    end

    local ok, name =
        pcall(function()
            return value:GetFullName()
        end)

    if ok and name then
        return sanitize(name)
    end

    return "<name-unavailable>"
end

local function safeRead(object, propertyName)
    if not isValidObject(object) then
        return false, nil
    end

    local ok, value =
        pcall(function()
            return object[propertyName]
        end)

    if not ok or value == nil then
        return false, nil
    end

    return true, value
end

local function findFirstValid(classNames)
    for _, className in ipairs(classNames) do
        local ok, value =
            pcall(function()
                return FindFirstOf(className)
            end)

        if ok and isValidObject(value) then
            return value, className
        end
    end

    return nil, nil
end

local function findUDS()
    local direct, directClass =
        findFirstValid({
            "Ultra_Dynamic_Sky_C",
            "Ultra_Dynamic_Sky",
            "UltraDynamicSky_C",
            "UltraDynamicSky"
        })

    if direct then
        return direct, "FindFirstOf:" .. tostring(directClass)
    end

    local manager =
        findFirstValid({
            "Random_Weather_Variation_C"
        })

    if not manager then
        return nil, "random-weather-manager-not-found"
    end

    local readableUDW, udw =
        safeRead(
            manager,
            "UDW"
        )

    if not readableUDW or not isValidObject(udw) then
        return nil, "manager-udw-link-not-found"
    end

    for _, propertyName in ipairs(udsLinkCandidates) do
        local readable, value =
            safeRead(
                udw,
                propertyName
            )

        if readable and isValidObject(value) then
            return value, "UDW." .. propertyName
        end
    end

    for _, propertyName in ipairs(udsLinkCandidates) do
        local readable, value =
            safeRead(
                manager,
                propertyName
            )

        if readable and isValidObject(value) then
            return value, "Manager." .. propertyName
        end
    end

    return nil, "uds-link-not-found"
end

local function findProperty(
    object,
    candidates,
    expectedLuaType
)
    for _, propertyName in ipairs(candidates) do
        local readable, value =
            safeRead(
                object,
                propertyName
            )

        if (
            readable and
            type(value) == expectedLuaType
        ) then
            return propertyName, value
        end
    end

    return nil, nil
end

local function getLocalPawn()
    local okController, controller =
        pcall(function()
            return UEHelpers:GetPlayerController()
        end)

    if (
        not okController or
        not isValidObject(controller)
    ) then
        return nil
    end

    local okPawn, pawn =
        pcall(function()
            if controller.Pawn then
                return controller.Pawn
            end

            return controller:GetPawn()
        end)

    if (
        not okPawn or
        not isValidObject(pawn)
    ) then
        return nil
    end

    return pawn
end

local function writeProbe(
    sky,
    source,
    timeProperty,
    beforeTime,
    animateProperty,
    beforeAnimate,
    systemTimeProperty,
    useSystemTime
)
    atomicWrite(
        probePath,
        "phase=9A\n" ..
        "readOnlyProbeComplete=1\n" ..
        "udsFound=" ..
            tostring(sky and 1 or 0) .. "\n" ..
        "udsSource=" ..
            sanitize(source) .. "\n" ..
        "udsObject=" ..
            objectName(sky) .. "\n" ..
        "timeProperty=" ..
            sanitize(timeProperty or "<not-found>") .. "\n" ..
        "timeBefore=" ..
            sanitize(beforeTime or "<not-readable>") .. "\n" ..
        "animateProperty=" ..
            sanitize(animateProperty or "<not-found>") .. "\n" ..
        "animateBefore=" ..
            sanitize(beforeAnimate) .. "\n" ..
        "systemTimeProperty=" ..
            sanitize(systemTimeProperty or "<not-found>") .. "\n" ..
        "useSystemTime=" ..
            sanitize(useSystemTime) .. "\n" ..
        "rawObjectPointerWrites=0\n" ..
        "weatherWrites=0\n" ..
        "networkWorldSync=0\n"
    )
end

local function applyFixedNoon()
    if attempted then
        return
    end
    -- Public launcher: run once automatically per game process.
if fileExists(disablePath) then
        attempted = true
        os.remove(triggerPath)

        atomicWrite(
            errorPath,
            "phase=9A\n" ..
            "disabledByKillSwitch=1\n" ..
            "normalMultiplayerUnaffected=1\n"
        )

        return
    end

    if not fileExists(localStatePath) then
        readyTicks = 0
        return
    end

    if not getLocalPawn() then
        readyTicks = 0
        return
    end

    readyTicks = readyTicks + 1

    if readyTicks < READY_TICKS_REQUIRED then
        return
    end

    if scheduled then
        return
    end

    scheduled = true

    ExecuteInGameThread(function()
        scheduled = false

        if attempted then
            return
        end

        attempted = true
        os.remove(triggerPath)
        os.remove(resultPath)
        os.remove(errorPath)

        local sky, source =
            findUDS()

        if not sky then
            writeProbe(
                nil,
                source,
                nil,
                nil,
                nil,
                nil,
                nil,
                nil
            )

            atomicWrite(
                errorPath,
                "phase=9A\n" ..
                "fixedNoonApplied=0\n" ..
                "reason=" ..
                    sanitize(source) .. "\n" ..
                "unrealPropertiesModified=0\n" ..
                "normalMultiplayerUnaffected=1\n"
            )

            return
        end

        local timeProperty, beforeTime =
            findProperty(
                sky,
                timePropertyCandidates,
                "number"
            )

        local animateProperty, beforeAnimate =
            findProperty(
                sky,
                animatePropertyCandidates,
                "boolean"
            )

        local systemTimeProperty, useSystemTime =
            findProperty(
                sky,
                systemTimePropertyCandidates,
                "boolean"
            )

        writeProbe(
            sky,
            source,
            timeProperty,
            beforeTime,
            animateProperty,
            beforeAnimate,
            systemTimeProperty,
            useSystemTime
        )

        if not timeProperty then
            atomicWrite(
                errorPath,
                "phase=9A\n" ..
                "fixedNoonApplied=0\n" ..
                "reason=time-property-not-found\n" ..
                "udsObject=" ..
                    objectName(sky) .. "\n" ..
                "unrealPropertiesModified=0\n" ..
                "normalMultiplayerUnaffected=1\n"
            )

            return
        end

        if not animateProperty then
            atomicWrite(
                errorPath,
                "phase=9A\n" ..
                "fixedNoonApplied=0\n" ..
                "reason=animate-time-property-not-found\n" ..
                "udsObject=" ..
                    objectName(sky) .. "\n" ..
                "unrealPropertiesModified=0\n" ..
                "normalMultiplayerUnaffected=1\n"
            )

            return
        end

        if (
            systemTimeProperty and
            useSystemTime == true
        ) then
            atomicWrite(
                errorPath,
                "phase=9A\n" ..
                "fixedNoonApplied=0\n" ..
                "reason=use-system-time-is-enabled\n" ..
                "unrealPropertiesModified=0\n" ..
                "normalMultiplayerUnaffected=1\n"
            )

            return
        end

        -- This is the entire Unreal write surface for Phase 9A:
        -- one Boolean scalar and one numeric scalar, once per game launch.
        local ok, errorMessage =
            pcall(function()
                sky[animateProperty] = false
                sky[timeProperty] = TARGET_TIME_OF_DAY
            end)

        if not ok then
            atomicWrite(
                errorPath,
                "phase=9A\n" ..
                "fixedNoonApplied=0\n" ..
                "reason=scalar-write-failed\n" ..
                "message=" ..
                    sanitize(errorMessage) .. "\n" ..
                "normalMultiplayerUnaffected=1\n"
            )

            return
        end

        local timeReadable, afterTime =
            safeRead(
                sky,
                timeProperty
            )

        local animateReadable, afterAnimate =
            safeRead(
                sky,
                animateProperty
            )

        local timeMatches =
            timeReadable and
            type(afterTime) == "number" and
            math.abs(afterTime - TARGET_TIME_OF_DAY) < 0.5

        local animateStopped =
            animateReadable and
            afterAnimate == false

        atomicWrite(
            resultPath,
            "phase=9A\n" ..
            "fixedNoonApplied=" ..
                tostring(
                    timeMatches and animateStopped and 1 or 0
                ) .. "\n" ..
            "udsSource=" ..
                sanitize(source) .. "\n" ..
            "udsObject=" ..
                objectName(sky) .. "\n" ..
            "timeProperty=" ..
                sanitize(timeProperty) .. "\n" ..
            "timeBefore=" ..
                sanitize(beforeTime) .. "\n" ..
            "timeRequested=" ..
                tostring(TARGET_TIME_OF_DAY) .. "\n" ..
            "timeAfter=" ..
                sanitize(afterTime) .. "\n" ..
            "animateProperty=" ..
                sanitize(animateProperty) .. "\n" ..
            "animateBefore=" ..
                sanitize(beforeAnimate) .. "\n" ..
            "animateAfter=" ..
                sanitize(afterAnimate) .. "\n" ..
            "oneShotScalarWrites=2\n" ..
            "weatherWrites=0\n" ..
            "objectPointerWrites=0\n" ..
            "repeatedWrites=0\n" ..
            "actorTickDisabled=0\n" ..
            "networkWorldSync=0\n" ..
            "normalMultiplayerModified=0\n"
        )

        if not (timeMatches and animateStopped) then
            atomicWrite(
                errorPath,
                "phase=9A\n" ..
                "fixedNoonApplied=0\n" ..
                "reason=readback-did-not-match\n" ..
                "timeAfter=" ..
                    sanitize(afterTime) .. "\n" ..
                "animateAfter=" ..
                    sanitize(afterAnimate) .. "\n"
            )

            return
        end

        log(
            "Fixed noon applied once. " ..
            "Time of Day is 1200 and animation is disabled."
        )
    end)
end

os.remove(probePath)
os.remove(resultPath)
os.remove(errorPath)

atomicWrite(
    loadedPath,
    "phase=9A\n" ..
    "fixedNoonLoader=1\n" ..
    "targetTimeOfDay=1200\n" ..
    "waitAfterGameplaySeconds=20\n" ..
    "oneShotOnly=1\n" ..
    "weatherWrites=0\n" ..
    "objectPointerWrites=0\n" ..
    "networkWorldSync=0\n" ..
    "globalObjectScanUsed=0\n" ..
    "classReflectionScanUsed=0\n" ..
    "actorTickDisabled=0\n"
)

log("Phase 9A fixed-noon loader started.")

LoopAsync(LOOP_INTERVAL_MS, function()
    local ok, errorMessage =
        pcall(applyFixedNoon)

    if not ok and not attempted then
        atomicWrite(
            errorPath,
            "phase=9A\n" ..
            "fixedNoonApplied=0\n" ..
            "reason=loader-error\n" ..
            "message=" ..
                sanitize(errorMessage) .. "\n" ..
            "normalMultiplayerUnaffected=1\n"
        )

        attempted = true
        os.remove(triggerPath)
    end

    if attempted then
        return true
    end

    return false
end)
