#!/usr/bin/env python3

# SPDX-FileCopyrightText: 2009 Fermi Research Alliance, LLC
# SPDX-License-Identifier: Apache-2.0

"""Command analyze_frontends, to analyze and print Frontend (client) statistics.

Focuses mostly on Client Info
"""

import datetime
import getopt
import os
import sys

from urllib.request import urlopen

from glideinwms.factory.tools.lib import analyze
from glideinwms.lib import xmlParse

STARTUP_DIR = sys.path[0]
sys.path.append(os.path.join(STARTUP_DIR, "../../.."))


def list_print(frontend, zero_supp, entry_data, sorting, attr_list, sort_attribute, div):
    """Print a formatted list of entry data.

    This function builds and prints a formatted list for a given Frontend based on the
    provided entry data. It computes additional percentages, sorts the entries if required,
    and prints a header along with each formatted entry line.

    Args:
        frontend (str): The Frontend name.
        zero_supp (int): Zero suppression flag; if 1, entries with all zero attributes are skipped.
        entry_data (dict): Dictionary of entry data for the Frontend.
        sorting (int): Flag to enable sorting (1 to sort).
        attr_list (list): List of attribute keys to consider.
        sort_attribute (int or str): The attribute (or its index) used for sorting.
        div (int or float): The divisor for normalizing values in the output.
    """
    to_be_printed = []
    sum2 = 0

    for entry_name, entry in entry_data.items():
        entry["RunDiff"] = entry["StatusRunning"] - entry["ClientGlideTotal"]

        # avoid division by zero
        unmatched_percent = 0
        rundiff_percent = 0
        if entry["StatusRunning"] != 0:
            rundiff_percent = float(entry["RunDiff"]) / float(entry["StatusRunning"]) * 100
        if entry["ClientGlideTotal"] != 0:
            unmatched_percent = float(entry["ClientGlideIdle"]) / float(entry["ClientGlideTotal"]) * 100
        entry["UM"] = unmatched_percent
        entry["RD"] = rundiff_percent

        if zero_supp == 1:
            sum_in = 0
            for a in attr_list:
                sum_in += entry[a]
            if sum_in == 0:
                continue
            sum2 += sum_in

        to_be_printed.append(
            (
                entry[attr_list[sort_attribute]],
                (
                    "%-40s %7s %7s %7s | %7s | %7s %7s %7s | %7s %4d%% %4d%%"
                    % (
                        entry_name.lstrip("entry_"),
                        analyze.km(float(entry["ClientGlideTotal"]) / div),
                        analyze.km(float(entry["ClientGlideRunning"]) / div),
                        analyze.km(float(entry["ClientGlideIdle"]) / div),
                        analyze.km(float(entry["ReqIdle"]) / div),
                        analyze.km(float(entry["ClientJobsRunning"]) / div),
                        analyze.km(float(entry["ClientJobsRunHere"]) / div),
                        analyze.km(float(entry["ClientJobsIdle"]) / div),
                        analyze.km(float(entry["RunDiff"]) / div),
                        entry["UM"],
                        entry["RD"],
                    )
                ),
            )
        )

    columns = "%-40s %7s %7s %7s | %7s | %7s %7s %7s | %7s %5s %5s\n" % (
        frontend,
        "Regd",
        "Claimd",
        "Unmtchd",
        "ReqIdle",
        "JobRun",
        "JobHere",
        "JobIdle",
        "RunDiff",
        "%UM",
        "%RD",
    )

    if sorting == 1:
        if zero_supp == 1 and sum2 == 0:
            return
        to_be_printed.sort()
        to_be_printed.reverse()
        to_be_printed.insert(0, (0, columns))
        for a in to_be_printed:
            print(a[1])
        print()
    else:
        if zero_supp == 1 and sum2 == 0:
            return
        to_be_printed.insert(0, (0, columns))
        for a in to_be_printed:
            print(a[1])
        print()


##########################################################################


