#!/usr/bin/env lua
-- PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
--
-- This is free public domain software for the public good of a permacomputer hosted
-- at permacomputer.com - an always-on computer by the people, for the people.
--
-- Learn more: https://www.permacomputer.com
--
-- Copyright 2025 TimeHexOn & foxhop & russell@unturf

local json = require("json")
local http = require("socket.http")
local https = require("ssl.https")
local ltn12 = require("ltn12")

local Un = {}
Un.API_BASE = "https://api.unsandbox.com"
Un.VERSION = "4.2.24"

-- Credential loading
function Un.load_accounts_csv(path)
    path = path or (os.getenv("HOME") .. "/.unsandbox/accounts.csv")
    local file = io.open(path, "r")
    if not file then return {} end

    local accounts = {}
    for line in file:lines() do
        line = line:match("^%s*(.-)%s*$")
        if line ~= "" then
            local pk, sk = line:match("([^,]+),(.+)")
            if pk and sk then
                table.insert(accounts, {pk, sk})
            end
        end
    end
    file:close()
    return accounts
end

function Un.get_credentials(opts)
    opts = opts or {}

    -- Tier 1: Arguments
    if opts.public_key and opts.secret_key then
        return opts.public_key, opts.secret_key
    end

    -- Tier 2: Environment
    local pk = os.getenv("UNSANDBOX_PUBLIC_KEY")
    local sk = os.getenv("UNSANDBOX_SECRET_KEY")
    if pk and sk then return pk, sk end

    -- Tier 3: Home directory
    local accounts = Un.load_accounts_csv()
    if #accounts > 0 then return accounts[1][1], accounts[1][2] end

    -- Tier 4: Local directory
    accounts = Un.load_accounts_csv("./accounts.csv")
    if #accounts > 0 then return accounts[1][1], accounts[1][2] end

    error("No credentials found")
end

-- HMAC signature
function Un.sign_request(secret, timestamp, method, endpoint, body)
    local hmac = require("crypto").hmac
    local message = timestamp .. ":" .. method .. ":" .. endpoint .. ":" .. body
    return hmac.digest("sha256", message, secret, true):hex()
end

-- API request
function Un.api_request(method, endpoint, body, opts)
    opts = opts or {}
    local pk, sk = Un.get_credentials(opts)

    local timestamp = tostring(os.time())
    local url = Un.API_BASE .. endpoint
    local body_str = body and json.encode(body) or "{}"
    local signature = Un.sign_request(sk, timestamp, method, endpoint, body_str)

    local headers = {
        ["Authorization"] = "Bearer " .. pk,
        ["X-Timestamp"] = timestamp,
        ["X-Signature"] = signature,
        ["Content-Type"] = "application/json"
    }

    local resp_body = {}
    local resp, status = https.request({
        url = url,
        method = method,
        headers = headers,
        source = body_str and ltn12.source.string(body_str),
        sink = ltn12.sink.table(resp_body)
    })

    if status ~= 200 then error("API error (" .. status .. ")") end
    return json.decode(table.concat(resp_body))
end

-- Languages with cache
function Un.languages(opts)
    opts = opts or {}
    local cache_ttl = opts.cache_ttl or 3600
    local cache_path = os.getenv("HOME") .. "/.unsandbox/languages.json"

    local file = io.open(cache_path, "r")
    if file then
        local mtime = os.time() - (lfs.attributes(cache_path, "modification") or 0)
        if mtime < cache_ttl then
            local content = file:read("*a")
            file:close()
            return json.decode(content)
        end
        file:close()
    end

    local result = Un.api_request("GET", "/languages", nil, opts)
    local langs = result.languages or {}

    os.execute("mkdir -p " .. os.getenv("HOME") .. "/.unsandbox")
    file = io.open(cache_path, "w")
    file:write(json.encode(langs))
    file:close()

    return langs
end

-- Execute functions
function Un.execute(language, code, opts)
    opts = opts or {}
    local body = {
        language = language,
        code = code,
        network_mode = opts.network_mode or "zerotrust",
        ttl = opts.ttl or 60
    }
    return Un.api_request("POST", "/execute", body, opts)
end

function Un.execute_async(language, code, opts)
    opts = opts or {}
    local body = {
        language = language,
        code = code,
        network_mode = opts.network_mode or "zerotrust",
        ttl = opts.ttl or 300
    }
    return Un.api_request("POST", "/execute/async", body, opts)
end

function Un.run(file, opts)
    local f = io.open(file, "r")
    local code = f:read("*a")
    f:close()
    return Un.execute(Un.detect_language(file), code, opts)
end

-- Job management
function Un.get_job(job_id, opts)
    opts = opts or {}
    return Un.api_request("GET", "/jobs/" .. job_id, nil, opts)
end

function Un.wait(job_id, timeout, opts)
    opts = opts or {}
    timeout = timeout or 3600
    local delays = {300, 450, 700, 900, 650, 1600, 2000}

    local start = os.time()
    for i = 0, 119 do
        local job = Un.get_job(job_id, opts)
        if job.status == "completed" then return job end
        if job.status == "failed" then error("Job failed") end

        if os.time() - start > timeout then error("Polling timeout") end

        local delay = delays[(i % 7) + 1] or 2000
        require("socket").sleep(delay / 1000)
    end

    error("Max polls exceeded")
