local MOD_NAME = "BedSleep"
local SLEEP_MORNING_FRACTION = 0.21
local COOLDOWN_SECONDS = 10
local SLEEP_DELAY_MS = 3000

local lastSleepTime = -9999
local sleepPending = false

local UEHelpers = require("UEHelpers")
local GetKismetSystemLibrary = UEHelpers.GetKismetSystemLibrary
local GetKismetMathLibrary = UEHelpers.GetKismetMathLibrary
local GetPlayerController = UEHelpers.GetPlayerController

local BED_KEYWORDS = {
    "bed", "biobed", "bunk", "sleep", "cot", "furniture"
}

local function Log(message)
    print("[" .. MOD_NAME .. "] " .. tostring(message) .. "\n")
end

local function IsValidObject(object)
    if object == nil then
        return false
    end

    local ok, result = pcall(function()
        return object:IsValid()
    end)

    return ok and result == true
end

local function NormalizeFraction(value)
    local n = tonumber(value)
    if n == nil then
        return nil
    end

    while n < 0.0 do
        n = n + 1.0
    end

    while n >= 1.0 do
        n = n - 1.0
    end

    return n
end

local function GetCurrentFraction(component)
    local ok, value = pcall(function()
        if component.GetTimeOfDayFraction ~= nil then
            return component:GetTimeOfDayFraction()
        end
        return nil
    end)

    if ok and value ~= nil then
        return tonumber(value)
    end

    return nil
end

local function FindTimeComponent()
    local components = FindAllOf("UWETimeOfDayComponent")
    if not components or #components == 0 then
        return nil
    end

    for _, component in ipairs(components) do
        if IsValidObject(component) then
            return component
        end
    end

    return nil
end

local function IsNightByFraction(component)
    local current = NormalizeFraction(GetCurrentFraction(component))
    local hourStartNight = tonumber(component.HourStartNight)
    local hourStartDay = tonumber(component.HourStartDay)

    if current == nil or hourStartNight == nil or hourStartDay == nil then
        return nil
    end

    local nightStart = NormalizeFraction(hourStartNight / 24.0)
    local dayStart = NormalizeFraction(hourStartDay / 24.0)

    if nightStart == nil or dayStart == nil then
        return nil
    end

    if nightStart < dayStart then
        return current >= nightStart and current < dayStart
    end

    return current >= nightStart or current < dayStart
end

local function IsCurrentlyNight(component)
    local okNight, nightResult = pcall(function()
        if component.IsNight ~= nil then
            return component:IsNight()
        end
        return nil
    end)
    if okNight and nightResult ~= nil then
        return nightResult == true
    end

    local okPhase, phase = pcall(function()
        if component.GetDayPhase ~= nil then
            return component:GetDayPhase()
        end
        return nil
    end)
    if okPhase and phase ~= nil then
        local phaseText = tostring(phase):lower()
        if string.find(phaseText, "night", 1, true) ~= nil then
            return true
        end
        if string.find(phaseText, "day", 1, true) ~= nil or string.find(phaseText, "dawn", 1, true) ~= nil then
            return false
        end
    end

    return IsNightByFraction(component)
end

local function GetActorFromHitResult(hitResult)
    if UnrealVersion:IsBelow(5, 0) then
        return hitResult.Actor:Get()
    elseif UnrealVersion:IsBelow(5, 4) then
        return hitResult.HitObjectHandle.Actor:Get()
    else
        return hitResult.HitObjectHandle.ReferenceObject:Get()
    end
end

