# The Admin4 Project
# (c) 2013-2025 Andreas Pflug
#
# Licensed under the Apache License, 
# see LICENSE.TXT for conditions of usage

import adm, wx, wx.grid as wxGrid
import logger
import time
from Validator import Validator
from wh import xlt, floatToTime, timeToFloat, Menu, shlexSplit, removeSmartQuote,\
  quoteIfNeeded
from ._dns import Rdataset, Rdata, RdataClass, rdatatype, rdataclass, rcode, DnsEnum
from ._dns import Name, DnsName, DnsAbsName, DnsRevName, DnsRevAddress, DnsSupportedTypes, checkIpAddress
from .Server import Server


individualTypes=['A', 'AAAA', 'CNAME', 'PTR']
otherTypes=['MX', 'NS', 'SRV', 'TXT', 'SOA', 'SPF', 'CAA']
noDeleteTypes=['SOA']

def admSetError(location, msg):
  adm.SetStatus(xlt("DNS %s failed: %s") % (location, rcode.to_text(msg.rcode())))
    
class Zone(adm.Node):
  typename=xlt("DNS Zone")
  shortname=xlt("Zone")

  def __init__(self, parentNode, name):
    super(Zone, self).__init__(parentNode, name)
    self.soa=None
    self.zonename=name
    self.zones=self.readZones()
    self.mx=None
    self.ns=None
    
   
  @staticmethod 
  def GetInstances(parentNode):
    instances=[]
      
    if parentNode.zones:
      for zone in parentNode.zones:
        instances.append(Zone(parentNode, zone))
    else:
      for zone in parentNode.GetServer().GetSubzones(parentNode):
        if not parentNode.GetServer().IsCatalogZone(zone):
          instances.append(Zone(parentNode, zone))
    return instances

  def MayHaveChildren(self):
    if self.zones:
      return True
    return self.GetServer().GetSubzones(self) != None

  def Updater(self):
    return self.GetConnection().Updater(self.zonename)
          
  def AddZone(self, zone):
    self.zones.append(zone.name)
    self.appendChild(zone)
    self.writeZones(self.zones)

  def RemoveZone(self, zone):
    self.zones.remove(zone.name)
    self.writeZones(self.zones)

  def GetProperties(self):
    if not self.properties:
      self.properties=[ (xlt("Zone not available"))]
      
      # if a first attempt to read the zone failed we don't try again
      self.zone=self.GetServer().GetZone(self.zonename)

      if self.zone:
        self.hosts4={}
        self.hosts6={}
        self.cnames={}
        self.ptrs={}
        self.others={}
        
        for name, rds in self.zone.iterate_rdatasets():
          name=name.to_text()
          if rds.rdtype == rdatatype.A:
            self.hosts4[name] = rds
          elif rds.rdtype == rdatatype.AAAA:
            self.hosts6[name] = rds
          elif rds.rdtype == rdatatype.CNAME:
            self.cnames[name] = rds
          elif rds.rdtype == rdatatype.PTR:
            self.ptrs[name] = rds
          else:
            if name == "@":
              if rds.rdtype == rdatatype.NS:
                self.ns=rds
              elif rds.rdtype == rdatatype.MX:
                self.mx=rds
              elif rds.rdtype == rdatatype.SOA:
                self.soa=rds
            if not self.others.get(name):
              self.others[name]={rds.rdtype: rds}
            else:
              self.others[name][rds.rdtype] = rds

        s0=self.soa[0]
        self.properties=[
              ( xlt(self.shortname),  self.name),
              ( xlt("Serial"),        s0.serial),
              (     "TTL",            floatToTime(self.soa.ttl, -1)),
              ( "Master Name Server", s0.mname),
              ( "Admin Mail",         s0.rname),
              ( xlt("Retry"),         floatToTime(s0.retry, -1)),
              ( "Expire",             floatToTime(s0.expire, -1)),
              ( "Refresh",            floatToTime(s0.refresh, -1)),
              ( "Default TTL",        floatToTime(s0.minimum, -1)),
              ]
        
        if self.ns:
          self.AddChildrenProperty(map(lambda x: str(x.target), self.ns), "NS Records", -1)
        if self.mx:
          self.AddChildrenProperty(map(lambda x: "%(mx)s  (priority %(prio)d)" % {'prio': x.preference, 'mx': str(x.exchange) }, self.mx), "MX Records", -1)
        if isinstance(self, RevZone):
          self.AddProperty(xlt("PTR record count"), len(self.ptrs), -1)
        elif isinstance(self, CatalogZone):
          self.AddProperty(xlt("Member zone record count"), len(self.ptrs), -1)
        else:
          self.AddProperty(xlt("Host record count"), len(self.hosts4)+len(self.hosts6), -1)
          self.AddProperty(xlt("CNAME record count"), len(self.cnames), -1)
        cnt=0
        for lst in self.others.items():
          cnt += len(lst)
        self.AddProperty(xlt("Other record count"), cnt, -1)
    return self.properties

   
  def readZones(self):
    return adm.config.Read("Zones/%s" % self.GetServer().name, [], self, self.name)

  def writeZones(self, val):
    return adm.config.Write("Zones/%s" % self.GetServer().name, val, self, self.name)



class CatalogZone(Zone):
  typename=xlt("Catalog DNS Zone")
  shortname=xlt("Catalog Zone")

  def __init__(self, parentNode, name):
    super().__init__(parentNode, name)
    self.partialZonename=self.zonename

  @staticmethod 
  def GetInstances(parentNode):
    instances=[]
      
    if parentNode.zones:
      for zone in parentNode.zones:
        instances.append(Zone(parentNode, zone))
    else:
      for zone in parentNode.GetServer().GetSubzones(parentNode):
        if parentNode.GetServer().IsCatalogZone(zone):
          instances.append(CatalogZone(parentNode, zone))
    return instances


