--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--

require "luci.util"
local docker = require "luci.model.docker"
local dk = docker.new()
container_id = arg[1]
local action = arg[2] or "info"

local images, networks, container_info
if not container_id then return end
local res = dk.containers:inspect({id = container_id})
if res.code < 300 then container_info = res.body else return end
res = dk.networks:list()
if res.code < 300 then networks = res.body else return end

local get_ports = function(d)
  local data
  if d.HostConfig and d.HostConfig.PortBindings then
    for inter, out in pairs(d.HostConfig.PortBindings) do
      data = (data and (data .. "<br>") or "") .. out[1]["HostPort"] .. ":" .. inter
    end
  end
  return data
end

local get_env = function(d)
  local data
  if d.Config and d.Config.Env then
    for _,v in ipairs(d.Config.Env) do
      data = (data and (data .. "<br>") or "") .. v
    end
  end
  return data
end

local get_command = function(d)
  local data
  if d.Config and d.Config.Cmd then
    for _,v in ipairs(d.Config.Cmd) do
      data = (data and (data .. " ") or "") .. v
    end
  end
  return data
end

local get_mounts = function(d)
  local data
  if d.Mounts then
    for _,v in ipairs(d.Mounts) do
      local v_sorce_d, v_dest_d
      local v_sorce = ""
      local v_dest = ""
      for v_sorce_d in v["Source"]:gmatch('[^/]+') do
        if v_sorce_d and #v_sorce_d > 12 then
          v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,12) .. "..."
        else
          v_sorce = v_sorce .."/".. v_sorce_d
        end
      end
      for v_dest_d in v["Destination"]:gmatch('[^/]+') do
        if v_dest_d and #v_dest_d > 12 then
          v_dest = v_dest .. "/" .. v_dest_d:sub(1,12) .. "..."
        else
          v_dest = v_dest .."/".. v_dest_d
        end
      end
      data = (data and (data .. "<br>") or "") .. v_sorce .. ":" .. v["Destination"] .. (v["Mode"] ~= "" and (":" .. v["Mode"]) or "")
    end
  end
  return data
end

local get_device = function(d)
  local data
  if d.HostConfig and d.HostConfig.Devices then
    for _,v in ipairs(d.HostConfig.Devices) do
      data = (data and (data .. "<br>") or "") .. v["PathOnHost"] .. ":" .. v["PathInContainer"] .. (v["CgroupPermissions"] ~= "" and (":" .. v["CgroupPermissions"]) or "")
    end
  end
  return data
end

local get_links = function(d)
  local data
  if d.HostConfig and d.HostConfig.Links then
    for _,v in ipairs(d.HostConfig.Links) do
      data = (data and (data .. "<br>") or "") .. v
    end
  end
  return data
end

local get_tmpfs = function(d)
  local data
  if d.HostConfig and d.HostConfig.Tmpfs then
    for k, v in pairs(d.HostConfig.Tmpfs) do
      data = (data and (data .. "<br>") or "") .. k .. (v~="" and ":" or "")..v
    end
  end
  return data
end

local get_dns = function(d)
  local data
  if d.HostConfig and d.HostConfig.Dns then
    for _, v in ipairs(d.HostConfig.Dns) do
      data = (data and (data .. "<br>") or "") .. v
    end
  end
  return data
end

local get_sysctl = function(d)
  local data
  if d.HostConfig and d.HostConfig.Sysctls then
    for k, v in pairs(d.HostConfig.Sysctls) do
      data = (data and (data .. "<br>") or "") .. k..":"..v
    end
  end
  return data
end

local get_networks = function(d)
  local data={}
  if d.NetworkSettings and d.NetworkSettings.Networks and type(d.NetworkSettings.Networks) == "table" then
    for k,v in pairs(d.NetworkSettings.Networks) do
      data[k] = v.IPAddress or ""
    end
  end
  return data
end


local start_stop_remove = function(m, cmd)
  docker:clear_status()
  docker:append_status("Containers: " .. cmd .. " " .. container_id .. "...")
  local res
  if cmd ~= "upgrade" then
    res = dk.containers[cmd](dk, {id = container_id})
  else
    res = dk.containers_upgrade(dk, {id = container_id})
  end
  if res and res.code >= 300 then
    docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
    luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
  else
    docker:clear_status()
    if cmd ~= "remove" and cmd ~= "upgrade" then
      luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
    else
      luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
    end
  end
end

