local x = mako or xedge
assert(x,"This app requires Mako or Xedge")
x.createloader(io)

local rw=require"rwfile"
local GITHUB_TOKEN,MCP_AUTH_TOKEN
local GITHUB_API=os.getenv"LSP_CLAW_GITHUB_API" or "https://api.github.com"

local function storeTokens(githubToken,authToken)
   local io=ba.openio(mako and "home" or "disk")
   local key=ba.crypto.hash"sha256"(ba.tpm.uniquekey("LSP-Claw-Keys",32))(true)
   local iv = "0123456789AB"
   local gcm=ba.crypto.symmetric("GCM",key,iv)
   local cipher,tag = gcm:encrypt(ba.json.encode{githubToken=githubToken,authToken=authToken})
   local ok,err=rw.file(io,"LSP-Claw-Keys.bin",tag..cipher)
   if not ok then return nil,err or "cannot save token settings" end
   GITHUB_TOKEN,MCP_AUTH_TOKEN=githubToken,authToken
   if app.tokensChanged then app.tokensChanged(githubToken,authToken) end
   return true
end

function app.getSetTokens(githubToken,authToken)
   if githubToken or authToken then
      local ok,err=storeTokens(githubToken,authToken)
      if not ok then return nil,nil,err end
   elseif GITHUB_TOKEN == nil and MCP_AUTH_TOKEN == nil then
      local io=ba.openio(mako and "home" or "disk")
      local data=rw.file(io,"LSP-Claw-Keys.bin")
      if data then
         local key=ba.crypto.hash"sha256"(ba.tpm.uniquekey("LSP-Claw-Keys",32))(true)
         local gcm=ba.crypto.symmetric("GCM",key,"0123456789AB")
         local tag,cipher=data:sub(1,16),data:sub(17)
         if tag and cipher then
            local t=ba.json.decode(gcm:decrypt(cipher,tag) or "")
            if t then GITHUB_TOKEN,MCP_AUTH_TOKEN=t.githubToken,t.authToken end
         end
      end
   end
   return GITHUB_TOKEN,MCP_AUTH_TOKEN
end

function app.saveTokens(githubToken,authToken)
   return storeTokens(githubToken,authToken)
end

if mako then
   GITHUB_TOKEN = os.getenv"GITHUB_TOKEN" or os.getenv"GH_TOKEN"
   MCP_AUTH_TOKEN = os.getenv"MCP_AUTH_TOKEN"
   if not GITHUB_TOKEN then
      GITHUB_TOKEN=require"loadconf".GITHUB_TOKEN
   end
   if not MCP_AUTH_TOKEN then
      MCP_AUTH_TOKEN=require"loadconf".MCP_AUTH_TOKEN
   end
end

GITHUB_TOKEN,MCP_AUTH_TOKEN = app.getSetTokens(GITHUB_TOKEN,MCP_AUTH_TOKEN)

if GITHUB_TOKEN then
   tracep(5, "Using GitHub token for authenticated API requests")
else
   tracep(0, "WARNING: no GitHub token. You have limited GitHub access")
end
if MCP_AUTH_TOKEN then
   tracep(5, "MCP bearer token authentication enabled")
else
   tracep(0, "WARNING: MCP bearer token authentication is disabled")
end

local ghio,info = require"GitHubIo".create{
   owner  = "RealTimeLogic",
   repo	  = "LSP-Examples",
   branch = "master",
   api=GITHUB_API,
   token=GITHUB_TOKEN,
   publicArchiveFallback=true,
   archiveCacheIo=ba.openio(mako and "home" or "disk"),
   archiveCachePath="LSP-Claw-GitHub-ReadCache.zip",
   log=function(url,code,message) trace(url,code,message,debug.traceback()) end,
   mtime=os.time()
}

function app.validateGitHubToken(token)
   return info.validateToken(token)
end

app.tokensChanged=function(githubToken)
   info.setToken(githubToken)
end

local appmgr=require"appmgr"
local archiveManager=require"lab_archive".create(appmgr,{
   transferTtl=tonumber(os.getenv"LSP_CLAW_TRANSFER_TTL_SECONDS"),
   transferReadTimeoutMs=tonumber(os.getenv"LSP_CLAW_TRANSFER_READ_TIMEOUT_MS"),
   transferTotalTimeoutSeconds=tonumber(os.getenv"LSP_CLAW_TRANSFER_TOTAL_TIMEOUT_SECONDS"),
   allowedTransferPorts=os.getenv"LSP_CLAW_TRANSFER_ALLOWED_PORTS"
})

