#!/usr/bin/env python3
#
# Copyright (C) 2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os

from sys import exit
from copy import deepcopy
from jinja2 import FileSystemLoader, Environment
from netifaces import interfaces

from vyos.config import Config
from vyos.defaults import directories as vyos_data_dir
from vyos.util import chown_file, chmod_x, subprocess_cmd
from vyos import ConfigError

default_config_data = {
    'address': [],
    'apn': '',
    'chat_script': '',
    'deleted': False,
    'description': '',
    'device': 'ttyUSB0',
    'disable': False,
    'disable_link_detect': 1,
    'on_demand': False,
    'logfile': '',
    'metric': '10',
    'mtu': '1500',
    'name_server': True,
    'intf': '',
    'vrf': ''
}

def check_kmod():
    modules = ['option', 'usb_wwan', 'usbserial']
    for module in modules:
        if not os.path.exists(f'/sys/module/{module}'):
            if os.system(f'modprobe {module}') != 0:
                raise ConfigError(f'Loading Kernel module {module} failed')

def get_config():
    wwan = deepcopy(default_config_data)
    conf = Config()

    # determine tagNode instance
    if 'VYOS_TAGNODE_VALUE' not in os.environ:
        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')

    wwan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
    wwan['logfile'] = f"/var/log/vyatta/ppp_{wwan['intf']}.log"
    wwan['chat_script'] = f"/etc/ppp/peers/chat.{wwan['intf']}"

    # Check if interface has been removed
    if not conf.exists('interfaces wirelessmodem ' + wwan['intf']):
        wwan['deleted'] = True
        return wwan

    # set new configuration level
    conf.set_level('interfaces wirelessmodem ' + wwan['intf'])

    # get metrick for backup default route
    if conf.exists(['apn']):
        wwan['apn'] = conf.return_value(['apn'])

    # get metrick for backup default route
    if conf.exists(['backup', 'distance']):
        wwan['metric'] = conf.return_value(['backup', 'distance'])

    # Retrieve interface description
    if conf.exists(['description']):
        wwan['description'] = conf.return_value(['description'])

    # System device name
    if conf.exists(['device']):
        wwan['device'] = conf.return_value(['device'])

    # disable interface
    if conf.exists('disable'):
        wwan['disable'] = True

    # ignore link state changes
    if conf.exists('disable-link-detect'):
        wwan['disable_link_detect'] = 2

    # Do not use DNS servers provided by the peer
    if conf.exists(['mtu']):
        wwan['mtu'] = conf.return_value(['mtu'])

    # Do not use DNS servers provided by the peer
    if conf.exists(['no-peer-dns']):
        wwan['name_server'] = False

    # Access concentrator name (only connect to this concentrator)
    if conf.exists(['ondemand']):
        wwan['on_demand'] = True

    # retrieve VRF instance
    if conf.exists('vrf'):
        wwan['vrf'] = conf.return_value(['vrf'])

    return wwan

def verify(wwan):
    if wwan['deleted']:
        return None

    if not wwan['apn']:
        raise ConfigError(f"APN for {wwan['intf']} not configured")

    # we can not use isfile() here as Linux device files are no regular files
    # thus the check will return False
    if not os.path.exists(f"/dev/{wwan['device']}"):
        raise ConfigError(f"Device {wwan['device']} does not exist")

    vrf_name = wwan['vrf']
    if vrf_name and vrf_name not in interfaces():
        raise ConfigError(f'VRF {vrf_name} does not exist')

    return None

def generate(wwan):
    # Prepare Jinja2 template loader from files
    tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'wwan')
    fs_loader = FileSystemLoader(tmpl_path)
    env = Environment(loader=fs_loader)

    # set up configuration file path variables where our templates will be
    # rendered into
    intf = wwan['intf']
    config_wwan = f'/etc/ppp/peers/{intf}'
    config_wwan_chat = wwan['chat_script']
    script_wwan_pre_up = f'/etc/ppp/ip-pre-up.d/1010-vyos-wwan-{intf}'
    script_wwan_ip_up = f'/etc/ppp/ip-up.d/1010-vyos-wwan-{intf}'
    script_wwan_ip_down = f'/etc/ppp/ip-down.d/1010-vyos-wwan-{intf}'

    config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up,
                    script_wwan_ip_up, script_wwan_ip_down]

    # Ensure directories for config files exist - otherwise create them on demand
    for file in config_files:
        dirname = os.path.dirname(file)
        if not os.path.isdir(dirname):
            os.mkdir(dirname)

    # Always hang-up WWAN connection prior generating new configuration file
    cmd = f'systemctl stop ppp@{intf}.service'
    subprocess_cmd(cmd)

    if wwan['deleted']:
        # Delete PPP configuration files
        for file in config_files:
            if os.path.exists(file):
                os.unlink(file)

    else:
        # Create PPP configuration files
        tmpl = env.get_template('peer.tmpl')
        config_text = tmpl.render(wwan)
        with open(config_wwan, 'w') as f:
            f.write(config_text)

        # Create PPP chat script
        tmpl = env.get_template('chat.tmpl')
        config_text = tmpl.render(wwan)
        with open(config_wwan_chat, 'w') as f:
            f.write(config_text)

        # Create script for ip-pre-up.d
        tmpl = env.get_template('ip-pre-up.script.tmpl')
        config_text = tmpl.render(wwan)
        with open(script_wwan_pre_up, 'w') as f:
            f.write(config_text)

        # Create script for ip-up.d
        tmpl = env.get_template('ip-up.script.tmpl')
        config_text = tmpl.render(wwan)
        with open(script_wwan_ip_up, 'w') as f:
            f.write(config_text)

        # Create script for ip-down.d
        tmpl = env.get_template('ip-down.script.tmpl')
        config_text = tmpl.render(wwan)
        with open(script_wwan_ip_down, 'w') as f:
            f.write(config_text)

        # make generated script file executable
        chmod_x(script_wwan_pre_up)
        chmod_x(script_wwan_ip_up)
        chmod_x(script_wwan_ip_down)

    return None

def apply(wwan):
    if wwan['deleted']:
        # bail out early
        return None

    if not wwan['disable']:
        # "dial" WWAN connection
        intf = wwan['intf']
        cmd = f'systemctl start ppp@{intf}.service'
        subprocess_cmd(cmd)

        # make logfile owned by root / vyattacfg
        chown_file(wwan['logfile'], 'root', 'vyattacfg')

    return None

if __name__ == '__main__':
    try:
        check_kmod()
        c = get_config()
        verify(c)
        generate(c)
        apply(c)
    except ConfigError as e:
        print(e)
        exit(1)