class RevZone(Zone):
  typename=xlt("Reverse DNS Zone")
  shortname=xlt("Reverse Zone")

  def __init__(self, parentNode, name):
    super(RevZone, self).__init__(parentNode, name)
    self.revzones=self.zones
    zp=self.zonename.split('.')
    if zp[0].find('-') >0:
      del zp[0]
      self.partialZonename='.'.join(zp)
    else:
      self.partialZonename=self.zonename
 
    
  @staticmethod 
  def GetInstances(parentNode):
    instances=[]
    if parentNode.revzones:
      for zone in parentNode.revzones:
        instances.append(RevZone(parentNode, zone))
    else:
      for zone in parentNode.GetServer().GetSubzones(parentNode, True):
        instances.append(RevZone(parentNode, zone))
    return instances


#=======================================================================
# Menus
#=======================================================================

class UnregisterZone:
  name="Unregister Zone"
  help="Unregister DNS zone"
  @staticmethod
  def CheckAvailableOn(node):
    if not isinstance(node, (Zone, RevZone)):
      return False
    return node.GetServer().stats == None

  @staticmethod
  def OnExecute(_parentWin, node):
    node.parentNode.RemoveZone(node)
    node.parentNode.Refresh()
    return True
  

class RegisterZone:
  name="Register Zone"
  help="Register DNS zone"

  @staticmethod
  def CheckAvailableOn(node):
    if isinstance(node, RevZone):
      return False
    if not isinstance(node, (Zone, Server)):
      return False
    return node.GetServer().stats == None

  @staticmethod
  def OnExecute(parentWin, parentNode):
    if isinstance(parentNode, Zone):
      txt=xlt("Sub zone name")
    else:
      txt=xlt("Zone name")
    dlg=wx.TextEntryDialog(parentWin, txt, xlt("Register new Zone"))
    if dlg.ShowModal() == wx.ID_OK:
      dlg.Hide()
      parentWin.SetFocus()
      if isinstance(parentNode, Zone):
        name="%s.%s" % (dlg.GetValue(), parentNode.name)
      else:
        name=dlg.GetValue()
      zone=Zone(parentNode, name)
      if zone.name not in parentNode.zones:
        parentNode.AddZone(zone)
        return True
    return False
    
class RegisterRevZone:
  name="Register Reverse Zone"
  help="Register Reverse DNS zone"

  @staticmethod
  def CheckAvailableOn(node):
    if not isinstance(node, (RevZone, Server)):
      return False
    return node.GetServer().stats == None

  @staticmethod
  def OnExecute(parentWin, parentNode):
    if isinstance(parentNode, Zone):
      txt=xlt("Reverse subzone")
    else:
      txt=xlt("Reverse Zone IP Network")
    dlg=wx.TextEntryDialog(parentWin, txt, xlt("Register new Reverse Zone"))
    if dlg.ShowModal() == wx.ID_OK:
      dlg.Hide()
      parentWin.SetFocus()
      if isinstance(parentNode, Zone):
        name="%s.%s" % (dlg.GetValue(), parentNode.name)
      else:
        name=dlg.GetValue()
        v6parts=name.split(':')
        
        if len(v6parts) > 1: #ipv6
          if name.count('::'):
            raise Exception("IPV6 network may not contain ::")

          mask=0
          strip=64-len(v6parts)*8
          v6end=v6parts[-1]
          if v6end != '':
            masks=name.split('/')
            if len(masks) > 0: # has mask
              name=masks[0] + ":"
              mask=int(masks[1])
              if mask % 4:
                raise Exception("mask must be multiple of 4")
              strip=64-mask/2
            else:
              name += ":"
          else:
            strip += 8
        else:
          dc=name.count('.')
          strip=0
          while dc < 3:
            strip += 2
            dc += 1
            name += ".0"
        name=DnsRevName(name).to_text(True)
        name = name[strip:]
      
      zone=RevZone(parentNode, name)
      if zone.name not in parentNode.revzones:
        parentNode.AddZone(zone)
        return True
    return False

class IncrementSerial:
  name=("Increment Serial")
  help=xlt("Increment SOA serial number")
  
  @staticmethod
  def OnExecute(_parentWin, node):
    now=time.localtime(time.time())
    serial=((now.tm_year*100 + now.tm_mon)*100 + now.tm_mday) *100
    s0= node.soa[0]
    if serial < s0.serial:
      serial=s0.serial+1
    
    soa=Rdataset(node.soa.ttl, s0.rdclass, s0.rdtype, s0.mname, s0.rname, serial,
               s0.refresh, s0.retry, s0.expire, s0.minimum )
    
    updater=node.Updater()
    updater.replace("@", soa)

    msg=node.GetServer().Send(updater)
    if msg.rcode() == rcode.NOERROR:
      wx.MessageBox(xlt("Incremented SOA serial number to %d") % serial, xlt("New SOA serial number"))
    else:
      admSetError("update", msg)
      
    return True
  
  
class CleanDanglingPtr:
  name=xlt("Clean dangling PTR")
  help=xlt("Clean dangling PTR records that have non-existent targets")
    
  @staticmethod
  def OnExecute(parentWin, node):
    candidates=[]

    for name in node.ptrs.keys():
      try:
        address=DnsRevAddress(DnsAbsName(name, node.partialZonename))
        target=node.ptrs[name][0].target
        if checkIpAddress(address) == 4:
          rdtype=rdatatype.A
        else:
          rdtype=rdatatype.AAAA
        rrset=node.GetConnection().Query(target, rdtype)
        if not rrset:
          candidates.append("%s %s" % (name, target))
      except:
        candidates.append("%s <invalid>" % name)

    if candidates:
      dlg=wx.MultiChoiceDialog(parentWin, xlt("Select PTR records to clean"), xlt("%d dangling PTR records") % len(candidates), candidates)
      cleaned=[]
      if not dlg.ShowModal() == wx.ID_OK:
        return False
      for i in dlg.GetSelections():
        name=candidates[i].split(' ')[0]
        cleaned.append(name)
        updater=node.Updater()
        updater.delete(DnsAbsName(name, node.partialZonename), rdatatype.PTR)
        msg=node.GetServer().Send(updater)
        if msg.rcode() == rcode.NOERROR:
          del node.ptrs[name]
        else:
          parentWin.SetStatus(xlt("DNS update failed: %s") % rcode.to_text(msg.rcode()))
          return False
        parentWin.SetStatus(xlt("%d dangling PTR records cleaned (%s)") % (len(cleaned), ", ".join(cleaned)))
      return True
    
    else:
      wx.MessageBox(xlt("No dangling PTR record."), xlt("DNS Cleanup"))
    

    return True

