#!/usr/bin/python3
import argparse
import copy
import json
import os
import re
import signal
import struct
import sys
import syslog
import threading
import time
import yaml
import lib.common
import lib.common_vars
import lib.file
import lib.icmp_packet_send
import lib.log
import lib.params
import lib.route
import lib.socket
import lib.service_manager_notify
import socket

# TODO: использовать один тип кавычек для строк.

DAEMON_PROTO_NUM = lib.common_vars.DAEMON_PROTO
DAEMON_NAME = lib.common_vars.DAEMON_NAME
# List with status of links used in threads
links_status = {}
# List with lock of threads that work with links
link_status_lock = threading.RLock()
clear_sa_conn_lock = threading.RLock()
reload_lsp_lock = threading.RLock()
clear_conntrack_lock = threading.RLock()
# service_notify is used for contain ServiceNotify object.
# ServiceNotify is used in many function and in lib.common.externcmd.
# for WATCHDOG.
service_notify = None


class ArgumentParser(argparse.ArgumentParser):
    """
    Change argparse class to handle errors.

    Before change, "error" function exited executable program
    """

    def error(self, message):
        raise AttributeError(message)


def set_exit_flag(*_):
    lib.common.exit_flag = True


def set_reload_flag(*_):
    lib.common.reload_config_flag = True


def set_updated_dhcp_flag(*_):
    lib.common.updated_dhcp_flag = True


def exec_notify_script(notify_script_params, group_name=None,
                       state=None, ignore_global_exit_flag=False):
    """
    Execute notify script.

    notify_script_params    - dictionary with notify script parameters.
    group_name              - name of group that called script
                              execution.
    state                   - multiwan state when calling the script.
    ignore_global_exit_flag - if exit_flag (global var) is set to True,
                              ignore it.

    return True if script was executed successfully
    """
    if group_name is None:
        env = {"STATE": state}
        msg_prefix = f'State "{state}": '
    else:
        env = {"GROUP_NAME": group_name, "STATE": state}
        msg_prefix = f'Group {group_name}: State "{state}": '

    file = notify_script_params["file"]
    timeout = notify_script_params["timeout"]

    if lib.common.reload_config_flag:
        return False
    if lib.common.exit_flag and not ignore_global_exit_flag:
        return False

    details_err_msg = lib.file.check_file_for_reading(file)
    if details_err_msg:
        msg = f'Cannot work with script: {file}.'
        lib.log.send_warning_msg(f'{msg_prefix}{msg}', details_msg=details_err_msg)
        return False

    msg = f'Starting "{file}" script ...'
    lib.log.send_info_msg(f'{msg_prefix}{msg}')

    stdout, stderr, code, exited_reason = lib.common.externcmd(
        command=f'{file}', timeout=timeout, env=env, no_stderr=True, no_stdout=True,
        ignore_global_exit_flag=ignore_global_exit_flag)

    if code != 0:
        if exited_reason == "exited":
            msg = f'Script {file} exited with exit code: {code}.'
        elif exited_reason == "global_exit_flag":
            msg = f'Script {file} received an administrative request to terminate.'
        elif exited_reason == "process_not_started":
            msg = f'Script {file} failed to run'
        elif exited_reason == "timeout":
            msg = f'Script {file} is taking too long. Timeout is {timeout} sec.'
        else:
            raise ValueError('Unexpected exited_reason from lib.common.externcmd')
        lib.log.send_warning_msg(f'{msg_prefix}{msg}')
        return False

    msg = f'Script "{file}" was successfully executed.'
    lib.log.send_info_msg(f'{msg_prefix}{msg}')

    return True


def icmp_hosts_check(hosts, interface, icmp_params, source_ip, reachability, msg_prefix=''):
    """
    Check host reachable via ICMP

    hosts        - dictionary with lists IPv4 hosts and IPv6 hosts.
    interface    - string with interface name.
    icmp_params  - dictionary with parameters for ICMP packet.
    source_ip    - dictionary with IPv4 address for ICMP packet and IPv6
                   address for ICMPv6 packet.
    reachability - host availability check mode.
    msg_prefix   - string with prefix for log messages.
    """
    link_ping = lib.icmp_packet_send.PingClass()
    msg = ''
    details = ''
    ping_result = 0
    for ip_ver_name in hosts:
        ip_version = lib.common_vars.IP_VER_MAP[ip_ver_name]["ver_number"]
        for host in hosts[ip_ver_name]:
            if lib.common.exit_flag or lib.common.reload_config_flag:
                return

            if host is None:
                ping_result = -100
            else:
                try:
                    ping_result, msg, details = link_ping.send_and_receive(
                        destination_host=host,
                        interface_name=interface,
                        source_ip=source_ip[ip_ver_name],
                        ip_version=ip_version,
                        data_len=icmp_params["data_size"],
                        tos=icmp_params["tos"],
                        wait_time=icmp_params["max_rtt"] / 1000,
                        fwmark=icmp_params["fwmark"],
                        force_resolve_dst_host=True)
                except Exception as exception_info:
                    lib.log.send_err_msg(f'Internal error. {msg_prefix}Ping exception detected.',
                                         details_msg=exception_info)
                    return

            if ping_result >= 0:
                msg = f'{ping_result} ms'

            if ping_result != -100:
                lib.log.send_debug_msg(f'{msg_prefix}Interface {interface}: {host}: {msg}',
                                       details_msg=details)

            if reachability == 'all':
                if ping_result < 0:
                    lib.log.send_debug_msg(f'{msg_prefix}Interface {interface}:'
                                           f' Not all hosts are available.')
                    # if someone not available, return False.
                    return False
            elif ping_result >= 0:
                # if someone available, return True.
                lib.log.send_debug_msg(f'{msg_prefix}Interface {interface}: Available host found.')
                break

        if reachability == 'all':
            lib.log.send_debug_msg(f'{msg_prefix}Interface {interface}:'
                                   f' All {ip_ver_name} hosts were available.')
            # All host available.
        else:
            if ping_result >= 0:
                continue

            lib.log.send_debug_msg(f'{msg_prefix}Interface {interface}: '
                                   f'No available {ip_ver_name} host found.')
            # All host not available.
            return False

    return True


