import re
import time
import datetime
import json
import os
import psutil
import boto3
from git import Repo
from unqlite import UnQLite
import grpc
from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2
from cpuinfo import get_cpu_info
from robot.api import logger
from dateutil import parser as date_parser
from Common import ctn_get_collect_version
from Common import ctn_is_using_direct_grpc
from Common import ctn_find_line_from


class delta_process_stat:
    """! This class calc difference between two GetProcessStats results
    """

    def __init__(self, process_stat1, process_stat2):
        """! constructor
        @param process_stat1 last GetProcessStats result
        @param process_stat2 previous GetProcessStats result
        """
        self.query_read_bytes = process_stat1.query_read_bytes - \
            process_stat2.query_read_bytes
        self.query_write_bytes = process_stat1.query_write_bytes - \
            process_stat2.query_write_bytes
        self.real_read_bytes = process_stat1.real_read_bytes - process_stat2.real_read_bytes
        self.real_write_bytes = process_stat1.real_write_bytes - \
            process_stat2.real_write_bytes
        self.user_time = self.sub_duration(
            process_stat1.user_time, process_stat2.user_time)
        self.kernel_time = self.sub_duration(
            process_stat1.kernel_time, process_stat2.kernel_time)

    def sub_duration(self, duration_left, duration_right):
        """! sub 2 protobuf TimeStamps
        @return a protobuf Duration
        """

        ret = google_dot_protobuf_dot_duration__pb2.Duration()
        ret.FromMilliseconds(duration_left.ToMilliseconds() -
                             duration_right.ToMilliseconds())
        a = ret.seconds
        b = ret.nanos
        return ret


def ctn_diff_process_stat(process_stat1, process_stat2):
    return delta_process_stat(process_stat1, process_stat2)


def ctn_add_value_to_delta_process_stat(delta, key, value):
    """! add an attribute to an object"""
    setattr(delta, key, value)


def ctn_get_last_bench_result(log: str, id: int, start, name_to_find: str):
    """Extract last bench trace in the given log file.
    Args:
        log: The log file that should contain the bench event.
        id: The ID of the bench event.
        start: The date from which to start the look up.
        name_to_find: The muxer name to find.
    
    Returns:
        A JSON object that contains all bench time points.
    """
    last_bench_str = ""
    p = re.compile(
        r".+bench\s+\w+\s+content:'(.\"id\":{}.+{}.+)'".format(id, name_to_find.replace(" ", "\s")))

    try:
        with open(log, "r") as f:
            lines = f.readlines()

        idx = ctn_find_line_from(lines, start)
        for i in range(idx, len(lines)):
            line = lines[i]
            extract = p.match(line)
            if extract is not None:
                last_bench_str = extract.group(1)

        if len(last_bench_str) == 0:
            return None
        return json.loads(last_bench_str)
    except IOError:
        return None


def ctn_get_last_bench_result_with_timeout(log: str, id: int, start, name_to_find: str, timeout: int):
    """Extract last bench trace in the given log file.

    Args:
        log: The log file.
        id: The ID of the bench event.
        start: The date from which we start to search.
        name_to_find: A string to find in the bench result like a muxer name
        timeout: A duration in seconds

    Returns:
        A JSON object that contains all bench time points
    """
    limit = time.time() + timeout
    while time.time() < limit:
        json_obj = ctn_get_last_bench_result(log, id, start, name_to_find)
        if json_obj is not None:
            logger.console(json_obj)
            return json_obj
        time.sleep(5)
    logger.console("The file '{}' does not contain bench".format(log))
    return None


def ctn_calc_bench_delay(bench_event_result, bench_muxer_begin: str, bench_muxer_begin_function: str, bench_muxer_end: str, bench_muxer_end_function: str):
    """ Compute a duration between two time points of a bench event json object

    Args:
        bench_event_result: bench result json object
        bench_muxer_begin: name of the muxer owned to the first time point (accepts regexp)
        bench_muxer_begin_function: name of the muxer function owned to the first time point
        bench_muxer_end: name of the muxer owned to the last time point
        bench_muxer_end_function: name of the muxer function owned to the last time point

    Returns:
        A delay in seconds.
    """

    logger.console(f"Bench Delay {bench_muxer_begin_function}: {bench_muxer_begin} => {bench_muxer_end_function}: {bench_muxer_end}")
    time_begin = 0
    time_end = 0
    begin_re = re.compile(bench_muxer_begin)
    for point in bench_event_result['points']:
        if begin_re.match(point['name']) and point['function'] == bench_muxer_begin_function:
            time_begin = date_parser.parse(point['time'])
        if point['name'] == bench_muxer_end and point['function'] == bench_muxer_end_function:
            time_end = date_parser.parse(point['time'])
    return time_end - time_begin