#=======================================================================
#  Page Menus
#=======================================================================


class PageEditRecord:
  name=("Edit")
  help=xlt("Edit Record")
  
  @staticmethod
  def CheckEnabled(page):
    ids=page.control.GetSelection()
    return len(ids) == 1

  @staticmethod
  def CheckAvailableOn(page):
    sel=page.control.GetSelection()
    if not sel: return False
    rdtype=page.GetDataType(sel[0])
    typestr=rdatatype.to_text(rdtype)
    return typestr in otherTypes
   
  @staticmethod
  def OnExecute(parentWin, page):
    idx=page.control.GetSelection()[0]
    while idx >= 0:
      name=page.GetName(idx)
      if name:
        break
      idx -= 1
    rdtype=page.GetDataType(idx)
    typestr=rdatatype.to_text(rdtype)
    if typestr not in otherTypes and typestr not in individualTypes:
      return
    dlg=page.EditDialog(rdtype)(parentWin, page.lastNode, name, rdtype)
    dlg.page=page
    if dlg.GoModal():
      page.Display(None, False)
   
 
class PageNewRecord:
  name=xlt("New")
  help=xlt("New Record")
  
  @staticmethod
  def OnExecute(parentWin, page):
    rdtype=rdatatype.from_text(page.GetDnsType())
    dlg=page.EditDialog(None)(parentWin, page.lastNode, "", rdtype)
    dlg.page=page
    if dlg.GoModal():
      page.Display(None, False)
   

class PageNewAskRecord:
  name=xlt("New")
  help=xlt("New Record")
  
  
  
  @staticmethod
  def OnExecute(parentWin, page):
    rdtype=None
    rtypes=[]
    for ptype in otherTypes:
      rtypes.append("%s - %s" % (ptype, DnsSupportedTypes[ptype]))
#    for ptype in sorted(DnsSupportedTypes.keys()):
#      if ptype not in prioTypes and ptype not in individualTypes:
#        rtypes.append("%s - %s" % (ptype, DnsSupportedTypes[ptype]))
      
    dlg=wx.SingleChoiceDialog(parentWin, xlt("record type"), "Select record type", rtypes)
    if dlg.ShowModal() == wx.ID_OK:
      rdtype=rdatatype.from_text(dlg.GetStringSelection().split(' ')[0])
      dlg=page.EditDialog(rdtype)(parentWin, page.lastNode, "", rdtype)
      dlg.page=page
      if dlg.GoModal():
        page.Display(None, False)

class PageDeleteRecord:
  name=("Delete")
  help=xlt("Delete Records")
  
  @staticmethod
  def CheckEnabled(page):
    ids=page.control.GetSelection()
    return len(ids) >0
    
  @staticmethod
  def CheckAvailableOn(page):
    sel=page.control.GetSelection()
    if not sel: return False
    rdtype=page.GetDataType(sel[0])
    typestr=rdatatype.to_text(rdtype)
    return typestr not in noDeleteTypes
  
  @staticmethod
  def OnExecute(parentWin, page):
    ids=page.control.GetSelection()
    names=[]
    types=[]
    for idx in ids:
      while idx >= 0:
        name=page.control.GetItemText(idx, 0)
        if name:
          types.append(page.control.GetItemText(idx, 1))
          break
        idx -= 1
      names.append(name)

    if len(names) > 3:
      msg=xlt("Delete multiple %s records?") % page.GetDnsType()
    else:
      msg=xlt("Delete %s?") % ", ".join(map(lambda x:'"%s"'%x , names))
    if adm.ConfirmDelete(msg, xlt("Deleting Records")):
      if page.Delete(parentWin, names, types):
        page.Display(None, False)


class PageDeleteHostRecord:
  name=("Delete")
  help=xlt("Delete Records")
  
  @staticmethod
  def CheckEnabled(page):
    ids=page.control.GetSelection()
    return len(ids) >0
    
  @staticmethod
  def OnExecute(parentWin, page):
    ids=page.control.GetSelection()
    names=[]
    for idx in ids:
      while idx >= 0:
        name=page.control.GetItemText(idx, 0)
        if name:
          break
        idx -= 1
      names.append(name)
    names=list(set(names))
    if len(names) > 3:
      msg=xlt("Delete multiple %s records?") % page.GetDnsType()
    else:
      msg=xlt("Delete %s?") % ", ".join(map(lambda x:'"%s"'%x , names))
    if adm.ConfirmDelete(msg, xlt("Deleting %s Records" % page.GetDnsType())):
      if page.Delete(parentWin, names, None):
        page.Display(None, False)


#=======================================================================
# Dialogs
#=======================================================================

