#!/opt/alt/python35/bin/python3.5
import hashlib
import io
# import ipaddress
import os
import re
import subprocess
import tempfile


class BasePanel:

    cmd = None
    ports_map = {'52224': '80', '52223': '443'}

    @classmethod
    def check(cls):
        if not cls.cmd:
            return False
        try:
            subprocess.check_call(
                cls.cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except (FileNotFoundError, subprocess.CalledProcessError):
            return False
        return True

    @classmethod
    def define_ports(cls):
        return cls.ports_map


class Cpanel(BasePanel):
    cmd = ('/usr/local/cpanel/cpanel', '-V')
    ports_map = {
        '52224': '80', '52228': '2086', '52230': '2082', '52232': '2095',
        '52223': '443', '52227': '2087', '52229': '2083', '52231': '2096'}


class Plesk(BasePanel):
    cmd = ('/usr/sbin/plesk', 'version')
    ports_map = {
        '52224': '80', '52234': '8880', '52223': '443', '52233': '8443'}


class DirectAdmin(BasePanel):
    cmd = ('/usr/local/directadmin/custombuild/build', 'version')
    ports_map = {'52224': '80', '52223': '443', '52235': '2222'}


class AddressHandler:
    """
    Gets the system IP addresses by calling 'ip' utility and generates
    upstream configuration based on the addresses
    """
    default_ports_map = {'52224': '80', '52223': '443'}
    default_keepalive = '32'
    config_dir = '/etc/imunify360-webshield'
    config_name = 'upstreams.conf'
    geo_template_prefix = 'geo $server_addr $upstream_hint {\n'
    map_template_prefix = 'map $upstream_hint$server_port $upstream_dest {\n'
    upstream_template = ('upstream {} {{\n'
                         '    server {}:{};\n'
                         '    keepalive {};\n'
                         '}}\n\n')
    block_record_template = '    {} {};\n'
    block_record_commented_template = '    {} {};\t# {}\n'
    block_finish = '}\n'

    def __init__(self, ports=None):
        self.ports_map = ports if ports else self.default_ports_map
        self._buffer = io.StringIO()
        self._buffer.write('# The file was generated automatically.\n')
        self._buffer.write('# Do not edit it.\n\n')
        self._addresses = tuple(self._get_ip_addresses())
        self._geo_map = {}
        self._upstreams = {}

    @classmethod
    def make(cls, ports=None):
        """
        Batch all-at-once method
        """
        instance = cls(ports)
        if not instance._addresses:
            return
        instance._add_geo()
        instance._add_map()
        instance._add_upstreams()
        instance._save()

    def _add_geo(self):
        """
        Prepares and writes to buffer 'geo' part,
        that creates names for request IP addresses
        """
        self._buffer.write(self.geo_template_prefix)
        for addr in sorted(self._addresses):
            hint = 'u' + hashlib.md5(addr.encode('ascii')).hexdigest() + 'x'
            self._geo_map[addr] = hint
            self._buffer.write(
                self.block_record_template.format(addr, hint))
        self._buffer.write(self.block_finish)
        self._buffer.write('\n')

    def _add_map(self):
        """
        Combines IP addresses names with request ports and maps them to
        upstream names and writes the map to buffer
        """
        self._buffer.write(self.map_template_prefix)
        for in_port in sorted(self.ports_map.keys()):
            out_port = self.ports_map[in_port]
            for addr in sorted(self._geo_map.keys()):
                hint = self._geo_map[addr]
                self._upstreams[hint + out_port] = (addr, out_port)
                self._buffer.write(
                    self.block_record_commented_template.format(
                        hint + in_port, hint + out_port, addr))
        self._buffer.write(self.block_finish)
        self._buffer.write('\n')

    def _add_upstreams(self):
        """
        Generates upstreams for every name-port pair and writes them to buffer
        """
        for upstream in sorted(self._upstreams.keys()):
            addr, port = self._upstreams[upstream]
            if ':' in addr:
                addr = '[' + addr + ']'
            self._buffer.write(
                self.upstream_template.format(
                    upstream, addr, port, self.default_keepalive))

    def _save(self):
        """
        Saves the buffer to the webshield config file
        """
        conf_desc, conf_path = tempfile.mkstemp(
            prefix=None, suffix='-upstream.conf_',
            dir=self.config_dir, text=True)
        conf_handle = os.fdopen(conf_desc, 'w')
        conf_handle.write(self._buffer.getvalue())
        os.rename(conf_path, os.path.join(self.config_dir, self.config_name))


    @staticmethod
    def _get_ip_addresses():
        addresses = set()
        patt = re.compile(
            r"""(?:\d+:\s?)?    # number (e.g. '1:') and optional space
                (?P<if>\S+)     # interface name (e.g. 'eth0')
                    \s+?        # space(s)
                inet6?\s        # word 'inet' or 'inet6'
                (?P<ip>(?:      # start IP capturing
                    \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}  # for IPv4
                        |                               # or
                    [0-9a-fA-F:]+)                      # for IPv6
                )                                       # end capturing
                (?:/(?P<mask>\d{1,3}))?     # capture mask (e.g.'/24'), if any
                """, re.VERBOSE)
        p = subprocess.Popen(['ip', '-o', 'address', 'show', 'up'],
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             universal_newlines=True)
        out, err = p.communicate()
        if not out:
            return
        for line in out.splitlines():
            m = patt.match(line)
            if not m:
                continue
            iface, ip, mask = m.group('if', 'ip', 'mask')
            # if iface == 'lo':
            #     continue
            # if ':' in ip and ipaddress.IPv6Address(ip).is_link_local:
            #     continue
            addresses.add(ip)
        return addresses


def get_ports():
    for panel_cls in Cpanel, Plesk, DirectAdmin:
        if panel_cls.check():
            return panel_cls.define_ports()
    return BasePanel.define_ports()


def main():
    ports = get_ports()
    AddressHandler.make(ports)


if __name__ == '__main__':
    main()
