#!/usr/bin/env python3

r"""
This module has functions to support various data structures such as the boot_table, valid_boot_list and
boot_results_table.
"""

import glob
import json
import os
import tempfile

from robot.libraries.BuiltIn import BuiltIn
from tally_sheet import *

try:
    from robot.utils import DotDict
except ImportError:
    import collections

import gen_cmd as gc
import gen_misc as gm
import gen_print as gp
import gen_valid as gv
import var_funcs as vf

# The code base directory will be one level up from the directory containing this module.
code_base_dir_path = os.path.dirname(os.path.dirname(__file__)) + os.sep

redfish_support_trans_state = int(
    os.environ.get("REDFISH_SUPPORT_TRANS_STATE", 0)
) or int(
    BuiltIn().get_variable_value("${REDFISH_SUPPORT_TRANS_STATE}", default=0)
)

platform_arch_type = os.environ.get(
    "PLATFORM_ARCH_TYPE", ""
) or BuiltIn().get_variable_value("${PLATFORM_ARCH_TYPE}", default="power")


def create_boot_table(file_path=None, os_host=""):
    r"""
    Read the boot table JSON file, convert it to an object and return it.

    Note that if the user is running without a global OS_HOST robot variable specified, this function will
    remove all of the "os_" start and end state requirements from the JSON data.

    Description of argument(s):
    file_path                       The path to the boot_table file.  If this value is not specified, it will
                                    be obtained from the "BOOT_TABLE_PATH" environment variable, if set.
                                    Otherwise, it will default to "data/boot_table.json".  If this value is a
                                    relative path, this function will use the code_base_dir_path as the base
                                    directory (see definition above).
    os_host                         The host name or IP address of the host associated with the machine being
                                    tested.  If the user is running without an OS_HOST (i.e. if this argument
                                    is blank), we remove os starting and ending state requirements from the
                                    boot entries.
    """
    if file_path is None:
        if redfish_support_trans_state and platform_arch_type != "x86":
            file_path = os.environ.get(
                "BOOT_TABLE_PATH", "data/boot_table_redfish.json"
            )
        elif platform_arch_type == "x86":
            file_path = os.environ.get(
                "BOOT_TABLE_PATH", "data/boot_table_x86.json"
            )
        else:
            file_path = os.environ.get(
                "BOOT_TABLE_PATH", "data/boot_table.json"
            )

    if not file_path.startswith("/"):
        file_path = code_base_dir_path + file_path

    # Pre-process the file by removing blank lines and comment lines.
    temp = tempfile.NamedTemporaryFile()
    temp_file_path = temp.name

    cmd_buf = "egrep -v '^[ ]*$|^[ ]*#' " + file_path + " > " + temp_file_path
    gc.cmd_fnc_u(cmd_buf, quiet=1)

    boot_file = open(temp_file_path)
    boot_table = json.load(boot_file, object_hook=DotDict)

    # If the user is running without an OS_HOST, we remove os starting and ending state requirements from
    # the boot entries.
    if os_host == "":
        for boot in boot_table:
            state_keys = ["start", "end"]
            for state_key in state_keys:
                for sub_state in list(boot_table[boot][state_key]):
                    if sub_state.startswith("os_"):
                        boot_table[boot][state_key].pop(sub_state, None)

    # For every boot_type we should have a corresponding mfg mode boot type.
    enhanced_boot_table = DotDict()
    for key, value in boot_table.items():
        enhanced_boot_table[key] = value
        enhanced_boot_table[key + " (mfg)"] = value

    return enhanced_boot_table


def create_valid_boot_list(boot_table):
    r"""
    Return a list of all of the valid boot types (e.g. ['REST Power On', 'REST Power Off', ...]).

    Description of argument(s):
    boot_table                      A boot table such as is returned by the create_boot_table function.
    """

    return list(boot_table.keys())


