##############################################################################
#
# Copyright (c) 2010, 2011, 2012 Vifib SARL and Contributors.
# All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly advised to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

import ipaddress
import logging
import subprocess


def callAndRead(argument_list, raise_on_error=True):
  popen = subprocess.Popen(
    argument_list,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    universal_newlines=True,
  )
  result = popen.communicate()[0]
  if raise_on_error and popen.returncode != 0:
    raise ValueError('Issue while invoking %r, result was:\n%s' % (
                     argument_list, result))
  return popen.returncode, result


def checkRequiredBinary(binary_list):
  missing_binary_list = []
  for b in binary_list:
    if type(b) is not list:
      b = [b]
    try:
      callAndRead(b, raise_on_error=False)
    except OSError:
      missing_binary_list.append(b[0])
  if missing_binary_list:
    raise UsageError(
      'Some required binaries are missing or not functional: %s' % (
        ','.join(missing_binary_list), )
    )


class Config(object):
  # Configuration options (mandatory options have no default)
  computer_id: str
  instance_root: str
  software_root: str
  master_url: str
  computer_xml: str
  partition_amount: int

  computer_json: str = None
  input_definition_file: str = None
  output_definition_file: str = None
  log_file: str = None
  dry_run: bool = False

  key_file: str = None
  cert_file: str = None
  master_ca_file: str = None

  alter_user = True
  instance_storage_home = None
  software_user = 'slapsoft'
  partition_base_name = 'slappart'
  user_base_name = 'slapuser'

  alter_network: bool = True
  interface_name: str = None
  ipv6_interface: str = None
  ipv4_local_network: str = None

  create_tap: bool = True
  tap_base_name: str = 'slaptap'
  tap_ipv6: bool = True
  tap_gateway_interface: str = None

  create_tun: bool = False
  tun_base_name: str = 'slaptun'

  # Constants
  DEPRECATED = ('bridge_name', 'bridge')
  SSL_FILES = ('key_file', 'cert_file', 'master_ca_file')
  STR_TO_BOOL = {'true': True, 'false': False}

  def __init__(self, logger):
    self.logger = logger

  def mergeConfig(self, args, configp):
    # args (from command line) override configp (from cfg) options
    for section in ('slapformat', 'slapos'):
      self.__dict__.update(configp.items(section))
    self.__dict__.update(args.__dict__)

  def setConfig(self):
    for option in self.DEPRECATED:
      if getattr(self, option, None):
        message = "Option %r is totally deprecated" % option
        self.logger.error(message)
        raise UsageError(message)

    for option, t in self.__annotations__.items():
      # check existence
      try:
        value = getattr(self, option)
      except AttributeError:
        raise UsageError("Parameter %r is not defined" % option)
      # convert bool
      if t is bool and isinstance(value, str):
        try:
          setattr(self, option, self.STR_TO_BOOL[value.lower()])
        except KeyError:
          message = "Option %r must be 'True' or 'False', not %r" % (option, attr)
          self.logger.error(message)
          raise UsageError(message)
      # convert int
      if t is int and isinstance(value, str):
        try:
          setattr(self, option, int(value))
        except ValueError:
          message = "Option %r must be an integer, not %r" % (option, attr)
          self.logger.error(message)
          raise UsageError(message)

    for option in self.SSL_FILES:
      path = getattr(self, option)
      if path is not None:
        if not os.path.exists(path):
          self.logger.fatal("File %r does not exist or is not readable" % path)
          sys.exit(1)

    if not self.dry_run:
      if self.alter_user:
        checkRequiredBinary(['groupadd', 'useradd', 'usermod', ['passwd', '-h']])
      if self.create_tap or self.alter_network:
        checkRequiredBinary(['ip'])

    self.computer_xml = os.path.abspath(self.computer_xml)
    if self.input_definition_file:
      self.input_definition_file = os.path.abspath(self.input_definition_file)
    if self.output_definition_file:
      self.output_definition_file = os.path.abspath(self.output_definition_file)

    self.logger.debug('Started.')
    if self.dry_run:
      self.logger.info("Dry-run mode enabled.")
    if self.create_tap:
      self.logger.info("Tap creation mode enabled (%s IPv6).", "with" if self.tap_ipv6 else "without")