def check_link(link, group_name='default', watch_interval=1):
    """
    Main function to check link status.

    link           - immutable link parameters throughout the life of
                     thread. Dictionary.
    group_name     - Group name for link. String.
    watch_interval - icmp rules and icmp routes update interval.
    """

    def set_link_status(link_status):
        if link_status:
            lib.log.send_info_msg(f'{msg_prefix}Link is reachable.')
        else:
            lib.log.send_warning_msg(f'{msg_prefix}Link is unreachable.')

        with link_status_lock:
            link["status"]["reachable"] = link_status

    def log_messages(msgs):
        for log_msg in msgs:
            if 'Internal error.' in log_msg[0]:
                lib.log.send_err_msg(log_msg[0], details_msg=log_msg[1])
            else:
                lib.log.send_debug_msg(log_msg[0], details_msg=log_msg[1])

    def maximum_time_to_determine_state():
        interval = link["check"]["method"]["icmp"]["interval"]
        restore_interval = link["check"]["method"]["icmp"]["restore_interval"]
        max_failure_count = link["check"]["method"]["icmp"]["failure_count"]
        max_restore_count = link["check"]["method"]["icmp"]["restore_count"]
        hosts_num = 0
        if "ipv4" in link["check"]:
            hosts_num += len(link["check"]["ipv4"]["hosts"])
        if "ipv6" in link["check"]:
            hosts_num += len(link["check"]["ipv6"]["hosts"])
        # time_to_check_all_hosts показывает максимальное
        # время получения результата выполнения icmp_check_result.
        # Время не зависит от режима проверки доступности хостов, потому
        # что для каждого режима, доступность может быть определена
        # последним хостом.
        # Для "any", это когда все кроме последнего точно недоступны.
        # Для "all", это когда все кроме последнего точно доступны.
        # Тогда именно доступность или недоступность последнего хоста
        # определит результат выполнения проверки.
        time_to_check_all_hosts = hosts_num * link["check"]["method"]["icmp"]["max_rtt"] / 1000

        # Между выполнениями проверок хостов проходит некий интервал.
        # Поэтому за период выполнения проверки можно считать сумму
        # интервала и время проверки хостов. Тогда время определения состояния доступности будет
        # умножением периода и количества проверок.
        time_to_determine_link_is_reachable = \
            (interval + time_to_check_all_hosts) * max_failure_count
        time_to_determine_link_is_unreachable = \
            (restore_interval + time_to_check_all_hosts) * max_restore_count

        return max(time_to_determine_link_is_reachable, time_to_determine_link_is_unreachable)

    def replace_icmp_rules_and_routes():
        _hosts_ip, _source_ip, _nexthop, _routes_src_ip = get_icmp_routes_params(link, msg_prefix)
        messages = lib.route.add_icmp_rules_and_routes(source_ip=_source_ip,
                                                       icmp_params=icmp_params,
                                                       hosts=_hosts_ip,
                                                       nexthop=_nexthop,
                                                       routes_src_ip=_routes_src_ip,
                                                       iface_name=iface_name,
                                                       ipver_map=lib.common_vars.IP_VER_MAP,
                                                       proto_num=DAEMON_PROTO_NUM,
                                                       msg_prefix=msg_prefix)
        log_messages(messages)

        return _hosts_ip, _source_ip, _nexthop

    icmp_params = link["check"]["method"]["icmp"]
    msg_prefix = f'Group {group_name}: Link name {link["name"]}: Prio {link["prio"]}: '

    iface_name = link["iface_name"]

    hosts_ip, source_ip, nexthop = replace_icmp_rules_and_routes()

    failure_count = 0
    restore_count = 0
    next_time_to_watch_icmp_routes_and_rules = time.time() + watch_interval

    # Available delay for determining link state.
    # 10 seconds were taken from head.
    AVAILABLE_DELAY = 10
    # Time we should log if link state is not defined
    determine_link_state_timeout = maximum_time_to_determine_state() + AVAILABLE_DELAY

    time_to_log_undefined_state = time.time() + determine_link_state_timeout
    # disables spam in log if not all link states are defined
    undefined_state_logged_flag = False
    while True:
        if lib.common.exit_flag or lib.common.reload_config_flag:
            break

        if link["ipv4_dhcp"]["enable"]:
            # Nexthop has value the 'DHCP' after start of multiwan and
            # before the DHCP hook signal is received.
            if not lib.params.check_ip_address(link["routes"]["ipv4"]["nexthop"], 'IPv4Address'):
                lib.log.send_debug_msg(f'{msg_prefix}Interface {iface_name} (DHCP). '
                                       f'Nexthop was not received via DHCP.')
                # Если интерфейс не получил nexthop адрес по DHCP,
                # то мы знаем, что линк недоступен для пингования, но
                # мы должны увеличить счётчик неудачных попыток.
                # Если не увеличивать счетчик неудачных попыток,
                # то линк никогда не станет unreachable и переключение
                # на другой доступный линк никогда не произойдет.
            elif nexthop["ipv4"] != link["routes"]["ipv4"]["nexthop"]:
                hosts_ip, source_ip, nexthop = replace_icmp_rules_and_routes()
                next_time_to_watch_icmp_routes_and_rules = time.time() + watch_interval

        if link["ipv4_dhcp"]["enable"] and nexthop["ipv4"] == 'DHCP':
            icmp_check_result = False
        else:
            if icmp_params["watch_rules"] and time.time() >= next_time_to_watch_icmp_routes_and_rules:
                hosts_ip, source_ip, nexthop = replace_icmp_rules_and_routes()
                next_time_to_watch_icmp_routes_and_rules = time.time() + watch_interval

            icmp_check_result = icmp_hosts_check(hosts=hosts_ip,
                                                 interface=iface_name,
                                                 icmp_params=icmp_params,
                                                 source_ip=source_ip,
                                                 reachability=link["check"]["hosts_reachability"],
                                                 msg_prefix=msg_prefix)

        if icmp_check_result is None:
            break

        if link["status"]["reachable"]:
            if icmp_check_result:
                failure_count = 0
            else:
                failure_count += 1

            if failure_count >= icmp_params["failure_count"]:
                set_link_status(False)
                failure_count = 0
            interruptible_sleep(icmp_params["interval"])
        else:
            if link["status"]["reachable"] is None:
                if not undefined_state_logged_flag and time.time() > time_to_log_undefined_state:
                    msg = (f'{msg_prefix}could not determine link state'
                           f' within timeout {determine_link_state_timeout}.')
                    lib.log.send_warning_msg(msg)
                    undefined_state_logged_flag = True

            if icmp_check_result:
                restore_count += 1
                if link["status"]["reachable"] is None:
                    failure_count = 0
            else:
                restore_count = 0
                if link["status"]["reachable"] is None:
                    failure_count += 1
                    if failure_count >= icmp_params["failure_count"]:
                        failure_count = 0
                        set_link_status(False)
                interruptible_sleep(icmp_params["interval"])
                continue

            if restore_count >= icmp_params["restore_count"]:
                set_link_status(True)
                restore_count = 0

            interruptible_sleep(icmp_params["restore_interval"])


def add_routes(link_params, ip_ver_name, group_name='default'):
    """
    Add link routes for a specific IP version.

    link_params - dictionary with link parameters.
    ip_ver_name - IP version of routes.
    group_name  - group name for link.
    """

    networks = link_params["routes"][ip_ver_name]["networks"]
    gateway = link_params["routes"][ip_ver_name]["nexthop"]
    iface_name = link_params["iface_name"]
    link_prio = link_params["prio"]
    metric = 1000 - link_prio
    src_ip = link_params["routes"][ip_ver_name]["source_address"]
    table_num = link_params["routes"]["routes_table_num"]
    msg_prefix = f'Group {group_name}: Link name {link_params["name"]}: Prio {link_prio}: '
    messages = lib.route.replace_routes(networks=networks,
                                        gateway=gateway,
                                        iface_name=iface_name,
                                        metric=metric,
                                        proto_num=DAEMON_PROTO_NUM,
                                        table_num=table_num,
                                        src_ip=src_ip,
                                        msg_prefix=msg_prefix)

    for log_msg in messages:
        lib.log.send_debug_msg(log_msg[0], details_msg=log_msg[1])


def del_routes(link_params, ip_ver_name, group_name='default'):
    """
    Removes link routes for a specific IP version.

    link_params - dictionary with link parameters.
    ip_ver_name - IP version of routes.
    group_name  - group name for link.

    returns list of tuples. The first element of the tuple is message
    about the success of the deletion. The second element is detailed
    error message if the deletion failed.
    """

    networks = link_params["routes"][ip_ver_name]["networks"]
    gateway = link_params["routes"][ip_ver_name]["nexthop"]
    iface_name = link_params["iface_name"]
    metric = 1000 - link_params["prio"]
    src_ip = link_params["routes"][ip_ver_name]["source_address"]
    table_num = link_params["routes"]["routes_table_num"]
    proto_num = DAEMON_PROTO_NUM

    messages = []

    msg_prefix = f'Group {group_name}: '
    iface_id = lib.route.get_link_id(iface_name)
    if not iface_id:
        error_msg = f'{msg_prefix}Interface {iface_name} not found.'
        details_msg = ''
        messages.append((error_msg, details_msg))
        return messages

    for network in networks:
        err_type, err_details_msg = lib.route.route_mgr(command='del',
                                                        dst=network,
                                                        gateway=gateway,
                                                        priority=metric,
                                                        proto=proto_num,
                                                        table_num=table_num,
                                                        oif_id=iface_id,
                                                        src=src_ip)
        if not err_type:
            msg = f'{msg_prefix}Route {network} was deleted via {gateway} in table {table_num}.'
            details_msg = ''
        else:
            msg = (f'{err_type} error. {msg_prefix}Cannot delete route {network} via {gateway}'
                   f' in table {table_num}.')
            details_msg = (f'destination address: {network}\n'
                           f'gateway: {gateway}\n'
                           f'priority: {metric}\n'
                           f'proto: {proto_num}\n'
                           f'table: {table_num}\n'
                           f'interface: {iface_name}\n'
                           f'Exception message: {err_details_msg}')

        messages.append((msg, details_msg))

    return messages


