# Copyright (c) ModelScope Contributors. All rights reserved.
import argparse
import functools
import inspect
import json
import numpy as np
import os
from typing import Any, Callable, Dict, List, Literal, Optional, TypeVar, Union

from swift.utils import find_free_port, find_node_ip
from .arguments import RayArguments
from .resource_manager import ResourceManager

T = TypeVar('T')


def get_args():
    parser = argparse.ArgumentParser()
    _, unknown = parser.parse_known_args()
    return json.dumps(unknown)


class RayHelper:
    resource_manager: Optional[ResourceManager] = None

    worker_cls: Dict = {}

    args: RayArguments = None

    worker_instance: Dict = {}

    initialized = False

    device_groups: Dict[str, Any] = None

    @staticmethod
    def initialize(device_groups: Dict[str, Any]):
        """Initialize RayHelper.

        Args:
            device_groups: The device groups to initialize.

        Returns:
            None
        """
        if RayHelper.ray_inited():
            return
        import ray
        RayHelper.device_groups = device_groups
        ray.init()
        if RayHelper.resource_manager is None:
            # Resource manager initialize only once in the pipeline process.
            RayHelper.resource_manager = ResourceManager(device_groups)

    @staticmethod
    def teardown():
        if RayHelper.resource_manager is not None:
            RayHelper.resource_manager.destroy_placement_group()
            RayHelper.resource_manager = None

    @staticmethod
    def is_called_from_init():
        """If some function called from __init__.

        Ray functions perform different behaviors depending on whether they are called from __init__.

        Returns:
            Boolean.
        """
        stack = inspect.stack()
        for frame_info in stack[1:]:
            if frame_info.function == '__init__':
                return True
        return False

    @staticmethod
    def ray_inited():
        try:
            import ray
        except ImportError:
            # not installed, not inited
            return False
        return ray.is_initialized()

    @staticmethod
    def is_worker():
        import ray
        return RayHelper.ray_inited() and ray._private.worker.global_worker.mode == ray._private.worker.WORKER_MODE

    @staticmethod
    def worker(group: Union[str, List[str]]):

        def decorator(cls):
            if not RayHelper.ray_inited():
                return cls
            if RayHelper.is_worker():
                return cls
            cls.decorated = True
            groups = [group] if isinstance(group, str) else group
            import ray
            _cls = ray.remote(cls)
            for g in groups:
                RayHelper.worker_cls[g] = _cls

            init_method = cls.__init__

            @functools.wraps(init_method)
            def new_init(self, *args, **kwargs):
                if not RayHelper.is_worker():
                    # Create remote workers
                    RayHelper._create_workers(group, *args, **kwargs)
                init_method(self, *args, **kwargs)

            cls.__init__ = new_init

            return cls

        return decorator

    @staticmethod
    def collect_func(method: Union[Literal['none', 'flatten'], Callable], result):
        if isinstance(result[0], tuple):
            output = []
            for i in range(len(result[0])):
                _single_result = [r[i] for r in result]
                output.append(RayHelper.collect_func(method, _single_result))
            return output
        if method == 'none':
            return result
        elif method == 'flatten':
            flatten = [item for sublist in result for item in sublist]
            if isinstance(result[0], np.ndarray):
                return np.array(flatten)
            return type(result[0])(flatten)
        elif isinstance(method, Callable):
            # Callable
            return method(result)
        else:
            raise ValueError(f'Unsupported collect method: {method}')

    @staticmethod
    def function(group: str,
                 dispatch: Union[Literal['slice', 'all'], Callable] = 'all',
                 execute: Literal['first', 'all'] = 'all',
                 collect: Union[Literal['none', 'flatten'], Callable] = 'none'):
        """Remote execution function.

        Args:
            group: The group to execute.
            dispatch: How to dispatch the arguments.
                'slice': load balance
                'all': all processes do the same thing
            execute: How to execute
                'first': Only first worker
                'all': All processes
            collect: How to collect the results.
                'none': Return as-is
                'flatten': Return a flattened list
        Returns:
            The execution result.
        """

        def decorator(func: Callable[..., T]) -> Callable[..., T]:

            @functools.wraps(func)
            def wrapper(self, *args, **kwargs) -> T:
                if not RayHelper.ray_inited():
                    return func(self, *args, **kwargs)
                if RayHelper.is_worker():
                    if not hasattr(self, 'group'):
                        # pass through env
                        self.group = os.environ['RAY_SWIFT_GROUP'].split(',')
                    if group not in self.group:
                        if RayHelper.is_called_from_init():
                            # Functions in init of different group, do nothing
                            return None
                        else:
                            # Should not happen
                            raise ValueError()
                    else:
                        return func(self, *args, **kwargs)
                else:
                    if RayHelper.is_called_from_init():
                        # each worker do its own init
                        return None
                    result = RayHelper.execute_all_sync(group, dispatch, execute, func.__name__, *args, **kwargs)
                    return RayHelper.collect_func(collect, result)

            return wrapper

        return decorator

    @staticmethod
    def execute_all_sync(group, dispatch, execute, method_name: str, *args, **kwargs):
        import ray
        return ray.get(RayHelper.execute_all_async(group, dispatch, execute, method_name, *args, **kwargs))

    @staticmethod
    def execute_all_async(group, dispatch, execute, method_name: str, *args, **kwargs):
        workers = RayHelper.worker_instance[group]
        length = len(workers)
        if execute == 'first':
            return getattr(workers[0], method_name).remote(*args, **kwargs)
        elif dispatch == 'all':
            return [getattr(worker, method_name).remote(*args, **kwargs) for worker in workers]
        elif dispatch == 'slice':
            result = []

            def dispatch_func(arg, n):
                if isinstance(arg, list):
                    k, m = divmod(len(arg), n)
                    return [arg[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n)]
                else:
                    return [arg] * n

            args = [dispatch_func(arg, length) for arg in args]
            kwargs = {k: dispatch_func(v, length) for k, v in kwargs.items()}
            for i in range(length):
                sliced_args = tuple(arg[i] for arg in args)
                sliced_kwargs = {k: v[i] for k, v in kwargs.items()}
                if (sliced_args and sliced_args[0]) or (kwargs and list(kwargs.values())):
                    # skip empty input
                    remote_call = getattr(workers[i], method_name)
                    result.append(remote_call.remote(*sliced_args, **sliced_kwargs))
            return result
        elif isinstance(dispatch, Callable):
            # dispatch is Callable
            result = []
            for i in range(length):
                sliced_args, sliced_kwargs = dispatch(length, i, *args, **kwargs)
                remote_call = getattr(workers[i], method_name)
                result.append(remote_call.remote(*sliced_args, **sliced_kwargs))
            return result
        else:
            raise ValueError(f'Invalid dispatch method: {dispatch}')

    @staticmethod
    def _create_workers(group: Union[str, List[str]], *args, **kwargs):
        import ray
        from ray.runtime_env import RuntimeEnv
        from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
        exp_name = os.environ.get('RAY_SWIFT_EXP_NAME')
        if not exp_name:
            exp_name = ''
        else:
            exp_name += '-'

        if isinstance(group, str):
            group = [group]

        for _group in group:
            if _group in RayHelper.worker_instance:
                continue

            worker_cls = RayHelper.worker_cls[_group]

            _config = None
            for name, config in RayHelper.device_groups.items():
                if name in RayHelper.resource_manager.possible_keys:
                    continue

                if _group in config['workers']:
                    _config = config
                    break

            assert _config is not None
            local_groups = _config['workers']

            VISIBLE_ENV_MAPPING = {
                'GPU': 'CUDA_VISIBLE_DEVICES',
                'NPU': 'ASCEND_VISIBLE_DEVICES',
            }

            if _config['device'].upper() != 'CPU':
                world_size = len(_config['ranks'])
                placement_groups: List[List[Dict]] = RayHelper.resource_manager.resource(_group)
                workers = []
                ip, port = None, None
                for rank, (deploy_pg, gpu) in enumerate(zip(placement_groups, _config['ranks'])):
                    deploy_pg: Dict
                    cluster_name = exp_name + '-'.join(local_groups)
                    worker_name = cluster_name + '-' + str(rank)
                    env_vars = os.environ.copy()
                    env_vars.update({
                        'WORLD_SIZE':
                        str(world_size),
                        'RANK':
                        str(rank),
                        'LOCAL_RANK':
                        str(0),
                        'CLUSTER_NAME':
                        cluster_name,
                        'WORKER_NAME':
                        worker_name,
                        VISIBLE_ENV_MAPPING[_config['device'].upper()]:
                        ','.join([str(r) for r in deploy_pg['gpu_rank']]),  # TODO npu
                        'RAY_SWIFT_ARGS':
                        get_args(),  # pass through env
                    })

                    @ray.remote
                    def get_node_address():
                        return find_node_ip(), find_free_port()

                    if rank == 0:
                        ip, port = ray.get(
                            get_node_address.options(placement_group=deploy_pg['placement_group']).remote())

                    env_vars['MASTER_ADDR'] = ip
                    env_vars['MASTER_PORT'] = str(port)
                    env_vars['RAY_SWIFT_GROUP'] = ','.join(local_groups)

                    runtime_env = RuntimeEnv(env_vars=env_vars)

                    worker_options = {
                        'scheduling_strategy':
                        PlacementGroupSchedulingStrategy(placement_group=deploy_pg['placement_group']),
                        'name':
                        worker_name,
                        'namespace':
                        'default',
                        'runtime_env':
                        runtime_env,
                        'num_cpus':
                        0.01,
                        'num_gpus':
                        0.01,
                    }

                    worker = worker_cls.options(**worker_options).remote(*args, **kwargs)
                    workers.append(worker)
            else:
                world_size = _config['ranks']
                placement_groups: List[List[Dict]] = RayHelper.resource_manager.resource(_group)
                workers = []
                for deploy_pg, index in zip(placement_groups, list(range(world_size))):
                    deploy_pg: Dict
                    cluster_name = '-'.join(local_groups)
                    worker_name = cluster_name + '-' + str(index)
                    env_vars = os.environ.copy()
                    env_vars.update({
                        'CLUSTER_NAME': cluster_name,
                        'WORKER_NAME': worker_name,
                        VISIBLE_ENV_MAPPING[_config['device'].upper()]: '',
                        'RAY_SWIFT_ARGS': get_args(),  # pass through env
                    })
                    env_vars['RAY_SWIFT_GROUP'] = ','.join(local_groups)

                    runtime_env = RuntimeEnv(env_vars=env_vars)

                    worker_options = {
                        'scheduling_strategy':
                        PlacementGroupSchedulingStrategy(placement_group=deploy_pg['placement_group']),
                        'name':
                        worker_name,
                        'namespace':
                        'default',
                        'runtime_env':
                        runtime_env,
                        'num_cpus':
                        0.01,
                    }

                    worker = worker_cls.options(**worker_options).remote(*args, **kwargs)
                    workers.append(worker)

            for g in local_groups:
                RayHelper.worker_instance[g] = workers