class Record(adm.CheckedDialog):
  def Check(self):
    ok=True
    if not self.rds:
      ok=self.CheckValid(ok, self.Recordname, xlt(("Enter %s") % self.RecordNameStatic))
      if self.rdtype == rdatatype.CNAME:
        foundSame=self.node.cnames.get(self.Recordname)
        foundOther=self.node.hosts4.get(self.Recordname)
        if not foundOther:
          foundOther=self.node.hosts6.get(self.Recordname)
        if not foundOther:
          foundOther=self.node.ptrs.get(self.Recordname)
        if not foundOther:
          # SIG, NXT and KEY allowed
          foundOther=self.node.others.get(self.Recordname)
            
        ok=self.CheckValid(ok, not foundOther, xlt("CNAME collides with existing record"))
      else:
        foundSame=self.node.others.get(self.Recordname, {}).get(self.rdtype)
        # SIG, NXT and KEY: CNAME allowed
        foundCname=self.node.cnames.get(self.Recordname)
        ok=self.CheckValid(ok, not foundCname, xlt("Record collides with existing CNAME record"))
      ok=self.CheckValid(ok, not foundSame, xlt("Record of same type and name already exists"))
        
    ok=self.CheckValid(ok, timeToFloat(self.TTL), xlt("Please enter valid TTL value"))
    return ok

  def SetError(self, msg):
    self.node.GetServer().lastError=xlt("DNS update failed: %s") % rcode.to_text(msg.rcode())
  
  
class SingleValRecords(Record):
  def __init__(self, wnd, node, name="", rdtype=None):
    adm.CheckedDialog.__init__(self, wnd, node)
    self.rdtype=rdtype
    self.RecordName=name
    self.Bind("Recordname Value TTL")
    
  def _getRds(self):
    ttl=86400
    self.rds=None
    self['Recordname'].Disable()
    rdsl=self.page.GetRdata(self.RecordName, self.rdtype)
    if isinstance(rdsl, list):
      for rds in rdsl:
        if rds.rdtype == self.rdtype:
          self.rds=rds
          ttl=rds.ttl
          break
    else:
      rds=self.rds=rdsl
      ttl=rds.ttl
    if not self.rds:
      rds=None
      logger.debug("rds not found")
    return ttl, rds

  def _save(self, rds):
    updater=self.node.Updater()
    if self.RecordName!=self.RecordName.lower():
      self.RecordName =self.RecordName.lower()
    if self.rdtype == rdatatype.SOA:
      updater.replace(self.RecordName, rds)
    elif self.rdtype == rdatatype.NS:
      targets=[]
      for rd in self.rds:
        targets.append(rd.target)
      for rd in rds:
        if rd.target in targets:
          targets.remove(rd.target)
      for target in targets:
        updater.delete(self.RecordName, self.rdtype, target.to_text())
      updater.add(self.RecordName, rds)
    else:
      if self.rds:
        updater.delete(self.Recordname, self.rdtype)
      updater.add(self.Recordname, rds)
    msg=self.node.GetServer().Send(updater)
    if msg.rcode() == rcode.NOERROR:
      self.page.SetRdata(self.RecordName, self.rdtype, rds)
      self.page.Display(None, False)
      return True
    else:
      self.SetError(msg)
      return False

  def Check(self):
    ok=Record.Check(self)
    ok=self.CheckValid(ok, self.Value.strip(), xlt(("Enter %s") % self.ValueStatic))
    return ok

  
  def Go(self):
    if self.RecordName:
      ttl, rds=self._getRds()
      vlist=[]
      for rd in self.rds:
        value=eval("rd.%s" % rd.__slots__[0])
        if isinstance(value, (list, tuple)):
          value=" ".join(map(quoteIfNeeded, value))
        vlist.append(str(value))
      self.value="\n".join(vlist)
    else:
      ttl=86400
      rds=Rdataset(ttl, rdataclass.IN, self.rdtype)
      self.rds=None
    
    typestr=rdatatype.to_text(self.rdtype)
    self.SetTitle(DnsSupportedTypes[typestr])
    self.RecordNameStatic = typestr
    self.ValueStatic=rds[0].__slots__[0].capitalize()
    self.dataclass=type(eval("rds[0].%s" % rds[0].__slots__[0]))
    if self.dataclass == int:
      validatorClass=Validator.Get("uint")
      if validatorClass:
        self['Value'].validator=validatorClass(self['Value'], "uint")
    elif self.dataclass == Name:
      self.dataclass = DnsName
    self.TTL=floatToTime(ttl, -1)


  def Save(self):
    ttl=int(timeToFloat(self.TTL))
    rds=None
    for value in self.Value.splitlines():
      value=value.strip()
      if not value:
        continue
      if self.dataclass in (list, tuple):
        value=removeSmartQuote(value)
        data=shlexSplit(value, ' ')
      else:
        logger.error("Save: dataclass not handled: %s", type(self.dataclass))
        data=self.dataclass(value)
      if not rds:
        rds=Rdataset(ttl, rdataclass.IN, self.rdtype, data)
      else:
        rds.add(Rdata(rds, data), ttl)
    return self._save(rds)
   
    
class SingleValRecord(SingleValRecords):
  pass



