#!/usr/bin/env python
import copy
from typing import Any
from typing import List
from typing import Optional

import service_configuration_lib
from mypy_extensions import TypedDict

from paasta_tools.long_running_service_tools import load_service_namespace_config
from paasta_tools.long_running_service_tools import LongRunningServiceConfig
from paasta_tools.long_running_service_tools import LongRunningServiceConfigDict
from paasta_tools.long_running_service_tools import ServiceNamespaceConfig
from paasta_tools.utils import BranchDictV2
from paasta_tools.utils import compose_job_id
from paasta_tools.utils import Constraint  # noqa, imported for typing.
from paasta_tools.utils import DEFAULT_SOA_DIR
from paasta_tools.utils import DockerParameter
from paasta_tools.utils import get_code_sha_from_dockerurl
from paasta_tools.utils import get_config_hash
from paasta_tools.utils import load_v2_deployments_json
from paasta_tools.utils import paasta_print
from paasta_tools.utils import SystemPaastaConfig


MESOS_TASK_SPACER = '.'


VolumeInfo = TypedDict(
    'VolumeInfo',
    {
        'container_path': str,
        'host_path': str,
        'mode': str,
    },
)

_Docker_PortMapping = TypedDict(
    '_Docker_PortMapping',
    {
        'host_port': int,
        'container_port': int,
        'protocol': str,
    },
)

DockerInfo = TypedDict(
    'DockerInfo',
    {
        'image': str,
        'network': str,
        'port_mappings': List[_Docker_PortMapping],
        'parameters': List[DockerParameter],
    },
)

ContainerInfo = TypedDict(
    'ContainerInfo',
    {
        'type': str,
        'docker': DockerInfo,
        'volumes': List[VolumeInfo],
    },
)


_CommandInfo_URI = TypedDict(
    '_CommandInfo_URI',
    {
        'value': str,
        'executable': bool,
        'extract': bool,
        'cache': bool,
        'output_file': str,
    },
    total=False,
)

CommandInfo = TypedDict(
    'CommandInfo',
    {
        'uris': List[_CommandInfo_URI],
        'environment': Any,
        'shell': bool,
        'value': str,
        'arguments': List[str],
        'user': str,
    },
    total=False,
)

Value_Scalar = TypedDict(
    'Value_Scalar',
    {
        'value': float,
    },
)

Value_Range = TypedDict(
    'Value_Range',
    {
        'begin': int,
        'end': int,
    },
)

Value_Ranges = TypedDict(
    'Value_Ranges',
    {
        'range': List[Value_Range],
    },
)

Resource = TypedDict(
    'Resource',
    {
        'name': str,
        'type': str,
        'scalar': Value_Scalar,
        'ranges': Value_Ranges,
    },
    total=False,
)


TaskID = TypedDict(
    'TaskID',
    {
        'value': str,
    },
)

SlaveID = TypedDict(
    'SlaveID',
    {
        'value': str,
    },
)


class TaskInfoBase(TypedDict):
    name: str
    task_id: TaskID
    agent_id: SlaveID
    resources: List[Resource]


class TaskInfo(TaskInfoBase):
    container: ContainerInfo
    command: CommandInfo


class NativeServiceConfigDict(LongRunningServiceConfigDict):
    pass