end

-- Utilities
function Un.detect_language(filename)
    local ext = filename:match("%.([^%.]+)$")
    local map = {py="python", lua="lua", sh="bash", rb="ruby"}
    return map[ext] or error("Unknown file type")
end

-- Image API functions
function Un.image_list(opts)
    opts = opts or {}
    return Un.api_request("GET", "/images", nil, opts)
end

function Un.image_get(image_id, opts)
    opts = opts or {}
    return Un.api_request("GET", "/images/" .. image_id, nil, opts)
end

function Un.image_delete(image_id, opts)
    opts = opts or {}
    return Un.api_request("DELETE", "/images/" .. image_id, nil, opts)
end

function Un.image_lock(image_id, opts)
    opts = opts or {}
    return Un.api_request("POST", "/images/" .. image_id .. "/lock", {}, opts)
end

function Un.image_unlock(image_id, opts)
    opts = opts or {}
    return Un.api_request("POST", "/images/" .. image_id .. "/unlock", {}, opts)
end

function Un.image_publish(source_id, source_type, name, opts)
    opts = opts or {}
    local body = {source_type = source_type, source_id = source_id}
    if name then body.name = name end
    return Un.api_request("POST", "/images/publish", body, opts)
end

function Un.image_visibility(image_id, visibility, opts)
    opts = opts or {}
    return Un.api_request("POST", "/images/" .. image_id .. "/visibility", {visibility = visibility}, opts)
end

function Un.image_spawn(image_id, name, ports, opts)
    opts = opts or {}
    local body = {}
    if name then body.name = name end
    if ports then body.ports = ports end
    return Un.api_request("POST", "/images/" .. image_id .. "/spawn", body, opts)
end

function Un.image_clone(image_id, name, opts)
    opts = opts or {}
    local body = {}
    if name then body.name = name end
    return Un.api_request("POST", "/images/" .. image_id .. "/clone", body, opts)
end

-- Service API functions
function Un.service_list(opts)
    opts = opts or {}
    return Un.api_request("GET", "/services", nil, opts)
end

function Un.service_get(service_id, opts)
    opts = opts or {}
    return Un.api_request("GET", "/services/" .. service_id, nil, opts)
end

function Un.service_set_unfreeze_on_demand(service_id, enabled, opts)
    opts = opts or {}
    return Un.api_request_patch("/services/" .. service_id, {unfreeze_on_demand = enabled}, opts)
end

function Un.api_request_patch(endpoint, body, opts)
    opts = opts or {}
    local pk, sk = Un.get_credentials(opts)

    local timestamp = tostring(os.time())
    local url = Un.API_BASE .. endpoint
    local body_str = body and json.encode(body) or "{}"
    local signature = Un.sign_request(sk, timestamp, "PATCH", endpoint, body_str)

    local headers = {
        ["Authorization"] = "Bearer " .. pk,
        ["X-Timestamp"] = timestamp,
        ["X-Signature"] = signature,
        ["Content-Type"] = "application/json"
    }

    local resp_body = {}
    local resp, status = https.request({
        url = url,
        method = "PATCH",
        headers = headers,
        source = body_str and ltn12.source.string(body_str),
        sink = ltn12.sink.table(resp_body)
    })

    if status ~= 200 then error("API error (" .. status .. ")") end
    return json.decode(table.concat(resp_body))
end