class Computer(object):
  reference : str
  interface : Interface
  partitions : list
  address : ipaddress.IPv4Interface or ipaddress.IPv6Interface
  user : User
  conf : Config

  def __init__(self, **kwargs):
    self.__dict__.update(kwargs)
    if not isinstance(self.address, (ipaddress.IPv4Interface, ipaddress.IPv6Interface)):
      self.address = ipaddress.ip_interface(address)

  @classmethod
  def load(cls, conf):
    reference = conf.computer_id
    interface = Interface.load(conf)

    if conf.input_definition_file:
      conf.logger.info('Using definition file %r' % conf.input_definition_file)
      computer_definition = configparser.ConfigParser(interpolation=None)
      computer_definition.read(conf.input_definition_file)

      username = computer_definition.get('computer', 'software_user', fallback=conf.software_user)
      user = User(conf, username)
      address = computer_definition.get('computer', 'address', fallback=None)
      partitions = Partition.from_definition(conf, computer_definition)

    elif os.path.exists(conf.computer_xml):
      conf.logger.debug('Loading previous computer data from %r' % conf.computer_xml)
      with open(conf.computer_xml, 'rb') as f:
        computer_xml = xml_marshaller.xml_marshaller.load(f)

      user = User(conf, computer_xml.get('software_user', conf.software_user))
      address = (computer_xml['address'], computer_xml['netmask'])
      partitions = Partition.from_xml(conf, computer_xml)

    else:
      conf.logger.warning('Creating new computer data with id %r', conf.computer_id)

      user = User(conf, conf.software_user)
      address = None
      partitions = Partition.from_conf(conf)
    
    return Computer(
      reference = reference,
      interface = interface,
      user = user,
      address = address,
      partitions = partitions,
      conf = conf,
    )


class Interface(object):
  ipv4_interface : str
  ipv6_interface : str
  ipv4_local_network : ipaddress.IPv4Network
  logger: logging.Logger

  def __init__(self, **kwargs):
    self.__dict__.update(kwargs)

  @classmethod
  def load(cls, conf):
    return Interface(
      logger = conf.logger,
      ipv4_interface = conf.interface_name,
      ipv6_interface = conf.ipv6_interface,
      ipv4_local_network = conf.ipv4_local_network,
    )


class User(object):
  alter_user: bool
  name: str
  path: str
  additional_group_list: list

  SHELL = '/bin/sh'

  def __init__(self, conf, name, path=None, additional_group_list=None):
    self.alter_user = conf.alter_user
    self.name = name
    self.path = path
    self.additional_group_list = additional_group_list

  def apply(self):
    if not self.alter_user:
      return
    grpname = 'grp_' + self.name if sys.platform == 'cygwin' else self.name
    if not self.isGroupAvailable(grpname):
      callAndRead(['groupadd', grpname])
    user_parameter_list = ['-d', self.path, '-g', self.name, '-s', self.SHELL]
    if self.additional_group_list:
      user_parameter_list.extend(['-G', ','.join(self.additional_group_list)])
    user_parameter_list.append(self.name)
    if self.isUserAvailable(self.name):
      # if the user is already created and used we should not fail
      callAndRead(['usermod'] + user_parameter_list, raise_on_error=False)
    else:
      user_parameter_list.append('-r')
      callAndRead(['useradd'] + user_parameter_list)      
    # lock the password of user
    callAndRead(['passwd', '-l', self.name])

  @classmethod
  def isGroupAvailable(cls, name):
    try:
      pwd.getgrnam(name)
      return True
    except KeyError:
      return False

  @classmethod
  def isUserAvailable(cls, name):
    try:
      pwd.getpwnam(name)
      return True
    except KeyError:
      return False