def clear_sa_conn(msg_prefix=''):
    if not os.access(lib.common_vars.TMP_VPNSVC, os.F_OK):
        msg = (f'{msg_prefix}No need to clear IKE/IPsec tunnels,'
               ' because "vpnsvc" not started.')
        lib.log.send_info_msg(msg)
        return True

    cmd = f'{lib.common_vars.SA_MGR} clear -all'
    with clear_sa_conn_lock:
        stdout, stderr, code, exited_reason = lib.common.externcmd(cmd)

    if code != 0:
        msg = f'{msg_prefix}IKE/IPsec tunnels were not cleared.'
        # TODO: подумать над выносом exited_reason в отдельную функцию. Данное сравнение в 4-ёх местах.
        if exited_reason == "exited":
            details_msg = f'{cmd} exited with exit code: {code}.'
        elif exited_reason == "global_exit_flag":
            details_msg = f'{cmd} received an administrative request to terminate.'
        elif exited_reason == "process_not_started":
            details_msg = f'{cmd} failed to run'
            lib.log.send_warning_msg(f'{msg}', details_msg=details_msg)
            return False
        elif exited_reason == "timeout":
            # Это защита от разработчика.
            # Команда может выполняться большое количество времени и
            # поэтому таймаут должен быть 0. Если кто-то выставит
            # таймаут отличный от 0, то команда может завершить своё
            # выполнение по таймауту. Исключение будет легче
            # заметить, чем одиночное сообщение.
            raise ValueError(f'Unexpected exited_reason("timeout") from lib.common.externcmd for {cmd}')
        else:
            raise ValueError('Unexpected exited_reason from lib.common.externcmd')

        details = (f'{details_msg}\n'
                   f'STDOUT:\n'
                   f'{stdout}\n'
                   f'STDERR:\n'
                   f'{stderr}\n'
                   f'Return code: {code}')
        lib.log.send_warning_msg(f'{msg}', details_msg=details)
        return False

    msg = f'{msg_prefix}IKE/IPsec tunnels were cleared.'
    lib.log.send_info_msg(msg)
    return True


def reload_lsp(msg_prefix=''):
    if not os.access(lib.common_vars.TMP_VPNSVC, os.F_OK):
        msg = f'{msg_prefix}No need to reload LSP, because "vpnsvc" not started.'
        lib.log.send_info_msg(msg)
        return True

    cmd = f'{lib.common_vars.LSP_MGR} reload'
    with reload_lsp_lock:
        stdout, stderr, code, exited_reason = lib.common.externcmd(cmd)

    if code != 0:
        msg = f'{msg_prefix}LSP was not reloaded.'
        if exited_reason == "exited":
            details_msg = f'{cmd} exited with exit code: {code}.'
        elif exited_reason == "global_exit_flag":
            details_msg = f'{cmd} received an administrative request to terminate.'
        elif exited_reason == "process_not_started":
            details_msg = f'{cmd} failed to run'
            lib.log.send_warning_msg(f'{msg}', details_msg=details_msg)
            return False
        elif exited_reason == "timeout":
            # Это защита от разработчика.
            # Команда может выполняться большое количество времени и
            # поэтому таймаут должен быть 0. Если кто-то выставит
            # таймаут отличный от 0, то команда может завершить своё
            # выполнение по таймауту. Исключение будет легче
            # заметить, чем одиночное сообщение.
            raise ValueError(f'Unexpected exited_reason("timeout") from lib.common.externcmd for {cmd}')
        else:
            raise ValueError('Unexpected exited_reason from lib.common.externcmd')
        details = (f'{details_msg}\n'
                   f'STDOUT:\n'
                   f'{stdout}\n'
                   f'STDERR:\n'
                   f'{stderr}\n'
                   f'Return code: {code}')
        lib.log.send_warning_msg(f'{msg}', details_msg=details)
        return False

    msg = f'{msg_prefix}LSP was reloaded.'
    lib.log.send_info_msg(msg)
    return True


def is_openwrt_os():
    """
    Checking that the OS is openwrt
    """

    if not os.access('/proc/version', os.R_OK):
        msg = 'Read permission is not granted to file: "/proc/version".'
        lib.log.send_warning_msg(msg)
        return False

    try:
        with open('/proc/version') as fd:
            inside_file = fd.read()
            if not re.search('OpenWrt', inside_file):
                return False
    except Exception:
        return False

    return True


def clear_all_conntrack_translations(msg_prefix=''):
    conntrack_path = lib.common_vars.CONNTRACK_PATH
    conntrack_count_path = '/proc/sys/net/netfilter/nf_conntrack_count'
    conntrack_count = None
    try:
        with open(conntrack_count_path) as fd:
            conntrack_count = int(fd.readline().strip())
    except FileNotFoundError:
        conntrack_count = 0
    except Exception as ex:
        msg = f'{msg_prefix}Failed to get conntrack entries count from {conntrack_count_path}.'
        lib.log.send_warning_msg(msg, details_msg=ex)

    if type(conntrack_count) is int:
        lib.log.send_info_msg(f'{msg_prefix}{conntrack_count} entries '
                              f'in conntrack table will be deleted.')

    if os.access(conntrack_path, os.F_OK):
        # TODO: обдумать очистку таблицы expect "-F expect"
        cmd = f'{conntrack_path} -F conntrack'
        # При удалении с помощью "conntrack -D conntrack" в stdout
        # выводятся все удалённые записи, что может привести
        # к зависанию выполнения python.
    elif is_openwrt_os():
        with clear_conntrack_lock:
            details_err_msg = lib.file.write_to_file(file='/proc/net/nf_conntrack', data='f')
        if details_err_msg:
            msg = f'{msg_prefix}conntrack table has not been flushed.'
            lib.log.send_warning_msg(msg, details_msg=details_err_msg)
            return False

        msg = f'{msg_prefix}conntrack table has been flushed.'
        lib.log.send_info_msg(msg)
        return True
    else:
        msg = (f'{msg_prefix}conntrack table has not been flushed, because '
               'there are no appropriate methods to do it on this OS.')
        lib.log.send_warning_msg(msg)
        return False

    with clear_conntrack_lock:
        stdout, stderr, code, exited_reason = lib.common.externcmd(cmd)
    if code != 0:
        msg = f'{msg_prefix}conntrack table has not been flushed.'
        if exited_reason == "exited":
            details_msg = f'{cmd} exited with exit code: {code}.'
        elif exited_reason == "global_exit_flag":
            details_msg = f'{cmd} received an administrative request to terminate.'
        elif exited_reason == "process_not_started":
            details_msg = f'{cmd} failed to run'
            lib.log.send_warning_msg(f'{msg}', details_msg=details_msg)
            return False
        elif exited_reason == "timeout":
            # Это защита от разработчика.
            # Команда может выполняться большое количество времени и
            # поэтому таймаут должен быть 0. Если кто-то выставит
            # таймаут отличный от 0, то команда может завершить своё
            # выполнение по таймауту. Исключение будет легче
            # заметить, чем одиночное сообщение.
            raise ValueError(f'Unexpected exited_reason("timeout") from lib.common.externcmd for {cmd}')
        else:
            raise ValueError('Unexpected exited_reason from lib.common.externcmd')

        details = (f'{details_msg}\n'
                   f'STDOUT:\n'
                   f'{stdout}\n'
                   f'STDERR:\n'
                   f'{stderr}\n'
                   f'Return code: {code}')
        lib.log.send_warning_msg(f'{msg}', details_msg=details)
        return False

    msg = f'{msg_prefix}conntrack table has been flushed.'
    lib.log.send_info_msg(msg)
    return True


def set_link_activity_status(link, status):
    """
    Set status for link "status"/"active".
    """
    with link_status_lock:
        link["status"]["active"] = status


def get_link_status_from_group_by_index(group_name, index):
    with link_status_lock:
        return links_status[group_name][index]