m=SimpleForm("docker", container_info.Name:sub(2), translate("Docker Container") )
m.redirect = luci.dispatcher.build_url("admin/docker/containers")
-- m:append(Template("dockerman/container"))
docker_status = m:section(SimpleSection)
docker_status.template = "dockerman/apply_widget"
docker_status.err=docker:read_status()
docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end


action_section = m:section(Table,{{}})
action_section.notitle=true
action_section.rowcolors=false
action_section.template = "cbi/nullsection"

btnstart=action_section:option(Button, "_start")
btnstart.template = "dockerman/cbi/inlinebutton"
btnstart.inputtitle=translate("Start")
btnstart.inputstyle = "apply"
btnstart.forcewrite = true
btnrestart=action_section:option(Button, "_restart")
btnrestart.template = "dockerman/cbi/inlinebutton"
btnrestart.inputtitle=translate("Restart")
btnrestart.inputstyle = "reload"
btnrestart.forcewrite = true
btnstop=action_section:option(Button, "_stop")
btnstop.template = "dockerman/cbi/inlinebutton"
btnstop.inputtitle=translate("Stop")
btnstop.inputstyle = "reset"
btnstop.forcewrite = true
btnkill=action_section:option(Button, "_kill")
btnkill.template = "dockerman/cbi/inlinebutton"
btnkill.inputtitle=translate("Kill")
btnkill.inputstyle = "reset"
btnkill.forcewrite = true
btnupgrade=action_section:option(Button, "_upgrade")
btnupgrade.template = "dockerman/cbi/inlinebutton"
btnupgrade.inputtitle=translate("Upgrade")
btnupgrade.inputstyle = "reload"
btnstop.forcewrite = true
btnduplicate=action_section:option(Button, "_duplicate")
btnduplicate.template = "dockerman/cbi/inlinebutton"
btnduplicate.inputtitle=translate("Duplicate/Edit")
btnduplicate.inputstyle = "add"
btnstop.forcewrite = true
btnremove=action_section:option(Button, "_remove")
btnremove.template = "dockerman/cbi/inlinebutton"
btnremove.inputtitle=translate("Remove")
btnremove.inputstyle = "remove"
btnremove.forcewrite = true

btnstart.write = function(self, section)
  start_stop_remove(m,"start")
end
btnrestart.write = function(self, section)
  start_stop_remove(m,"restart")
end
btnupgrade.write = function(self, section)
  start_stop_remove(m,"upgrade")
end
btnremove.write = function(self, section)
  start_stop_remove(m,"remove")
end
btnstop.write = function(self, section)
  start_stop_remove(m,"stop")
end
btnkill.write = function(self, section)
  start_stop_remove(m,"kill")
end
btnduplicate.write = function(self, section)
  luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer/duplicate/"..container_id))
end

tab_section = m:section(SimpleSection)
tab_section.template = "dockerman/container"

