local ADDON_NAME, RPAL = ...

-- ODYN RP Currency -----------------------------------------------------------
-- Fictional character money only. This module never reads or writes WoW's
-- real player-money APIs. Amounts are stored as copper for exact conversion:
-- 1 gold = 100 silver, 1 silver = 100 copper.

local PREFIX="ODYNCUR1"
local MAX_COPPER=999999999999
local pendingOut={}
local pendingIn={}
local serial=0

local function Now()
    return time and time() or 0
end

local function ClampCopper(value)
    value=math.floor(tonumber(value) or 0)
    if value<0 then return 0 end
    if value>MAX_COPPER then return MAX_COPPER end
    return value
end

local function EnsureState()
    local owner=RPAL.char or _G.RPAL_CHAR_DB
    if type(owner)~="table" then return nil end
    owner.rpCurrency=type(owner.rpCurrency)=="table" and owner.rpCurrency or {}
    local state=owner.rpCurrency
    state.version=1
    state.pouch=ClampCopper(state.pouch)
    state.bank=ClampCopper(state.bank)
    state.receivedTransfers=type(state.receivedTransfers)=="table" and state.receivedTransfers or {}
    state.log=type(state.log)=="table" and state.log or {}
    return state
end

local function AddLog(kind,amount,actor,note)
    local state=EnsureState();if not state then return end
    table.insert(state.log,1,{kind=tostring(kind or "change"),amount=math.floor(tonumber(amount) or 0),actor=tostring(actor or ""),note=tostring(note or ""),at=Now()})
    while #state.log>30 do table.remove(state.log) end
end

local function Changed(reason)
    if type(RPAL.OnRPCurrencyChanged)=="function" then pcall(RPAL.OnRPCurrencyChanged,reason) end
end

local function Status(text,kind)
    if type(RPAL.OnCurrencyTransferStatus)=="function" then pcall(RPAL.OnCurrencyTransferStatus,tostring(text or ""),kind) end
end

local function CleanName(name)
    return tostring(name or ""):gsub("^%s+",""):gsub("%s+$","")
end

local function ShortName(name)
    return CleanName(name):match("^([^%-]+)") or CleanName(name)
end

local function SamePlayer(a,b)
    a=CleanName(a):lower();b=CleanName(b):lower()
    if a=="" or b=="" then return false end
    if a:find("%-",1,true) and b:find("%-",1,true) then return a==b end
    return ShortName(a):lower()==ShortName(b):lower()
end

local function Send(message,target)
    target=CleanName(target)
    if target=="" then return false,"no_target" end
    if RPAL.SendAddonMessageSafe then return RPAL.SendAddonMessageSafe(PREFIX,message,"WHISPER",target,true) end
    if C_ChatInfo and C_ChatInfo.SendAddonMessage then
        local ok=pcall(C_ChatInfo.SendAddonMessage,PREFIX,message,"WHISPER",target)
        return ok,ok and "sent" or "send_failed"
    end
    return false,"transport_unavailable"
end

local function NewTransferID()
    serial=serial+1
    local stamp=Now()
    local rnd=math.random and math.random(1000,9999) or serial
    return string.format("%x%x%x",stamp,rnd,serial):sub(1,22)
end