def delete_routes_of_link(link_params, group_name='default', ip_ver_name=None):
    """
    Delete route of link.

    notify_scripts_params - dictionary with notify scripts parameters.
    link_params           - dictionary with link parameters.
    group_name            - group name for link.
    ip_ver_name           - IP version of routes. If None, IPv4 and IPv6
                            routes should be removed.
    """
    msg_prefix = (f'Group {group_name}: Link name {link_params["name"]}: '
                  f'Prio {link_params["prio"]}: ')
    # If the ip_ver_name is not specified, then delete IPv4 and
    # IPv6 routes
    messages = []
    if ip_ver_name is None:
        for ip_ver in lib.common_vars.IP_VER_MAP:
            if ip_ver in link_params["routes"]:
                messages.extend(del_routes(link_params=link_params,
                                           ip_ver_name=ip_ver,
                                           group_name=group_name))
    else:
        messages.extend(del_routes(link_params=link_params,
                                   ip_ver_name=ip_ver_name,
                                   group_name=group_name))

    old_table_num = link_params["routes"]["routes_table_num"]
    # messages contain main message and error message details.
    # Error message details exists if we found route and try to delete
    # this, but catch exception.
    if len([message for message in messages if message[1]]) != 0:
        lib.log.send_err_msg(f'Internal error. {msg_prefix}Cannot delete all routes.')
        for log_msg in messages:
            lib.log.send_debug_msg(f'{msg_prefix}{log_msg[0]}', details_msg=log_msg[1])
    else:
        msg = f'{msg_prefix}All {DAEMON_NAME} routes are removed from table {old_table_num}.'
        lib.log.send_debug_msg(msg)


def change_active_link(group_params, old_link_index, new_link_index,
                       group_name, first_change_link_flag=False):
    """
    Delete old daemon route and add route via new link.

    group_params   - Dictionary with parameters of group.
    old_link_index - index of old link in the group's link list.
    new_link_index - index of new link in the group's link list.
    group_name     - group name
    first_change_flag - flag to change message after change link.
                        Boolean.
    """

    notify_scripts = group_params["notify"]["scripts"]
    new_link_params = get_link_status_from_group_by_index(group_name=group_name,
                                                          index=new_link_index)
    clear_sa_conn_flag = group_params["links_params"]["clear_sa_conn"]
    reload_lsp_flag = group_params["links_params"]["reload_lsp"]
    clear_conntrack_flag = group_params["links_params"]["clear_all_conntrack_translations"]
    msg_prefix = f'Group {group_name}: '

    # Порядок изменения активного линка должен быть такой:
    #       1. Установка нового маршрута
    #       2. Удаление старого маршрута
    #       3. Очистка соединений conntrack
    #       4. Перезагрузка LSP
    # Такой порядок необходим, чтобы избежать образования некорректной
    # записи conntrack, которая будет препятствовать корректной работе
    # SNAT.
    # Некорректная запись conntrack может формироваться при выполнении
    # следующих условий:
    # 1. В ОС есть маршрут в сеть назначения маршрутов multiwan с более
    #    низким приоритетом, чем маршруты multiwan;
    # 2. Удаление старого маршрута и очистка conntrack выполняется _ДО_
    #    установки нового.
    # В ненулевом интервале времени между окончанием чистки conntrack
    # и установкой нового маршрута multiwan работает чужой маршрут по
    # пункту 1. Для пакета, поступившего в этот интервал времени,
    # создастся некорректная conntrack запись и SNAT в соответствии
    # с новым маршрутом применяться не будет.
    if notify_scripts["prechangelink"]:
        _ = exec_notify_script(notify_script_params=notify_scripts["prechangelink"],
                               group_name=group_name, state="pre_change_link")

    if lib.common.exit_flag or lib.common.reload_config_flag:
        return

    nexthop = []
    routes = new_link_params["routes"]
    for ip_ver_name in lib.common_vars.IP_VER_MAP:
        if ip_ver_name in routes:
            add_routes(link_params=new_link_params, ip_ver_name=ip_ver_name, group_name=group_name)
            if routes[ip_ver_name]["nexthop"]:
                nexthop.append(routes[ip_ver_name]["nexthop"])

    if old_link_index is not None:
        set_link_activity_status(link=links_status[group_name][old_link_index], status=False)

        old_link_params = get_link_status_from_group_by_index(group_name=group_name,
                                                              index=old_link_index)
        delete_routes_of_link(link_params=old_link_params, group_name=group_name)

    if lib.common.exit_flag or lib.common.reload_config_flag:
        return

    if clear_conntrack_flag:
        clear_all_conntrack_translations(msg_prefix)

    if lib.common.exit_flag or lib.common.reload_config_flag:
        return

    # Отправка Delete payload пакетов должна происходит до reload lsp,
    # чтобы уведомить смежников о завершении SA. При выполнении reload
    # LSP так же будут удалены SA, но будет действовать ddp dropall и
    # Delete payload при завершении SA не будут доставлены смежникам.
    # Для них SA будут еще действующими и они будут присылать DPD,
    # которые будут drop'аться при приеме. Из-за этого у смежников могут
    # наблюдаться временные, ничейные ISAKMP, не привязанные к IPsec.
    if clear_sa_conn_flag:
        clear_sa_conn(msg_prefix)

    if lib.common.exit_flag or lib.common.reload_config_flag:
        return

    if reload_lsp_flag:
        reload_lsp(msg_prefix)

    # postchangelink не будет выполняться при reload или выходе.
    # Данное поведение считается нормальным, так как нет гарантии завершения
    # перехода с одного линка на другой.
    if notify_scripts["postchangelink"]:
        _ = exec_notify_script(notify_script_params=notify_scripts["postchangelink"],
                               group_name=group_name, state="post_change_link")

    if first_change_link_flag:
        text_msg = "Used interface"
    else:
        text_msg = "Switched to interface"

    active_nexthops = '"/"'.join(nexthop)
    interface = new_link_params["iface_name"]
    if len(nexthop) == 1:
        msg = f'{msg_prefix}{text_msg} "{interface}". Nexthop "{active_nexthops}".'
    else:
        msg = f'{msg_prefix}{text_msg} "{interface}". Nexthops "{active_nexthops}".'

    lib.log.send_info_msg(msg)
    set_link_activity_status(link=links_status[group_name][new_link_index], status=True)


def clear_daemon_route_and_rules():
    internal_error = False
    _, exception_info = lib.route.delete_routes(proto_num=DAEMON_PROTO_NUM)

    if exception_info:
        lib.log.send_warning_msg('Internal error. Cannot delete routes.',
                                 details_msg=exception_info)
        internal_error = True

    for ipv_map in lib.common_vars.IP_VER_MAP.values():
        _, exception_info = lib.route.delete_rules(family=ipv_map['family_enum'],
                                                   proto_num=DAEMON_PROTO_NUM)
        if exception_info:
            lib.log.send_warning_msg(f'Internal error. Cannot delete {ipv_map["name"]} rules.',
                                     details_msg=exception_info)
            internal_error = True

    return internal_error


def interruptible_sleep(timeout):
    """
    Sleep until exit or reset signals are received or timeout expires.
    """

    wake_up_time = time.time() + timeout
    while time.time() < wake_up_time:
        if lib.common.exit_flag or lib.common.reload_config_flag:
            break

        time.sleep(1)


def cleanup():
    """
    Delete multiwan rules and routes.

    return 0 if all good.
    return 1 if threads still work.
    return 2 if routes were not deleted.
    """
    return_code = 0
    # After 90 seconds (set in the init file) SIGKILL signal will be
    # send to multiwan.
    # Maximum timeout for user script is 60 sec. We assume all processes
    # in multiwan will be completed within 10 seconds of scripts
    # completed.
    time_count = 60 + 10
    while time_count:
        if lib.common.exit_flag:
            # При получении сигнала на завершение, выставляется
            # lib.common.exit_flag. Все треды кроме основного, должны
            # отследить установку этого флага и завершиться.
            thread_limit = 1  # main thread
        else:  # get reload signal
            # При получении сигнала на перечитывание конфигурационного файла,
            # выставляется lib.common.reload_config_flag. Все треды кроме
            # основного и треда сервера для mwan_mgr, должны отследить
            # установку флага и завершиться. Во время перечитывания, тред
            # сервера останавливать свою работу НЕ должен.
            thread_limit = 2  # main and server threads
        service_notify.set_watchdog()
        if threading.active_count() > thread_limit:
            time.sleep(1)
            time_count -= 1
            continue
        break
    else:
        lib.log.send_err_msg('Internal error. Threads still work.')
        return_code = 1

    global link_status_lock, links_status
    with link_status_lock:
        links_status = {}
    link_status_lock = threading.RLock()

    error_msg = lib.file.delete_path(lib.common_vars.RUN_CONFIG)
    if error_msg:
        lib.log.send_warning_msg(error_msg)

    error_msg = lib.file.delete_path(lib.common_vars.IS_RUNNING_FILE)
    if error_msg:
        lib.log.send_warning_msg(error_msg)

    if clear_daemon_route_and_rules():
        return 2

    return return_code


