-- File: lua/entities/ai_npc/init.lua

AddCSLuaFile("cl_init.lua")
AddCSLuaFile("shared.lua")

include("shared.lua")

-- Define valid actions and items as entity properties for accessibility
ENT.validActions = {
    "Move to player",
    "Move away",
    "Equip shotgun",
    "Random move",
    "Say hello",
    "Dance",
    "Jump",
    "Give item to player",
    "Attack player",
    "Wave",
    "Sit down",
    "Follow player",
    "Stop following",
    "Tell joke",
    "Compliment player",
    "Insult player",
    "Pickup item",
    "Drop item"
}

ENT.validItems = {"weapon_pistol", "weapon_shotgun", "weapon_crowbar"}

-- Initialize the entity
function ENT:Initialize()
    print("Initializing NPC:", self:GetName())

    local models = {
        "models/Humans/Group01/male_01.mdl",
        "models/Humans/Group01/male_02.mdl",
        "models/Humans/Group01/male_03.mdl",
        "models/Humans/Group01/male_04.mdl",
        "models/Humans/Group01/male_05.mdl",
        "models/Humans/Group01/male_06.mdl",
        "models/Humans/Group01/male_07.mdl",
        "models/Humans/Group01/male_08.mdl",
        "models/Humans/Group01/male_09.mdl",
        "models/Humans/Group01/female_01.mdl",
        "models/Humans/Group01/female_02.mdl",
        "models/Humans/Group01/female_03.mdl",
        "models/Humans/Group01/female_04.mdl",
        "models/Humans/Group01/female_06.mdl",
        "models/Humans/Group01/female_07.mdl"
    }

    self:SetModel(models[math.random(#models)])
    self:SetHullType(HULL_HUMAN)
    self:SetHullSizeNormal()
    self:SetNPCState(NPC_STATE_SCRIPT)
    self:SetSolid(SOLID_BBOX)
    self:CapabilitiesAdd(bit.bor(
        CAP_MOVE_GROUND,
        CAP_ANIMATEDFACE,
        CAP_USE_WEAPONS,
        CAP_WEAPON_MELEE_ATTACK1,
        CAP_INNATE_MELEE_ATTACK1
    ))
    self:SetUseType(SIMPLE_USE)
    self:DropToFloor()
    self.NextThinkTime = 0
    self:SetHealth(100)
    self:SetMaxHealth(100)
    self:SetName("NPC_" .. math.random(1000, 9999))
    self.recentMessages = {}  -- Ensure recentMessages is initialized
    self.FollowTarget = nil
    self.IsThinking = false  -- **Flag to manage thinking state**

    print("NPC Initialized with model:", self:GetModel())

    -- Capture 'self' in a local variable for use within the hook
    local entity = self

    -- Hook to listen to chat messages with a unique identifier using EntIndex
    hook.Add("PlayerSay", "ChatListener_" .. self:EntIndex(), function(ply, text)
        if not IsValid(ply) or not ply:IsPlayer() then return end
        if not entity.recentMessages then
            -- Initialize recentMessages if it's somehow nil
            entity.recentMessages = {}
            print("Warning: recentMessages was nil. It has been re-initialized.")
        end
        table.insert(entity.recentMessages, {player = ply:Nick(), text = text})
        if #entity.recentMessages > 5 then
            table.remove(entity.recentMessages, 1)
        end
    end)

    print("Hook for PlayerSay added for NPC:", self:GetName())
end

-- Handle damage taken by the NPC
function ENT:OnTakeDamage(dmginfo)
    self:SetHealth(self:Health() - dmginfo:GetDamage())
    if self:Health() <= 0 then
        self:Remove()
    end
end

-- Function to schedule the next Think cycle
function ENT:ScheduleNextThink()
    -- Schedule the next Think between 15 to 30 seconds
    self.NextThinkTime = CurTime() + math.random(15, 30)
    self.IsThinking = false  -- **Clear the thinking flag**
    print(os.date("[%Y-%m-%d %H:%M:%S]") .. " Next Think scheduled at", os.date("%X", self.NextThinkTime))
end

-- Function to perform a health check on the API
function ENT:IsAPIReachable(callback)
    http.Fetch(
        "http://88.141.148.196:11434/api/health", -- Replace with your actual health endpoint
        function(result, statusCode, headers)
            if statusCode == 200 then
                callback(true)
            else
                callback(false)
            end
        end,
        function(errorMessage)
            callback(false)
        end,
        {
            method = "GET",
            headers = {
                ["Content-Type"] = "application/json"
            }
        }
    )
end

-- Function to send a message to a specific player
function ENT:SayToPlayer(ply, message)
    if IsValid(ply) and ply:IsPlayer() then
        ply:ChatPrint(self:GetName() .. ": " .. message)
    end
end

-- Function to send a message to all players
function ENT:SayToWorld(message)
    for _, ply in ipairs(player.GetAll()) do
        ply:ChatPrint(self:GetName() .. ": " .. message)
    end
end

-- Function to broadcast a message to all players
function ENT:BroadcastMessage(message)
    self:SayToWorld(message)
end

-- Function to handle errors gracefully
function ENT:HandleError(errorMessage)
    local timestamp = os.date("[%Y-%m-%d %H:%M:%S]")
    -- Print to server console
    print(timestamp .. " [NPC Error] " .. errorMessage)
    
    -- Send error message to all players
    for _, ply in ipairs(player.GetAll()) do
        if IsValid(ply) and ply:IsPlayer() then
            ply:ChatPrint(self:GetName() .. ": [Error] " .. errorMessage)
        end
    end
end

-- Execute the received command
function ENT:ExecuteCommand(command)
    print("ExecuteCommand called with command:", command)
    
    -- Check if the command is valid
    local validCommand = false
    for _, action in ipairs(self.validActions) do
        if string.lower(action) == string.lower(command) then
            validCommand = true
            command = action -- Use the correctly cased command
            break
        end
    end

    if not validCommand then
        self:HandleError("Unknown command received: '" .. command .. "'")
        return
    end

    local players = player.GetAll()

    if command == "Move to player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            self:SetLastPosition(target:GetPos())
            self:SetSchedule(SCHED_FORCED_GO_RUN)
            print(self:GetName() .. " is moving to player:", target:Nick())
        end
    elseif command == "Move away" then
        local newPos = self:GetPos() + VectorRand() * 500
        self:SetLastPosition(newPos)
        self:SetSchedule(SCHED_FORCED_GO_RUN)
        print(self:GetName() .. " is moving away to position:", newPos)
    elseif command == "Equip shotgun" then
        self:Give("weapon_shotgun")
        print(self:GetName() .. " equipped a shotgun.")
    elseif command == "Random move" then
        local newPos = self:GetPos() + VectorRand() * 300
        self:SetLastPosition(newPos)
        self:SetSchedule(SCHED_FORCED_GO)
        print(self:GetName() .. " is moving randomly to position:", newPos)
    elseif command == "Say hello" then
        self:EmitSound("vo/npc/male01/hi0" .. math.random(1, 2) .. ".wav")
        print(self:GetName() .. " says hello.")
    elseif command == "Dance" then
        local sequence = self:LookupSequence("taunt_dance")
        if sequence and sequence >= 0 then
            self:ResetSequence(sequence)
            print(self:GetName() .. " is dancing.")
        else
            self:HandleError("Cannot perform 'Dance' action: Sequence 'taunt_dance' not found.")
        end
    elseif command == "Jump" then
        self:SetVelocity(Vector(0, 0, 200))
        print(self:GetName() .. " is jumping.")
    elseif command == "Give item to player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            target:Give("weapon_pistol")
            self:SayToPlayer(target, "Here you go!")
            print(self:GetName() .. " gave a pistol to player:", target:Nick())
        end
    elseif command == "Attack player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            self:SetEnemy(target)
            self:SetNPCState(NPC_STATE_COMBAT)
            print(self:GetName() .. " is attacking player:", target:Nick())
        end
    elseif command == "Wave" then
        local sequence = self:LookupSequence("wave")
        if sequence and sequence >= 0 then
            self:ResetSequence(sequence)
            print(self:GetName() .. " is waving.")
        else
            self:HandleError("Cannot perform 'Wave' action: Sequence 'wave' not found.")
        end
    elseif command == "Sit down" then
        local sequence = self:LookupSequence("sit_ground")
        if sequence and sequence >= 0 then
            self:ResetSequence(sequence)
            print(self:GetName() .. " is sitting down.")
        else
            self:HandleError("Cannot perform 'Sit down' action: Sequence 'sit_ground' not found.")
        end
    elseif command == "Follow player" then
        if #players > 0 then
            self.FollowTarget = players[math.random(#players)]
            self:SayToPlayer(self.FollowTarget, "I'll follow you!")
            print(self:GetName() .. " is now following player:", self.FollowTarget:Nick())
        end
    elseif command == "Stop following" then
        if IsValid(self.FollowTarget) then
            self:SayToPlayer(self.FollowTarget, "I'll stop following you.")
            print(self:GetName() .. " has stopped following player:", self.FollowTarget:Nick())
            self.FollowTarget = nil
        end
    elseif command == "Tell joke" then
        self:BroadcastMessage("Why did the physicist cross the road? Because the chicken was on the other side!")
        print(self:GetName() .. " told a joke.")
    elseif command == "Compliment player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            self:SayToPlayer(target, "You're doing great, " .. target:Nick() .. "!")
            print(self:GetName() .. " complimented player:", target:Nick())
        end
    elseif command == "Insult player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            self:SayToPlayer(target, "I bet you can't even code in Lua, " .. target:Nick() .. "!")
            print(self:GetName() .. " insulted player:", target:Nick())
        end
    elseif command == "Pickup item" then
        -- Find nearby items and pick one up
        local items = ents.FindInSphere(self:GetPos(), 100)
        local pickedUp = false
        for _, item in ipairs(items) do
            if item:GetClass() == "prop_physics" then
                item:Remove()
                self:SayToWorld("I picked up an item!")
                pickedUp = true
                print(self:GetName() .. " picked up an item.")
                break
            end
        end
        if not pickedUp then
            self:HandleError("No items nearby to pick up.")
        end
    elseif command == "Drop item" then
        -- Drop an item at the NPC's position
        local ent = ents.Create("prop_physics")
        ent:SetModel("models/props_junk/wood_crate001a.mdl")
        ent:SetPos(self:GetPos() + Vector(0, 0, 50))
        ent:Spawn()
        self:SayToWorld("I dropped an item!")
        print(self:GetName() .. " dropped an item.")
    else
        self:HandleError("Unknown command received: '" .. command .. "'")
    end
end

-- Function triggered when a player uses the NPC
function ENT:Use(activator, caller)
    if activator:IsPlayer() then
        self:SayToPlayer(activator, "Hello, " .. activator:Nick() .. "!")
    end
end

-- Main thinking loop
function ENT:Think()
    if CurTime() >= self.NextThinkTime then
        if not self.IsThinking then
            self.IsThinking = true  -- **Set flag to indicate thinking has started**
            print("NPC Think invoked for:", self:GetName())
            
            -- Perform thinking pose
            local sequence = self:LookupSequence("idle_allthinking") or self:LookupSequence("pose_standing_02")
            if sequence and sequence >= 0 then
                self:ResetSequence(sequence)
                print("Set thinking sequence:", sequence)
            end
            self:SetNPCState(NPC_STATE_SCRIPT)
            
            -- Preemptive API health check
            self:IsAPIReachable(function(isUp)
                if isUp then
                    self:PerformAction()
                else
                    self:HandleError("AI API is unreachable. Scheduling next think.")
                    self:ScheduleNextThink()
                end
            end)
            -- **Do not set NextThinkTime here; it will be set after receiving response**
        end
    end

    -- Follow target if set
    if IsValid(self.FollowTarget) then
        self:SetLastPosition(self.FollowTarget:GetPos())
        self:SetSchedule(SCHED_FORCED_GO_RUN)
        print(self:GetName() .. " is following:", self.FollowTarget:GetName())
    end

    self:NextThink(CurTime())
    return true
end

-- Perform an action based on AI response with retries and exponential backoff
function ENT:PerformAction(retryCount)
    retryCount = retryCount or 0
    if retryCount > 3 then
        self:HandleError("Maximum retry attempts reached for PerformAction.")
        self:ScheduleNextThink()
        return
    end

    local players = player.GetAll()
    local playerNames = {}
    for _, ply in ipairs(players) do
        table.insert(playerNames, ply:Nick())
    end

    -- Compile recent messages
    local messagesText = ""
    for _, msg in ipairs(self.recentMessages) do
        messagesText = messagesText .. msg.player .. ": " .. msg.text .. "\n"
    end

    -- Construct the optimized prompt
    local prompt = string.format(
        "GMod NPC. Actions: %s. Items: %s. Nearby Players: %s. Position: %s. Health: %d. Recent Chats:\n%s. Choose one action without additional text.",
        table.concat(self.validActions, ", "),
        table.concat(self.validItems, ", "),
        table.concat(playerNames, ", "),
        tostring(self:GetPos()),
        self:Health(),
        messagesText ~= "" and messagesText or "None"
    )
    prompt = prompt .. " Respond exactly with one of the available commands as plain text without any quotes, e.g., Say hello."

    -- Construct the JSON payload
    local payload = {
        model = "phi:latest",  -- Ensure this matches a valid model name
        prompt = prompt,
        stream = false
    }

    -- Convert the table to JSON
    local jsonData = util.TableToJSON(payload)

    -- Debugging: Print the JSON data with timestamp
    print(os.date("[%Y-%m-%d %H:%M:%S]") .. " Sending JSON Data:", jsonData)

    -- Use the custom hostname instead of 'localhost'
    local url = "http://88.141.148.196:11434/api/generate"

    -- Send the HTTP request
    http.Fetch(
        url,
        function(result, statusCode, headers)
            if statusCode == 200 then
                -- Parse the JSON response
                local status, response = pcall(util.JSONToTable, result)
                if status and response then
                    local aiResponse = response.response or ""
                    local command = string.Trim(aiResponse)

                    -- Debugging: Print the AI response with timestamp
                    print(os.date("[%Y-%m-%d %H:%M:%S]") .. " AI Response:", command)

                    if command ~= "" then
                        -- Strip any leading/trailing quotes just in case
                        command = string.Trim(command, "\"'")
                        self:ExecuteCommand(command)
                    else
                        self:HandleError("AI responded with an empty command.")
                    end
                else
                    self:HandleError("Failed to parse AI response as JSON.\nParsing Error:\n" .. tostring(response) .. "\nResponse:\n" .. result)
                end
            else
                self:HandleError("HTTP request failed with status code " .. statusCode .. ".\nResponse:\n" .. result)
            end

            -- Schedule the next Think after handling the response
            self:ScheduleNextThink()
        end,
        function(errorMessage)
            -- Handle HTTP failure
            self:HandleError("HTTP request failed:\n" .. errorMessage)

            -- Implement Exponential Backoff Retry
            if retryCount < 3 then
                local delay = math.min(2^retryCount, 30) -- Caps the delay to 30 seconds
                print(os.date("[%Y-%m-%d %H:%M:%S]") .. " Retrying PerformAction in " .. delay .. " seconds (Attempt " .. (retryCount + 1) .. ")")
                timer.Simple(delay, function() self:PerformAction(retryCount + 1) end)
            else
                -- Schedule the next Think after maximum retries
                self:ScheduleNextThink()
            end
        end,
        {
            method = "POST",
            headers = {
                ["Content-Type"] = "application/json"
            },
            body = jsonData
        }
    )

    print(os.date("[%Y-%m-%d %H:%M:%S]") .. " HTTP request sent.")
end
Initializing NPC:	
NPC Initialized with model:	models/humans/group01/female_03.mdl
Hook for PlayerSay added for NPC:	NPC_4812
NPC Think invoked for:	NPC_4812
[2024-10-08 22:47:17] [NPC Error] AI API is unreachable. Scheduling next think.
[2024-10-08 22:47:17] Next Think scheduled at	01:03:47
NPC_4812: [Error] AI API is unreachable. Scheduling next think.

i dont think its recieving the response, and add "stream" support