# component.py
#
# Copyright Uberware. All Rights Reserved

import logging
import os
import shlex
import subprocess
import typing
from collections.abc import Mapping
from datetime import datetime
from uuid import UUID

from .config import SMEDGE

"""Common component functionality"""


# Use Python standard logging
logger = logging.getLogger("smedge")


NameOrID = typing.Union[str, UUID]
"""Type for a name or ID used to interact with Smedge entities"""

NameOrIDList = typing.Union[NameOrID, typing.List[NameOrID]]
"""Type for a list of names or IDs to interact with"""


class SmedgeComponentError(RuntimeError):
    """Custom RuntimeError derived exception class for Smedge errors"""

    pass


def run_component(cmd: typing.List[str]) -> subprocess.CompletedProcess:
    """Common wrapper for running the Smedge components"""
    master = os.getenv("SMEDGE_MASTER")
    if master:
        cmd.extend(["-Master", master])
    master_port = os.getenv("SMEDGE_MASTER_PORT")
    if master_port:
        cmd.extend(["-MasterPort", master_port])
    log_level = os.getenv("SMEDGE_LOG_LEVEL")
    if log_level:
        cmd.extend(["-LogFileLevel", log_level])
    cmd.extend(["-ConnectTimeout", os.getenv("SMEDGE_CONNECT_TIMEOUT", "10")])
    logger.debug(f"Running Component command: {cmd}")
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode:
        cmd = shlex.join(str(x) for x in cmd)
        abort(result, f"Component command failed: {cmd}")
    return result


def run_simple_command(
    component: str,
    command: str,
    items: NameOrIDList,
    warn: typing.Optional[str] = None,
    argument: typing.Optional[str] = None,
) -> typing.Optional[subprocess.CompletedProcess]:
    """Common wrapper for simple commands"""
    if not items:
        if warn:
            logger.warning(f"Did not supply required {warn}")
        return
    cmd = [component, command]
    if argument:
        cmd.append(argument)
    cmd.extend(ensure_list(items))
    return run_component(cmd)


def abort(result: subprocess.CompletedProcess, message: str):
    """Helper to log a component failure and raise an exception"""
    logger.error(message)
    logger.error(f"return: {result.returncode}")
    logger.error(f"stdout: {result.stdout}")
    logger.error(f"stderr: {result.stderr}")
    raise SmedgeComponentError(f"Component command failed to run: {message}")


def ensure_list(value: typing.Optional[NameOrIDList]) -> typing.List[NameOrID]:
    """Ensures value is a list of strings"""
    if isinstance(value, list):
        return [str(it) for it in value]
    elif value is None:
        return []
    else:
        return [str(value)]


def extract_datetime(from_string: str) -> typing.Optional[datetime]:
    """Convert the standard Smedge date time string to a datetime"""
    if not from_string or from_string == "Never":
        return None
    tokens = from_string.split(" ")
    if not tokens or len(tokens) > 2:
        raise ValueError(f"Could not determine the time: {from_string}")
    elif len(tokens) == 2:
        ymd = [int(x) for x in tokens[0].split("/")]
        time = tokens[1].split(".")
        hms = [int(x) for x in time[0].split(":")]
        ms = int(f"{time[1]}000")
        return datetime(ymd[0], ymd[1], ymd[2], hms[0], hms[1], hms[2], ms)
    else:
        value = str(int(tokens[0]))
        result = run_component([SMEDGE / "Job", "--from-time", value])
        return extract_datetime(result.stdout.split("\n")[0])


def datetime_to_rtime(from_datetime: typing.Optional[datetime]) -> str:
    """Convert a datetime object to an RLib time number value"""
    if from_datetime is None:
        return "0"
    else:
        ms = from_datetime.strftime("%f")[:-3]
        from_datetime = from_datetime.strftime(f"%Y/%m/%d %H:%M:%S.{ms}")
        result = run_component([SMEDGE / "Job", "--to-time", from_datetime])
        return result.stdout.split("\n")[0]


def find_value(from_dict: Mapping, name: str, default: any = None) -> any:
    """Finds a value in a dict case insensitive or returns default."""
    name = name.lower()
    for key in from_dict:
        if name == key.lower():
            return from_dict[key]
    return default