def stopping_cleanup():
    """
    Cleanup when get stop signal

    return 0 if all good.
    return 1 if threads still work.
    return 2 if routes were not deleted
    return 255 if is_running-file has not been deleted.
    """
    exit_code_num = cleanup()
    lib.log.send_info_msg(f'{DAEMON_NAME} was stopped.')

    # Not necessary. sys.exit must close netlink socket.
    lib.route.ipr_socket.close()
    error_msg = lib.file.delete_path(lib.common_vars.MULTIWAN_FILE_PID)
    if error_msg:
        lib.log.send_warning_msg(error_msg)

    error_msgs = service_notify.set_stopped()
    if error_msgs:
        lib.log.send_warning_msg(error_msgs[0], details_msg=error_msgs[1])
        return 255
    return exit_code_num


def get_icmp_routes_params(link_params, msg_prefix=''):
    """
    Get IP addresses for hosts, source IP address, nexthop IP address
    and routes source IP address for link.

    link_params - immutable link parameters throughout the life of
                  thread. Dictionary.
    msg_prefix  - prefix to add to log messages. String.

    IP rules require IP address. If you cannot get IP address for
    hostname, return None for the hostname.
    Example input list of IPv4 hostnames:
        ['google.com', 'test.test.com', '10.0.0.1']
    Output list of IP address for example input list:
        ['64.233.165.193', None, '10.0.0.1']
    """

    route_params = link_params["routes"]
    check_params = link_params["check"]
    hosts_ip = {}
    source_ip = {}
    nexthop = {}
    routes_src_ip = {}
    ping = lib.icmp_packet_send.PingClass()
    for ip_ver_name in lib.common_vars.IP_VER_MAP:
        if ip_ver_name not in check_params:
            continue

        hosts_ip[ip_ver_name] = []
        for host in check_params[ip_ver_name]["hosts"]:
            ip_address, error_msg, details_err_msg = ping.get_ip_address_to_host(
                destination_host=host,
                ip_version=lib.common_vars.IP_VER_MAP[ip_ver_name]["ver_number"])
            hosts_ip[ip_ver_name].append(ip_address)

            if ip_address is None:
                lib.log.send_debug_msg(f'{msg_prefix}{error_msg}', details_msg=details_err_msg)
            elif host != ip_address:
                msg = f'{msg_prefix}{ip_ver_name} host {host} converted to {ip_address}'
                lib.log.send_debug_msg(msg)

        source_ip[ip_ver_name] = check_params[ip_ver_name]["source_address"]
        nexthop[ip_ver_name] = route_params[ip_ver_name]["nexthop"]
        routes_src_ip[ip_ver_name] = route_params[ip_ver_name]["source_address"]

    return hosts_ip, source_ip, nexthop, routes_src_ip


def get_networks_to_watch(grouped_params):
    """
    Get the networks that need to be added using multiwan.

    grouped_params - dictionary with grouped parameters.
    """

    params = copy.deepcopy(grouped_params)
    self_routes = {}
    for group_params in params.values():
        for link_params in group_params["links_params"]["links"]:
            if not link_params["routes"]["watch_foreign_routes"]:
                continue
            table = link_params["routes"]["routes_table_num"]
            for ipver in lib.common_vars.IP_VER_MAP:
                if ipver in link_params["routes"]:
                    if ipver not in self_routes:
                        self_routes.update({ipver: {}})
                    link_networks = link_params["routes"][ipver]["networks"]
                    if table in self_routes[ipver]:
                        self_routes[ipver][table].extend(link_networks)
                    else:
                        self_routes[ipver].update({table: link_networks})

    return self_routes


def watch_foreign_routes(self_routes_params):
    """
    Mechanism for deleting not daemon routes.

    Foreign routes are specified routes with protocol number different
    from daemon protocol number.
    link_params   - link parameters with interface name, priority of
                    link, networks, nexthop, source address and routes
                    table number. Dictionary.
    msg_prefix  - prefix for log messages. String.
    """

    def delete_route(route_dst, table_number, socket_family, dst_len, scope, priority):
        err_type, err_details_msg = lib.route.route_mgr(command='del',
                                                        dst=route_dst,
                                                        table_num=table_number,
                                                        family=socket_family,
                                                        dst_len=dst_len,
                                                        scope=scope,
                                                        priority=priority)

        if socket_family == 2:
            ip_version = 'IPv4'
        else:  # socket_family=10
            ip_version = 'IPv6'

        if dst_len == 0:
            dst = route_dst  # 'default'
        else:
            dst = f'{route_dst}/{dst_len}'  # '192.168.100.0/24'

        if err_type:
            message = f'{err_type} error. Cannot delete route.'
            details_msg = (f'destination {ip_version} address: {dst}\n'
                           f'table: {table_number}\n'
                           f'scope: {scope}\n'
                           f'priority: {priority}\n'
                           f'Exception message: {err_details_msg}')
        else:
            message = (f'Route deleted:\n'
                       f'  destination {ip_version} address: {dst}\n'
                       f'  table: {table_number}\n'
                       f'  scope: {scope}\n'
                       f'  priority: {priority}\n')
            details_msg = ''

        if err_type == 'Internal':
            lib.log.send_err_msg(message, details_msg=details_msg)
        else:
            lib.log.send_debug_msg(message, details_msg=details_msg)

    # self_routes_params = {'ipv4':{100: ['192.168.1.0/24', '192.168.2.0/24'],
    #                               101: ['192.168.3.0/24', '192.168.4.0/24']},
    #                       'ipv6': {100: ['2001:db8::192.168.1.0/24', '2001:db8::192.168.2.0/24',],
    #                                101: ['2001:db8::192.168.3.0/24', '2001:db8::192.168.4.0/24',}}
    for ip_ver, networks_params in self_routes_params.items():
        family = lib.common_vars.IP_VER_MAP[ip_ver]["family_enum"]
        for table_num, self_networks in networks_params.items():
            routes_in_table, exception_info = lib.route.get_routes(table_num=table_num,
                                                                   family=family)

            if exception_info:
                msg = f'Internal error. Cannot get routes.'
                lib.log.send_err_msg(msg, details_msg=exception_info)
                return False

            # Example route_data(class 'pyroute2.netlink.rtnl.rtmsg.rtmsg'):
            #   'family': 2,
            #   'dst_len': 0,
            #   'src_len': 0,
            #   'tos': 0,
            #   'table': 254,
            #   'proto': 3,
            #   'scope': 0, 'type': 1, 'flags': 0,
            #   'attrs': [('RTA_TABLE', 254),
            #             ('RTA_PRIORITY', 100),
            #             ('RTA_GATEWAY', '192.168.0.1'),
            #             ('RTA_OIF', 2)],
            #   'header': {'length': 60,
            #              'type': 24,
            #              'flags': 2,
            #              'sequence_number': 270,
            #              'pid': 505,
            #              'error': None,
            #              'target': 'localhost',
            #              'stats': Stats(qsize=0, delta=0, delay=0)},
            #   'event': 'RTM_NEWROUTE'
            for route_data in routes_in_table:
                if route_data["proto"] == DAEMON_PROTO_NUM:
                    continue

                route_netmask = route_data["dst_len"]
                if route_netmask == 0:  # dst_len is zero for default route
                    route_data_dst = 'default'
                    route_network = lib.common_vars.IP_VER_MAP[ip_ver]["default_route"]
                else:
                    route_data_dst = route_data.get_attr('RTA_DST')
                    route_network = f'{route_data_dst}/{route_netmask}'

                if route_network in self_networks:
                    delete_route(route_dst=route_data_dst,
                                 table_number=route_data.get_attr('RTA_TABLE'),
                                 socket_family=route_data["family"],
                                 dst_len=route_data["dst_len"],
                                 priority=route_data.get_attr('RTA_PRIORITY'),
                                 scope=route_data["scope"])

    return True


