import glob
import inspect
import json
import os
import subprocess
import sys
import getpass
import logging.config

import psutil
from psutil import AccessDenied

from dry_pipe import env_variables

LOCAL_PROCESS_IDENTIFIER_VAR = "____DRY_PIPE_TASK"

log_conf_file = env_variables.log_config()


def DEBUG_CONFIG(main_level, drypype_level, root_level):
    return {
        'version': 1,
        'disable_existing_loggers': False,
        'formatters': {
            'standard': {
                'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
            },
        },
        'handlers': {
            'console': {
                'level': 'DEBUG',
                'formatter': 'standard',
                'class': 'logging.StreamHandler',
                'stream': 'ext://sys.stdout',
            },
        },
        'loggers': {

            'zoz': {  # root logger
                'handlers': ['console'],
                'level': root_level,
                'propagate': False
            },
            'drypipe.*': {
                'handlers': ['console'],
                'level': drypype_level,
                'propagate': False
            },
            '__main__': {
                'handlers': ['console'],
                'level': main_level,
                'propagate': False
            }
        }
    }


def setup_debug_log_config(drypype_level="INFO", root_level="INFO", main_level="INFO", extra_loggers={}):
    print("DEBUG CONFIG")

    conf = DEBUG_CONFIG(main_level, drypype_level, root_level)

    conf["loggers"] = {
        ** conf["loggers"],
        ** extra_loggers
    }

    logging.config.dictConfig(conf)

if log_conf_file is not None:
    import yaml
    if log_conf_file == "DEBUG_CONFIG":
        setup_debug_log_config()
    else:
        with open(log_conf_file) as f:

            print(f"log config: {log_conf_file}")
            config = yaml.load(f, Loader=yaml.FullLoader)
            logging.config.dictConfig(config)


logger = logging.getLogger(__name__)


class ValidationError(Exception):
    def __init__(self, message, code=None):
        super(ValidationError, self).__init__(message)
        self.code = code

    consumes_has_bad_positional_arg_val = 0

    consumes_has_bad_positional_arg = 1

    consumes_has_invalid_kwarg_type = 2

    produces_cant_take_positional_args = 3

    produces_only_takes_files = 4

    call_has_bad_arg = 5


class Val:

    def __init__(self, value):
        t = type(value)
        if t == int or t == str or t == float:
            self.value = value
        elif t == type(int):
            raise ValidationError(f"invalid val type dsl.val(int), you probably want to use 'dsl.var(int)'")
        elif t == t == type(str):
            raise ValidationError(f"invalid val type dsl.var(str), you probably want to use 'dsl.var(str)'")
        elif type(float):
            raise ValidationError(f"invalid val type, dsl.var(float) you probably want to use 'dsl.var(float)'")
        else:
            raise ValidationError(f"invalid val type: {type(value)}, must be int, string, float")

    def to_hash(self):
        return str(self.value)

    def serialized_value(self):

        if type(self.value) == str:
            return f'"{self.value}"'

        return f"{self.value}"


class IncompleteVar:

    def __init__(self, type=str, may_be_none=True):
        self.type = type
        self.may_be_none = may_be_none


class OutputVar:

    def __init__(self, type, may_be_none, name, producing_task):

        supported = [str, int, float]

        if type not in supported:
            raise ValidationError(f"var {name} can't be of type {type} supported types are: {','.join(supported)}")

        self.name = name
        self.type = type
        self.producing_task = producing_task
        self.may_be_none = may_be_none

    def format_for_python(self, v):

        if self.type == int:
            return str(v)
        elif self.type == str:
            return f'"{v}"'
        elif self.type == float:
            return str(v)

        raise Exception(f"unsupported type {self.type}")

    def unformat_for_python(self, v):

        if self.type == int:
            return int(v)
        elif self.type == str:
            s = v.rstrip('\"\'')
            s = s.lstrip('\"\'')
            return s
        elif self.type == float:
            return float(v)

        raise Exception(f"unsupported type {self.type}")

    def input_var(self, var_name_in_consuming_task):
        return InputVar(self, var_name_in_consuming_task)

    def fetch(self):
        varz = self.producing_task.out_vars_by_name()
        v = varz.get(self.name)
        return self.unformat_for_python(v)


class InputVar:

    def __init__(self, output_var, var_name_in_consuming_task):
        self.output_var = output_var
        self.var_name_in_consuming_task = var_name_in_consuming_task

    def write(self, string_value):
        return string_value


class Executor:

    def key(self):
        return None

    def is_remote(self):
        return False