class Partition(object):
  reference : str
  path : str
  user : User
  address_list : list
  tap : Tap
  tun : Tun
  external_storage_list: list

  def __init__(self, **kwargs):
    self.__dict__.update(kwargs)

  @classmethod
  def from_definition(cls, conf, computer_definition):
    partition_list = []
    for index in range(conf.partition_amount):
      section = 'partition_%s' % index
      reference = computer_definition.get(section, 'pathname')
      path = os.path.join(conf.instance_root, reference)
      user = User(conf, computer_definition.get(section, 'user'), path)
      address_list = [
        ipaddress.ip_interface(addr)
        for add in computer_definition.get(section, 'address')
      ]
      tap = Tap(computer_definition.get(section, 'network_interface'))
      tun = Tun.load(conf, index)
      partition = Partition(
        reference = reference,
        path = path,
        user = user,
        address_list = address_list,
        tap = tap,
        tun = tun
      )
      partition_list.append(partition)
    return partition_list
  
  @classmethod
  def from_xml(cls, conf, computer_xml):
    partition_list = []
    for index, partition_xml in enumerate(computer_xml['partition_list']):
      reference = partition_xml['reference']
      path = partition_dict['path']
      try:
        user = User(conf, partition_xml['user']['name'], path)
      except KeyError:
        user = User(conf, 'root')
      address_list = [
        ipaddress.ip_interface((addr['addr'], addr['netmask']))
        for addr in partition_dict['address_list']
      ]
      tap = Tap.from_xml(partition_xml['tap'])
      tun = Tun.from_xml(partition_xml.get('tun'))
      partition = Partition(
        reference = reference,
        path = path,
        user = user,
        address_list = address_list,
        tap = tap,
        tun = tun,
      )
    if len(partition_list) > conf.partition_amout:
      conf.logger.critical(
        "Requested amount of computer partitions (%d) is lower"
        " than already configured (%d), cannot continue",
        conf.partition_amount,
        len(partition_list),
      )
      sys.exit(1)
    elif len(partition_list) < conf.partition_amout:
      return cls.from_conf(conf, partitions_list)
    else:
      conf.logger.info("All requested computer partitions already exist")
      return partition_list

  @classmethod
  def from_conf(cls, conf, partitions_list=None):
    conf.logger.info(
      "Requested amount of computer partitions (%d) is higher"
      " than already configured (%d), adding %d new partitions",
      conf.partition_amount,
      len(partition_list),
      conf.partition_amount - len(partition_list),
    )
    partitions_list = partitions_list or []
    for i in range(len(partition_list), conf.partition_amount):
      reference = '%s%d' % (conf.partition_base_name, i)
      path = os.path.join(conf.instance_root, reference)
      user = User(conf, '%s%d' % (conf.user_base_name, i), path)
      address_list = None
      tap = 
      partition = Partition(
        reference = reference,
        path = path,
        user = user,
        address_list = address_list,
        tap = tap,
        tun = tun,
      )
      partition_list.append(partition)
    return partition_list


class Tap(object):
  name : str
  ipv4_address : ipaddress.IPv4Interface
  ipv4_gateway : ipaddress.IPv4Address
  ipv6_address : ipaddress.IPv6Interface
  ipv6_gateway : ipaddress.IPv6Address

  IFF_TAP = 0x0002
  TUNSETIFF = 0x400454ca
  KEEP_TAP_ATTACHED_EVENT = threading.Event()
  MODE = 'tap'

  def __init__(self, name, **kwargs):
    self.name = name
    self.__dict__.update(kwargs)

  @classmethod
  def from_xml(cls, conf, tap_xml):
    if not conf.create_tap:
      return None
    name = tap_xml['name']
    ipv4 = tap_xml.get('ipv4_addr', None)
    if ipv4:
      ipv4_network = ipaddress.Ipv4Network(tap_xml['ipv4_network'])
      ipv4_address = ipaddress.IPv4Interface((ipv4, ipv4_network.prefixlen()))
      ipv4_gateway = ipaddress.Ipv4Address(tap_xml['ipv4_gateway'])
    else:
      
    if not conf.tap_ipv6:
    ipv6 = tap_xml.get('ipv6_addr', None)
    if ipv6:
      ipv6_network = ipaddress.Ipv6Network(tap_xml['ipv4_network'])
      ipv6_address = ipaddress.IPv6Interface((ipv6, ipv6_network.prefixlen()))
      ipv6_gateway = ipaddress.Ipv6Address(tap_xml['ipv4_gateway'])
    return Tap(
      name,
      ipv4_address=ipv4_address,
      ipv4_gateway=ipv4_gateway,
      ipv6_address=ipv6_address,
      ipv6_gateway=ipv6_gateway,
    )

  @classmethod
  def from_conf(self, conf):
     

  
class Tun(object):