def read_boot_lists(dir_path="data/boot_lists/"):
    r"""
    Read the contents of all the boot lists files found in the given boot lists directory and return
    dictionary of the lists.

    Boot lists are simply files containing a boot test name on each line.  These files are useful for
    categorizing and organizing boot tests.  For example, there may be a "Power_on" list, a "Power_off" list,
    etc.

    The names of the boot list files will be the keys to the top level dictionary.  Each dictionary entry is
    a list of all the boot tests found in the corresponding file.

    Here is an abbreviated look at the resulting boot_lists dictionary.

    boot_lists:
      boot_lists[All]:
        boot_lists[All][0]:                           REST Power On
        boot_lists[All][1]:                           REST Power Off
    ...
      boot_lists[Code_update]:
        boot_lists[Code_update][0]:                   BMC oob hpm
        boot_lists[Code_update][1]:                   BMC ib hpm
    ...

    Description of argument(s):
    dir_path                        The path to the directory containing the boot list files.  If this value
                                    is a relative path, this function will use the code_base_dir_path as the
                                    base directory (see definition above).
    """

    if not dir_path.startswith("/"):
        # Dir path is relative.
        dir_path = code_base_dir_path + dir_path

    # Get a list of all file names in the directory.
    boot_file_names = os.listdir(dir_path)

    boot_lists = DotDict()
    for boot_category in boot_file_names:
        file_path = gm.which(dir_path + boot_category)
        boot_list = gm.file_to_list(file_path, newlines=0, comments=0, trim=1)
        boot_lists[boot_category] = boot_list

    return boot_lists


def valid_boot_list(boot_list, valid_boot_types):
    r"""
    Verify that each entry in boot_list is a supported boot test.

    Description of argument(s):
    boot_list                       An array (i.e. list) of boot test types (e.g. "REST Power On").
    valid_boot_types                A list of valid boot types such as that returned by
                                    create_valid_boot_list.
    """

    for boot_name in boot_list:
        boot_name = boot_name.strip(" ")
        error_message = gv.valid_value(
            boot_name, valid_values=valid_boot_types, var_name="boot_name"
        )
        if error_message != "":
            BuiltIn().fail(gp.sprint_error(error_message))


class boot_results:
    r"""
    This class defines a boot_results table.
    """

    def __init__(
        self, boot_table, boot_pass=0, boot_fail=0, obj_name="boot_results"
    ):
        r"""
        Initialize the boot results object.

        Description of argument(s):
        boot_table                  Boot table object (see definition above).  The boot table contains all of
                                    the valid boot test types.  It can be created with the create_boot_table
                                    function.
        boot_pass                   An initial boot_pass value.  This program may be called as part of a
                                    larger test suite.  As such there may already have been some successful
                                    boot tests that we need to keep track of.
        boot_fail                   An initial boot_fail value.  This program may be called as part of a
                                    larger test suite.  As such there may already have been some unsuccessful
                                    boot tests that we need to keep track of.
        obj_name                    The name of this object.
        """

        # Store the method parms as class data.
        self.__obj_name = obj_name
        self.__initial_boot_pass = boot_pass
        self.__initial_boot_fail = boot_fail

        # Create boot_results_fields for use in creating boot_results table.
        boot_results_fields = DotDict([("total", 0), ("pass", 0), ("fail", 0)])
        # Create boot_results table.
        self.__boot_results = tally_sheet(
            "boot type", boot_results_fields, "boot_test_results"
        )
        self.__boot_results.set_sum_fields(["total", "pass", "fail"])
        self.__boot_results.set_calc_fields(["total=pass+fail"])
        # Create one row in the result table for each kind of boot test in the boot_table (i.e. for all
        # supported boot tests).
        for boot_name in list(boot_table.keys()):
            self.__boot_results.add_row(boot_name)

    def add_row(self, *args, **kwargs):
        r"""
        Add row to tally_sheet class object.

        Description of argument(s):
        See add_row method in tally_sheet.py for a description of all arguments.
        """
        self.__boot_results.add_row(*args, **kwargs)

    def return_total_pass_fail(self):
        r"""
        Return the total boot_pass and boot_fail values.  This information is comprised of the pass/fail
        values from the table plus the initial pass/fail values.
        """

        totals_line = self.__boot_results.calc()
        return (
            totals_line["pass"] + self.__initial_boot_pass,
            totals_line["fail"] + self.__initial_boot_fail,
        )

    def update(self, boot_type, boot_status):
        r"""
        Update our boot_results_table.  This includes:
        - Updating the record for the given boot_type by incrementing the pass or fail field.
        - Calling the calc method to have the totals calculated.

        Description of argument(s):
        boot_type                   The type of boot test just done (e.g. "REST Power On").
        boot_status                 The status of the boot just done.  This should be equal to either "pass"
                                    or "fail" (case-insensitive).
        """

        self.__boot_results.inc_row_field(boot_type, boot_status.lower())
        self.__boot_results.calc()

    def sprint_report(self, header_footer="\n"):
        r"""
        String-print the formatted boot_resuls_table and return them.

        Description of argument(s):
        header_footer               This indicates whether a header and footer are to be included in the
                                    report.
        """

        buffer = ""

        buffer += gp.sprint(header_footer)
        buffer += self.__boot_results.sprint_report()
        buffer += gp.sprint(header_footer)

        return buffer

    def print_report(self, header_footer="\n", quiet=None):
        r"""
        Print the formatted boot_resuls_table to the console.

        Description of argument(s):
        See sprint_report for details.
        quiet                       Only print if this value is 0.  This function will search upward in the
                                    stack to get the default value.
        """

        quiet = int(gm.dft(quiet, gp.get_stack_var("quiet", 0)))

        gp.qprint(self.sprint_report(header_footer))

    def sprint_obj(self):
        r"""
        sprint the fields of this object.  This would normally be for debug purposes only.
        """

        buffer = ""

        buffer += "class name: " + self.__class__.__name__ + "\n"
        buffer += gp.sprint_var(self.__obj_name)
        buffer += self.__boot_results.sprint_obj()
        buffer += gp.sprint_var(self.__initial_boot_pass)
        buffer += gp.sprint_var(self.__initial_boot_fail)

        return buffer

    def print_obj(self):
        r"""
        Print the fields of this object to stdout.  This would normally be for debug purposes.
        """

        gp.gp_print(self.sprint_obj())


