# -*- coding: utf-8 -*-
'''
Support for firewalld.

.. versionadded:: 2015.2.0
'''

# Import Python Libs
from __future__ import absolute_import, print_function, unicode_literals
import logging
import re

# Import Salt Libs
from salt.exceptions import CommandExecutionError
import salt.utils.path
import salt.utils.versions

log = logging.getLogger(__name__)


def __virtual__():
    '''
    Check to see if firewall-cmd exists
    '''
    if salt.utils.path.which('firewall-cmd'):
        return True

    return (False, 'The firewalld execution module cannot be loaded: the firewall-cmd binary is not in the path.')


def __firewall_cmd(cmd):
    '''
    Return the firewall-cmd location
    '''
    firewall_cmd = '{0} {1}'.format(salt.utils.path.which('firewall-cmd'), cmd)
    out = __salt__['cmd.run_all'](firewall_cmd)

    if out['retcode'] != 0:
        if not out['stderr']:
            msg = out['stdout']
        else:
            msg = out['stderr']
        raise CommandExecutionError(
            'firewall-cmd failed: {0}'.format(msg)
        )
    return out['stdout']


def __mgmt(name, _type, action):
    '''
    Perform zone management
    '''
    # It's permanent because the 4 concerned functions need the permanent option, it's wrong without
    cmd = '--{0}-{1}={2} --permanent'.format(action, _type, name)

    return __firewall_cmd(cmd)


def version():
    '''
    Return version from firewall-cmd

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.version
    '''
    return __firewall_cmd('--version')


def reload_rules():
    '''
    Reload the firewall rules, which makes the permanent configuration the new
    runtime configuration without losing state information.

    .. versionadded:: 2016.11.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.reload
    '''
    return __firewall_cmd('--reload')


def default_zone():
    '''
    Print default zone for connections and interfaces

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.default_zone
    '''
    return __firewall_cmd('--get-default-zone')


def list_zones(permanent=True):
    '''
    List everything added for or enabled in all zones

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.list_zones
    '''
    zones = {}

    cmd = '--list-all-zones'

    if permanent:
        cmd += ' --permanent'

    for i in __firewall_cmd(cmd).splitlines():
        if i.strip():
            if bool(re.match('^[a-z0-9]', i, re.I)):
                zone_name = i.rstrip()
            else:
                (id_, val) = i.strip().split(':')

                if zones.get(zone_name, None):
                    zones[zone_name].update({id_: val})
                else:
                    zones[zone_name] = {id_: val}

    return zones


def get_zones(permanent=True):
    '''
    Print predefined zones

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.get_zones
    '''
    cmd = '--get-zones'

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd).split()


def get_services(permanent=True):
    '''
    Print predefined services

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.get_services
    '''
    cmd = '--get-services'

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd).split()


def get_icmp_types(permanent=True):
    '''
    Print predefined icmptypes

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.get_icmp_types
    '''
    cmd = '--get-icmptypes'

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd).split()


def new_zone(zone, restart=True):
    '''
    Add a new zone

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.new_zone my_zone

    By default firewalld will be reloaded. However, to avoid reloading
    you need to specify the restart as False

    .. code-block:: bash

        salt '*' firewalld.new_zone my_zone False
    '''
    out = __mgmt(zone, 'zone', 'new')

    if restart:

        if out == 'success':
            return __firewall_cmd('--reload')

    return out


def delete_zone(zone, restart=True):
    '''
    Delete an existing zone

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.delete_zone my_zone

    By default firewalld will be reloaded. However, to avoid reloading
    you need to specify the restart as False

    .. code-block:: bash

        salt '*' firewalld.delete_zone my_zone False
    '''
    out = __mgmt(zone, 'zone', 'delete')

    if restart:

        if out == 'success':
            return __firewall_cmd('--reload')

    return out


def set_default_zone(zone):
    '''
    Set default zone

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.set_default_zone damian
    '''
    return __firewall_cmd('--set-default-zone={0}'.format(zone))


def new_service(name, restart=True):
    '''
    Add a new service

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.new_service my_service

    By default firewalld will be reloaded. However, to avoid reloading
    you need to specify the restart as False

    .. code-block:: bash

        salt '*' firewalld.new_service my_service False
    '''
    out = __mgmt(name, 'service', 'new')

    if restart:

        if out == 'success':
            return __firewall_cmd('--reload')

    return out