local function GetLookTargetActor()
    local ok, actorOrError = pcall(function()
        local ksl = GetKismetSystemLibrary()
        local kml = GetKismetMathLibrary()
        local playerController = GetPlayerController()
        if not IsValidObject(ksl) then
            return nil, "KismetSystemLibrary invalid"
        end
        if not IsValidObject(kml) then
            return nil, "KismetMathLibrary invalid"
        end
        if not IsValidObject(playerController) then
            return nil, "PlayerController invalid"
        end

        local playerPawn = playerController.Pawn
        local cameraManager = playerController.PlayerCameraManager
        if not IsValidObject(playerPawn) then
            return nil, "PlayerPawn invalid"
        end
        if not IsValidObject(cameraManager) then
            return nil, "PlayerCameraManager invalid"
        end

        local startVector = cameraManager:GetCameraLocation()
        local addValue = kml:Multiply_VectorInt(kml:GetForwardVector(cameraManager:GetCameraRotation()), 50000.0)
        local endVector = kml:Add_VectorVector(startVector, addValue)

        local traceColor = { R = 0, G = 0, B = 0, A = 0 }
        local hitResult = {}
        local wasHit = ksl:LineTraceSingle(
            playerPawn,
            startVector,
            endVector,
            0,
            false,
            {},
            0,
            hitResult,
            true,
            traceColor,
            traceColor,
            0.0
        )

        if not wasHit then
            return nil, "LineTraceSingle no hit"
        end

        local actor = GetActorFromHitResult(hitResult)
        if not IsValidObject(actor) then
            return nil, "LineTrace hit has no valid actor"
        end

        return actor, nil
    end)

    if not ok then
        return nil, "look target query failed: " .. tostring(actorOrError)
    end

    return actorOrError
end

local function SafeToLower(text)
    if text == nil then
        return ""
    end
    return tostring(text):lower()
end

local function ContainsBedKeyword(text)
    local lower = SafeToLower(text)
    for _, keyword in ipairs(BED_KEYWORDS) do
        if string.find(lower, keyword, 1, true) ~= nil then
            return true
        end
    end
    return false
end

local function GetActorClassName(actor)
    local ok, className = pcall(function()
        if actor:GetClass() ~= nil and actor:GetClass().GetFullName ~= nil then
            return actor:GetClass():GetFullName()
        end
        return "UnknownClass"
    end)

    if ok and className ~= nil then
        return tostring(className)
    end

    return "UnknownClass"
end

local function GetActorName(actor)
    local ok, name = pcall(function()
        if actor.GetName ~= nil then
            return actor:GetName()
        end
        return "UnknownName"
    end)

    if ok and name ~= nil then
        return tostring(name)
    end

    return "UnknownName"
end

local function GetActorFullName(actor)
    local ok, fullName = pcall(function()
        return actor:GetFullName()
    end)

    if ok and fullName ~= nil then
        return tostring(fullName)
    end

    return "UnknownFullName"
end

local function IsBedCandidate(actor)
    local fullName = GetActorFullName(actor)
    local name = GetActorName(actor)
    local className = GetActorClassName(actor)

    return ContainsBedKeyword(fullName) or ContainsBedKeyword(name) or ContainsBedKeyword(className)
end

local function SleepUntilMorning()
    local now = os.clock()
    if (now - lastSleepTime) < COOLDOWN_SECONDS then
        return false
    end

    local timeComponent = FindTimeComponent()
    if not IsValidObject(timeComponent) then
        Log("failed: no valid UWETimeOfDayComponent")
        return false
    end

    local isNight = IsCurrentlyNight(timeComponent)
    if isNight ~= true then
        Log("not night, skipping sleep")
        return false
    end

    Log("sleeping until morning")

    if timeComponent.SetTimeOfDay == nil then
        Log("failed: SetTimeOfDay unavailable")
        return false
    end

    local morningFraction = NormalizeFraction(SLEEP_MORNING_FRACTION)
    Log("sleep target fraction fixed: " .. tostring(morningFraction))

    local ok, err = pcall(function()
        timeComponent:SetTimeOfDay(morningFraction)
    end)

    if not ok then
        Log("failed: " .. tostring(err))
        return false
    end

    lastSleepTime = now
    Log("success")
    return true
end

local function TriggerBedSleepAttempt()
    ExecuteInGameThread(function()
        if sleepPending then
            return
        end

        local lookTarget, _ = GetLookTargetActor()
        if not IsValidObject(lookTarget) then
            return
        end

        if not IsBedCandidate(lookTarget) then
            return
        end

        Log("sleep started")
        sleepPending = true

        ExecuteWithDelay(SLEEP_DELAY_MS, function()
            ExecuteInGameThread(function()
                if not sleepPending then
                    return
                end

                SleepUntilMorning()
                sleepPending = false
            end)
        end)
    end)
end

local function OnLeftMousePressed()
    TriggerBedSleepAttempt()
end

local function RegisterHotkey()
    local ok, err = pcall(function()
        RegisterKeyBind(Key.LEFT_MOUSE_BUTTON, OnLeftMousePressed)
    end)

    if not ok then
        Log("failed: " .. tostring(err))
    end
end

RegisterHotkey()
Log("loaded")
