--
-- Bank (PocketOS Store app)
--
-- Numismatics banking over the ender modem. All operations go through
-- the bank backend computer (backend/bank_backend.lua in the PocketOS
-- repo), which owns the Card Reader and Bank Terminal peripherals.
--
-- Self-contained: ships its own protocol client. Reads the bank channel
-- from OS config ("BankChannel").
--
-- Two ways to pay:
--   Quick Pay (card session): tap card at the bank's reader, transfer
--     within 60s. Session consumed. Physical presence = auth.
--   Linked Pay (sub-account): enroll ONCE with an authorized card
--     (bound to a sub-account with a spend limit set by YOU in the
--     Bank Terminal). After that, pay anytime; the limit is enforced
--     by Numismatics itself, not by any script.
--

local core = core
local cpair = core.cpair
local ALIGN = core.ALIGN

local container = pocketos.container
local os_config = pocketos.os_config
local tcd = pocketos.tcd

local unpackf = unpack or table.unpack

--#region MONEY

local COG_CHAR, SPUR_CHAR = "\162", "\164"

local function money(spurs)
    spurs = math.floor(tonumber(spurs) or 0)
    local cogs = math.floor(spurs / 64)
    local rem = spurs % 64
    if cogs > 0 and rem > 0 then
        return string.format("%d%s %d%s", cogs, COG_CHAR, rem, SPUR_CHAR)
    elseif cogs > 0 then
        return string.format("%d%s", cogs, COG_CHAR)
    end
    return string.format("%d%s", rem, SPUR_CHAR)
end

--#endregion

--#region PROTOCOL CLIENT

local modem = peripheral.find("modem")
local channel = os_config.get("BankChannel") or 54123
local req_counter = 0

-- synchronous request: sends, waits for the matching response, keeps the
-- UI alive by forwarding timers to tcd and re-queueing unrelated events
local function request(op, args, timeout)
    timeout = timeout or 3
    if not modem then return nil, "no modem" end

    pcall(modem.open, channel)

    req_counter = req_counter + 1
    local payload = {
        t = "pkt.bank.req/1",
        id = req_counter,
        src = os.getComputerID(),
        op = op,
        args = args or {},
    }

    local ok, err = pcall(modem.transmit, channel, channel, textutils.serialiseJSON(payload))
    if not ok then return nil, err end

    local deadline = os.epoch("local") / 1000 + timeout
    local replay = {}

    while os.epoch("local") / 1000 < deadline do
        local wake = os.startTimer(0.25)
        local ev = { os.pullEvent() }
        os.cancelTimer(wake)

        if ev[1] == "modem_message" and ev[3] == channel then
            local okp, msg = pcall(textutils.unserialiseJSON, ev[5])
            if okp and type(msg) == "table" and msg.t == "pkt.bank.res/1" and msg.id == req_counter then
                for _, e in ipairs(replay) do os.queueEvent(unpackf(e)) end
                if msg.ok then
                    return msg.data
                else
                    return nil, msg.err or "backend error"
                end
            end
            table.insert(replay, ev)
        elseif ev[1] == "timer" then
            -- service timer callbacks so the UI keeps animating
            tcd.handle(ev[2])
        else
            table.insert(replay, ev)
        end
    end

    for _, e in ipairs(replay) do os.queueEvent(unpackf(e)) end
    return nil, "timeout waiting for bank backend"
end

--#endregion