class Local(Executor):

    def count_running_tasks(self):

        current_user = getpass.getuser()

        c = 0

        for it in psutil.process_iter(['pid', 'exe', 'username', 'environ']):

            if it.username() != current_user:
                continue
            try:

                if not it.exe().endswith("/bash"):
                    continue

                v = it.environ().get(LOCAL_PROCESS_IDENTIFIER_VAR)

                if v is not None: # and v.startswith("____"):
                    c += 1

            except AccessDenied:
                pass

        return c

    def kill_task(self, task_key):

        current_user = getpass.getuser()

        for it in psutil.process_iter(['pid', 'exe', 'username', 'environ']):

            if it.username() != current_user:
                continue
            try:

                if not it.exe().endswith("/bash"):
                    continue

                v = it.environ().get(LOCAL_PROCESS_IDENTIFIER_VAR)

                if v is not None:
                    if v == f"____{task_key}":
                        it.kill()

            except AccessDenied:
                pass

        #pids = list(get_pids())
        #print(f"pids: {pids}")
        #for pid in pids:
        #    os.kill(pid. signal.SIGKILL)

    def __init__(self, before_execute_bash):
        self.before_execute_bash = before_execute_bash
        self.cpu_count = len(psutil.Process().cpu_affinity())

    def has_cpu_capacity_to_launch(self):

        return self.count_running_tasks() <= self.cpu_count

    fail_silently_for_test = False

    """
        experimental, for scenario:
        + launcher runs in a LXC container
        + launcher runs tasks with singularity,launch on the physical host of the LXC container  
    """
    def __command_on_sibling_container(self, task, user, back_ground):
        return [
            f"ssh {user}@127.0.0.1 'nohup {task.v_abs_script_file()} " +
            f"  >{task.v_abs_out_log()} 2>{task.v_abs_err_log()} {back_ground}'"
        ]

    def execute(self, task, touch_pid_file_func, wait_for_completion=False, fail_silently=False):
        b4_command = ""
        if self.before_execute_bash is not None:
            b4_command = f"{self.before_execute_bash} &&"

        back_ground = "&"

        if wait_for_completion:
            back_ground = ""

        with subprocess.Popen(
                [f"nohup bash -c '{b4_command} . {task.v_abs_script_file()} " +
                 f"  >{task.v_abs_out_log()} 2>{task.v_abs_err_log()}' {back_ground}"],
                shell=True,
                text=True,
                env={
                    **dict([
                        (LOCAL_PROCESS_IDENTIFIER_VAR, f"____{task.key}")
                    ]),
                    **os.environ
                },
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL
        ) as p:

            p.wait()

            if p.returncode != 0:

                # If we were in async mode, we wouldn't see the error...
                if wait_for_completion:
                    if fail_silently:
                        return

                    if Local.fail_silently_for_test:
                        return

                    task_state = task.get_state()
                    err_if_failed = task_state.tail_err_if_failed(5)

                    if err_if_failed is not None:
                        raise Exception(
                            f"task {task.key} failed \n{err_if_failed}\n further details in {task.v_abs_err_log()}"
                        )
                    else:
                        return

                raise Exception(
                    f"the command for {task} returned non zero result, " +
                    f"see logs at {task.v_abs_out_log()} and {task.v_abs_err_log()}.\n",
                    f"invocation script: {task.v_abs_script_file()}"
                )

            touch_pid_file_func(p.pid)


class Slurm(Executor):

    def __init__(self, account, sbatch_options):

        if account is None:
            raise Exception(f"account account can't be None")

        self.account = account
        self.sbatch_options = sbatch_options

    def execute(self, task, touch_pid_file_func, wait_for_completion=False, fail_silently=False):

        env = {
            **os.environ
        }

        if wait_for_completion:
            cmd = [f"bash -c 'export SBATCH_WAIT=True && {task.v_abs_sbatch_launch_script()}'"]
        else:
            cmd = [task.v_abs_sbatch_launch_script()]

        with subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            shell=True,
            env=env,
            text=True
        ) as p:
            p.wait()

            if p.returncode != 0:
                err = p.stderr.read().strip()
                raise Exception(f"call to sbatch failed: {cmd}\n {err}")


class IndeterminateFile:

    def __init__(self, file_path, manage_signature):
        self.file_path = file_path.strip()
        self.manage_signature = manage_signature

    def produced_file(self, var_name, producing_task):
        return ProducedFile(self.file_path, var_name, self.manage_signature, producing_task)

    def pre_existing_file(self):
        return PreExistingFile(self.file_path, self.manage_signature)


class FileSet:

    def __init__(self, glob_pattern):
        self.glob_pattern = glob_pattern

    def out_file_set(self, producing_task, name_in_producing_task):
        return OutFileSet(self, producing_task, name_in_producing_task)