def create_boot_results_file_path(pgm_name, openbmc_nickname, master_pid):
    r"""
    Create a file path to be used to store a boot_results object.

    Description of argument(s):
    pgm_name                        The name of the program.  This will form part of the resulting file name.
    openbmc_nickname                The name of the system.  This could be a nickname, a hostname, an IP,
                                    etc.  This will form part of the resulting file name.
    master_pid                      The master process id which will form part of the file name.
    """

    USER = os.environ.get("USER", "")
    dir_path = "/tmp/" + USER + "/"
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)

    file_name_dict = vf.create_var_dict(pgm_name, openbmc_nickname, master_pid)
    return vf.create_file_path(
        file_name_dict, dir_path=dir_path, file_suffix=":boot_results"
    )


def cleanup_boot_results_file():
    r"""
    Delete all boot results files whose corresponding pids are no longer active.
    """

    # Use create_boot_results_file_path to create a globex to find all of the existing boot results files.
    globex = create_boot_results_file_path("*", "*", "*")
    file_list = sorted(glob.glob(globex))
    for file_path in file_list:
        # Use parse_file_path to extract info from the file path.
        file_dict = vf.parse_file_path(file_path)
        if gm.pid_active(file_dict["master_pid"]):
            gp.qprint_timen("Preserving " + file_path + ".")
        else:
            gc.cmd_fnc("rm -f " + file_path)


def update_boot_history(boot_history, boot_start_message, max_boot_history=10):
    r"""
    Update the boot_history list by appending the boot_start_message and by removing all but the last n
    entries.

    Description of argument(s):
    boot_history                    A list of boot start messages.
    boot_start_message              This is typically a time-stamped line of text announcing the start of a
                                    boot test.
    max_boot_history                The max number of entries to be kept in the boot_history list.  The
                                    oldest entries are deleted to achieve this list size.
    """

    boot_history.append(boot_start_message)

    # Trim list to max number of entries.
    del boot_history[: max(0, len(boot_history) - max_boot_history)]


def print_boot_history(boot_history, quiet=None):
    r"""
    Print the last ten boots done with their time stamps.

    Description of argument(s):
    quiet                           Only print if this value is 0.  This function will search upward in the
                                    stack to get the default value.
    """

    quiet = int(gm.dft(quiet, gp.get_stack_var("quiet", 0)))

    # indent 0, 90 chars wide, linefeed, char is "="
    gp.qprint_dashes(0, 90)
    gp.qprintn("Last 10 boots:\n")

    for boot_entry in boot_history:
        gp.qprint(boot_entry)
    gp.qprint_dashes(0, 90)