class MultiValRecords(SingleValRecords):
  def __init__(self, wnd, node, name="", rdtype=None):
    adm.CheckedDialog.__init__(self, wnd, node)
    self.rdtype=rdtype
    self.RecordName=name
    self.Bind("Recordname TTL")

  def AddExtraControls(self, res):
    self.grid=wxGrid.Grid(self)
    res.AttachUnknownControl("ValueGrid", self.grid)
    self.grid.Bind(wxGrid.EVT_GRID_CELL_CHANGED, self.OnCellChange)
    self.grid.Bind(wxGrid.EVT_GRID_EDITOR_SHOWN, self.OnEditorShown)
    self.grid.Bind(wxGrid.EVT_GRID_CELL_RIGHT_CLICK, self.OnRightClick)


  def OnEditorShown(self, evt):
    self.changed=True
    self.OnCheck(evt)
    
  def GetChanged(self):
    return self.changed
  
  def OnCellChange(self, evt):
    if evt.GetRow() == self.grid.GetNumberRows()-1:
      self.grid.AppendRows(1)
    self.changed=True
    return self.OnCheck(evt)

  
  def OnRightClick(self, evt):
    self.cmRow=evt.GetRow()
    cm=Menu(self)
    cm.Add(self.OnDelete, xlt("Delete"), xlt("Delete line"))
    cm.Popup(evt)
    
    
  def OnDelete(self, evt):
    self.changed=True
    self.grid.DeleteRows(self.cmRow, 1)
    self.OnCheck(evt)
  
  
  def Go(self):
    if self.RecordName:
      ttl, rds=self._getRds()
      self.changed=False
    else:
      ttl=86400
      self.rds=None
      self.changed=True
      cls=RdataClass(rdataclass.IN, self.rdtype)
      rds=Rdataset(ttl, rdataclass.IN, self.rdtype)

    typestr=rdatatype.to_text(self.rdtype)
    self.SetTitle(DnsSupportedTypes[typestr])
    self.RecordNameStatic = typestr

    self.slots=rds[0].__slots__
    self.slotValSamples=[]
    for slot in self.slots:
      self.slotValSamples.append(eval("rds[0].%s" % slot))

      
    self.grid.CreateGrid(1, len(self.slots))
    self.grid.SetRowLabelSize(0)
    for col in range(len(self.slots)):
      self.grid.SetColLabelValue(col, self.slots[col].capitalize())
      self.grid.AutoSizeColLabelSize(col)
      colsize=self.grid.GetColSize(col)
      if isinstance(self.slotValSamples[col], int):
        minwidth, _h=self.grid.GetTextExtent("99999")
        self.grid.SetColFormatNumber(col)
      else:
        minwidth, _h=self.grid.GetTextExtent("TheSampleTarget.admin.org")
      MARGIN=8
      minwidth += MARGIN
      if colsize < minwidth:
        self.grid.SetColSize(col, minwidth)
    
    if self.RecordName:
      row=0
      for _rd in self.rds:
        for col in range(len(self.slots)):
          val=eval("_rd.%s" % self.slots[col])
          if isinstance(val, list):
            val=" ".join(val)
          elif isinstance(val, DnsEnum.IntEnum):
            val=int(val)
          elif isinstance(val, bytes):
            try:
              val=val.decode()
            except:
              val=xlt("binary data")
              
          self.grid.SetCellValue(row, col, str(val))
        self.grid.AppendRows(1)
        row += 1
    self.TTL=floatToTime(ttl, -1)

    self.Show()
    self.grid.AutoSizeColumns()

    maxwidth, _h=self.grid.GetSize()
    width=0
    cn=self.grid.GetNumberCols()-1
    for col in range(cn+1):
      width += self.grid.GetColSize(col)
    if width < maxwidth:
      self.grid.SetColSize(cn, self.grid.GetColSize(cn) + maxwidth-width)



  def Check(self):
    ok=Record.Check(self)
    ok=self.CheckValid(ok, self.grid.GetNumberRows()>1, xlt("At least one record line required"))
    for row in range(self.grid.GetNumberRows()-1):
      if not ok:
        break
      vals=[]
      for col in range(self.grid.GetNumberCols()):
        val=self.grid.GetCellValue(row, col).strip()
        if val:
          vals.append(val)
      ok=self.CheckValid(ok, len(vals) == len(self.slotValSamples), xlt("Enter all values in row %d" % (row+1)))
    return ok
  
  
  def Save(self):
    self.grid.SaveEditControlValue()
    ttl=int(timeToFloat(self.TTL))
    rds=None
    
    for row in range(self.grid.GetNumberRows()-1):
      vals=[]
      for col in range(self.grid.GetNumberCols()):
        val=self.grid.GetCellValue(row, col).strip()
        sv=self.slotValSamples[col]

        if isinstance(sv, Name):
          vals.append(DnsName(val))
        elif isinstance(sv, list):
          vals.append(val.split(' '))
        elif isinstance(sv, DnsEnum.IntEnum):
          vals.append(int(val))
        elif isinstance(sv, bytes):
          try:
            val=val.encode()
          except:
            val=xlt("binary data")
          vals.append(val)
        else:
          coltype=type(sv)
          vals.append(coltype(val))
        
      if not rds:
        rds=Rdataset(ttl, rdataclass.IN, self.rdtype, *tuple(vals))
      else:
        rds.add(Rdata(rds, *tuple(vals)), ttl)

    return self._save(rds)