def delete_service(name, restart=True):
    '''
    Delete an existing service

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.delete_service my_service

    By default firewalld will be reloaded. However, to avoid reloading
    you need to specify the restart as False

    .. code-block:: bash

        salt '*' firewalld.delete_service my_service False
    '''
    out = __mgmt(name, 'service', 'delete')

    if restart:

        if out == 'success':
            return __firewall_cmd('--reload')

    return out


def list_all(zone=None, permanent=True):
    '''
    List everything added for or enabled in a zone

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.list_all

    List a specific zone

    .. code-block:: bash

        salt '*' firewalld.list_all my_zone
    '''
    _zone = {}
    id_ = ''

    if zone:
        cmd = '--zone={0} --list-all'.format(zone)
    else:
        cmd = '--list-all'

    if permanent:
        cmd += ' --permanent'

    for i in __firewall_cmd(cmd).splitlines():
        if re.match('^[a-z0-9]', i, re.I):
            zone_name = i.rstrip()
        else:
            if i.startswith('\t'):
                _zone[zone_name][id_].append(i.strip())
                continue

            (id_, val) = i.split(':', 1)

            id_ = id_.strip()

            if _zone.get(zone_name, None):
                _zone[zone_name].update({id_: [val.strip()]})
            else:
                _zone[zone_name] = {id_: [val.strip()]}

    return _zone


def list_services(zone=None, permanent=True):
    '''
    List services added for zone as a space separated list.
    If zone is omitted, default zone will be used.

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.list_services

    List a specific zone

    .. code-block:: bash

        salt '*' firewalld.list_services my_zone
    '''
    if zone:
        cmd = '--zone={0} --list-services'.format(zone)
    else:
        cmd = '--list-services'

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd).split()


def add_service(service, zone=None, permanent=True):
    '''
    Add a service for zone. If zone is omitted, default zone will be used.

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.add_service ssh

    To assign a service to a specific zone:

    .. code-block:: bash

        salt '*' firewalld.add_service ssh my_zone
    '''
    if zone:
        cmd = '--zone={0} --add-service={1}'.format(zone, service)
    else:
        cmd = '--add-service={0}'.format(service)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def remove_service(service, zone=None, permanent=True):
    '''
    Remove a service from zone. This option can be specified multiple times.
    If zone is omitted, default zone will be used.

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.remove_service ssh

    To remove a service from a specific zone

    .. code-block:: bash

        salt '*' firewalld.remove_service ssh dmz
    '''
    if zone:
        cmd = '--zone={0} --remove-service={1}'.format(zone, service)
    else:
        cmd = '--remove-service={0}'.format(service)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def add_service_port(service, port):
    '''
    Add a new port to the specified service.

    .. versionadded:: 2016.11.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.add_service_port zone 80
    '''
    if service not in get_services(permanent=True):
        raise CommandExecutionError('The service does not exist.')

    cmd = '--permanent --service={0} --add-port={1}'.format(service, port)
    return __firewall_cmd(cmd)


def remove_service_port(service, port):
    '''
    Remove a port from the specified service.

    .. versionadded:: 2016.11.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.remove_service_port zone 80
    '''
    if service not in get_services(permanent=True):
        raise CommandExecutionError('The service does not exist.')

    cmd = '--permanent --service={0} --remove-port={1}'.format(service, port)
    return __firewall_cmd(cmd)


def get_service_ports(service):
    '''
    List ports of a service.

    .. versionadded:: 2016.11.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.get_service_ports zone
    '''
    cmd = '--permanent --service={0} --get-ports'.format(service)
    return __firewall_cmd(cmd).split()


def add_service_protocol(service, protocol):
    '''
    Add a new protocol to the specified service.

    .. versionadded:: 2016.11.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.add_service_protocol zone ssh
    '''
    cmd = '--permanent --service={0} --add-protocol={1}'.format(service,
                                                                protocol)
    return __firewall_cmd(cmd)


def remove_service_protocol(service, protocol):
    '''
    Remove a protocol from the specified service.

    .. versionadded:: 2016.11.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.remove_service_protocol zone ssh
    '''
    cmd = '--permanent --service={0} --remove-protocol={1}'.format(service,
                                                                   protocol)
    return __firewall_cmd(cmd)