local function Split(message)
    local fields={}
    for field in tostring(message or ""):gmatch("([^|]+)") do fields[#fields+1]=field end
    return fields
end

function RPAL.GetRPCurrencyState()
    return EnsureState()
end

function RPAL.CurrencyFromParts(gold,silver,copper)
    gold=math.max(0,math.floor(tonumber(gold) or 0))
    silver=math.max(0,math.floor(tonumber(silver) or 0))
    copper=math.max(0,math.floor(tonumber(copper) or 0))
    return ClampCopper((gold*10000)+(silver*100)+copper)
end

function RPAL.CurrencyToParts(total)
    total=ClampCopper(total)
    local gold=math.floor(total/10000)
    local remainder=total-(gold*10000)
    local silver=math.floor(remainder/100)
    local copper=remainder-(silver*100)
    return gold,silver,copper
end

function RPAL.FormatRPCurrency(total)
    local g,s,c=RPAL.CurrencyToParts(total)
    return string.format("%dg  %ds  %dc",g,s,c)
end

function RPAL.GetPouchCopper()
    local state=EnsureState();return state and state.pouch or 0
end

function RPAL.GetBankCopper()
    local state=EnsureState();return state and state.bank or 0
end

function RPAL.AdjustPouch(delta,reason)
    local state=EnsureState();if not state then return false,"Character data is not ready." end
    delta=math.floor(tonumber(delta) or 0)
    if delta==0 then return false,"Enter an amount greater than zero." end
    local nextValue=state.pouch+delta
    if nextValue<0 then return false,"Your Coin Pouch does not contain that much." end
    if nextValue>MAX_COPPER then return false,"That amount is too large." end
    state.pouch=nextValue
    AddLog(delta>0 and "pouch_add" or "pouch_remove",delta,"",reason or "manual")
    Changed(reason or "pouch")
    return true,state.pouch
end

-- Direct Bank editing is used by the compact Currency widget. Deposit and
-- Withdraw remain available for API/backward compatibility, while the widget
-- can now change one displayed Gold/Silver/Copper unit without a transfer form.
function RPAL.AdjustBank(delta,reason)
    local state=EnsureState();if not state then return false,"Character data is not ready." end
    delta=math.floor(tonumber(delta) or 0)
    if delta==0 then return false,"Enter an amount greater than zero." end
    local nextValue=state.bank+delta
    if nextValue<0 then return false,"Your Bank does not contain that much." end
    if nextValue>MAX_COPPER then return false,"That amount is too large." end
    state.bank=nextValue
    AddLog(delta>0 and "bank_add" or "bank_remove",delta,"",reason or "manual")
    Changed(reason or "bank")
    return true,state.bank
end

function RPAL.DepositRPCurrency(amount)
    local state=EnsureState();if not state then return false,"Character data is not ready." end
    amount=ClampCopper(amount)
    if amount<=0 then return false,"Enter an amount greater than zero." end
    if amount>state.pouch then return false,"Your Coin Pouch does not contain that much." end
    if state.bank+amount>MAX_COPPER then return false,"Your Bank cannot hold that amount." end
    state.pouch=state.pouch-amount;state.bank=state.bank+amount
    AddLog("deposit",amount,"","bank")
    Changed("deposit")
    return true
end

function RPAL.WithdrawRPCurrency(amount)
    local state=EnsureState();if not state then return false,"Character data is not ready." end
    amount=ClampCopper(amount)
    if amount<=0 then return false,"Enter an amount greater than zero." end
    if amount>state.bank then return false,"Your Bank does not contain that much." end
    if state.pouch+amount>MAX_COPPER then return false,"Your Coin Pouch cannot hold that amount." end
    state.bank=state.bank-amount;state.pouch=state.pouch+amount
    AddLog("withdraw",amount,"","bank")
    Changed("withdraw")
    return true
end

function RPAL.DepositAllRPCurrency()
    local state=EnsureState();if not state or state.pouch<=0 then return false,"Your Coin Pouch is empty." end
    return RPAL.DepositRPCurrency(state.pouch)
end

function RPAL.WithdrawAllRPCurrency()
    local state=EnsureState();if not state or state.bank<=0 then return false,"Your Bank is empty." end
    return RPAL.WithdrawRPCurrency(state.bank)
end

function RPAL.GetRPCurrencyLog()
    local state=EnsureState();return state and state.log or {}
end

function RPAL.GetPendingCurrencyOffers()
    return pendingIn,pendingOut
end

function RPAL.StartCurrencyTransfer(target,amount)
    local state=EnsureState();if not state then return false,"Character data is not ready." end
    target=CleanName(target);amount=ClampCopper(amount)
    if target=="" then return false,"Choose a player first." end
    if amount<=0 then return false,"Enter an amount greater than zero." end
    if amount>state.pouch then return false,"Your Coin Pouch does not contain that much." end
    local selfName=(GetUnitName and GetUnitName("player",true)) or (UnitName and UnitName("player")) or ""
    if SamePlayer(target,selfName) then return false,"You cannot send RP Currency to yourself." end
    local id=NewTransferID()
    local ok,reason=Send("O|"..id.."|"..tostring(amount),target)
    if not ok then return false,"Could not send the transfer offer ("..tostring(reason or "unknown")..")." end
    pendingOut[id]={id=id,target=target,amount=amount,createdAt=Now(),committed=false}
    Status("Currency offer sent to "..target..".","pending")
    if C_Timer and C_Timer.After then
        C_Timer.After(60,function()
            local offer=pendingOut[id]
            if offer and not offer.committed then
                pendingOut[id]=nil
                Status("Currency offer to "..target.." expired.","expired")
            end
        end)
        C_Timer.After(180,function()
            local offer=pendingOut[id]
            if offer and offer.committed then
                pendingOut[id]=nil
                Status("Currency transfer to "..target.." was sent, but no delivery receipt returned.","warning")
            end
        end)
    end
    return true,id
end

function RPAL.AcceptCurrencyOffer(id)
    id=tostring(id or "")
    local offer=pendingIn[id]
    if not offer then return false,"That offer is no longer available." end
    offer.accepted=true
    local ok,reason=Send("A|"..id,offer.sender)
    if not ok then offer.accepted=false;return false,"Could not accept the offer ("..tostring(reason or "unknown")..")." end
    Status("Accepted "..RPAL.FormatRPCurrency(offer.amount).." from "..ShortName(offer.sender)..".","accepted")
    return true
end

function RPAL.DeclineCurrencyOffer(id,reason)
    id=tostring(id or "")
    local offer=pendingIn[id]
    if not offer then return false,"That offer is no longer available." end
    pendingIn[id]=nil
    Send("X|"..id,offer.sender)
    Status("Currency offer from "..ShortName(offer.sender).." declined.",reason or "declined")
    return true
end

local function HandleOffer(id,amount,sender)
    amount=ClampCopper(amount);sender=CleanName(sender)
    if id=="" or amount<=0 or sender=="" then return end
    local settings=RPAL.GetSettings and RPAL.GetSettings() or {}
    if settings.autoDeclineExchanges then Send("X|"..id,sender);return end
    pendingIn[id]={id=id,amount=amount,sender=sender,createdAt=Now(),accepted=false}
    local function Show()
        local offer=pendingIn[id]
        if offer and type(RPAL.ShowCurrencyOfferPrompt)=="function" then RPAL.ShowCurrencyOfferPrompt(sender,id,amount) end
    end
    if type(RPAL.ShowCurrencyOfferPrompt)=="function" then Show()
    elseif C_Timer and C_Timer.After then C_Timer.After(0,Show) end
    if C_Timer and C_Timer.After then
        C_Timer.After(60,function()
            local offer=pendingIn[id]
            if offer and not offer.accepted then pendingIn[id]=nil end
        end)
    end
end

local function HandleAccept(id,sender)
    local offer=pendingOut[id]
    if not offer or not SamePlayer(sender,offer.target) then return end
    if not offer.committed then
        local state=EnsureState();if not state then return end
        if state.pouch<offer.amount then
            Send("X|"..id,offer.target);pendingOut[id]=nil
            Status("Currency transfer cancelled: your Coin Pouch balance changed.","failed")
            return
        end
        state.pouch=state.pouch-offer.amount
        offer.committed=true;offer.committedAt=Now()
        AddLog("transfer_sent",-offer.amount,offer.target,"player_transfer")
        Changed("transfer_sent")
    end
    Send("C|"..id.."|"..tostring(offer.amount),offer.target)
    Status("Sending "..RPAL.FormatRPCurrency(offer.amount).." to "..ShortName(offer.target).."…","sending")
end

local function HandleCommit(id,amount,sender)
    local offer=pendingIn[id]
    amount=ClampCopper(amount);sender=CleanName(sender)
    if not offer or not offer.accepted or not SamePlayer(sender,offer.sender) or amount~=offer.amount then return end
    local state=EnsureState();if not state then return end
    local receiptKey=sender:lower().."|"..id
    if not state.receivedTransfers[receiptKey] then
        if state.pouch+amount>MAX_COPPER then
            Status("Incoming currency could not fit in your Coin Pouch.","failed")
            return
        end
        state.pouch=state.pouch+amount
        state.receivedTransfers[receiptKey]=Now()
        AddLog("transfer_received",amount,sender,"player_transfer")
        -- Keep duplicate protection bounded.
        local receipts={}
        for key,stamp in pairs(state.receivedTransfers) do receipts[#receipts+1]={key=key,stamp=tonumber(stamp) or 0} end
        if #receipts>80 then
            table.sort(receipts,function(a,b)return a.stamp>b.stamp end)
            for i=81,#receipts do state.receivedTransfers[receipts[i].key]=nil end
        end
        Changed("transfer_received")
    end
    pendingIn[id]=nil
    Send("D|"..id,sender)
    Status("Received "..RPAL.FormatRPCurrency(amount).." from "..ShortName(sender)..".","received")
end

local function HandleDecline(id,sender)
    local offer=pendingOut[id]
    if not offer or not SamePlayer(sender,offer.target) then return end
    if not offer.committed then
        pendingOut[id]=nil
        Status(ShortName(sender).." declined the currency offer.","declined")
    end
end

local function HandleDelivered(id,sender)
    local offer=pendingOut[id]
    if not offer or not SamePlayer(sender,offer.target) then return end
    pendingOut[id]=nil
    Status("Transfer delivered to "..ShortName(sender)..".","delivered")
end

local function HandleMessage(message,sender)
    local f=Split(message);local kind=f[1];local id=tostring(f[2] or "")
    if kind=="O" then HandleOffer(id,f[3],sender)
    elseif kind=="A" then HandleAccept(id,sender)
    elseif kind=="C" then HandleCommit(id,f[3],sender)
    elseif kind=="X" then HandleDecline(id,sender)
    elseif kind=="D" then HandleDelivered(id,sender) end
end

local events=CreateFrame("Frame")
events:RegisterEvent("PLAYER_LOGIN")
events:RegisterEvent("CHAT_MSG_ADDON")
events:SetScript("OnEvent",function(_,event,...)
    if event=="PLAYER_LOGIN" then
        EnsureState()
        if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix then pcall(C_ChatInfo.RegisterAddonMessagePrefix,PREFIX) end
    elseif event=="CHAT_MSG_ADDON" then
        local prefix,message,channel,sender=...
        if prefix==PREFIX and channel=="WHISPER" and sender then HandleMessage(message,sender) end
    end
end)
if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix then pcall(C_ChatInfo.RegisterAddonMessagePrefix,PREFIX) end

RPAL.CURRENCY_PREFIX=PREFIX