class HostRecord(adm.CheckedDialog):
  def __init__(self, wnd, node, name="", _unused=None):
    adm.CheckedDialog.__init__(self, wnd, node)
    self.Hostname=name
    self.Bind("Hostname IpAddress TTL TTL6 CreatePtr")
  
  def Go(self):
    ttl=86400
    ttl6=None
    if self.Hostname:
      self.isNew=False
      hasPtr=False
      query=self.node.GetConnection().Query
      name="%s.%s." % (self.Hostname, self.node.zonename)

      h4=self.node.hosts4.get(self.Hostname)
      h6=self.node.hosts6.get(self.Hostname)
      adr=[]
      
      def _handleAddress(h, hasPtr):
          adr.append(h.address)
          if not hasPtr:
            rrs=query(DnsRevName(h.address), rdatatype.PTR)
            if rrs and rrs[0].target.to_text() == name:
              return True
          return hasPtr

      if h4:
        ttl=h4.ttl
        for h in h4:
          hasPtr=_handleAddress(h, hasPtr)
      if h6:
        ttl6=h6.ttl
        for h in h6:
          hasPtr=_handleAddress(h, hasPtr)

      self['Hostname'].Disable()
      self.IpAddress="\n".join(adr)
      self.CreatePtr=hasPtr
    else:
      self.isNew=True
    self.TTL=floatToTime(ttl, -1)
    if ttl6 and ttl6 != ttl:
      self.TTL6=floatToTime(ttl6, -1)
    self.SetUnchanged()
  
  def Save(self):
    ttl4=int(timeToFloat(self.TTL))
    if self.ttl6:
      ttl6=int(timeToFloat(self.TTL6))
    else:
      ttl6=ttl4
    name=str(self.Hostname)
    updater=self.node.Updater()
    if self.node.hosts4.get(name):
      updater.delete(name, "A")
    if self.node.hosts4.get(name):
      updater.delete(name, "AAAA")
    
    h4=None
    h6=None
    addresses=self.IpAddress.splitlines()
    for address in addresses:
      address=address.strip()
      if not address:
        continue
      if checkIpAddress(address) == 4:
        if h4:
          h4.add(Rdata(h4, address), ttl4)
        else:
          h4=Rdataset(ttl4, rdataclass.IN, rdatatype.A, address)
      else:
        if h6:
          h6.add(Rdata(h6, address), ttl6)
        else:
          h6=Rdataset(ttl6, rdataclass.IN, rdatatype.AAAA, address)
      
    if h4:
      updater.add(name, h4)
    if h6:
      updater.add(name, h6)
    msg=self.node.GetServer().Send(updater)
    if msg.rcode() != rcode.NOERROR:
      self.SetError(msg)
      return False
    self.node.hosts4[name] = h4
    self.node.hosts6[name] = h6
    
    
    prevAddresses=self['IpAddress'].unchangedValue.splitlines()
    if self.CreatePtr:
      dnsName=DnsAbsName(name, self.node.zonename)

      for address in addresses:
        if address in prevAddresses:
          prevAddresses.remove(address)
        ptr, zone=self.node.GetServer().GetZoneName(DnsRevName(address))
        if zone:
          if zone.endswith("in-addr.arpa"):
            ttl=ttl4
          else:
            ttl=ttl6
          updater=self.node.GetConnection().Updater(zone)
          updater.delete(ptr, 'PTR')
          
          updater.add(ptr, Rdataset(ttl, rdataclass.IN, rdatatype.PTR, dnsName))
          _msg=self.node.GetServer().Send(updater)
      for address in prevAddresses:
        ptr, zone=self.node.GetServer().GetZoneName(DnsRevName(address))
        if zone:
          updater=self.node.GetConnection().Updater(zone)
          updater.delete(ptr, 'PTR')
          _msg=self.node.GetServer().Send(updater)
    return True
  
  def Check(self):
    ok=True
    ips=self.IpAddress.splitlines()
    ok=self.CheckValid(ok, self.Hostname, xlt("Please enter Host Name"))
    if self.isNew:
      ok=self.CheckValid(ok, not self.node.hosts4.get(self.Hostname) and not self.node.hosts6.get(self.Hostname), xlt("Host record already exists"))
    ok=self.CheckValid(ok, len(ips)>0, xlt("Please enter IP Address"))
    for ip in ips:
      ip=ip.strip()
      ok = self.CheckValid(ok, checkIpAddress(ip), xlt("Please enter valid IP Address"))
    ok=self.CheckValid(ok, timeToFloat(self.ttl), xlt("Please enter valid TTL"))
    if self.ttl6:
      ok=self.CheckValid(ok, timeToFloat(self.ttl6), xlt("Please enter valid AAAA TTL"))
    return ok


#=======================================================================
# Pages
#=======================================================================
  
def filledIp(ip):
  if ip.count(':'):
    parts=[]
    xp=ip.split(':')
    for n in xp:
      if n:
        parts.append(("0000"+n)[-4:])
      else:
        for _i in range(8-len(xp)):
          parts.append("0000")
    return ":".join(parts)
  else:
    n=ip.split('.')
    try:
      return "%02x.%02x.%02x.%02x" % (int(n[0]), int(n[1]), int(n[2]), int(n[3]))
    except:
      return ip


class zonePage(adm.NotebookPage):
  menus=[PageNewRecord, PageEditRecord, PageDeleteRecord]

  @staticmethod
  def EditDialog(rdtype):
    cls=RdataClass(rdataclass.IN, rdtype)
    if len(cls.__slots__) > 1:
      return MultiValRecords
    else:
      return SingleValRecords

  def prepare(self, node):
    if node:
      self.lastNode=node
    else:
      node=self.lastNode
    node.GetProperties()
    self.control.ClearAll()
    if not node.zone:
      self.control.AddColumn("", -1)
      self.control.AppendItem(0, xlt("Zone not available"))
      return False
    return True 
  
  def storeLastItem(self):
    self.lastHost=self.control.GetFocusText()
 
  def restoreLastItem(self):
    self.control.SetSelectFocus(self.lastHost)
    
  def OnItemDoubleClick(self, _evt):
    PageEditRecord.OnExecute(self.control, self)

  def GetName(self, idx):
    return self.control.GetItemText(idx, 0)

  def GetRdata(self, name, rdtype):
    return self.lastNode.others.get(name, {}).get(rdtype)
  
  def SetRdata(self, name, rdtype, data):
    rds=self.lastNode.others.get(name, {})
    if rds:
      rds[rdtype] = data
    else:
      self.lastNode.others[name] = {rdtype: data}

  def Delete(self, _parentWin, names, types):
    node=self.lastNode
    updater=node.Updater()
    for i in range(len(names)):
      if isinstance(types, list):
        rtype=types[i]
      else:
        rtype=types
      updater.delete(names[i], rtype)

    msg=node.GetServer().Send(updater)
    if msg.rcode() != rcode.NOERROR:
      admSetError("delete", msg)

      return False
    for i in range(len(names)):
      name=names[i]
      if isinstance(types, list):
        rdtype=rdatatype.from_text(types[i])
        rdsl=node.others.get(name)
        if rdsl:
          if rdtype in rdsl:
            del rdsl[rdtype]
      elif types == rdatatype.CNAME:
        if node.cnames.get(name):
          del node.cnames[name]
      elif types == rdatatype.PTR:  
        if node.ptrs.get(name):
          del node.ptrs[name]
    return True
    

