--[[
Copyright (c) 2012-2015 Kaarle Ritvanen
See LICENSE file for license details
--]]

local M = require('aconf.model')

local posix = require('posix')


local Host = M.new()
Host.address = M.net.IPAddress{required=true, addr='ipaddr'}
Host.canonical = M.String{required=true, ui_name='Canonical name'}
Host.alias = M.Set{
   type=M.String,
   be_mode='enumerate',
   ui_name='Aliases',
   ui_member='Alias',
   detail=false
}

local Resolv = M.new()
Resolv.servers = M.List{
   type=M.net.IPAddress,
   addr='nameserver',
   be_mode='enumerate',
   widget='inline'
}
Resolv.search_domains = M.List{
   type=M.String,
   addr='search/domain',
   be_mode='enumerate',
   widget='inline'
}


local iface_aug_addr = '/augeas/etc/network/interfaces'
local iface_sys_dir = '/sys/class/net'
local vlan_pattern = '^(.+)%.(%d+)$'

for _, trigger in ipairs{
   {phase='pre', action='stop'}, {phase='post', action='start'}
} do
   M.trigger(
      trigger.phase,
      iface_aug_addr,
      function() os.execute('rc-service networking '..trigger.action) end
   )
end

M.defer(iface_aug_addr)


-- TODO allow multiple addresses of same family

local IPv4 = M.new()
IPv4.method = M.String{
   required=true,
   choice={
      {'dhcp', 'DHCP'},
      {'loopback', enabled=false},
      {'unconfigured', be_value='manual'},
      'static'
   },
   default='unconfigured'
}
IPv4.address = M.net.IPv4Address{
   condition={method='static'}, required=true, cidr=true, mask_addr='netmask'
}
IPv4.gateway = M.net.IPv4Address{condition={method='static'}}


local IPv6 = M.new()
IPv6.method = M.String{
   required=true,
   choice={
      {'loopback', enabled=false}, {'unconfigured', be_value='manual'}, 'static'
   },
   default='unconfigured'
}
IPv6.address = M.net.IPv6Address{
   condition={method='static'}, required=true, cidr=true, mask_addr='netmask'
}
IPv6.gateway = M.net.IPv6Address{condition={method='static'}}



local Interface = M.new()

function Interface:validate()
   if self.status == 'attached' then
      for _, version in ipairs{4, 6} do
	 self['ipv'..version].method =
	    self.class == 'loopback' and 'loopback' or 'unconfigured'
      end
   end
end

function Interface:is_removable() return self.class == 'logical' end

function Interface:auto_vlan_tag()
   local name = M.node.name(self)
   local _, tag = name:match(vlan_pattern)
   if tag then return tag end
   return name:match('^vlan(%d+)$')
end

Interface.enabled = M.Boolean{
   compute=function(iface)
      return M.node.contains(iface:fetch('../../enabled-ifaces'), iface)
   end,
   store=function(iface, value)
      local set = iface:fetch('../../enabled-ifaces')
      if value then M.node.insert(set, iface)
      else set[iface] = nil end
   end
}

Interface.class = M.String{
   compute=function(iface, txn)
	      local name = M.node.name(iface)
	      if name == 'lo' then return 'loopback' end

	      local saddr = M.path.rawjoin('/files', iface_sys_dir, name, '.')
	      if not txn:get(saddr) then return 'logical' end

	      for _, addr in ipairs{
		 {saddr, 'bonding'},
		 {saddr, 'bridge'},
		 {'/files/proc/net/vlan', name}
	      } do
		 if txn:get(M.path.join(table.unpack(addr))) then
		    return 'logical'
		 end
	      end
	      return 'physical'
	   end,
   choice={'loopback', 'logical', 'physical'}
}

Interface.type = M.String{
   condition={class='logical'},
   compute=function(iface)
	      if #iface.slaves > 0 then return 'bond' end
	      if #iface.ports > 0 then return 'bridge' end
	      if iface.vlan_tag then return 'vlan' end
	   end,
   editable=function(iface) return not iface:auto_vlan_tag() end,
   required=true,
   choice={'bond', 'bridge', {'vlan', 'VLAN'}}
}