local mcp = require"fastmcp.engine".create {
   name = "BAS Example Workspace Assistant",
   version = "0.2.0",
   instructions = rw.file(io,".info/instructions.md")
}

local traceDir = ba.create.tracelogger()
if "function" ~= type(traceDir.ontrace) then error"Error: server too old; Upgrade your server!" end
local transport
local traceStreamCount = 0
local traceHookActive = false
local runtimeTraceBufferSize = 10 * 1024
local runtimeTrace = {}

local function traceMessage(msg)
   local data = tostring(msg):gsub("\r\n", "\n"):gsub("\r", "\n")
   if transport then
      transport:notifyAll("notifications/message", {
	 level = "info",
	 logger = "trace",
	 data = data
      })
   end
   if runtimeTrace.record then runtimeTrace.record(data) end
end

local function updateTraceHook()
   local shouldTrace = traceStreamCount > 0 or runtimeTraceBufferSize > 0
   if shouldTrace and not traceHookActive then
      traceDir:ontrace(traceMessage)
      traceHookActive = true
   elseif not shouldTrace and traceHookActive then
      traceDir:ontrace()
      traceHookActive = false
   end
end

local function attachTraceStream()
   traceStreamCount = traceStreamCount + 1
   updateTraceHook()
end

local function detachTraceStream()
   if traceStreamCount > 0 then traceStreamCount = traceStreamCount - 1 end
   updateTraceHook()
end

local function authorizeLocalOrigin(origin, cmd, ctx)
   if origin == nil then return true end
   if string.match(origin, "^https?://localhost:%d+$") then return true end
   if string.match(origin, "^https?://127%.0%.0%.1:%d+$") then return true end
   if string.match(origin, "^https?://%[::1%]:%d+$") then return true end
   return false, "Origin forbidden"
end

local function authorizeBearerToken(cmd)
   if not MCP_AUTH_TOKEN then return true end
   if cmd:header"Authorization" == "Bearer " .. MCP_AUTH_TOKEN then return true end
   return false, "Unauthorized"
end

local function authorizeRequest(origin, cmd, ctx)
   local ok, reason = authorizeLocalOrigin(origin, cmd, ctx)
   if not ok then return false, reason end
   if cmd:method() == "OPTIONS" then return true end
   return authorizeBearerToken(cmd)
end

local function tokenConfigured(token)
   return token ~= nil and token ~= ""
end

local function getConfigurationStatus()
   local baseUri = ""
   if dir and type(dir.baseuri) == "function" then
      baseUri = dir:baseuri() or ""
   end
   return {
      githubTokenSet = tokenConfigured(GITHUB_TOKEN),
      mcpAuthTokenSet = tokenConfigured(MCP_AUTH_TOKEN),
      setupBaseUri = baseUri
   }
end

-- Codex, add code here
require"lspclaw".register(mcp, ghio, info, appmgr, {
   instructions = mcp.instructions,
   io = io,
   runtimeTrace = runtimeTrace,
   runtimeTraceBufferSize = runtimeTraceBufferSize,
   configurationStatus = getConfigurationStatus,
   archiveManager = archiveManager
})

local function authorizeArchiveRequest(cmd)
   if not MCP_AUTH_TOKEN then return true end
   if cmd:user() == "lsp-claw-admin" then return true end
   return cmd:header"Authorization" == "Bearer "..MCP_AUTH_TOKEN
end

local function archivePath(ticket)
   local base=(dir and dir:baseuri()) or "/"
   if base == "" then base="/" end
   if base:sub(-1) ~= "/" then base=base.."/" end
   return base.."archive.lsp?ticket="..ticket
end