def main():
    """Main function to parse arguments and perform Frontend analysis.

    Parses command-line arguments to set various flags for sorting, filtering, and output formatting.
    Loads RRD data from a specified source, rearranges the data into a structured format, and prints
    both summary and detailed statistics about the client (Frontend) status attributes.
    """
    usage = """
USAGE:
    -x [#] : interval to do verbose stats (default 24)
    --source [ABSPATH or http addr] : factory base (default current directory)
    -s [attribute]: sort by attribute
              (-s x to see list of choices)
    -f [frontend] : filter by a single frontend
         (can omit "frontend_" before name)
    -z : zero suppression (don't show entries with 0s across all attributes)
    -p : show all periods (default off: just show 24 hours)
    -m : frontend mode - emphasize frontend data (don't show entry data)
                default unit: slots
    --ms : frontend mode, using seconds instead of slots
    --mh : frontend mode, using hours instead of slots
    -h : this usage message
"""

    # flags
    x = 24
    w_dir = os.getcwd()
    sorting = 0
    sort_attribute = 0
    filter_frontend = 0
    frontend_mode = 0
    show_all_periods = 0
    zero_supp = 0

    try:
        opts, args = getopt.getopt(sys.argv[1:], "x:hwf:s:zmp", ["source=", "ms", "mh"])
    except getopt.GetoptError:
        print("\n Option not recognized or missing parameters.")
        print(" Use -h for usage.\n")
        sys.exit(0)
    for o, a in opts:
        if o == "-x":
            x = a
        elif o == "--source":
            w_dir = a
        elif o in ("-h", "-help"):
            print(usage)
            return
        elif o == "-s":
            if a == "%UM" or a == "UM":
                a = "%unmatched"
            if a == "%RD" or a == "RD":
                a = "%rundiff"
            sort_attribute = a.lower()
            sorting = 1
        elif o == "-z":
            zero_supp = 1
        elif o == "-p":
            show_all_periods = 1
        elif o == "-m":
            frontend_mode = 1
        elif o == "--ms":
            frontend_mode = 2
        elif o == "--mh":
            frontend_mode = 3
        elif o == "-f":
            filter_frontend = a
            if "frontend_" not in filter_frontend:
                filter_frontend = "frontend_" + filter_frontend

    attributes = {
        "ClientGlideTotal": 0,
        "ClientGlideIdle": 0,
        "ClientGlideRunning": 0,
        "ClientJobsRunning": 0,
        "ClientJobsRunHere": 0,
        "ClientJobsIdle": 0,
        "ReqIdle": 0,
        "StatusRunning": 0,
        "StatusHeld": 0,
    }

    sort_attributes = [
        "registered",
        "claimed",
        "unmatched",
        "jobsrunning",
        "jobsrunhere",
        "jobsidle",
        "reqidle",
        "rundiff",
        "%unmatched",
        "%rundiff",
    ]

    attr_list = [
        "ClientGlideTotal",
        "ClientGlideRunning",
        "ClientGlideIdle",
        "ClientJobsRunning",
        "ClientJobsRunHere",
        "ClientJobsIdle",
        "ReqIdle",
        "RunDiff",
        "UM",
        "RD",
    ]

    if sorting != 0:
        if sort_attribute not in sort_attributes:
            print("%s not in list of attributes. Choices are:\n" % sort_attribute)
            for a in sort_attributes:
                print(a)
            print()
            return
        sort_attribute = sort_attributes.index(sort_attribute)

    data = {}  # sorted data
    rrd_data = {}
    rrd = "rrd_Status_Attributes.xml"

    if "http" in w_dir:
        file_dir = os.path.join(w_dir, rrd)
    else:
        file_dir = os.path.join(w_dir, "monitor", rrd)

        if not os.path.exists(file_dir):
            # Try RPM location
            rpmdir = "/var/lib/gwms-factory/work-dir"
            rpmfile_dir = os.path.join(rpmdir, "monitor", rrd)
            if not os.path.exists(rpmfile_dir):
                print("\nCannot open", file_dir)
                print("Please set --source to the factory work dir and try again.")
                sys.exit(1)
            if w_dir != os.getcwd():
                # Directory explicitly set, print warning
                print("\nWARNING: Cannot open", file_dir)
                print("Using RPM default %s instead" % rpmfile_dir)
            w_dir = rpmdir
            file_dir = rpmfile_dir

    try:
        # request.urlopen cannot handle local files
        if file_dir.startswith("http"):
            u = urlopen(file_dir)
        else:
            u = open(file_dir)
        rrd_data = xmlParse.xmlfile2dict(u)
    except Exception:
        print("\nCannot open", file_dir, "\n\tor", rrd, "was not found there.\n")
        print("Please set --source to the factory work dir and try again.")
        sys.exit(1)
    u.close()

    # rrd_data[updated,total,entries[entry[total[periods], frontends[periods]]]]
    # rrd_data numbers verified by hand

    ##############################################################################
    #   Rearranges rrd_data from data[entries][frontends][periods]
    #                   into data = [periods][frontends][entries][elements]
    #      (periods are integers and in seconds)
    ###############################################################################

    frontend_list = []

    for entry in rrd_data["entries"]:
        for frontend in rrd_data["entries"][entry]["frontends"]:
            if frontend not in frontend_list:
                frontend_list.append(frontend)

    if filter_frontend != 0:
        if filter_frontend not in frontend_list:
            print("\nFrontend", filter_frontend, "not found at source.\n")
            print("Choices are:\n ")
            for frontend in frontend_list:
                print(frontend)
            print()
            sys.exit(1)

    for entry in rrd_data["entries"]:
        for frontend in rrd_data["entries"][entry]["frontends"]:
            # if filtering, only choose selected frontend
            if filter_frontend != 0:
                if frontend != filter_frontend:
                    continue

            for period, elements in rrd_data["entries"][entry]["frontends"][frontend]["periods"].items():
                if int(period) not in data:
                    data[int(period)] = {}
                if frontend not in data[int(period)]:
                    data[int(period)][frontend] = {}
                if entry not in data[int(period)][frontend]:
                    data[int(period)][frontend][entry] = {}

                for a in attributes.keys():
                    if a not in data[int(period)][frontend][entry]:
                        data[int(period)][frontend][entry][a] = 0
                    try:
                        data[int(period)][frontend][entry][a] += int(float(elements[a]) * int(period))
                    except Exception:
                        pass

    # data[period[frontend[entry[element[value]]]]]
    # 'data' numbers verified by hand
    # debug_print_dict(data)

    #####################################################################
    # Organize totals/stats for each period, frontend, and entry independently
    ######################################################################

    if filter_frontend == 0:
        print(
            """
Status Attributes (Clients) analysis for All Entries - %s
"""
            % datetime.datetime.now().strftime("%d-%m-%Y_%H:%M:%S")
        )
    else:
        print(
            """
Status Attributes (Clients) analysis for %s - %s
"""
            % (filter_frontend, datetime.datetime.now().strftime("%d-%m-%Y_%H:%M:%S"))
        )

    period_data = {}
    frontend_data = {}
    entry_data = {}
    entry_data_all_frontends = {}

    for period, frontends in data.items():
        period = int(period)
        period_data[period] = {}
        frontend_data[period] = {}
        entry_data[period] = {}
        entry_data_all_frontends[period] = {}
        for a in attributes.keys():
            period_data[period][a] = 0

        for frontend, entries in frontends.items():
            frontend_data[period][frontend] = {}
            entry_data[period][frontend] = {}
            for a in attributes.keys():
                frontend_data[period][frontend][a] = 0

            for entry, elements in entries.items():
                entry_data[period][frontend][entry] = {}
                entry_data_all_frontends[period][entry] = {}
                for a in attributes.keys():
                    entry_data[period][frontend][entry][a] = 0
                    entry_data_all_frontends[period][entry][a] = 0

                for a in attributes.keys():
                    entry_data[period][frontend][entry][a] += elements[a]
                    frontend_data[period][frontend][a] += elements[a]
                    period_data[period][a] += elements[a]
                    entry_data_all_frontends[period][entry][a] += elements[a]

    ######################################################################
    #   Print
    ######################################################################

    # sort periods from least to greatest, with 24 hours at the top
    period_list = list(period_data.keys())
    period_list.sort()
    period_list.remove(86400)
    period_list.insert(0, 86400)

    period = int(x) * 3600

    # if filtering by period, make sure it's in the data
    if period not in period_list:
        print("Interval", x, "does not exist in data.\n Choices are:")
        for a in period_list:
            print(a / 3600)
        print()
        return

    if show_all_periods == 0:
        period_list = [period]

    for period in period_list:
        title = "Past %.1f hours" % (float(period) / 3600)

        print(
            """----------------------------------------
%s:

Registered:     %s
Claimed:        %s
Unmatched :     %s

Requested Idle: %s

Jobs Running:   %s
Jobs Run Here:  %s
Jobs Idle:      %s

RunDiff (Running-ClientRegistered): %s
"""
            % (
                title,
                analyze.printline(period_data[period]["ClientGlideTotal"], 1, period),
                analyze.printline(
                    period_data[period]["ClientGlideRunning"], period_data[period]["ClientGlideTotal"], period
                ),
                analyze.printline(
                    period_data[period]["ClientGlideIdle"], period_data[period]["ClientGlideTotal"], period
                ),
                analyze.printline(period_data[period]["ReqIdle"], 1, period),
                analyze.printline(period_data[period]["ClientJobsRunning"], 1, period),
                analyze.printline(period_data[period]["ClientJobsRunHere"], 1, period),
                analyze.printline(period_data[period]["ClientJobsIdle"], 1, period),
                analyze.printline(
                    period_data[period]["StatusRunning"] - period_data[period]["ClientGlideTotal"],
                    period_data[period]["StatusRunning"],
                    period,
                ),
            )
        )

    ################################################################################
    #    Print list-style stats
    ################################################################################

    period = int(x) * 3600

    if filter_frontend == 0 and frontend_mode == 0:
        print(
            """
---------------------------------------
---------------------------------------
Per Entry (all frontends) stats for the past %s hours.\n"""
            % x
        )
        list_print("", zero_supp, entry_data_all_frontends[period], sorting, attr_list, sort_attribute, 1)

    if frontend_mode == 0:
        print(
            """
---------------------------------------
---------------------------------------
Per Entry (per frontend) stats for the past %s hours.\n"""
            % x
        )

    if frontend_mode == 0:
        for frontend, entries in data[period].items():
            list_print(frontend, zero_supp, entry_data[period][frontend], sorting, attr_list, sort_attribute, 1)

    else:  # print frontend like entries, and omit entries
        units = ["Slots", "Seconds", "Hours"]
        divs = [period, 1.0, 3600.0]
        print(
            """
---------------------------------------
---------------------------------------
Frontend stats for the past %s hours, units = %s.\n"""
            % (x, units[frontend_mode - 1])
        )
        list_print("", zero_supp, frontend_data[period], sorting, attr_list, sort_attribute, divs[frontend_mode - 1])

    ################################################################################
    #    Print Key
    ################################################################################

    print(
        """-----------------------------------
LEGEND:

K = x   1,000
M = x 100,000

Regd - Registered (ClientGlideTotal)
Claimd - Claimed (ClientGlideRunning)
Unmtchd - Unmatched (ClientGlideIdle)
ReqIdle - Requested Idle
JobRun - Client Jobs Running
JobHere - Client Jobs Run Here
JobIdle - Client Jobs Idle
RunDiff - StatusRunning - ClientRegistered (ClientGlideTotal)
%UM - Percent Unmatched (Unmatched/Registered)
%RD - Percent RunDiff over Running (RunDiff/StatusRunning)
-------------------------------------
        \n"""
    )


if __name__ == "__main__":
    main()