class HostsPage(zonePage):
  name=xlt("A/AAAA Hosts")
  order=1
  sorting=0
  menus=[PageNewRecord, PageEditRecord, PageDeleteHostRecord]

  @staticmethod
  def EditDialog(_rdtype):
    return HostRecord

  def GetDnsType(self):
    return xlt("A")
  
  def GetDataType(self, idx):
    ip=self.control.GetItemText(idx, 1)
    if checkIpAddress(ip) == 4:
      return rdatatype.A
    else:
      return rdatatype.AAAA
  
  def Delete(self, _parentWin, names, _types):
    node=self.lastNode
    updater=node.Updater()
    for name in names:
      if name in node.hosts4:
        updater.delete(name, "A")
      if name in node.hosts6:
        updater.delete(name, "AAAA")
    msg=node.GetServer().Send(updater)
    if msg.rcode() != rcode.NOERROR:
      admSetError("delete", msg)
      return False
    for name in names:
      if name in node.hosts4:
        del node.hosts4[name]
      if name in node.hosts6:
        del node.hosts6[name]
    return True


  def SortedByHost(self):
    hostnames=list(self.lastNode.hosts4.keys())
    hostnames.extend(list(self.lastNode.hosts6.keys()))
    hostnames = sorted(set(hostnames), key=lambda n: n.lower())

    self.control.DeleteAllItems()
    for name in hostnames:
      h4=self.lastNode.hosts4.get(name)
      h6=self.lastNode.hosts6.get(name)
      
      icon=self.lastNode.GetImageId('host')

      if h4:
        ttl=floatToTime(h4.ttl, -1)
        for h in h4:
          self.control.AppendItem(icon, [name, h.address, ttl])
          icon=0
          name=""
          ttl=""
      if h6:
        ttl=floatToTime(h6.ttl, -1)
        for h in h6:
          self.control.AppendItem(icon, [name, h.address, ttl])
          icon=0
          name=""
          ttl=""

  def SortedByIp(self):
    ip4=[]
    for name, h4 in self.lastNode.hosts4.items():
      if h4:
        for h in h4:
          ip4.append( (name, h.address, h4.ttl, 4))
    ip6=[]
    for name, h6 in self.lastNode.hosts6.items():
      if h6:
        for h in h6:
          ip6.append( (name, h.address, h6.ttl, 6))

                        
    ips =      sorted(ip4, key=lambda x: filledIp(x[1]))
    ips.extend(sorted(ip6, key=lambda x: filledIp(x[1])))

    self.control.DeleteAllItems()

    last=None    
    for name, addr, ttl, protocol in ips:
      icon=self.lastNode.GetImageId('ipaddr%d' % protocol)
      if last==name:
        self.control.AppendItem(0, ["", addr, ""])
      else:
        last=name
        self.control.AppendItem(icon, [name, addr, floatToTime(ttl, -1)])
      

  def Display(self, node, _detached=False):
    if not node or node != self.lastNode:
      self.storeLastItem()
      
      if not self.prepare(node):
        return
      
      self.control.AddColumn(xlt("Name"), 30)
      self.control.AddColumn(xlt("Address"), 20)
      self.control.AddColumn(xlt("TTL"), 10)
      self.RestoreListcols()

    self.OnColClick()
    self.restoreLastItem()
        

  def OnColClick(self, evt=None):
    if not self.lastNode.soa:
      return
    
    if evt:
      col=evt.GetColumn()
      if col in [0,1]:
        if col != self.sorting:
          HostsPage.sorting=col
        else:
          return
      else:
        return
        
    if self.sorting:
      self.SortedByIp()
    else:
      self.SortedByHost()


  
class CNAMEsPage(zonePage):
  name=xlt("CNAMEs")
  order=2
  
  @staticmethod
  def EditDialog(_rdtype):
    return SingleValRecord

  def GetDnsType(self):
    return "CNAME"
  def GetDataType(self, _unused):
    return rdatatype.CNAME

  def GetRdata(self, name, _rdtype):
    return self.lastNode.cnames.get(name)

  def SetRdata(self, name, _rdtype, data):
    self.lastNode.cnames[name] = data
    
  def Delete(self, parentWin, names, _types):
    return zonePage.Delete(self, parentWin, names, rdatatype.CNAME)
  
  def Display(self, node, _detached):
    if not node or node != self.lastNode:
      if not self.prepare(node):
        return

      self.storeLastItem()
            
      node=self.lastNode
      self.control.AddColumn(xlt("Name"), 30)
      self.control.AddColumn(xlt("Target"), 30)
      self.control.AddColumn(xlt("TTL"), 5)
      self.RestoreListcols()

      icon=node.GetImageId('cname')
      for cname in sorted(node.cnames.keys()):
        rds=node.cnames[cname]
        self.control.AppendItem(icon, [cname, rds[0].target, floatToTime(rds.ttl, -1)])

      self.restoreLastItem()


  
class _PTRsPage(zonePage):
  order=1
  menus=[PageNewRecord, PageEditRecord, PageDeleteRecord]

  @staticmethod
  def EditDialog(_rdtype):
    return SingleValRecord

  def GetDnsType(self):
    return "PTR"
  def GetDataType(self, _unused):
    return rdatatype.PTR
  
  
  def GetRdata(self, name, _rdtype):
    return self.lastNode.ptrs.get(name)
  
  def SetRdata(self, name, _rdtype, data):
    self.lastNode.ptrs[name] = data

  
class PTRsRevPage(_PTRsPage):
  name=xlt("PTR")
  def GetName(self, idx):
    name=self.control.GetItemText(idx, 0)
    addr=(DnsRevName(name).to_text())[:-len(self.lastNode.partialZonename)-2]
    return addr

  def Delete(self, parentWin, names, _types):
    ptrs=[]
    for n in names:
      try:
        ptrs.append(str(DnsRevName(n))[:-2-len(self.lastNode.partialZonename)])
      except:
        ptrs.append(n.split(' ')[0])
    
    return zonePage.Delete(self, parentWin, ptrs, rdatatype.PTR)
  
  def Display(self, node, _detached):
    if not node or node != self.lastNode:
      self.storeLastItem()
      
      if not self.prepare(node):
        return
      node=self.lastNode
      self.control.AddColumn(xlt("Address"), 10)
      self.control.AddColumn(xlt("Target"), 30)
      self.control.AddColumn(xlt("TTL"), 5)
      self.RestoreListcols()

      icon=node.GetImageId('ptr')
      
      ips=[]
      for ptr in node.ptrs.keys():
        rds=node.ptrs[ptr]
        name=DnsAbsName(ptr, node.partialZonename)
        try:
          adr=DnsRevAddress(name)
        except:
          adr="%s <invalid>" % ptr
        ips.append( (adr, rds[0].target, floatToTime(rds.ttl, -1)))

      for ip in sorted(ips, key=(lambda x: filledIp(x[0]))):
        self.control.AppendItem(icon, list(ip))
      
      self.restoreLastItem()