-- CLI
if arg and arg[1] then
    if arg[1] == "languages" then
        -- Languages command
        local json_output = arg[2] == "--json"
        local langs = Un.languages()

        if json_output then
            print(json.encode(langs))
        else
            for _, lang in ipairs(langs) do
                print(lang)
            end
        end
        os.exit(0)
    elseif arg[1] == "service" then
        -- Service command
        local i = 2
        local action = nil
        local service_id = nil
        local unfreeze_on_demand_value = nil

        while i <= #arg do
            if arg[i] == "--list" or arg[i] == "-l" then
                action = "list"
            elseif arg[i] == "--info" then
                action = "info"
                i = i + 1
                service_id = arg[i]
            elseif arg[i] == "--unfreeze-on-demand" then
                action = "unfreeze-on-demand"
                i = i + 1
                service_id = arg[i]
                if i + 1 <= #arg and (arg[i + 1] == "true" or arg[i + 1] == "false") then
                    i = i + 1
                    unfreeze_on_demand_value = arg[i] == "true"
                end
            end
            i = i + 1
        end

        if action == "list" then
            local result = Un.service_list()
            print(json.encode(result))
        elseif action == "info" then
            local result = Un.service_get(service_id)
            print(json.encode(result))
        elseif action == "unfreeze-on-demand" then
            if unfreeze_on_demand_value == nil then
                io.stderr:write("Error: --unfreeze-on-demand requires true or false\n")
                os.exit(1)
            end
            Un.service_set_unfreeze_on_demand(service_id, unfreeze_on_demand_value)
            print("Service unfreeze_on_demand set to " .. tostring(unfreeze_on_demand_value) .. ": " .. service_id)
        else
            io.stderr:write("Error: Use --list, --info ID, or --unfreeze-on-demand ID true|false\n")
            os.exit(1)
        end
        os.exit(0)
    elseif arg[1] == "image" then
        -- Image command
        local i = 2
        local action = nil
        local image_id = nil
        local name = nil
        local ports = nil
        local source_type = nil
        local visibility_mode = nil

        while i <= #arg do
            if arg[i] == "--list" or arg[i] == "-l" then
                action = "list"
            elseif arg[i] == "--info" then
                action = "info"
                i = i + 1
                image_id = arg[i]
            elseif arg[i] == "--delete" then
                action = "delete"
                i = i + 1
                image_id = arg[i]
            elseif arg[i] == "--lock" then
                action = "lock"
                i = i + 1
                image_id = arg[i]
            elseif arg[i] == "--unlock" then
                action = "unlock"
                i = i + 1
                image_id = arg[i]
            elseif arg[i] == "--publish" then
                action = "publish"
                i = i + 1
                image_id = arg[i]
            elseif arg[i] == "--source-type" then
                i = i + 1
                source_type = arg[i]
            elseif arg[i] == "--visibility" then
                action = "visibility"
                i = i + 1
                image_id = arg[i]
                if i + 1 <= #arg and not arg[i + 1]:match("^%-") then
                    i = i + 1
                    visibility_mode = arg[i]
                end
            elseif arg[i] == "--spawn" then
                action = "spawn"
                i = i + 1
                image_id = arg[i]
            elseif arg[i] == "--clone" then
                action = "clone"
                i = i + 1
                image_id = arg[i]
            elseif arg[i] == "--name" then
                i = i + 1
                name = arg[i]
            elseif arg[i] == "--ports" then
                i = i + 1
                ports = {}
                for p in arg[i]:gmatch("[^,]+") do
                    table.insert(ports, tonumber(p))
                end
            end
            i = i + 1
        end

        if action == "list" then
            local result = Un.image_list()
            print(json.encode(result))
        elseif action == "info" then
            local result = Un.image_get(image_id)
            print(json.encode(result))
        elseif action == "delete" then
            Un.image_delete(image_id)
            print("Image deleted: " .. image_id)
        elseif action == "lock" then
            Un.image_lock(image_id)
            print("Image locked: " .. image_id)
        elseif action == "unlock" then
            Un.image_unlock(image_id)
            print("Image unlocked: " .. image_id)
        elseif action == "publish" then
            if not source_type then
                io.stderr:write("Error: --source-type required (service or snapshot)\n")
                os.exit(1)
            end
            local result = Un.image_publish(image_id, source_type, name)
            print("Image published")
            print(json.encode(result))
        elseif action == "visibility" then
            if not visibility_mode then
                io.stderr:write("Error: --visibility requires MODE (private, unlisted, or public)\n")
                os.exit(1)
            end
            Un.image_visibility(image_id, visibility_mode)
            print("Image visibility set to " .. visibility_mode .. ": " .. image_id)
        elseif action == "spawn" then
            local result = Un.image_spawn(image_id, name, ports)
            print("Service spawned from image")
            print(json.encode(result))
        elseif action == "clone" then
            local result = Un.image_clone(image_id, name)
            print("Image cloned")
            print(json.encode(result))
        else
            io.stderr:write("Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID\n")
            os.exit(1)
        end
        os.exit(0)
    elseif arg[1] == "--help" or arg[1] == "-h" then
        print("Usage: lua un.lua [options] <source_file>")
        print("       lua un.lua languages [--json]")
        print("       lua un.lua service [options]")
        print("       lua un.lua image [options]")
        print("")
        print("Commands:")
        print("  languages [--json]  List available programming languages")
        print("  service [options]   Manage services")
        print("  image [options]     Manage images")
        print("")
        print("Languages options:")
        print("  --json              Output as JSON array")
        print("")
        print("Service options:")
        print("  --list              List all services")
        print("  --info ID           Get service details")
        print("  --unfreeze-on-demand ID true|false  Enable/disable auto-unfreeze on HTTP request")
        print("")
        print("Image options:")
        print("  --list              List all images")
        print("  --info ID           Get image details")
        print("  --delete ID         Delete an image")
        print("  --lock ID           Lock image to prevent deletion")
        print("  --unlock ID         Unlock image")
        print("  --publish ID        Publish image (requires --source-type)")
        print("  --source-type TYPE  Source type: service or snapshot")
        print("  --visibility ID MODE  Set visibility: private, unlisted, public")
        print("  --spawn ID          Spawn service from image")
        print("  --clone ID          Clone an image")
        print("  --name NAME         Name for spawned service or cloned image")
        print("  --ports PORTS       Ports for spawned service")
        os.exit(0)
    else
        local result = Un.run(arg[1])
        if result.stdout then print(result.stdout) end
        if result.stderr then io.stderr:write(result.stderr) end
        os.exit(result.exit_code or 0)
    end
end

return Un