class OutFileSet:

    def __init__(self, file_set, producing_task, name_in_producing_task):
        self.file_set = file_set
        self.producing_task = producing_task
        self.name_in_producing_task = name_in_producing_task

    def fetch(self):
        if not self.producing_task.get_state().is_completed():
            raise Exception(f"task must be complete before out value can be fetched")

        producing_task_work_dir = self.producing_task.v_abs_work_dir()

        pattern = os.path.join(producing_task_work_dir, self.file_set.glob_pattern)

        def make_path_relative_to_pipeline_instance_dir(p):
            return os.path.relpath(p, producing_task_work_dir)

        for f in glob.glob(pattern):
            f = make_path_relative_to_pipeline_instance_dir(f)
            yield ProducedFile(f, self.name_in_producing_task, True, self.producing_task)


class SubPipeline:
    def __init__(self, pipeline, task_namespance_prefix, dsl):
        self.pipeline = pipeline
        self.task_namespance_prefix = task_namespance_prefix
        self.dsl = dsl

    def with_completed_tasks(self, *args):
        args = [f"{self.task_namespance_prefix}{a}" for a in args]
        return self.dsl.with_completed_tasks(*args)


class TaskMatcher:

    def __init__(self, task_keys_glob_pattern):
        self.task_keys_glob_pattern = task_keys_glob_pattern


class PreExistingFile:

    def __init__(self, file_path, manage_signature=False):

        if file_path is None or file_path == "" or file_path == ".":
            raise ValidationError(f"invalid file {file_path}")

        self.file_path = file_path
        self.manage_signature = manage_signature

    def to_hash(self):
        return self.file_path

    def value(self):
        return self.file_path

    def absolute_path(self, task):

        if os.path.isabs(self.file_path):
            return self.file_path

        return self.file_path


class ProducedFile:

    def __init__(self, file_path, var_name, manage_signature, producing_task, is_dummy=False):

        if file_path is None or file_path == "" or file_path == ".":
            raise ValidationError(f"invalid file {file_path}")

        if var_name is None or var_name == "":
            raise ValidationError(f"can't be {var_name}")

        self.var_name = var_name
        self.file_path = file_path
        self.producing_task = producing_task
        self.manage_signature = manage_signature
        self.is_dummy = is_dummy

    def to_hash(self):
        return self.file_path

    def value(self):
        return self.file_path

    def absolute_path(self, task):

        if os.path.isabs(self.file_path):
            return self.file_path

        return os.path.join(task.work_dir(), self.file_path)

    def input_file(self, var_name_in_consuming_task):
        return InputFile(self, var_name_in_consuming_task)


class InputFile:

    def __init__(self, produced_file, var_name_in_consuming_task):
        self.produced_file = produced_file
        self.var_name_in_consuming_task = var_name_in_consuming_task

def env_from_sourcing(env_file):

    p = os.path.abspath(sys.executable)

    dump_with_python_script = f'{p} -c "import os, json; print(json.dumps(dict(os.environ)))"'

    with subprocess.Popen(
            ['/bin/bash', '-c', f". {env_file} && {dump_with_python_script}"],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
    ) as pipe:
        pipe.wait()
        err = pipe.stderr.read()
        out = pipe.stdout.read()
        if pipe.returncode != 0:
            raise Exception(f"Failed sourcing env file: {env_file}\n{err}")
        return json.loads(out)


class PythonCall:

    def __init__(self, func, tests=[]):
        self.func = func
        self.signature = inspect.signature(self.func)
        self.tests = tests

    def signature_spec(self):
        raise Exception(f"implement me")

    def mod_func(self):
        mod = inspect.getmodule(self.func)
        file_name = os.path.basename(mod.__file__)
        func_name = self.func.__name__
        importable_module_name = f"{mod.__package__}.{file_name[:-3]}:{func_name}"
        return importable_module_name


class Wait:

    def __init__(self, tasks):
        self.tasks = tasks

    def is_ready(self):
        for t in self.tasks:
            if not t.has_completed():
                return False

        return True

class TaskProps:

    def __init__(self, task, props):
        self.props = props
        self.task = task

    def __getattr__(self, name):
        if name not in self.props:
            raise ValidationError(f"{self.task} has no prop {name}")
        return self.props.get(name)


def flatten(the_list):
    return [item for sublist in the_list for item in sublist]


class MessageUtils:

    @staticmethod
    def safe_utf8(s):
        try:
            return s.decode("utf8")
        except UnicodeDecodeError as ex:
            return str(s)