def get_service_protocols(service):
    '''
    List protocols of a service.

    .. versionadded:: 2016.11.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.get_service_protocols zone
    '''
    cmd = '--permanent --service={0} --get-protocols'.format(service)
    return __firewall_cmd(cmd).split()


def get_masquerade(zone=None, permanent=True):
    '''
    Show if masquerading is enabled on a zone.
    If zone is omitted, default zone will be used.

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.get_masquerade zone
    '''
    zone_info = list_all(zone, permanent)

    if 'no' in [zone_info[i]['masquerade'][0] for i in zone_info.keys()]:
        return False

    return True


def add_masquerade(zone=None, permanent=True):
    '''
    Enable masquerade on a zone.
    If zone is omitted, default zone will be used.

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.add_masquerade

    To enable masquerade on a specific zone

    .. code-block:: bash

        salt '*' firewalld.add_masquerade dmz
    '''
    if zone:
        cmd = '--zone={0} --add-masquerade'.format(zone)
    else:
        cmd = '--add-masquerade'

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def remove_masquerade(zone=None, permanent=True):
    '''
    Remove masquerade on a zone.
    If zone is omitted, default zone will be used.

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.remove_masquerade

    To remove masquerade on a specific zone

    .. code-block:: bash

        salt '*' firewalld.remove_masquerade dmz
    '''
    if zone:
        cmd = '--zone={0} --remove-masquerade'.format(zone)
    else:
        cmd = '--remove-masquerade'

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


# TODO: remove force_masquerade parameter in future release
def add_port(zone, port, permanent=True, force_masquerade=None):
    '''
    Allow specific ports in a zone.

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.add_port internal 443/tcp
    '''

    # Previously, masquerading was always enabled here
    # This will be deprecated in a future release
    if force_masquerade is None:
        force_masquerade = True
        salt.utils.versions.warn_until(
            'Neon',
            'add_port function will no longer force enable masquerading '
            'in future releases. Use add_masquerade to enable masquerading.')

    # (DEPRECATED) Force enable masquerading
    # TODO: remove in future release
    if force_masquerade and not get_masquerade(zone):
        add_masquerade(zone)

    cmd = '--zone={0} --add-port={1}'.format(zone, port)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def remove_port(zone, port, permanent=True):
    '''
    Remove a specific port from a zone.

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.remove_port internal 443/tcp
    '''
    cmd = '--zone={0} --remove-port={1}'.format(zone, port)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def list_ports(zone, permanent=True):
    '''
    List all ports in a zone.

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.list_ports
    '''
    cmd = '--zone={0} --list-ports'.format(zone)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd).split()


# TODO: remove force_masquerade parameter in future release
def add_port_fwd(zone, src, dest, proto='tcp', dstaddr='', permanent=True, force_masquerade=None):
    '''
    Add port forwarding.

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.add_port_fwd public 80 443 tcp
    '''

    # Previously, masquerading was always enabled here
    # This will be deprecated in a future release
    if force_masquerade is None:
        force_masquerade = True
        salt.utils.versions.warn_until(
            'Neon',
            'add_port_fwd function will no longer force enable masquerading '
            'in future releases. Use add_masquerade to enable masquerading.')

    # (DEPRECATED) Force enable masquerading
    # TODO: remove in future release
    if force_masquerade and not get_masquerade(zone):
        add_masquerade(zone)

    cmd = '--zone={0} --add-forward-port=port={1}:proto={2}:toport={3}:toaddr={4}'.format(
        zone,
        src,
        proto,
        dest,
        dstaddr
    )

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def remove_port_fwd(zone, src, dest, proto='tcp', dstaddr='', permanent=True):
    '''
    Remove Port Forwarding.

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.remove_port_fwd public 80 443 tcp
    '''
    cmd = '--zone={0} --remove-forward-port=port={1}:proto={2}:toport={3}:toaddr={4}'.format(
        zone,
        src,
        proto,
        dest,
        dstaddr
    )

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def list_port_fwd(zone, permanent=True):
    '''
    List port forwarding

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.list_port_fwd public
    '''
    ret = []

    cmd = '--zone={0} --list-forward-ports'.format(zone)

    if permanent:
        cmd += ' --permanent'

    for i in __firewall_cmd(cmd).splitlines():
        (src, proto, dest, addr) = i.split(':')

        ret.append(
            {'Source port': src.split('=')[1],
             'Protocol': proto.split('=')[1],
             'Destination port': dest.split('=')[1],
             'Destination address': addr.split('=')[1]}
        )

    return ret