def get_params_from_config(path_to_config):
    """
    Get parameters from config file and write to file

    return link parameters grouped by group names, dictionary with
    intervals for watch_foreign_routes, watch_self_routes
    and watch_rules, dictionary with poststart script params and
    dictionary with poststart script params.
    """
    config_params, regrouped_params = lib.params.get_params_from_config(path_to_config,
                                                                        lib.log.msg_level_output)
    if not config_params:
        msg = f'Cannot get parameters from config file "{path_to_config}".'
        lib.log.send_err_msg(msg)
        return None, None, None, None

    error_msg = lib.file.write_to_file(file=lib.common_vars.RUN_CONFIG,
                                       data=yaml.dump(config_params))
    if error_msg:
        lib.log.send_warning_msg(error_msg)

    watch_intervals = config_params['daemon_params']['watch_intervals']
    lib.log.send_debug_msg(f'Parsed parameters:\n{config_params}')
    poststart = config_params['notify']['scripts']['poststart']
    prestop = config_params['notify']['scripts']['prestop']

    return regrouped_params, watch_intervals, poststart, prestop


def get_reachable_link_index_with_highest_prio(group_name='default'):
    """
    Get link index with the highest priority and state is reachable.

    return -2, if not all links state defined;
    return -1, if all links is down;
    return index of links_status.
    """

    link_index_with_max_prio = None
    with link_status_lock:
        for link_index in range(len(links_status[group_name])):
            reachable = links_status[group_name][link_index]["status"]["reachable"]

            if reachable and link_index_with_max_prio is None:
                link_index_with_max_prio = link_index
            elif reachable is None:
                return -2

    if link_index_with_max_prio is None:
        return -1
    return link_index_with_max_prio


def start_link_threads(links_params, group_name='default', watch_rules_interval=1):
    """
    Start link threads and return time to warning if link state is not determine.

    links_params                 - list with links parameters. List.
    notify_poststart             - dictionary with parameters to notify.
                                   Dictionary.
    group_name                   - group name from yaml config. String.
    watch_rules_interval         - watch rules interval. Integer.

    Used global dictionary 'links_status'.

    return time to warning if link state is not determine.
    """

    with link_status_lock:
        links_status[group_name] = []
    for link in links_params:
        link_params = copy.deepcopy(link)
        link_params["status"] = {"reachable": None, "active": False}

        link_params["ipv4_dhcp"] = {"enable": False,
                                    "need_set_new_nexthop": False,
                                    "new_nexthop": None}
        if "ipv4" in link["routes"] and link["routes"]["ipv4"]["nexthop"] == 'DHCP':
            link_params["ipv4_dhcp"]["enable"] = True

        with link_status_lock:
            links_status[group_name].append(link_params)

        thread = threading.Thread(target=check_link,
                                  args=(link_params,
                                        group_name,
                                        watch_rules_interval),
                                  daemon=True)
        thread.start()


def server_socket(socket_file):
    """
    Create server socket thread.

    socket_file - file to unix socket.

    Server socket used for work with mwan_mgr.
    """

    def get_links_status():
        params_list = {}
        for group in links_status:
            params_list[group] = []
            for index in range(len(links_status[group])):
                params_list[group].append(links_status[group][index])

        return json.dumps(params_list)

    server = lib.socket.prepare_server_socket(socket_file)
    while not lib.common.exit_flag:
        server.listen(1)
        # Required to check socket file every 10 second.
        server.settimeout(10)
        try:
            conn, _ = server.accept()
        except socket.timeout:
            continue

        # If connection is accepted, peer has 5 seconds to send
        # message.
        conn.settimeout(5)
        try:
            header_bytes, _ = lib.socket.read_bytes_from_socket(conn, 5)
        except socket.timeout:
            conn.close()
            continue

        if not header_bytes:
            conn.close()
            continue

        packet_type, data_len = struct.unpack('!BI', header_bytes)
        reply_data = ''
        reply_type = 255
        request_types = copy.deepcopy(lib.common_vars.PACKET_REQUEST_TYPES)
        if packet_type in request_types:
            packets_info = request_types[packet_type]
            try:
                data_bytes, _ = lib.socket.read_bytes_from_socket(conn, data_len)
            except socket.timeout:
                data_bytes = b''

            data = data_bytes.decode('utf-8')
            supported_request_data = packets_info['request_data']
            if data and (supported_request_data is None or data in supported_request_data):
                reply_type = packets_info['reply_type']
                if packet_type == 1:
                    with link_status_lock:
                        reply_data = get_links_status()
                elif packet_type == 3:
                    lib.log.set_log_level(data)
                    reply_data = 'success'
                elif packet_type == 5:
                    reply_data = lib.log.get_log_level()

        reply_data_bytes = reply_data.encode('utf-8')
        if not lib.socket.send_data_to_socket(conn, reply_type, reply_data_bytes):
            continue

        # Required to close the connection after 5 seconds
        # if the peer has not closed.
        conn.settimeout(5)
        try:
            lib.socket.read_bytes_from_socket(conn, 5)
        except socket.timeout:
            conn.close()


def group_thread(group_params, group_name, watch_intervals):
    """
    Creates tread for group logic.

    group_params    - dictionary with group params;
    group_name      - sting with group name;
    watch_intervals - dictionary with intervals for watch_self_routes
                      and watch_rules.
    """
    notify = group_params["notify"]["scripts"]
    clear_unreachable_link_routes_flag = \
        group_params["links_params"]["clear_routes_if_all_links_unreachable"]
    watch_self_routes_interval = watch_intervals['watch_self_routes']

    # start_link_threads create global variable "links_status"
    # Example links_status:
    #  {"<Group1 name>": [{<Link1 parameters>,
    #                     "status": {"reachable": None,
    #                                "active": False},
    #                     "ipv4_dhcp": {"enable": False, 
    #                                   "need_set_new_nexthop": False, 
    #                                   "new_nexthop": None}},
    #                     {<Link2 parameters>,
    #                      "status": {"reachable": None,
    #                                 "active": False},
    #                      "ipv4_dhcp": {"enable": False, 
    #                                   "need_set_new_nexthop": False, 
    #                                   "new_nexthop": None}}]
    #   "<Group2 name>": [{<Link1 parameters>,
    #                      "status": {"reachable": None,
    #                                 "active": False},
    #                      "ipv4_dhcp": {"enable": False, 
    #                                   "need_set_new_nexthop": False, 
    #                                   "new_nexthop": None}},
    #                       {<Link2 parameters>,
    #                        "status": {"reachable": None,
    #                                   "active": False},
    #                        "ipv4_dhcp": {"enable": False,
    #                                      "need_set_new_nexthop": False,
    #                                      "new_nexthop": None}}]
    #  }

    start_link_threads(links_params=group_params["links_params"]["links"],
                       group_name=group_name,
                       watch_rules_interval=watch_intervals['watch_rules'])

    # We need load all data files from DHCP hook for specific group, 
    # if multiwan is starting after data files were created
    # For example, multiwan is starting after dhclient updated nexthop in the data files
    dhcp_load_new_nexthop_to_links_of_group(msg_prefix=f'Group {group_name}: ',
                                            group_params_list=links_status[group_name])
    for link in links_status[group_name]:
        if link["ipv4_dhcp"]["enable"] and link["ipv4_dhcp"]["need_set_new_nexthop"]:
            dhcp_replace_nexthop_and_routes_for_link(link, group_name)
    first_change_link_flag = True
    active_link_index = None
    active_link = None
    watch_link_routes = False
    next_time_to_watch_self_routes = 0

    # disables spam in log if all links are unreachable
    detected_unreachable_all_links_flag = False
    if not group_params["links_params"]["check_links_reachability_on_start"]:
        # links_status[group_name] is sorted list by priority.
        # First element in list has the highest priority.
        for index in range(len(links_status[group_name])):
            link = get_link_status_from_group_by_index(group_name=group_name,
                                                       index=index)
            if 'ipv4' in link["routes"] and link["routes"]['ipv4']['nexthop'] == 'DHCP':
                continue
            active_link_index = index
            active_link = link
            break

        if active_link_index is not None:
            watch_link_routes = active_link["routes"]["watch_self_routes"]
            change_active_link(group_params=group_params, old_link_index=None,
                               new_link_index=active_link_index, group_name=group_name,
                               first_change_link_flag=first_change_link_flag)
            next_time_to_watch_self_routes = time.time() + watch_self_routes_interval
        else:
            msg = f"Group {group_name}: All links do not have a nexthop via DHCP."
            lib.log.send_warning_msg(msg)
            # We have found that all links are down and have logged it.
            # No further notice is required until the link becomes
            # reachable.
            detected_unreachable_all_links_flag = True
        first_change_link_flag = False

    while True:
        if lib.common.exit_flag or lib.common.reload_config_flag:
            break

        for link in links_status[group_name]:
            if link["ipv4_dhcp"]["enable"] and link["ipv4_dhcp"]["need_set_new_nexthop"]:
                dhcp_replace_nexthop_and_routes_for_link(link, group_name)

        if watch_link_routes and time.time() >= next_time_to_watch_self_routes:
            for ip_ver in lib.common_vars.IP_VER_MAP:
                if ip_ver in active_link["routes"]:
                    add_routes(link_params=active_link, ip_ver_name=ip_ver, group_name=group_name)
            next_time_to_watch_self_routes = time.time() + watch_self_routes_interval

        prioritest_reachable_link_index = get_reachable_link_index_with_highest_prio(group_name)
        if prioritest_reachable_link_index == -2:  # some link states are undefined
            time.sleep(1)
            continue
        elif prioritest_reachable_link_index == -1:  # all link states are unreachable
            if not detected_unreachable_all_links_flag:
                msg = f'Group {group_name}: All links unreachable.'
                lib.log.send_warning_msg(msg)
                detected_unreachable_all_links_flag = True
                if clear_unreachable_link_routes_flag and active_link is not None:
                    if notify["prechangelink"]:
                        _ = exec_notify_script(notify_script_params=notify["prechangelink"],
                                               group_name=group_name,
                                               state="pre_clear_group_routes")

                    delete_routes_of_link(link_params=active_link, group_name=group_name)

                    if group_params["links_params"]["clear_all_conntrack_translations"]:
                        clear_all_conntrack_translations(msg_prefix=f'Group {group_name}: ')

                    set_link_activity_status(link=links_status[group_name][active_link_index],
                                             status=False)
                    if notify["postchangelink"]:
                        _ = exec_notify_script(notify_script_params=notify["postchangelink"],
                                               group_name=group_name,
                                               state="post_clear_group_routes")
                    active_link_index = None
                    active_link = None
                    watch_link_routes = False
            time.sleep(1)
            continue
        elif detected_unreachable_all_links_flag:
            detected_unreachable_all_links_flag = False

        # All link states are defined and some link is reachable

        if prioritest_reachable_link_index == active_link_index:
            # Current active link is prioritest link
            time.sleep(1)
            continue

        # If active link is reachable and prempt_flag is False, then do
        # nothing
        if active_link:
            with link_status_lock:
                is_active_link_reachable = active_link["status"]["reachable"]

            if is_active_link_reachable and not group_params["links_params"]["preempt"]:
                time.sleep(1)
                continue

        change_active_link(group_params=group_params, old_link_index=active_link_index,
                           new_link_index=prioritest_reachable_link_index, group_name=group_name,
                           first_change_link_flag=first_change_link_flag)
        active_link_index = prioritest_reachable_link_index
        active_link = get_link_status_from_group_by_index(group_name=group_name,
                                                          index=active_link_index)
        watch_link_routes = active_link["routes"]["watch_self_routes"]
        first_change_link_flag = False
        next_time_to_watch_self_routes = time.time() + watch_self_routes_interval
        time.sleep(1)


