#!/usr/bin/env python3
#
# Copyright (C) 2022-2023 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 json
import os
import sys
import typing
from tabulate import tabulate

import vyos.opmode
from vyos.utils.convert import bytes_to_human
from vyos.utils.commit import commit_in_progress
from vyos.utils.process import call
from vyos.utils.process import rc_cmd
from vyos.config import Config

ArgMode = typing.Literal['client', 'server', 'site_to_site']

def _get_tunnel_address(peer_host, peer_port, status_file):
    peer = peer_host + ':' + peer_port
    lst = []

    with open(status_file, 'r') as f:
        lines = f.readlines()
        for line in lines:
            if peer in line:
                lst.append(line)

        # filter out subnet entries if iroute:
        # in the case that one sets, say:
        # [ ..., 'vtun10', 'server', 'client', 'client1', 'subnet','10.10.2.0/25']
        # the status file will have an entry:
        # 10.10.2.0/25,client1,...
        lst = [l for l in lst[1:] if '/' not in l.split(',')[0]]

        tunnel_ip = lst[0].split(',')[0]

        return tunnel_ip

def _get_interface_status(mode: str, interface: str) -> dict:
    status_file = f'/run/openvpn/{interface}.status'

    data: dict = {
        'mode': mode,
        'intf': interface,
        'local_host': '',
        'local_port': '',
        'date': '',
        'clients': [],
    }

    if not os.path.exists(status_file):
        return data

    with open(status_file, 'r') as f:
        lines = f.readlines()
        for line_no, line in enumerate(lines):
            # remove trailing newline character first
            line = line.rstrip('\n')

            # check first line header
            if line_no == 0:
                if mode == 'server':
                    if not line == 'OpenVPN CLIENT LIST':
                        raise vyos.opmode.InternalError('Expected "OpenVPN CLIENT LIST"')
                else:
                    if not line == 'OpenVPN STATISTICS':
                        raise vyos.opmode.InternalError('Expected "OpenVPN STATISTICS"')

                continue

            # second line informs us when the status file has been last updated
            if line_no == 1:
                data['date'] = line.lstrip('Updated,').rstrip('\n')
                continue

            if mode == 'server':
                # for line_no > 1, lines appear as follows:
                #
                # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
                # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019
                # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019
                # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019
                # ...
                # ROUTING TABLE
                # ...
                if line_no >= 3:
                    # indicator that there are no more clients
                    if line == 'ROUTING TABLE':
                        break
                    # otherwise, get client data
                    remote = (line.split(',')[1]).rsplit(':', maxsplit=1)

                    client = {
                        'name': line.split(',')[0],
                        'remote_host': remote[0],
                        'remote_port': remote[1],
                        'tunnel': 'N/A',
                        'rx_bytes': bytes_to_human(int(line.split(',')[2]),
                                                   precision=1),
                        'tx_bytes': bytes_to_human(int(line.split(',')[3]),
                                                   precision=1),
                        'online_since': line.split(',')[4]
                    }
                    client['tunnel'] = _get_tunnel_address(client['remote_host'],
                                                           client['remote_port'],
                                                           status_file)
                    data['clients'].append(client)
                    continue
            else: # mode == 'client' or mode == 'site-to-site'
                if line_no == 2:
                    client = {
                        'name': 'N/A',
                        'remote_host': 'N/A',
                        'remote_port': 'N/A',
                        'tunnel': 'N/A',
                        'rx_bytes': bytes_to_human(int(line.split(',')[1]),
                                                   precision=1),
                        'tx_bytes': '',
                        'online_since': 'N/A'
                    }
                    continue

                if line_no == 3:
                    client['tx_bytes'] = bytes_to_human(int(line.split(',')[1]),
                                                        precision=1)
                    data['clients'].append(client)
                    break

    return data


def _get_interface_state(iface):
    rc, out = rc_cmd(f'ip --json link show dev {iface}')
    try:
        data = json.loads(out)
    except:
        return 'DOWN'
    return data[0].get('operstate', 'DOWN')


def _get_interface_description(iface):
    rc, out = rc_cmd(f'ip --json link show dev {iface}')
    try:
        data = json.loads(out)
    except:
        return ''
    return data[0].get('ifalias', '')


def _get_raw_data(mode: str) -> list:
    data: list = []
    conf = Config()
    conf_dict = conf.get_config_dict(['interfaces', 'openvpn'],
                                     get_first_key=True)
    if not conf_dict:
        return data

    interfaces = [x for x in list(conf_dict) if
                  conf_dict[x]['mode'].replace('-', '_') == mode]
    for intf in interfaces:
        d = _get_interface_status(mode, intf)
        d['state'] = _get_interface_state(intf)
        d['description'] = _get_interface_description(intf)
        d['local_host'] = conf_dict[intf].get('local-host', '')
        d['local_port'] = conf_dict[intf].get('local-port', '')
        if conf.exists(f'interfaces openvpn {intf} server client'):
            d['configured_clients'] = conf.list_nodes(f'interfaces openvpn {intf} server client')
        if mode in ['client', 'site_to_site']:
            for client in d['clients']:
                if 'shared-secret-key-file' in list(conf_dict[intf]):
                    client['name'] = 'None (PSK)'
                client['remote_host'] = conf_dict[intf].get('remote-host', [''])[0]
                client['remote_port'] = conf_dict[intf].get('remote-port', '1194')
        data.append(d)

    return data

def _format_openvpn(data: list) -> str:
    if not data:
        out = 'No OpenVPN interfaces configured'
        return out

    headers = ['Client CN', 'Remote Host', 'Tunnel IP', 'Local Host',
               'TX bytes', 'RX bytes', 'Connected Since']

    out = ''
    for d in data:
        data_out = []
        intf = d['intf']
        l_host = d['local_host']
        l_port = d['local_port']
        for client in d['clients']:
            r_host = client['remote_host']
            r_port = client['remote_port']

            out += f'\nOpenVPN status on {intf}\n\n'
            name = client['name']
            remote = r_host + ':' + r_port if r_host and r_port else 'N/A'
            tunnel = client['tunnel']
            local = l_host + ':' + l_port if l_host and l_port else 'N/A'
            tx_bytes = client['tx_bytes']
            rx_bytes = client['rx_bytes']
            online_since = client['online_since']
            data_out.append([name, remote, tunnel, local, tx_bytes,
                             rx_bytes, online_since])

        if data_out:
            out += tabulate(data_out, headers)
            out += "\n"

    return out

def show(raw: bool, mode: ArgMode) -> typing.Union[list,str]:
    openvpn_data = _get_raw_data(mode)

    if raw:
        return openvpn_data

    return _format_openvpn(openvpn_data)

def reset(interface: str):
    if os.path.isfile(f'/run/openvpn/{interface}.conf'):
        if commit_in_progress():
            raise vyos.opmode.CommitInProgress('Retry OpenVPN reset: commit in progress.')
        call(f'systemctl restart openvpn@{interface}.service')
    else:
        raise vyos.opmode.IncorrectValue(f'OpenVPN interface "{interface}" does not exist!')

if __name__ == '__main__':
    try:
        res = vyos.opmode.run(sys.modules[__name__])
        if res:
            print(res)
    except (ValueError, vyos.opmode.Error) as e:
        print(e)
        sys.exit(1)