def block_icmp(zone, icmp, permanent=True):
    '''
    Block a specific ICMP type on a zone

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.block_icmp zone echo-reply
    '''
    if icmp not in get_icmp_types(permanent):
        log.error('Invalid ICMP type')
        return False

    if icmp in list_icmp_block(zone, permanent):
        log.info('ICMP block already exists')
        return 'success'

    cmd = '--zone={0} --add-icmp-block={1}'.format(zone, icmp)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def allow_icmp(zone, icmp, permanent=True):
    '''
    Allow a specific ICMP type on a zone

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.allow_icmp zone echo-reply
    '''
    if icmp not in get_icmp_types(permanent):
        log.error('Invalid ICMP type')
        return False

    if icmp not in list_icmp_block(zone, permanent):
        log.info('ICMP Type is already permitted')
        return 'success'

    cmd = '--zone={0} --remove-icmp-block={1}'.format(zone, icmp)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def list_icmp_block(zone, permanent=True):
    '''
    List ICMP blocks on a zone

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewlld.list_icmp_block zone
    '''
    cmd = '--zone={0} --list-icmp-blocks'.format(zone)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd).split()


def make_permanent():
    '''
    Make current runtime configuration permanent.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.make_permanent
    '''
    return __firewall_cmd('--runtime-to-permanent')


def get_interfaces(zone, permanent=True):
    '''
    List interfaces bound to a zone

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.get_interfaces zone
    '''
    cmd = '--zone={0} --list-interfaces'.format(zone)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd).split()


def add_interface(zone, interface, permanent=True):
    '''
    Bind an interface to a zone

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.add_interface zone eth0
    '''
    if interface in get_interfaces(zone, permanent):
        log.info('Interface is already bound to zone.')

    cmd = '--zone={0} --add-interface={1}'.format(zone, interface)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def remove_interface(zone, interface, permanent=True):
    '''
    Remove an interface bound to a zone

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.remove_interface zone eth0
    '''
    if interface not in get_interfaces(zone, permanent):
        log.info('Interface is not bound to zone.')

    cmd = '--zone={0} --remove-interface={1}'.format(zone, interface)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def get_sources(zone, permanent=True):
    '''
    List sources bound to a zone

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.get_sources zone
    '''
    cmd = '--zone={0} --list-sources'.format(zone)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd).split()


def add_source(zone, source, permanent=True):
    '''
    Bind a source to a zone

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.add_source zone 192.168.1.0/24
    '''
    if source in get_sources(zone, permanent):
        log.info('Source is already bound to zone.')

    cmd = '--zone={0} --add-source={1}'.format(zone, source)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def remove_source(zone, source, permanent=True):
    '''
    Remove a source bound to a zone

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.remove_source zone 192.168.1.0/24
    '''
    if source not in get_sources(zone, permanent):
        log.info('Source is not bound to zone.')

    cmd = '--zone={0} --remove-source={1}'.format(zone, source)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def get_rich_rules(zone, permanent=True):
    '''
    List rich rules bound to a zone

    .. versionadded:: 2016.11.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.get_rich_rules zone
    '''
    cmd = '--zone={0} --list-rich-rules'.format(zone)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd).splitlines()


def add_rich_rule(zone, rule, permanent=True):
    '''
    Add a rich rule to a zone

    .. versionadded:: 2016.11.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.add_rich_rule zone 'rule'
    '''
    cmd = "--zone={0} --add-rich-rule='{1}'".format(zone, rule)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)


def remove_rich_rule(zone, rule, permanent=True):
    '''
    Add a rich rule to a zone

    .. versionadded:: 2016.11.0

    CLI Example:

    .. code-block:: bash

        salt '*' firewalld.remove_rich_rule zone 'rule'
    '''
    cmd = "--zone={0} --remove-rich-rule='{1}'".format(zone, rule)

    if permanent:
        cmd += ' --permanent'

    return __firewall_cmd(cmd)