local function sendJson(response,status,value)
   local encoded=ba.json.encode(value)
   response:setstatus(status)
   response:setcontenttype("application/json; charset=utf-8")
   response:setheader("Cache-Control","no-store")
   response:setheader("Content-Length",tostring(#encoded))
   response:send(encoded)
end

function app.listLabs()
   return appmgr.listLabs()
end

function app.prepareBrowserLabExport(labName)
   local lab,err,code=appmgr.getLab(labName)
   if not lab then return nil,err,code end
   local prepared
   prepared,err,code=archiveManager:prepareExport(lab)
   if not prepared then return nil,err,code end
   prepared.downloadPath=archivePath(prepared.ticket)
   prepared.ticket=nil
   return prepared
end

function app.setBrowserLabRunning(labName,running)
   local lab,err,code=appmgr.getLab(labName)
   if not lab then return nil,err,code end
   if running == lab:isRunning() then
      return {labName=lab:name(),running=running,changed=false}
   end
   local ok
   if running then
      ok,err=lab:start()
      code="labStartFailed"
   else
      ok,err=lab:stop()
      code="labStopFailed"
   end
   if not ok then return nil,err,code end
   return {labName=lab:name(),running=lab:isRunning(),changed=true}
end

function app.labApiService(cmd,response)
   if not authorizeArchiveRequest(cmd) then sendJson(response,401,{ok=false,error="Unauthorized"}) return end
   if cmd:method() ~= "POST" then sendJson(response,405,{ok=false,error="Method not allowed"}) return end
   if cmd:data("action") == "setRunning" then
      local value=cmd:data("running")
      if value ~= "true" and value ~= "false" then
         sendJson(response,400,{ok=false,error="The running field must be true or false",code="invalidRunningState"})
         return
      end
      local result,err,code=app.setBrowserLabRunning(cmd:data("labName"),value == "true")
      if not result then sendJson(response,409,{ok=false,error=err,code=code}) return end
      sendJson(response,200,{ok=true,result=result})
      return
   end
   if cmd:data("action") == "prepareExport" then
      local prepared,err,code=app.prepareBrowserLabExport(cmd:data("labName"))
      if not prepared then sendJson(response,409,{ok=false,error=err,code=code}) return end
      sendJson(response,200,{ok=true,result=prepared})
      return
   end
   if cmd:data("action") ~= "prepareImport" then sendJson(response,400,{ok=false,error="Unknown action"}) return end
   local prepared,err,code=archiveManager:prepareImport(cmd:data("labName"),cmd:data("conflictAction"),cmd:data("confirmed") == "true")
   if not prepared then sendJson(response,409,{ok=false,error=err,code=code}) return end
   prepared.uploadPath=archivePath(prepared.ticket)
   prepared.ticket=nil
   sendJson(response,200,{ok=true,result=prepared})
end

function app.archiveService(cmd,response)
   return archiveManager:handle(cmd,response,authorizeArchiveRequest(cmd))
end

function app.transferService(cmd,response)
   return archiveManager:handleTransfer(cmd,response)
end

function app.setRuntimeTraceBufferSize(size)
   if runtimeTrace.setBufferSize then
      runtimeTraceBufferSize = runtimeTrace.setBufferSize(size)
   else
      runtimeTraceBufferSize = math.floor(tonumber(size) or 0)
      if runtimeTraceBufferSize < 0 then runtimeTraceBufferSize = 0 end
   end
   updateTraceHook()
   return runtimeTraceBufferSize
end

local Http = require"fastmcp.http"
transport = Http.streamable(mcp, {
   stateful = true,
   enableGetStream = true,
   protocolVersion = "2025-11-25",
   requireStrictAccept = false,
   allowNonSseGet = true,
   -- LSP-Claw is commonly installed behind the Xedge application router.
   -- FastMCP itself ignores forwarded headers unless this is explicitly set.
   trustForwardedHeaders = true,
   maxRequestBodyBytes = 1024 * 1024,
   maxResponseBytes = 4 * 1024 * 1024,
   maxNotificationBytes = 256 * 1024,
   maxSessions = 16,
   maxStreamsPerSession = 2,
   sessionIdleTimeoutSeconds = 30 * 60,
   sessionMaxLifetimeSeconds = 8 * 60 * 60,
   authorizeOrigin = authorizeRequest,
   onStreamOpen = function(session, stream, self)
      attachTraceStream()
   end,
   onStreamClose = function(session, stream, self)
      detachTraceStream()
   end
})
updateTraceHook()

function app.cmdService(cmd)
   return transport:handle(cmd)
end

function onunload()
   traceDir:ontrace()
end
