#!/usr/bin/env python

r"""
This module provides utilities for code updates.
"""

import os
import re
import sys
import tarfile
import time

robot_pgm_dir_path = os.path.dirname(__file__) + os.sep
repo_data_path = re.sub('/lib', '/data', robot_pgm_dir_path)
sys.path.append(repo_data_path)

import bmc_ssh_utils as bsu
import gen_robot_keyword as keyword
import gen_print as gp
import variables as var
from robot.libraries.BuiltIn import BuiltIn


def verify_no_duplicate_image_priorities(image_purpose):
    r"""
    Check that there are no active images with the same purpose and priority.

    Description of argument(s):
    image_purpose                   The purpose that images must have to be
                                    checked for priority duplicates.
    """

    taken_priorities = {}
    _, image_names = keyword.run_key("Get Software Objects  "
                                     + "version_type=" + image_purpose)

    for image_name in image_names:
        _, image = keyword.run_key("Get Host Software Property  " + image_name)
        if image["Activation"] != var.ACTIVE:
            continue
        image_priority = image["Priority"]
        if image_priority in taken_priorities:
            BuiltIn().fail("Found active images with the same priority.\n"
                           + gp.sprint_vars(image,
                                            taken_priorities[image_priority]))
        taken_priorities[image_priority] = image


def get_non_running_bmc_software_object():
    r"""
    Get the URI to a BMC image from software that is not running on the BMC.
    """

    # Get the version of the image currently running on the BMC.
    _, cur_img_version = keyword.run_key("Get BMC Version")
    # Remove the surrounding double quotes from the version.
    cur_img_version = cur_img_version.replace('"', '')

    _, images = keyword.run_key("Read Properties  "
                                + var.SOFTWARE_VERSION_URI + "enumerate")

    for image_name in images:
        _, image_properties = keyword.run_key(
            "Get Host Software Property  " + image_name)
        if 'Purpose' in image_properties and 'Version' in image_properties \
                and image_properties['Purpose'] != var.VERSION_PURPOSE_HOST \
                and image_properties['Version'] != cur_img_version:
            return image_name
    BuiltIn().fail("Did not find any non-running BMC images.")


def delete_all_pnor_images():
    r"""
    Delete all PNOR images from the BMC.
    """

    keyword.run_key("Initiate Host PowerOff")

    status, images = keyword.run_key("Get Software Objects  "
                                     + var.VERSION_PURPOSE_HOST)
    for image_name in images:
        keyword.run_key("Delete Image And Verify  " + image_name + "  "
                        + var.VERSION_PURPOSE_HOST)


def wait_for_activation_state_change(version_id, initial_state):
    r"""
    Wait for the current activation state of ${version_id} to
    change from the state provided by the calling function.

    Description of argument(s):
    version_id                      The version ID whose state change we are
                                    waiting for.
    initial_state                   The activation state we want to wait for.
    """

    keyword.run_key_u("Open Connection And Log In")
    retry = 0
    num_read_errors = 0
    read_fail_threshold = 1
    while (retry < 60):
        # TODO: Use retry option in run_key when available.
        status, software_state = keyword.run_key("Read Properties  "
                                                 + var.SOFTWARE_VERSION_URI
                                                 + str(version_id),
                                                 ignore=1)
        if status == 'FAIL':
            num_read_errors += 1
            if num_read_errors > read_fail_threshold:
                message = "Read errors exceeds threshold:\n " \
                    + gp.sprint_vars(num_read_errors, read_fail_threshold)
                BuiltIn().fail(message)
            time.sleep(10)
            continue

        current_state = (software_state)["Activation"]
        if (initial_state == current_state):
            time.sleep(10)
            retry += 1
            num_read_errors = 0
        else:
            return
    return


def get_latest_file(dir_path):
    r"""
    Get the path to the latest uploaded file.

    Description of argument(s):
    dir_path                        Path to the dir from which the name of the
                                    last updated file or folder will be
                                    returned to the calling function.
    """

    stdout, stderr, rc = \
        bsu.bmc_execute_command("cd " + dir_path
                                + "; stat -c '%Y %n' * |"
                                + " sort -k1,1nr | head -n 1")
    return stdout.split(" ")[-1]