if action == "info" then 
  m.submit = false
  m.reset  = false
  table_info = {
    ["01name"] = {_key = translate("Name"),  _value = container_info.Name:sub(2)  or "-", _button=translate("Update")},
    ["02id"] = {_key = translate("ID"),  _value = container_info.Id  or "-"},
    ["03image"] = {_key = translate("Image"),  _value = container_info.Config.Image .. "<br>" .. container_info.Image},
    ["04status"] = {_key = translate("Status"),  _value = container_info.State and container_info.State.Status  or "-"},
    ["05created"] = {_key = translate("Created"),  _value = container_info.Created  or "-"},
  }
  table_info["06start"] = container_info.State.Status == "running" and {_key = translate("Start Time"),  _value = container_info.State and container_info.State.StartedAt or "-"} or {_key = translate("Finish Time"),  _value = container_info.State and container_info.State.FinishedAt or "-"}
  table_info["07healthy"] = {_key = translate("Healthy"),  _value = container_info.State and container_info.State.Health and container_info.State.Health.Status or "-"}
  table_info["08restart"] = {_key = translate("Restart Policy"),  _value = container_info.HostConfig and container_info.HostConfig.RestartPolicy and container_info.HostConfig.RestartPolicy.Name or "-", _button=translate("Update")}
  table_info["081user"] = {_key = translate("User"),  _value = container_info.Config and (container_info.Config.User ~="" and container_info.Config.User or "-") or "-"}
  table_info["09mount"] = {_key = translate("Mount/Volume"),  _value = get_mounts(container_info)  or "-"}
  table_info["10cmd"] = {_key = translate("Command"),  _value = get_command(container_info) or "-"}
  table_info["11env"] = {_key = translate("Env"),  _value = get_env(container_info)  or "-"}
  table_info["12ports"] = {_key = translate("Ports"),  _value = get_ports(container_info) or "-"}
  table_info["13links"] = {_key = translate("Links"),  _value = get_links(container_info)  or "-"}
  table_info["14device"] = {_key = translate("Device"),  _value = get_device(container_info)  or "-"}
  table_info["15tmpfs"] = {_key = translate("Tmpfs"),  _value = get_tmpfs(container_info)  or "-"}
  table_info["16dns"] = {_key = translate("DNS"),  _value = get_dns(container_info)  or "-"}
  table_info["17sysctl"] = {_key = translate("Sysctl"),  _value = get_sysctl(container_info)  or "-"}
  info_networks = get_networks(container_info)
  list_networks = {}
  for _, v in ipairs (networks) do
    if v.Name then
      local parent = v.Options and v.Options.parent or nil
      local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
      ipv6 =  v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil
      local network_name = v.Name .. " | " .. v.Driver  .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "")
      list_networks[v.Name] = network_name
    end
  end

  if type(info_networks)== "table" then
    for k,v in pairs(info_networks) do
      table_info["14network"..k] = {
        _key = translate("Network"),  _value = k.. (v~="" and (" | ".. v) or ""), _button=translate("Disconnect")
      }
      list_networks[k]=nil
    end
  end

  table_info["15connect"] = {_key = translate("Connect Network"),  _value = list_networks ,_opts = "", _button=translate("Connect")}


  d_info = m:section(Table,table_info)
  d_info.nodescr=true
  d_info.formvalue=function(self, section)
    return table_info
  end
  dv_key = d_info:option(DummyValue, "_key", translate("Info"))
  dv_key.width = "20%"
  dv_value = d_info:option(ListValue, "_value")
  dv_value.render = function(self, section, scope)
    if table_info[section]._key == translate("Name") then
      self:reset_values()
      self.template = "cbi/value"
      self.size = 30
      self.keylist = {}
      self.vallist = {}
      self.default=table_info[section]._value
      Value.render(self, section, scope)
    elseif table_info[section]._key == translate("Restart Policy") then
      self.template = "cbi/lvalue"
      self:reset_values()
      self.size = nil
      self:value("no", "No")
      self:value("unless-stopped", "Unless stopped")
      self:value("always", "Always")
      self:value("on-failure", "On failure")
      self.default=table_info[section]._value
      ListValue.render(self, section, scope)
    elseif table_info[section]._key == translate("Connect Network") then
      self.template = "cbi/lvalue"
      self:reset_values()
      self.size = nil
      for k,v in pairs(list_networks) do
        if k ~= "host" then
          self:value(k,v)
        end
      end
      self.default=table_info[section]._value
      ListValue.render(self, section, scope)
    else
      self:reset_values()
      self.rawhtml=true
      self.template = "cbi/dvalue"
      self.default=table_info[section]._value
      DummyValue.render(self, section, scope)
    end
  end
  dv_value.forcewrite = true -- for write function using simpleform 
  dv_value.write = function(self, section, value)
    table_info[section]._value=value
  end
  dv_value.validate = function(self, value)
    return value
  end
  dv_opts = d_info:option(Value, "_opts")
  dv_opts.forcewrite = true -- for write function using simpleform 
  dv_opts.write = function(self, section, value)

    table_info[section]._opts=value
  end
  dv_opts.validate = function(self, value)
    return value
  end
  dv_opts.render = function(self, section, scope)
    if table_info[section]._key==translate("Connect Network") then
      self.template = "cbi/value"
      self.keylist = {}
      self.vallist = {}
      self.placeholder = "10.1.1.254"
      self.datatype = "ip4addr"
      self.default=table_info[section]._opts
      Value.render(self, section, scope)
    else
      self.rawhtml=true
      self.template = "cbi/dvalue"
      self.default=table_info[section]._opts
      DummyValue.render(self, section, scope)
    end
  end
  btn_update = d_info:option(Button, "_button")
  btn_update.forcewrite = true
  btn_update.render = function(self, section, scope)
    if table_info[section]._button and table_info[section]._value ~= nil then
      btn_update.inputtitle=table_info[section]._button
      self.template = "cbi/button"
      self.inputstyle = "edit"
      Button.render(self, section, scope)
    else 
      self.template = "cbi/dvalue"
      self.default=""
      DummyValue.render(self, section, scope)
    end
  end
  btn_update.write = function(self, section, value)
    local res
    docker:clear_status()
    if section == "01name" then
      docker:append_status("Containers: rename " .. container_id .. "...")
      local new_name = table_info[section]._value
      res = dk.containers:rename({id = container_id, query = {name=new_name}})
    elseif section == "08restart" then
      docker:append_status("Containers: update " .. container_id .. "...")
      local new_restart = table_info[section]._value
      res = dk.containers:update({id = container_id, body = {RestartPolicy = {Name = new_restart}}})
    elseif table_info[section]._key == translate("Network") then
      local _,_,leave_network = table_info[section]._value:find("(.-) | .+")
      leave_network = leave_network or table_info[section]._value
      docker:append_status("Network: disconnect " .. leave_network .. container_id .. "...")
      res = dk.networks:disconnect({name = leave_network, body = {Container = container_id}})
    elseif section == "15connect" then
      local connect_network = table_info[section]._value
      local network_opiton
      if connect_network ~= "none" and connect_network ~= "bridge" and connect_network ~= "host" then
        network_opiton = table_info[section]._opts ~= "" and {
            IPAMConfig={
              IPv4Address=table_info[section]._opts
            }
        } or nil
      end
      docker:append_status("Network: connect " .. connect_network .. container_id .. "...")
      res = dk.networks:connect({name = connect_network, body = {Container = container_id, EndpointConfig= network_opiton}})
    end
    if res and res.code > 300 then
      docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
    else
      docker:clear_status()
    end
    luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/info"))
  end
  