def main(path_to_config):
    msg = lib.file.write_to_file(file=lib.common_vars.MULTIWAN_FILE_PID, data=str(os.getpid()))
    if msg:
        lib.log.send_err_msg(msg)
        return 255
    # Starting processing daemon parameters from yaml config file.
    # watch_intervals = {"watch_rules": 10,
    #                    "watch_self_routes": 10,
    #                    "watch_foreign_routes": 10}
    params, watch_intervals, poststart_script, prestop_script = get_params_from_config(path_to_config)
    if params is None:
        return 1

    with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as file_socket:
        try:
            file_socket.bind(lib.common_vars.RUN_SOCKET)
        except OSError as exception_info:
            lib.log.send_err_msg('Cannot open server socket.',
                                 details_msg='OSError: {}'.format(exception_info))
            return 255

    err_msg_list = lib.common.check_rpf_on_ifaces(params)
    if err_msg_list:
        for error_msg in err_msg_list:
            lib.log.send_err_msg(error_msg)
        return 255

    server_thread = threading.Thread(target=server_socket,
                                     args=(lib.common_vars.RUN_SOCKET,),
                                     daemon=True)
    server_thread.start()

    lib.log.send_info_msg(msg=f'Starting {DAEMON_NAME}...')

    error_msgs = service_notify.set_ready()
    if error_msgs:
        lib.log.send_err_msg(error_msgs[0], details_msg=error_msgs[1])
        return 255

    clear_daemon_route_and_rules()

    # Executing poststart script, if specified in the daemon
    # configuration file.
    if poststart_script:
        poststart_thread = threading.Thread(target=exec_notify_script,
                                            args=(poststart_script, None, "post_start", False),
                                            daemon=True)
        poststart_thread.start()
        # exec_notify_script завершается при превышении timeout
        # или при получении сигнала на остановку.
        while poststart_thread.is_alive():
            service_notify.set_watchdog()
            time.sleep(1)

    # networks_to_watch = {'ipv4': {100: ['192.168.1.0/24', '192.168.2.0/24'],
    #                               101: ['192.168.3.0/24', '192.168.4.0/24']},
    #                      'ipv6': {100: ['2001:db8::192.168.1.0/24', '2001:db8::192.168.2.0/24',],
    #                               101: ['2001:db8::192.168.3.0/24', '2001:db8::192.168.4.0/24',}}
    networks_to_watch = get_networks_to_watch(grouped_params=params)
    watch_foreign_routes(networks_to_watch)
    groups_threads = {}
    dhcp_in_used_flag = False
    for group_name, group_params in params.items():

        if not dhcp_in_used_flag:
            dhcp_in_used_flag = is_dhcp_used_in_group(group_params["links_params"]["links"])

        groups_threads[group_name] = threading.Thread(target=group_thread,
                                                      args=(group_params,
                                                            group_name,
                                                            watch_intervals),
                                                      daemon=True)
        groups_threads[group_name].start()

    timestamp = time.time()
    watch_foreign_routes_timer = timestamp + watch_intervals['watch_foreign_routes']
    while True:
        service_notify.set_watchdog()
        # Only signal handler sets lib.common.exit_flag to True.
        if lib.common.exit_flag:
            lib.log.send_info_msg('SIGTERM signal was received.')
            service_notify.set_stopping()
            if prestop_script:
                _ = exec_notify_script(notify_script_params=prestop_script,
                                       state="pre_stop",
                                       ignore_global_exit_flag=True)
            return stopping_cleanup()

        # Only signal handler sets lib.common.reload_config_flag to True.
        if lib.common.reload_config_flag:
            lib.log.send_info_msg('SIGHUP signal was received.')
            service_notify.set_reloading()
            exit_code_num = cleanup()
            if exit_code_num != 0:
                error_msgs = service_notify.set_ready()
                if error_msgs:
                    lib.log.send_err_msg(error_msgs[0], details_msg=error_msgs[1])
                return exit_code_num
            # must be False before initialize
            lib.common.reload_config_flag = False
            params, watch_intervals, _, prestop_script = get_params_from_config(path_to_config)
            error_msgs = service_notify.set_ready()
            if error_msgs:
                lib.log.send_err_msg(error_msgs[0], details_msg=error_msgs[1])
                return 255
            if params is None:
                return 1

            err_msg_list = lib.common.check_rpf_on_ifaces(params)
            if err_msg_list:
                for error_msg in err_msg_list:
                    lib.log.send_err_msg(error_msg)
                return 255

            networks_to_watch = get_networks_to_watch(grouped_params=params)
            watch_foreign_routes(networks_to_watch)
            timestamp = time.time()
            watch_foreign_routes_timer = timestamp + watch_intervals['watch_foreign_routes']

            groups_threads = {}
            dhcp_in_used_flag = False
            for group_name, group_params in params.items():

                if not dhcp_in_used_flag:
                    dhcp_in_used_flag = is_dhcp_used_in_group(group_params["links_params"]["links"])

                groups_threads[group_name] = threading.Thread(target=group_thread,
                                                              args=(group_params,
                                                                    group_name,
                                                                    watch_intervals),
                                                              daemon=True)
                groups_threads[group_name].start()

            lib.log.send_info_msg('Config file has been reloaded.')

        # Only signal handler sets lib.common.updated_dhcp_flag to True.
        if lib.common.updated_dhcp_flag:
            lib.common.updated_dhcp_flag = False
            if dhcp_in_used_flag:
                lib.log.send_info_msg('SIGUSR1 signal from DHCP hook is received. '
                                      'Checking data files from DHCP hook is started...')

                for group_name, group_params in links_status.items():
                    dhcp_load_new_nexthop_to_links_of_group(msg_prefix='',
                                                            group_params_list=group_params)

                lib.log.send_info_msg('Checking data files from DHCP hook is finished.')
            else:
                lib.log.send_debug_msg('SIGUSR1 signal from DHCP hook is received. '
                                       'Signal will be ignored (DHCP is not enabled in config).')

        new_timestamp = time.time()
        if new_timestamp < timestamp:
            lib.log.send_warning_msg('System time has been set back')
            watch_foreign_routes_timer -= new_timestamp - timestamp
        if new_timestamp >= watch_foreign_routes_timer:
            watch_foreign_routes(networks_to_watch)
            new_timestamp = time.time()
            watch_foreign_routes_timer = new_timestamp + watch_intervals['watch_foreign_routes']
        timestamp = new_timestamp
        time.sleep(1)