def ctn_store_result_in_unqlite(file_path: str, test_name: str,  broker_or_engine: str, resources_consumed: delta_process_stat, end_process_stat, bench_event_result, bench_muxer_begin: str, bench_muxer_begin_function: str, bench_muxer_end: str, bench_muxer_end_function: str, other_bench_data: dict = None):
    """! store a bench result and process stat difference in an unqlite file
    it also stores git information
    @param file_path  path of the unqlite file
    @param test_name name of the state if we are using direct grpc serialization, _grpc is appended to
    @param broker_or_engine a string equal to engine or broker to identify stats owning
    @param resources_consumed a delta_process_stat object
    @param end_process_stat stat of the process at the end of the test
    @param bench_event_result a json bench result
    @param bench_muxer_begin name of the muxer owned to the first time point
    @param bench_muxer_begin_function name of the muxer function owned to the first time point
    @param bench_muxer_end name of the muxer owned to the last time point
    @param bench_muxer_end_function name of the muxer function owned to the last time point
    @param other_bench_data  arbitrary datas added to database
    """

    row = vars(resources_consumed).copy()
    row['user_time'] = row['user_time'].seconds + \
        row['user_time'].nanos / 1000000000.0
    row['kernel_time'] = row['kernel_time'].seconds + \
        row['kernel_time'].nanos / 1000000000.0
    info = get_cpu_info()
    if 'brand_raw' in info:
        row['cpu'] = info["brand_raw"]
    else:
        row['cpu'] = 'unknown cpu'
        logger.console(f"unable to get cpu info: {info}")
    row['nb_core'] = info["count"]
    row['memory_size'] = psutil.virtual_memory().total
    row['memory_used'] = end_process_stat.vm_size
    row['event_propagation_delay'] = ctn_calc_bench_delay(
        bench_event_result, bench_muxer_begin, bench_muxer_begin_function, bench_muxer_end, bench_muxer_end_function).total_seconds()
    logger.console(f"event_propagation_delay = {row['event_propagation_delay']}")

    if other_bench_data is not None:
        for key, value in other_bench_data.items():
            row[key] = value

    version = ctn_get_collect_version()
    version = version[0:version.rfind(".")] + ".x"
    row['origin'] = version

    # git branch and commit
    try:
        repo = Repo(os.getcwd())
    except:
        # if we launch from tests directory => open ..directory
        repo = Repo(os.getcwd() + "/..")
    commit = repo.head.commit
    row['date_commit'] = commit.authored_datetime.timestamp()
    if repo.is_dirty():
        row['commit'] = f'uncommited files last_commit:{repo.head.commit.hexsha}'
    else:
        row['commit'] = repo.head.commit.hexsha
    row['t'] = time.time()
    row['branch'] = repo.head.name
    if ctn_is_using_direct_grpc():
        test_name += "_grpc"
    db = UnQLite(file_path)
    test_name = f'collectbench_{test_name}_{broker_or_engine}_{bench_event_result["id"]}'
    logger.console(f"Storing in DB: {test_name}")
    benchs = db.collection(test_name)
    benchs.create()
    benchs.store(row)
    test = benchs.all()
    db.close()
    return True


def ctn_upload_database_to_s3(file_path: str):
    """! upload a file to s3
    @param file_path path of the file on the disk
    """
    aws_access_key_id = os.getenv('AWS_ACCESS_KEY_ID')
    aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY')
    bucket = os.getenv('AWS_BUCKET')
    if aws_access_key_id is None or aws_secret_access_key is None:
        logger.console(
            "AWS_ACCESS_KEY_ID OR AWS_SECRET_ACCESS_KEY or AWS_BUCKET not set")
        return False
    try:
        s3_resource = boto3.resource('s3')
        s3_resource.Object(bucket, os.path.basename(
            file_path)).upload_file(file_path)
        return True
    except Exception as e:
        logger.console(f"upload to s3  exception:{e}")
        return False


def ctn_download_database_from_s3(file_path: str):
    """! upload a file to s3
    @param file_path path of the file on the disk
    @bucket s3 bucket
    """
    aws_access_key_id = os.getenv('AWS_ACCESS_KEY_ID')
    aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY')
    bucket = os.getenv('AWS_BUCKET')
    if aws_access_key_id is None or aws_secret_access_key is None:
        logger.console(
            "AWS_ACCESS_KEY_ID OR AWS_SECRET_ACCESS_KEY or AWS_BUCKET not set")
        return False
    try:
        s3_resource = boto3.resource('s3')
        s3_resource.Object(bucket, os.path.basename(
            file_path)).download_file(file_path)
        return True
    except Exception as e:
        logger.console(f"upload to s3  exception:{e}")
        return False