local function init_container()
    local panes = {}
    local page_pane

    -- forward declarations
    local refresh_accounts, refresh_sessions, refresh_link, pick_recipient

    local btn_fg_bg  = cpair(colors.yellow, colors.black)
    local btn_active = cpair(colors.white, colors.black)
    local label      = cpair(colors.lightGray, colors.black)

    -- shared recipient selection state (used by both pay pages)
    local selected_recipient = nil

    --#region PAGE 1: HOME

    local home = Div{parent=container, y=1}
    table.insert(panes, home)

    TextBox{parent=home, y=1, text="Bank", alignment=ALIGN.CENTER, fg_bg=cpair(colors.yellow, colors._INHERIT)}

    local status = TextBox{parent=home, y=2, text="", fg_bg=cpair(colors.lightGray, colors._INHERIT)}

    local function set_status(msg) status.set_value(msg or "") end

    if not modem then
        set_status("no modem found!")
    else
        set_status("ch " .. channel)
    end

    PushButton{parent=home, y=4, text="Accounts        >", fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function ()
            refresh_accounts()
            page_pane.set_value(2)
        end}
    PushButton{parent=home, y=5, text="Quick Pay (card)>", fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function () page_pane.set_value(3) end}
    PushButton{parent=home, y=6, text="Linked Pay      >", fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function ()
            refresh_link()
            page_pane.set_value(4)
        end}
    PushButton{parent=home, y=7, text="Card Sessions   >", fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function ()
            refresh_sessions()
            page_pane.set_value(5)
        end}

    --#endregion

    --#region PAGE 2: ACCOUNTS

    local accounts_page = Div{parent=container, y=1}
    table.insert(panes, accounts_page)

    TextBox{parent=accounts_page, y=1, text="Accounts", alignment=ALIGN.CENTER, fg_bg=cpair(colors.yellow, colors._INHERIT)}
    PushButton{parent=accounts_page, x=1, y=1, text="<", fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function () page_pane.set_value(1) end}

    local accounts_status = TextBox{parent=accounts_page, y=2, text="loading...", fg_bg=label}

    local accounts_list = ListBox{parent=accounts_page, y=4, height=14, scroll_height=200,
        nav_fg_bg=cpair(colors.lightGray, colors.gray), nav_active=cpair(colors.white, colors.gray)}

    refresh_accounts = function ()
        accounts_list.remove_all()

        local info, err = request("ACCOUNTS", {})
        if not info then
            accounts_status.set_value("error: " .. tostring(err))
            return
        end

        accounts_status.set_value(#info .. " accounts")

        for _, acc in ipairs(info) do
            local row = Div{parent=accounts_list, height=2}
            local name_color = acc.isPlayer and colors.cyan or colors.white
            TextBox{parent=row, text=string.sub((acc.label or "?") .. " (" .. money(acc.balance) .. ")", 1, 21),
                fg_bg=cpair(name_color, colors._INHERIT)}
            TextBox{parent=row, y=2, text=string.sub(acc.id, 1, 21), fg_bg=cpair(colors.gray, colors._INHERIT)}
            local _ = Div{parent=accounts_list, height=1}
        end
    end

    --#endregion

    --#region PAGE 3: QUICK PAY (card session)

    local quick_page = Div{parent=container, y=1}
    table.insert(panes, quick_page)

    TextBox{parent=quick_page, y=1, text="Quick Pay", alignment=ALIGN.CENTER, fg_bg=cpair(colors.yellow, colors._INHERIT)}
    PushButton{parent=quick_page, x=1, y=1, text="<", fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function () page_pane.set_value(1) end}

    local quick_status = TextBox{parent=quick_page, y=2, text="tap card at bank, then Refresh", fg_bg=label}

    local session_box = TextBox{parent=quick_page, y=4, text="(no session)", fg_bg=cpair(colors.white, colors._INHERIT)}

    local selected_session = nil

    PushButton{parent=quick_page, y=5, text="Refresh Session", min_width=8, fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function ()
            local sessions, err = request("SESSIONS", {})
            if not sessions then
                session_box.set_value("err: " .. tostring(err))
                return
            end
            if #sessions == 0 then
                selected_session = nil
                session_box.set_value("none - tap card at bank")
                return
            end
            selected_session = sessions[#sessions]
            local who = string.sub(selected_session.scannedByName or "?", 1, 12)
            local bal = selected_session.balance and money(selected_session.balance) or ""
            session_box.set_value(who .. " " .. bal)
        end}

    -- recipient picker (shared)
    local recipient_box

    pick_recipient = function ()
        local accounts, err = request("ACCOUNTS", {})
        if not accounts then
            recipient_box.set_value("err: " .. tostring(err))
            return
        end

        -- cycle: pick next account after the currently selected one
        local start = 1
        if selected_recipient then
            for i, acc in ipairs(accounts) do
                if acc.id == selected_recipient.id then
                    start = i + 1
                    if start > #accounts then start = 1 end
                    break
                end
            end
        end

        selected_recipient = accounts[start]
        recipient_box.set_value(string.sub(selected_recipient.label or "?", 1, 20))
    end

    TextBox{parent=quick_page, y=7, text="recipient:", fg_bg=label}
    recipient_box = TextBox{parent=quick_page, y=8, text="(tap Next)", fg_bg=cpair(colors.white, colors._INHERIT)}

    PushButton{parent=quick_page, y=9, text="Next Recipient", min_width=8, fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=pick_recipient}

    TextBox{parent=quick_page, y=11, text="amount (cogs):", fg_bg=label}
    local quick_amount = NumberField{parent=quick_page, y=12, width=8, default=0, min=0, max=99999,
        max_int_digits=5, fg_bg=cpair(colors.white, colors.gray)}

    PushButton{parent=quick_page, y=14, text="Send (consumes card session)", min_width=8,
        fg_bg=cpair(colors.green, colors.black), active_fg_bg=btn_active,
        callback=function ()
            if not selected_session then
                quick_status.set_value("no session - refresh")
                return
            end
            if not selected_recipient then
                quick_status.set_value("no recipient - tap Next")
                return
            end

            local amount = math.floor(tonumber(quick_amount.get_value()) or 0) * 64
            if amount <= 0 then
                quick_status.set_value("amount must be > 0")
                return
            end

            quick_status.set_value("sending...")

            local receipt, err = request("TRANSFER", { selected_session.sessionID, selected_recipient.id, amount })
            if receipt then
                quick_status.set_value("sent " .. money(amount) .. " to " .. (selected_recipient.label or "?"))
                selected_session = nil
                session_box.set_value("(consumed - retap card)")
            else
                quick_status.set_value("failed: " .. tostring(err))
            end
        end}

    --#endregion

    --#region PAGE 4: LINKED PAY (sub-account)

    local link_page = Div{parent=container, y=1}
    table.insert(panes, link_page)

    TextBox{parent=link_page, y=1, text="Linked Pay", alignment=ALIGN.CENTER, fg_bg=cpair(colors.yellow, colors._INHERIT)}
    PushButton{parent=link_page, x=1, y=1, text="<", fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function () page_pane.set_value(1) end}

    local link_status = TextBox{parent=link_page, y=2, text="", fg_bg=label}
    local link_info = TextBox{parent=link_page, y=3, text="not linked", fg_bg=cpair(colors.white, colors._INHERIT)}

    refresh_link = function ()
        local info, err = request("LINK_STATUS", { os.getComputerID() })
        if not info then
            link_info.set_value("not linked")
            return
        end

        local avail = info.maxAvailable and money(info.maxAvailable) or "?"
        link_info.set_value(string.sub(info.label or "?", 1, 10) .. " avail " .. avail)
        return info
    end

    PushButton{parent=link_page, y=5, text="Enroll (tap authorized card at bank first)", min_width=8,
        fg_bg=cpair(colors.green, colors.black), active_fg_bg=btn_active,
        callback=function ()
            link_status.set_value("enrolling...")
            local res, err = request("LINK_ENROLL", { os.getComputerID() })
            if res then
                link_status.set_value("enrolled! (limit set by your sub-account)")
                refresh_link()
            else
                link_status.set_value("failed: " .. tostring(err))
            end
        end}

    PushButton{parent=link_page, y=7, text="Unlink this pocket", min_width=8,
        fg_bg=cpair(colors.red, colors.black), active_fg_bg=btn_active,
        callback=function ()
            local res, err = request("LINK_UNENROLL", { os.getComputerID() })
            if res then
                link_info.set_value("not linked")
                link_status.set_value("unlinked")
            else
                link_status.set_value("failed: " .. tostring(err))
            end
        end}

    TextBox{parent=link_page, y=9, text="recipient:", fg_bg=label}
    local link_recipient_box = TextBox{parent=link_page, y=10, text="(tap Next)", fg_bg=cpair(colors.white, colors._INHERIT)}

    PushButton{parent=link_page, y=11, text="Next Recipient", min_width=8, fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function ()
            -- temporarily point the shared picker's display at this page
            local real_box = recipient_box
            recipient_box = link_recipient_box
            pick_recipient()
            recipient_box = real_box
        end}

    TextBox{parent=link_page, y=13, text="amount (cogs):", fg_bg=label}
    local link_amount = NumberField{parent=link_page, y=14, width=8, default=0, min=0, max=99999,
        max_int_digits=5, fg_bg=cpair(colors.white, colors.gray)}

    PushButton{parent=link_page, y=16, text="Pay (no card needed)", min_width=8,
        fg_bg=cpair(colors.green, colors.black), active_fg_bg=btn_active,
        callback=function ()
            if not selected_recipient then
                link_status.set_value("no recipient - tap Next")
                return
            end

            local amount = math.floor(tonumber(link_amount.get_value()) or 0) * 64
            if amount <= 0 then
                link_status.set_value("amount must be > 0")
                return
            end

            link_status.set_value("paying...")

            local receipt, err = request("LINK_PAY", { os.getComputerID(), selected_recipient.id, amount })
            if receipt then
                link_status.set_value("paid " .. money(amount) .. " to " .. (selected_recipient.label or "?"))
                refresh_link()
            else
                link_status.set_value("failed: " .. tostring(err))
            end
        end}

    --#endregion

    --#region PAGE 5: SESSIONS

    local session_page = Div{parent=container, y=1}
    table.insert(panes, session_page)

    TextBox{parent=session_page, y=1, text="Card Sessions", alignment=ALIGN.CENTER, fg_bg=cpair(colors.yellow, colors._INHERIT)}
    PushButton{parent=session_page, x=1, y=1, text="<", fg_bg=btn_fg_bg, active_fg_bg=btn_active,
        callback=function () page_pane.set_value(1) end}

    local sessions_status = TextBox{parent=session_page, y=2, text="tap your card at the bank's reader", fg_bg=label}

    local sessions_list = ListBox{parent=session_page, y=4, height=14, scroll_height=100,
        nav_fg_bg=cpair(colors.lightGray, colors.gray), nav_active=cpair(colors.white, colors.gray)}

    refresh_sessions = function ()
        sessions_list.remove_all()

        local sessions, err = request("SESSIONS", {})
        if not sessions then
            sessions_status.set_value("error: " .. tostring(err))
            return
        end

        sessions_status.set_value(#sessions .. " active")

        for _, s in ipairs(sessions) do
            local row = Div{parent=sessions_list, height=2}
            TextBox{parent=row, text=(s.scannedByName or "?") .. " [" .. (s.type or "?") .. "]",
                fg_bg=cpair(s.type == "authorized" and colors.lime or colors.cyan, colors._INHERIT)}
            TextBox{parent=row, y=2, text=string.sub(s.accountID or "", 1, 21), fg_bg=cpair(colors.gray, colors._INHERIT)}
            local _ = Div{parent=sessions_list, height=1}
        end
    end

    --#endregion

    -- build the multipane after all panes exist
    page_pane = MultiPane{parent=container, y=1, panes=panes}
    page_pane.set_value(1)
end

init_container()