def arg_parse():
    parser = ArgumentParser()
    parser.add_argument("--config", dest="config_file",
                        help="path to config file", required=False, type=str)
    parser.add_argument('--check_config', action='store_true',
                        help='check config file', dest="check_config")
    parser.add_argument('--log_level',
                        choices=['error', 'warning', 'info', 'debug'],
                        help='Set log level',
                        dest='log_lvl')
    check_config_group = parser.add_argument_group('check_config optional arguments')
    check_config_group.add_argument("--result", dest='result', action='store_true',
                                    help="show the resulting configuration "
                                         "as it will be used in the daemon")
    try:
        arguments = parser.parse_args()
    except AttributeError as exception_info:
        lib.log.send_err_msg(exception_info, msg_lvl_output=5)
        return

    return arguments


def is_dhcp_used_in_group(links_of_group_from_config: list) -> bool:
    """
    The function checks setup of config file and return
        True if DHCP is enabled for any link in links of group
        False if DHCP is disabled for all links of group.

    :param links_of_group_from_config: Links list from config file (list)

    :return: True if DHCP is enabled for any link
             False if DHCP is disabled for all links
    """

    for link in links_of_group_from_config:
        if "ipv4" in link["routes"] and link["routes"]["ipv4"]["nexthop"] == 'DHCP':
            return True

    return False


def dhcp_load_new_nexthop_to_links_of_group(msg_prefix: str, group_params_list: list):
    """
    The function loads data files from DHCP hook for all links to the
    global dictionary links_status.

    :param msg_prefix: Prefix for messages
    :param group_params_list: Links list of group (list of dictionary)
    """

    for link in group_params_list:
        if not link["ipv4_dhcp"]["enable"]:
            continue

        msg_local_prefix = f'{msg_prefix}Interface {link["iface_name"]} (DHCP). '

        new_nexthop = dhcp_get_ipv4_nexthop_from_file(msg_prefix, link["iface_name"])

        if new_nexthop:
            used_nexthop = link["routes"]["ipv4"]["nexthop"]

            if link["routes"]["ipv4"]["nexthop"] == new_nexthop:
                msg = (f'{msg_local_prefix}The nexthop from data file, received from DHCP hook, and'
                       f' the used nexthop {used_nexthop} are equal. Nexthop will not replaced.')
                lib.log.send_debug_msg(f'{msg}')
                continue
            else:
                with link_status_lock:
                    link["ipv4_dhcp"]["need_set_new_nexthop"] = True
                    link["ipv4_dhcp"]["new_nexthop"] = new_nexthop

                msg = (f'{msg_local_prefix}Used nexthop {used_nexthop} '
                       f'will changed on new nexthop {new_nexthop}.')
                lib.log.send_info_msg(f'{msg}')


def dhcp_get_ipv4_nexthop_from_file(msg_prefix: str, iface_name: str) -> str:
    """
    Function checks the availability of data files from the dhcp hook
    for the iface_name interface, reads value from the file and checks
    this value is the IPv4 address.

    :param msg_prefix: Prefix for messages
    :type msg_prefix: str
    :param iface_name: Name of network interface
    :type iface_name: str

    :return: string with IPv4 address or None
    """

    nexthop_dhcp_file_name = lib.common_vars.NEXTHOP_IP_DHCP_FILE_NAME_FOR_IFACE.format(iface_name)

    msg = lib.file.check_file_for_reading(nexthop_dhcp_file_name)
    if msg:
        msg = f'{msg_prefix}Failed to read value from data file, received from DHCP hook.'
        lib.log.send_warning_msg(f'{msg}', details_msg=msg)
        return ''

    with open(nexthop_dhcp_file_name) as fd:
        value = fd.read().rstrip('\n')

    if not lib.params.check_ip_address(value, 'IPv4Address'):
        msg = (f'{msg_prefix}Value {value} from data file {nexthop_dhcp_file_name}, received'
               f' from DHCP hook, is not IPv4 address.')
        lib.log.send_warning_msg(f'{msg}')
        return ''

    return value


def dhcp_replace_nexthop_and_routes_for_link(link_dict, group_name: str):
    """
    The function will copy new nexthop to used nexthop and replace user
    traffic routes for the link.

    :param link_dict: Link dictionary from link list of group (item of
                    link array from global links_status dictionary)
    :type link_dict: dict
    :param group_name: Name of group
    :type group_name: str
    """
    with link_status_lock:
        link_dict["routes"]["ipv4"]["nexthop"] = link_dict["ipv4_dhcp"]["new_nexthop"]
        link_dict["ipv4_dhcp"]["need_set_new_nexthop"] = False
        link_dict["ipv4_dhcp"]["new_nexthop"] = None

    msg_prefix = f'Group {group_name}: Interface {link_dict["iface_name"]} (DHCP). '
    lib.log.send_info_msg(f'{msg_prefix}Nexthop was changed on'
                          f' {link_dict["routes"]["ipv4"]["nexthop"]}.')

    if link_dict["status"]["active"]:
        lib.log.send_info_msg(f'Group {group_name}: Interface {link_dict["iface_name"]} (DHCP). '
                              f'Interface is active. User traffic\'s routes will replaced.')
        add_routes(link_params=link_dict, ip_ver_name='ipv4', group_name=group_name)


if __name__ == '__main__':
    syslog.openlog(facility=syslog.LOG_LOCAL7,
                   ident=lib.common_vars.SYSLOG_TAG,
                   logoption=syslog.LOG_NDELAY)
    err_msg = lib.file.create_dir(lib.common_vars.RUN_DIRECTORY)
    if err_msg:
        lib.log.send_err_msg(err_msg)
        sys.exit(255)

    lib.log.msg_level_output = lib.common_vars.MSG_LVL_OUTPUT

    TERMINATION_SIGNALS = [signal.SIGTERM, signal.SIGINT]
    for termination_signal in TERMINATION_SIGNALS:
        signal.signal(termination_signal, set_exit_flag)

    RELOAD_CONF_SIGNAL = signal.SIGHUP
    signal.signal(RELOAD_CONF_SIGNAL, set_reload_flag)

    UPDATED_DHCP_SIGNAL = signal.SIGUSR1
    signal.signal(UPDATED_DHCP_SIGNAL, set_updated_dhcp_flag)

    args = arg_parse()
    if not args:
        sys.exit(1)

    file_path = lib.params.get_abs_path_to_config(args.config_file)

    err_msg = lib.file.check_file_for_reading(file_path)
    if err_msg:
        lib.log.send_err_msg(err_msg, msg_lvl_output=5)
        sys.exit(1)

    if args.check_config:
        exit_code = lib.params.check_config(file_path, args.result)
    else:
        service_notify = lib.service_manager_notify.ServiceNotify()
        if service_notify and service_notify.is_watchdog_error_received:
            lib.log.send_err_msg('Cannot determine watchdog timer to work '
                                 'with multiwan systemd unit.',
                                 details_msg=service_notify.err_msg)
            sys.exit(255)

        if args.log_lvl:
            lib.log.set_log_level(args.log_lvl)

        exit_code = main(file_path)
    sys.exit(exit_code)