class NativeServiceConfig(LongRunningServiceConfig):
    config_dict: NativeServiceConfigDict
    config_filename_prefix = 'paasta_native'

    def __init__(
        self,
        service: str,
        instance: str,
        cluster: str,
        config_dict: NativeServiceConfigDict,
        branch_dict: Optional[BranchDictV2],
        soa_dir: str,
        service_namespace_config: Optional[ServiceNamespaceConfig]=None,
    ) -> None:
        super().__init__(
            cluster=cluster,
            instance=instance,
            service=service,
            config_dict=config_dict,
            branch_dict=branch_dict,
            soa_dir=soa_dir,
        )
        # service_namespace_config may be omitted/set to None at first, then set
        # after initializing. e.g. we do this in load_paasta_native_job_config
        # so we can call get_nerve_namespace() to figure out what SNC to read.
        # It may also be set to None if this service is not in nerve.
        if service_namespace_config is not None:
            self.service_namespace_config = service_namespace_config
        else:
            self.service_namespace_config = ServiceNamespaceConfig()

    def task_name(self, base_task: TaskInfo) -> str:
        code_sha = get_code_sha_from_dockerurl(
            base_task['container']['docker']['image'],
        )

        filled_in_task = copy.deepcopy(base_task)
        filled_in_task['name'] = ''
        filled_in_task['task_id'] = {'value': ''}
        filled_in_task['agent_id'] = {'value': ''}

        config_hash = get_config_hash(
            filled_in_task,
            force_bounce=self.get_force_bounce(),
        )

        return compose_job_id(
            self.service,
            self.instance,
            git_hash=code_sha,
            config_hash=config_hash,
            spacer=MESOS_TASK_SPACER,
        )

    def base_task(self, system_paasta_config: SystemPaastaConfig, portMappings=True) -> TaskInfo:
        """Return a TaskInfo Dict with all the fields corresponding to the
        configuration filled in.

        Does not include task.agent_id or a task.id; those need to be
        computed separately.
        """
        docker_volumes = self.get_volumes(
            system_volumes=system_paasta_config.get_volumes(),
        )
        task: TaskInfo = {
            'name': '',
            'task_id': {'value': ''},
            'agent_id': {'value': ''},
            'container': {
                'type': 'DOCKER',
                'docker': {
                    'image': self.get_docker_url(),
                    'parameters': [
                        {'key': param['key'], 'value': param['value']}
                        for param in self.format_docker_parameters()
                    ],
                    'network': self.get_mesos_network_mode(),
                    'port_mappings': [],
                },
                'volumes': [
                    {
                        'container_path': volume['containerPath'],
                        'host_path': volume['hostPath'],
                        'mode': volume['mode'].upper(),
                    }
                    for volume in docker_volumes
                ],
            },
            'command': {
                'value': self.get_cmd(),
                'uris': [
                    {
                        'value': system_paasta_config.get_dockercfg_location(),
                        'extract': False,
                    },
                ],
            },
            'resources': [
                {
                    'name': 'cpus',
                    'type': 'SCALAR',
                    'scalar': {'value': self.get_cpus()},
                },
                {
                    'name': 'mem',
                    'type': 'SCALAR',
                    'scalar': {'value': self.get_mem()},
                },
            ],
        }

        if portMappings:
            task['container']['docker']['port_mappings'] = [
                {
                    'container_port': self.get_container_port(),
                    # filled by tasks_and_state_for_offer()
                    'host_port': 0,
                    'protocol': 'tcp',
                },
            ]

            task['resources'].append(
                {
                    'name': 'ports',
                    'type': 'RANGES',
                    'ranges': {
                        # filled by tasks_and_state_for_offer
                        'range': [{'begin': 0, 'end': 0}],
                    },
                },
            )

        task['name'] = self.task_name(task)

        return task

    def get_mesos_network_mode(self) -> str:
        return self.get_net().upper()


def load_paasta_native_job_config(
    service,
    instance,
    cluster,
    load_deployments=True,
    soa_dir=DEFAULT_SOA_DIR,
    instance_type='paasta_native',
    config_overrides=None,
) -> NativeServiceConfig:
    service_paasta_native_jobs = read_service_config(
        service=service,
        instance=instance,
        instance_type=instance_type,
        cluster=cluster,
        soa_dir=soa_dir,
    )
    branch_dict: Optional[BranchDictV2] = None
    instance_config_dict = service_paasta_native_jobs[instance].copy()
    instance_config_dict.update(config_overrides or {})
    if load_deployments:
        deployments_json = load_v2_deployments_json(service, soa_dir=soa_dir)
        temp_instance_config = NativeServiceConfig(
            service=service,
            cluster=cluster,
            instance=instance,
            config_dict=instance_config_dict,
            branch_dict=None,
            soa_dir=soa_dir,
        )
        branch = temp_instance_config.get_branch()
        deploy_group = temp_instance_config.get_deploy_group()
        branch_dict = deployments_json.get_branch_dict(service, branch, deploy_group)

    service_config = NativeServiceConfig(
        service=service,
        cluster=cluster,
        instance=instance,
        config_dict=instance_config_dict,
        branch_dict=branch_dict,
        soa_dir=soa_dir,
    )

    service_namespace_config = load_service_namespace_config(
        service=service,
        namespace=service_config.get_nerve_namespace(),
        soa_dir=soa_dir,
    )
    service_config.service_namespace_config = service_namespace_config

    return service_config


def read_service_config(service, instance, instance_type, cluster, soa_dir=DEFAULT_SOA_DIR):
    conf_file = f'{instance_type}-{cluster}'
    full_path = f'{soa_dir}/{service}/{conf_file}.yaml'
    paasta_print("Reading paasta-remote configuration file: %s" % full_path)

    config = service_configuration_lib.read_extra_service_information(
        service,
        conf_file,
        soa_dir=soa_dir,
    )

    if instance not in config:
        raise UnknownNativeServiceError(
            'No job named "{}" in config file {}: \n{}'.format(
                instance, full_path, open(full_path).read(),
            ),
        )

    return config


class UnknownNativeServiceError(Exception):
    pass
