NPC_8774: [Error] HTTP request failed:
Operation was aborted by an application callback
-- File: lua/entities/ai_npc/init.lua
AddCSLuaFile("cl_init.lua")
AddCSLuaFile("shared.lua")

include("shared.lua")

-- Store recent chat messages
local recentMessages = {}

-- Hook to listen to chat messages
hook.Add("PlayerSay", "ChatListener", function(ply, text)
    table.insert(recentMessages, {player = ply:Nick(), text = text})
    if #recentMessages > 5 then
        table.remove(recentMessages, 1)
    end
end)

ENT.FollowTarget = nil

function ENT:Initialize()
    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))
end

function ENT:OnTakeDamage(dmginfo)
    self:SetHealth(self:Health() - dmginfo:GetDamage())
    if self:Health() <= 0 then
        self:Remove()
    end
end

function ENT:Think()
    if CurTime() >= self.NextThinkTime then
        -- Perform thinking pose
        local sequence = self:LookupSequence("idle_allthinking") or self:LookupSequence("pose_standing_02")
        if sequence and sequence >= 0 then
            self:ResetSequence(sequence)
        end
        self:SetNPCState(NPC_STATE_SCRIPT)
        self:PerformAction()
        self.NextThinkTime = CurTime() + math.random(5, 10)
    else
        self:NextThink(CurTime())
    end

    -- Follow target if set
    if IsValid(self.FollowTarget) then
        self:SetLastPosition(self.FollowTarget:GetPos())
        self:SetSchedule(SCHED_FORCED_GO_RUN)
    end

    return true
end

function ENT:PerformAction()
    local actions = {
        "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"
    }
    local items = {"weapon_pistol", "weapon_shotgun", "weapon_crowbar"}
    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(recentMessages) do
        messagesText = messagesText .. msg.player .. ": " .. msg.text .. "\n"
    end

    local prompt = "You are an NPC in Garry's Mod."
    prompt = prompt .. " You can perform actions: " .. table.concat(actions, ", ") .. "."
    prompt = prompt .. " You have items: " .. table.concat(items, ", ") .. "."
    prompt = prompt .. " Players nearby: " .. table.concat(playerNames, ", ") .. "."
    if messagesText ~= "" then
        prompt = prompt .. " Recent chat messages:\n" .. messagesText
    end
    prompt = prompt .. " Your position: " .. tostring(self:GetPos()) .. "."
    prompt = prompt .. " Your health: " .. self:Health() .. "."
    prompt = prompt .. " Decide your next action and respond with one of the available commands."
    prompt = prompt .. " Only respond with the command as plain text, e.g., 'Say hello'."

    local jsonData = util.TableToJSON({
        model = "llama3.2:3b",  -- Replace "llama2" with the actual model name you have
        prompt = prompt,
        stream = false
    })

    local url = "http://localhost:11434/api/generate"

    -- Send the HTTP request using http.Fetch with custom headers and method
    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)
                    if command ~= "" then
                        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
        end,
        function(errorMessage)
            -- HTTP request failed
            self:HandleError("HTTP request failed:\n" .. errorMessage)
        end,
        {
            method = "POST",
            headers = {
                ["Content-Type"] = "application/json"
            },
            body = jsonData
        }
    )
end

function ENT:ExecuteCommand(command)
    print("NPC Command: " .. command)
    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)
        end
    elseif command == "Move away" then
        local newPos = self:GetPos() + VectorRand() * 500
        self:SetLastPosition(newPos)
        self:SetSchedule(SCHED_FORCED_GO_RUN)
    elseif command == "Equip shotgun" then
        self:Give("weapon_shotgun")
    elseif command == "Random move" then
        local newPos = self:GetPos() + VectorRand() * 300
        self:SetLastPosition(newPos)
        self:SetSchedule(SCHED_FORCED_GO)
    elseif command == "Say hello" then
        self:EmitSound("vo/npc/male01/hi0" .. math.random(1, 2) .. ".wav")
    elseif command == "Dance" then
        local sequence = self:LookupSequence("taunt_dance")
        if sequence and sequence >= 0 then
            self:ResetSequence(sequence)
        else
            self:HandleError("Cannot perform 'Dance' action: Sequence 'taunt_dance' not found.")
        end
    elseif command == "Jump" then
        self:SetVelocity(Vector(0, 0, 200))
    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!")
        end
    elseif command == "Attack player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            self:SetEnemy(target)
            self:SetNPCState(NPC_STATE_COMBAT)
        end
    elseif command == "Wave" then
        local sequence = self:LookupSequence("wave")
        if sequence and sequence >= 0 then
            self:ResetSequence(sequence)
        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)
        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!")
        end
    elseif command == "Stop following" then
        if IsValid(self.FollowTarget) then
            self:SayToPlayer(self.FollowTarget, "I'll stop following you.")
            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!")
    elseif command == "Compliment player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            self:SayToPlayer(target, "You're doing great, " .. 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() .. "!")
        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
                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!")
    else
        self:HandleError("Unknown command received: '" .. command .. "'")
    end
end

function ENT:SayToPlayer(ply, message)
    if IsValid(ply) and ply:IsPlayer() then
        ply:ChatPrint(self:GetName() .. ": " .. message)
    end
end

function ENT:SayToWorld(message)
    for _, ply in ipairs(player.GetAll()) do
        ply:ChatPrint(self:GetName() .. ": " .. message)
    end
end

function ENT:BroadcastMessage(message)
    self:SayToWorld(message)
end

function ENT:HandleError(errorMessage)
    -- Print to server console
    print("[NPC Error] " .. errorMessage)
    -- Send error message to nearby players
    local nearbyPlayers = ents.FindInSphere(self:GetPos(), 500)
    for _, ent in ipairs(nearbyPlayers) do
        if ent:IsPlayer() then
            ent:ChatPrint(self:GetName() .. ": [Error] " .. errorMessage)
        end
    end
end

function ENT:Use(activator, caller)
    if activator:IsPlayer() then
        self:SayToPlayer(activator, "Hello, " .. activator:Nick() .. "!")
    end
end