#!/usr/bin/env python3
#
# Copyright (C) 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 socket
import sys
import typing

from sys import exit
from tabulate import tabulate
from vyos.configquery import ConfigTreeQuery

import vyos.opmode

socket_path = '/run/haproxy/admin.sock'
timeout = 5


def _execute_haproxy_command(command):
    """Execute a command on the HAProxy UNIX socket and retrieve the response.

    Args:
        command (str): The command to be executed.

    Returns:
        str: The response received from the HAProxy UNIX socket.

    Raises:
        socket.error: If there is an error while connecting or communicating with the socket.

    Finally:
        Closes the socket connection.

    Notes:
        - HAProxy expects a newline character at the end of the command.
        - The socket connection is established using the HAProxy UNIX socket.
        - The response from the socket is received and decoded.

    Example:
        response = _execute_haproxy_command('show stat')
        print(response)
    """
    try:
        # HAProxy expects new line for command
        command = f'{command}\n'

        # Connect to the HAProxy UNIX socket
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        sock.connect(socket_path)

        # Set the socket timeout
        sock.settimeout(timeout)

        # Send the command
        sock.sendall(command.encode())

        # Receive and decode the response
        response = b''
        while True:
            data = sock.recv(4096)
            if not data:
                break
            response += data
        response = response.decode()

        return (response)

    except socket.error as e:
        print(f"Error: {e}")

    finally:
        # Close the socket
        sock.close()


def _convert_seconds(seconds):
    """Convert seconds to days, hours, minutes, and seconds.

    Args:
        seconds (int): The number of seconds to convert.

    Returns:
        tuple: A tuple containing the number of days, hours, minutes, and seconds.
    """
    minutes = seconds // 60
    hours = minutes // 60
    days = hours // 24

    return days, hours % 24, minutes % 60, seconds % 60


def _last_change_format(seconds):
    """Format the time components into a string representation.

    Args:
        seconds (int): The total number of seconds.

    Returns:
        str: The formatted time string with days, hours, minutes, and seconds.

    Examples:
        >>> _last_change_format(1434)
        '23m54s'
        >>> _last_change_format(93734)
        '1d0h23m54s'
        >>> _last_change_format(85434)
        '23h23m54s'
    """
    days, hours, minutes, seconds = _convert_seconds(seconds)
    time_format = ""

    if days:
        time_format += f"{days}d"
    if hours:
        time_format += f"{hours}h"
    if minutes:
        time_format += f"{minutes}m"
    if seconds:
        time_format += f"{seconds}s"

    return time_format


def _get_json_data():
    """Get haproxy data format JSON"""
    return _execute_haproxy_command('show stat json')


def _get_raw_data():
    """Retrieve raw data from JSON and organize it into a dictionary.

    Returns:
        dict: A dictionary containing the organized data categorized
              into frontend, backend, and server.
    """

    data = json.loads(_get_json_data())
    lb_dict = {'frontend': [], 'backend': [], 'server': []}

    for key in data:
        frontend = []
        backend = []
        server = []
        for entry in key:
            obj_type = entry['objType'].lower()
            position = entry['field']['pos']
            name = entry['field']['name']
            value = entry['value']['value']

            dict_entry = {'pos': position, 'name': {name: value}}

            if obj_type == 'frontend':
                frontend.append(dict_entry)
            elif obj_type == 'backend':
                backend.append(dict_entry)
            elif obj_type == 'server':
                server.append(dict_entry)

        if len(frontend) > 0:
            lb_dict['frontend'].append(frontend)
        if len(backend) > 0:
            lb_dict['backend'].append(backend)
        if len(server) > 0:
            lb_dict['server'].append(server)

    return lb_dict


def _get_formatted_output(data):
    """
    Format the data into a tabulated output.

    Args:
        data (dict): The data to be formatted.

    Returns:
        str: The tabulated output representing the formatted data.
    """
    table = []
    headers = [
        "Proxy name", "Role", "Status", "Req rate", "Resp time", "Last change"
    ]

    for key in data:
        for item in data[key]:
            row = [None] * len(headers)

            for element in item:
                if 'pxname' in element['name']:
                    row[0] = element['name']['pxname']
                elif 'svname' in element['name']:
                    row[1] = element['name']['svname']
                elif 'status' in element['name']:
                    row[2] = element['name']['status']
                elif 'req_rate' in element['name']:
                    row[3] = element['name']['req_rate']
                elif 'rtime' in element['name']:
                    row[4] = f"{element['name']['rtime']} ms"
                elif 'lastchg' in element['name']:
                    row[5] = _last_change_format(element['name']['lastchg'])
            table.append(row)

    out = tabulate(table, headers, numalign="left")
    return out


def show(raw: bool):
    config = ConfigTreeQuery()
    if not config.exists('load-balancing reverse-proxy'):
        raise vyos.opmode.UnconfiguredSubsystem('Reverse-proxy is not configured')

    data = _get_raw_data()
    if raw:
        return data
    else:
        return _get_formatted_output(data)


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)