class OTHERsPage(zonePage):
  name=xlt("Others")
  order=5
  menus=[PageNewAskRecord, PageEditRecord, PageDeleteRecord]

  def GetDnsType(self):
    return "WHAT"
  
  def GetDataType(self, idx):
    while idx >= 0:
      txt=self.control.GetItemText(idx, 1)
      if txt:
        return rdatatype.from_text(txt)
      idx -= 1
    return None

  
  def Display(self, node, _detached):
    if not node or node != self.lastNode:
      self.storeLastItem()
      if not self.prepare(node):
        return
      node=self.lastNode
      self.control.AddColumn(xlt("Name"), 10)
      self.control.AddColumn(xlt("Type"), 5)
      self.control.AddColumn(xlt("Values"), 40)
      self.control.AddColumn(xlt("TTL"), 5)
      self.RestoreListcols()

      for other in sorted(node.others.keys()):
        rdss=node.others[other]
        for rds in rdss.values():
          icon=node.GetImageId('other')
          dnstype=rdatatype.to_text(rds.rdtype)
          name=other
          for rd in rds:
            values=[]
            for slot in rd.__slots__:
              value=eval("rd.%s" % slot)
              if not isinstance(value, (list, tuple)):
                value=[value]
              if len(value) > 1:
                logger.debug("Value list dimensions > 1: %s", str(value))
              vl=[]
              for v in value:
                try:
                  v=v.decode()
                except: 
                  if isinstance(v, bytes):
                    v=standard_b64encode(v).decode()
                vl.append(str(v))
              values.append(f"{slot}={' '.join(vl)}")
            self.control.AppendItem(icon, [name, dnstype, ", ".join(values), floatToTime(rds.ttl, -1)])
            icon=0
            name=""
            dnstype=""
      self.restoreLastItem()



class CatalogMemberRecord(SingleValRecord):
  def wurst__init__(self, wnd, node, name="", rdtype=None):
    adm.CheckedDialog.__init__(self, wnd, node)
    self.rdtype=rdatatype.PTR
    self.RecordName=name
    self.Bind("Recordname Value TTL")
    
  def Go(self):
    super().Go()
    self.RecordNameStatic = xlt("Unique Label")
    self.ValueStatic=xlt("Member Zone")

  def Save(self):
    ttl=int(timeToFloat(self.TTL))
    data=self.dataclass(self.Value.strip())
    rds=Rdataset(ttl, rdataclass.IN, self.rdtype, data)

    if not self.RecordName.endswith(".zones"):
      self.RecordName =f"{self.RecordName.lower()}.zones"
    return self._save(rds)


class CatalogMemberPage(_PTRsPage):
  name=xlt("Member Zones")
  menus=[PageNewRecord, PageDeleteRecord]
 
  def Display(self, node, _detached):
    if not node or node != self.lastNode:
      self.storeLastItem()
      
      if not self.prepare(node):
        return
      node=self.lastNode
      self.control.AddColumn(xlt("Unique Label"), 10)
      self.control.AddColumn(xlt("Member Zone"), 30)
      self.control.AddColumn(xlt("TTL"), 5)
      self.RestoreListcols()

      icon=node.GetImageId('ptr')
      
      for ptr in node.ptrs.keys():
        rds=node.ptrs[ptr]
        self.control.AppendItem(icon, [ptr, rds[0].target, floatToTime(rds.ttl, -1)])
      
      self.restoreLastItem()

  def Delete(self, parentWin, names, _types):
    return zonePage.Delete(self, parentWin, names, rdatatype.PTR)

  @staticmethod
  def EditDialog(_rdtype):
    return CatalogMemberRecord



class CatalogHostRecord(HostRecord):
  def __init__(self, wnd, node, name="", _unused=None):
    adm.CheckedDialog.__init__(self, wnd, node)
    self.Hostname=name
    self.Bind("Hostname IpAddress TTL TTL6")
    self.CreatePtr=False

class CatalogHostPage(HostsPage):       
  name=xlt("Primaries")
  menus=[PageNewRecord, PageEditRecord, PageDeleteRecord]

  @staticmethod
  def EditDialog(_rdtype):
    return CatalogHostRecord
    


pageinfo=[HostsPage, CNAMEsPage, PTRsRevPage, CatalogMemberPage, CatalogHostPage, OTHERsPage]
nodeinfo= [ 
           { "class": CatalogZone, "parents": ["Server", "Zone", "CatalogZone"], "sort": 5, "pages": "CatalogMemberPage CatalogHostPage OTHERsPage" },
           { "class": Zone, "parents": ["Server", "Zone"], "sort": 10, "pages": "HostsPage CNAMEsPage OTHERsPage" },
           { "class": RevZone, "parents": ["Server", "RevZone"], "sort": 20, "pages": "PTRsRevPage OTHERsPage" },
           ]

menuinfo=[ { 'class': IncrementSerial, 'nodeclasses': [Zone, RevZone], 'sort': 10 },
           { 'class': CleanDanglingPtr, 'nodeclasses': RevZone, 'sort': 30 },
           { 'class': RegisterZone, 'sort': 80 },
           { 'class': RegisterRevZone, 'sort': 81 },
           { 'class': UnregisterZone, 'sort': 82 }
           ]