def get_version_tar(tar_file_path):
    r"""
    Read the image version from the MANIFEST inside the tarball.

    Description of argument(s):
    tar_file_path                   The path to a tar file that holds the image
                                    version inside the MANIFEST.
    """

    tar = tarfile.open(tar_file_path)
    for member in tar.getmembers():
        f = tar.extractfile(member)
        content = f.read()
        if content.find(b"version=") == -1:
            # This tar member does not contain the version.
            continue
        content = content.decode("utf-8").split("\n")
        content = [x for x in content if "version=" in x]
        version = content[0].split("=")[-1]
        break
    tar.close()
    return version


def get_image_version(file_path):
    r"""
    Read the file for a version object.

    Description of argument(s):
    file_path                       The path to a file that holds the image
                                    version.
    """

    stdout, stderr, rc = \
        bsu.bmc_execute_command("cat " + file_path
                                + " | grep \"version=\"", ignore_err=1)
    return (stdout.split("\n")[0]).split("=")[-1]


def get_image_purpose(file_path):
    r"""
    Read the file for a purpose object.

    Description of argument(s):
    file_path                       The path to a file that holds the image
                                    purpose.
    """

    stdout, stderr, rc = \
        bsu.bmc_execute_command("cat " + file_path
                                + " | grep \"purpose=\"", ignore_err=1)
    return stdout.split("=")[-1]


def get_image_path(image_version):
    r"""
    Query the upload image dir for the presence of image matching
    the version that was read from the MANIFEST before uploading
    the image. Based on the purpose verify the activation object
    exists and is either READY or INVALID.

    Description of argument(s):
    image_version                   The version of the image that should match
                                    one of the images in the upload dir.
    """

    stdout, stderr, rc = \
        bsu.bmc_execute_command("ls -d " + var.IMAGE_UPLOAD_DIR_PATH + "*/")

    image_list = stdout.split("\n")
    retry = 0
    while (retry < 10):
        for i in range(0, len(image_list)):
            version = get_image_version(image_list[i] + "MANIFEST")
            if (version == image_version):
                return image_list[i]
        time.sleep(10)
        retry += 1


def verify_image_upload(image_version,
                        timeout=3):
    r"""
    Verify the image was uploaded correctly and that it created
    a valid d-bus object. If the first check for the image
    fails, try again until we reach the timeout.

    Description of argument(s):
    image_version                   The version from the image's manifest file
                                    (e.g. "v2.2-253-g00050f1").
    timeout                         How long, in minutes, to keep trying to
                                    find the image on the BMC. Default is 3 minutes.
    """

    image_path = get_image_path(image_version)
    image_version_id = image_path.split("/")[-2]

    keyword.run_key_u("Open Connection And Log In")
    image_purpose = get_image_purpose(image_path + "MANIFEST")
    if (image_purpose == var.VERSION_PURPOSE_BMC
            or image_purpose == var.VERSION_PURPOSE_HOST):
        uri = var.SOFTWARE_VERSION_URI + image_version_id
        ret_values = ""
        for itr in range(timeout * 2):
            status, ret_values = \
                keyword.run_key("Read Attribute  " + uri + "  Activation")

            if ((ret_values == var.READY) or (ret_values == var.INVALID)
                    or (ret_values == var.ACTIVE)):
                return True, image_version_id
            else:
                time.sleep(30)

        # If we exit the for loop, the timeout has been reached
        gp.print_var(ret_values)
        return False, None
    else:
        gp.print_var(image_purpose)
        return False, None


def verify_image_not_in_bmc_uploads_dir(image_version, timeout=3):
    r"""
    Check that an image with the given version is not unpacked inside of the
    BMCs image uploads directory. If no image is found, retry every 30 seconds
    until the given timeout is hit, in case the BMC takes time
    unpacking the image.

    Description of argument(s):
    image_version                   The version of the image to look for on
                                    the BMC.
    timeout                         How long, in minutes, to try to find an
                                    image on the BMC. Default is 3 minutes.
    """

    for i in range(timeout * 2):
        stdout, stderr, rc = \
            bsu.bmc_execute_command('ls ' + var.IMAGE_UPLOAD_DIR_PATH
                                    + '*/MANIFEST 2>/dev/null '
                                    + '| xargs grep -rl "version='
                                    + image_version + '"')
        image_dir = os.path.dirname(stdout.split('\n')[0])
        if '' != image_dir:
            bsu.bmc_execute_command('rm -rf ' + image_dir)
            BuiltIn().fail('Found invalid BMC Image: ' + image_dir)
        time.sleep(30)