-- info end
elseif action == "resources" then
  local resources_section= m:section(SimpleSection)
  d = resources_section:option( Value, "cpus", translate("CPUs"), translate("Number of CPUs. Number is a fractional number. 0.000 means no limit."))
  d.placeholder = "1.5"
  d.rmempty = true
  d.datatype="ufloat"
  d.default = container_info.HostConfig.NanoCpus / (10^9)

  d = resources_section:option(Value, "cpushares", translate("CPU Shares Weight"), translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024."))
  d.placeholder = "1024"
  d.rmempty = true
  d.datatype="uinteger"
  d.default = container_info.HostConfig.CpuShares

  d = resources_section:option(Value, "memory", translate("Memory"), translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M."))
  d.placeholder = "128m"
  d.rmempty = true
  d.default = container_info.HostConfig.Memory ~=0 and ((container_info.HostConfig.Memory / 1024 /1024) .. "M") or 0

  d = resources_section:option(Value, "blkioweight", translate("Block IO Weight"), translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000."))
  d.placeholder = "500"
  d.rmempty = true
  d.datatype="uinteger"
  d.default = container_info.HostConfig.BlkioWeight

  m.handle = function(self, state, data)
    if state == FORM_VALID then
      local memory = data.memory
      if memory and memory ~= 0 then
        _,_,n,unit = memory:find("([%d%.]+)([%l%u]+)")
        if n then
          unit = unit and unit:sub(1,1):upper() or "B"
          if  unit == "M" then
            memory = tonumber(n) * 1024 * 1024
          elseif unit == "G" then
            memory = tonumber(n) * 1024 * 1024 * 1024
          elseif unit == "K" then
            memory = tonumber(n) * 1024
          else
            memory = tonumber(n)
          end
        end
      end
      request_body = {
        BlkioWeight = tonumber(data.blkioweight),
        NanoCPUs = tonumber(data.cpus)*10^9,
        Memory = tonumber(memory),
        CpuShares = tonumber(data.cpushares)
        }
      docker:write_status("Containers: update " .. container_id .. "...")
      local res = dk.containers:update({id = container_id, body = request_body})
      if res and res.code >= 300 then
        docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
      else
        docker:clear_status()
      end
      luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/resources"))
    end
  end
elseif action == "file" then
  local filesection= m:section(SimpleSection)
  m.submit = false
  m.reset  = false
  filesection.template = "dockerman/container_file"
  filesection.container = container_id
elseif action == "inspect" then
  local inspectsection= m:section(SimpleSection)
  inspectsection.syslog = luci.jsonc.stringify(container_info, true)
  inspectsection.title = translate("Container Inspect")
  inspectsection.template = "dockerman/logs"
  m.submit = false
  m.reset  = false
elseif action == "logs" then
  local logsection= m:section(SimpleSection)
  local logs = ""
  local query ={
    stdout = 1,
    stderr = 1,
    tail = 1000
  }
  local logs = dk.containers:logs({id = container_id, query = query})
  if logs.code == 200 then
    logsection.syslog=logs.body
  else
    logsection.syslog="Get Logs ERROR\n"..logs.code..": "..logs.body
  end
  logsection.title=translate("Container Logs")
  logsection.template = "dockerman/logs"
  m.submit = false
  m.reset  = false
elseif action == "console" then
  m.submit = false
  m.reset  = false
  local cmd_docker = luci.util.exec("which docker"):match("^.+docker") or nil
  local cmd_ttyd = luci.util.exec("which ttyd"):match("^.+ttyd") or nil
  if cmd_docker and cmd_ttyd and container_info.State.Status == "running" then
    local consolesection= m:section(SimpleSection)
    local cmd = "/bin/sh"
    local uid
    local vcommand = consolesection:option(Value, "command", translate("Command"))
    vcommand:value("/bin/sh", "/bin/sh")
    vcommand:value("/bin/ash", "/bin/ash")
    vcommand:value("/bin/bash", "/bin/bash")
    vcommand.default = "/bin/sh"
    vcommand.forcewrite = true
    vcommand.write = function(self, section, value)
      cmd = value
    end
    local vuid = consolesection:option(Value, "uid", translate("UID"))
    vuid.forcewrite = true
    vuid.write = function(self, section, value)
      uid = value
    end
    local btn_connect = consolesection:option(Button, "connect")
    btn_connect.render = function(self, section, scope)
      self.inputstyle = "add"
      self.title = " "
      self.inputtitle = translate("Connect")
      Button.render(self, section, scope)
    end
    btn_connect.write = function(self, section)
      local cmd_docker = luci.util.exec("which docker"):match("^.+docker") or nil
      local cmd_ttyd = luci.util.exec("which ttyd"):match("^.+ttyd") or nil
      if not cmd_docker or not cmd_ttyd or cmd_docker:match("^%s+$") or cmd_ttyd:match("^%s+$") then return end
      local kill_ttyd = 'netstat -lnpt | grep ":7682[ \t].*ttyd$" | awk \'{print $NF}\' | awk -F\'/\' \'{print "kill -9 " $1}\' | sh > /dev/null'
      luci.util.exec(kill_ttyd)
      local hosts
      local uci = (require "luci.model.uci").cursor()
      local remote = uci:get("dockerd", "globals", "remote_endpoint")
      local socket_path = (remote == "false" or not remote) and  uci:get("dockerd", "globals", "socket_path") or nil
      local host = (remote == "true") and uci:get("dockerd", "globals", "remote_host") or nil
      local port = (remote == "true") and uci:get("dockerd", "globals", "remote_port") or nil
      if remote and host and port then
        hosts = host .. ':'.. port
      elseif socket_path then
        hosts = "unix://" .. socket_path
      else
        return
      end
      local start_cmd = cmd_ttyd .. ' -d 2 --once -p 7682 '.. cmd_docker .. ' -H "'.. hosts ..'" exec -it ' .. (uid and uid ~= "" and (" -u ".. uid  .. ' ') or "").. container_id .. ' ' .. cmd .. ' &'
      os.execute(start_cmd)
      local console = consolesection:option(DummyValue, "console")
      console.container_id = container_id
      console.template = "dockerman/container_console"
    end
  end
elseif action == "stats" then
  local response = dk.containers:top({id = container_id, query = {ps_args="-aux"}})
  local container_top
  if response.code == 200 then
    container_top=response.body
  else
    response = dk.containers:top({id = container_id})
    if response.code == 200 then
      container_top=response.body
    end
  end

  if type(container_top) == "table" then
    container_top=response.body
    stat_section = m:section(SimpleSection)
    stat_section.container_id = container_id
    stat_section.template = "dockerman/container_stats"
    table_stats = {cpu={key=translate("CPU Useage"),value='-'},memory={key=translate("Memory Useage"),value='-'}}
    stat_section = m:section(Table, table_stats, translate("Stats"))
    stat_section:option(DummyValue, "key", translate("Stats")).width="33%"
    stat_section:option(DummyValue, "value")
    top_section= m:section(Table, container_top.Processes, translate("TOP"))
    for i, v in ipairs(container_top.Titles) do
      top_section:option(DummyValue, i, translate(v))
  end
end
m.submit = false
m.reset  = false
end

return m