Interface.status = M.String{
   visible=false,
   compute=function(obj)
	      if obj.class == 'loopback' then return 'attached' end

	      for _, iface in pairs(M.node.parent(obj)) do
		 if (
		    iface.type == 'bond' and M.node.contains(iface.slaves, obj)
		 ) or (
		    iface.type == 'bridge' and M.node.contains(iface.ports, obj)
		 ) then
		    return 'attached'
		 end

		 if iface.type == 'vlan' and iface.trunk == obj then
		    return 'configured'
		 end
	      end

	      for _, version in ipairs{4, 6} do
		 if obj['ipv'..version].method ~= 'unconfigured' then
		    return 'configured'
		 end
	      end

	      return 'detached'
	   end
}

Interface.slaves = M.Set{
   condition={type='bond'},
   type=M.Reference{scope='../..', filter={status='detached'}},
   required=true,
   addr='link/none/bond-slaves'
}
Interface.ports = M.Set{
   condition={type='bridge'},
   type=M.Reference{scope='../..', filter={status='detached'}},
   required=true,
   addr='link/none/bridge-ports'
}
-- TODO do not allow VLAN creation for non-existent interfaces
Interface.trunk = M.Reference{
   condition={type='vlan'},
   compute=function(iface)
	      local trunk = M.node.name(iface):match(vlan_pattern)
	      if trunk and iface:fetch('..')[trunk] then return trunk end
	   end,
   required=true,
   scope='..',
   filter={status={'detached', 'configured'}},
   addr='link/none/vlan-raw-device'
}
-- TODO ensure that (trunk, tag) is unique
Interface.vlan_tag = M.Integer{
   condition={type='vlan'},
   compute='auto_vlan_tag',
   required=true,
   min=0,
   max=4095,
   addr='link/none/vlan-id',
   ui_name='VLAN tag'
}

Interface.ipv4 = M.Model{
   model=IPv4,
   condition={status={'detached', 'configured'}},
   create=true,
   addr='inet',
   ui_name='IPv4 configuration',
   widget='inline'
}
Interface.ipv6 = M.Model{
   model=IPv6,
   condition={status={'detached', 'configured'}},
   create=true,
   addr='inet6',
   ui_name='IPv6 configuration',
   widget='inline'
}

Interface.stats = M.Collection{
   type=M.Number{editable=false},
   editable=false,
   addr=function(path)
      return M.path.join(
	 '/files/sys/class/net', M.path.name(path), 'statistics'
      )
   end,
   ui_name='Statistics',
   ui_member='',
   widget='inline'
}


local Net = M.new()
Net.host_name = M.String{addr='/augeas/etc/hostname/hostname'}
Net.hosts = M.List{type=Host, addr='/augeas/etc/hosts'}
Net.resolver = M.Model{
   model=Resolv, addr='/augeas/etc/resolv.conf', ui_name='DNS resolver'
}

Net.enabled_ifaces = M.Set{
   type=M.Reference{scope='../interfaces', addr='*/*', on_delete='set-null'},
   addr=iface_aug_addr..'/auto',
   be_mode='enumerate',
   visible=false
}
Net.interfaces = M.Collection{
   type=Interface,
   addr=iface_aug_addr..'/iface',
   be_mode={
      ['.']='value',
      ['*']='child-value:family',
      ['*/link']='child-value:method'
   },
   widget='inline'
}

M.register('net', Net, {ui_name='Network'})
M.permission.defaults('/net')

return function(txn)
   local ifaces = txn:fetch('/net/interfaces')
   for _, name in ipairs(posix.dir(iface_sys_dir)) do
      if name:sub(1, 1) ~= '.' and not ifaces[name] and posix.stat(
	 M.path.join(iface_sys_dir, name), 'type'
      ) == 'link' then
	 ifaces[name] = {}
	 if ifaces[name].class == 'logical' then ifaces[name] = nil
	 else
	    for _, version in ipairs{4, 6} do
	       ifaces[name]['ipv'..version].method = 'unconfigured'
	    end
	 end
      end
   end
end
