class BaseInstanceCommand(BaseCommand):
    """Command for Instance based commands: fetch data from an instance."""
    def set_instance(self, instance_name):
        self._set_instance(instance_name)

    def _set_instance(self, instance_name):
        self.instance = get_instance(self.config_path, instance_name) class BottomSpacedOpt(OptionsBase):
    OPTIONS = ['space_bottom']

    def __init__(self, space_bottom=True, **kwargs):
        super(BottomSpacedOpt, self).__init__(space_bottom=space_bottom, **kwargs) class SpacedOpt(OptionsBase):
    OPTIONS = ['space_top', 'space_bottom']

    def __init__(self, space_top=True, space_bottom=True, **kwargs):
        super(SpacedOpt, self).__init__(space_top=space_top, space_bottom=space_bottom, **kwargs) class TopSpacedOpt(OptionsBase):
    OPTIONS = ['space_top']

    def __init__(self, space_top=True, **kwargs):
        super(TopSpacedOpt, self).__init__(space_top=space_top, **kwargs) class EndpointNotFoundException(CLIBaseException):
    default_message = u'Endpoint `{}` not found' class SocketAPIException(CLIBaseException):
    pass class SocketFileFetchException(CLIBaseException):
    default_message = u'Can not fetch the file: {}.' class SocketYMLParseException(CLIBaseException):
    default_message = u'Can not parse yml file.' class SocketFormatter(Formatter):

    SOCKET_LIST_FIELDS = ['name', 'description', 'status', 'status_info', 'endpoints']
    SOCKET_DETAILS_FIELDS = ['config', 'endpoints', 'dependencies', 'metadata']
    ENDPOINT_FIELDS = ['calls', 'acl', 'metadata']

    def display_socket_list(self, socket_list, instance_name):
        if not socket_list:
            self.write('Sockets not defined for `{}` instance.'.format(instance_name),
                       WarningOpt(), SpacedOpt())
            sys.exit(1)
        self.write('Sockets for `{}` instance.'.format(instance_name), SpacedOpt())

        for cs in socket_list:
            lines = self._display_list_details(custom_socket=cs)
            lines.append('See: `syncano sockets details {}` for details.'.format(cs.name))
            self.write_block(lines, DefaultOpt(indent=2))

    def display_socket_details(self, custom_socket, api_key):
        self.write_block(
            [
                'Details for Socket `{}` in `{}` instance.'.format(
                    custom_socket.name,
                    custom_socket.instance_name
                )
            ],
            SpacedOpt()
        )

        lines = self._display_list_details(custom_socket)
        self.write_block(lines)

        for socket_field in self.SOCKET_DETAILS_FIELDS:
            self.display_block(
                socket_field,
                custom_socket,
                base_link=custom_socket.links.links_dict['endpoints'],
                api_key=api_key,
                indent=1,
                skip_fields=['source']
            )

    def display_block(self, block_name, socket, **kwargs):
        handler, kw_names = self.display_handlers[block_name]
        self.write(block_name.capitalize())
        socket_data = getattr(socket, block_name)
        new_kwargs = {key: value for key, value in six.iteritems(kwargs) if key in kw_names}
        handler(socket_data, **new_kwargs)
        self.separator()

    @property
    def display_handlers(self):
        handlers = [
            (self.display_config, ()),
            (self.display_endpoints, ('base_link', 'api_key')),
            (self.format_list, ('indent', 'skip_fields')),
            (self.format_object, 'indent')
        ]
        return dict(zip(self.SOCKET_DETAILS_FIELDS, handlers))

    def _display_list_details(self, custom_socket):
        lines = []
        for field in self.SOCKET_LIST_FIELDS:
            value = getattr(custom_socket, field)
            if field == 'endpoints':
                value = ', '.join(value.keys())
            if not value:
                value = '-- not set --'
            lines.append("{}: {}".format(field.capitalize().replace('_', ' '), value))
        return lines

    def display_all_endpoints(self, endpoints, api_key):
        self.write('Endpoints', SpacedOpt())
        if not endpoints:
            self.write('Not endpoint specified yet.', BottomSpacedOpt())

        for endpoint in endpoints:
            self.write('{}: {}'.format(
                click.style('URL', fg=ColorSchema.PROMPT),
                click.style(self._format_link(endpoint.links.links_dict['self'], '', api_key),
                            fg=ColorSchema.INFO)
            ), DefaultOpt(indent=2))
            self.write('{}: {}'.format(
                click.style('Name', fg=ColorSchema.PROMPT),
                click.style(endpoint.name, fg=ColorSchema.INFO)
            ), DefaultOpt(indent=2))
            self.write('{}: {}'.format(
                click.style('Methods', fg=ColorSchema.PROMPT),
                click.style(', '.join(endpoint.allowed_methods), fg=ColorSchema.INFO)
            ), DefaultOpt(indent=2))
            self.separator(size=66, indent=2)

    def display_endpoints(self, endpoints, base_link, api_key):

        for endpoint_name, endpoint_data in six.iteritems(endpoints):
            self.write('{e_name}:'.format(e_name=endpoint_name, ), DefaultOpt(indent=2), WarningOpt(), TopSpacedOpt())
            self.write('{url_label}: {link}'.format(
                url_label=click.style('URL', fg=ColorSchema.PROMPT),
                link=self._format_link(base_link, endpoint_name, api_key)
            ), DefaultOpt(indent=3))
            self._display_endpoints_fields(endpoint_data)

    @classmethod
    def _format_link(cls, base_link, endpoint_name, api_key):
        instance_part, endpoints_part = base_link.split('endpoints')
        return '{link}'.format(
            link=click.style(
                """{host}{instance_part}
                {endpoints_part}{name}
                ?api_key={key}""".format(
                    host='https://api.syncano.io',
                    instance_part=instance_part,
                    endpoints_part='endpoints{}'.format(endpoints_part),
                    name='{}/'.format(endpoint_name) if endpoint_name else '',
                    key=api_key
                ), fg=ColorSchema.INFO
            )
        )

    def _display_endpoints_fields(self, endpoint_data):
        for field in self.ENDPOINT_FIELDS:
            handler_name = '_display_{}'.format(field)
            data = endpoint_data.get(field)
            try:
                handler = getattr(self, handler_name)
            except AttributeError:
                continue
            handler(data, indent=3)

    def _display_calls(self, data, indent):
        if not data:
            self.write('{}: {}'.format('Calls', self.not_set), DefaultOpt(indent=indent))
            return
        self.write(click.style('Calls:', fg=ColorSchema.PROMPT), DefaultOpt(indent=indent))
        for call in data:
            self.format_object(call, indent=indent)

    def _display_acl(self, data, indent):
        if not data:
            self.write('{}: {}'.format(
                click.style('Acl', fg=ColorSchema.PROMPT),
                click.style(self.not_set, fg=ColorSchema.INFO)
            ), DefaultOpt(indent=indent))
            return
        self.write(click.style('Acl:', fg=ColorSchema.PROMPT), DefaultOpt(indent=indent))
        self.format_object(data, DefaultOpt(indent=indent))

    def _display_metadata(self, data, indent):
        if not data:
            self.write('{}: {}'.format(
                click.style('Metadata', fg=ColorSchema.PROMPT),
                click.style(self.not_set, fg=ColorSchema.INFO)
            ), DefaultOpt(indent=indent))
            return
        self.write(click.style('Metadata:', fg=ColorSchema.PROMPT), DefaultOpt(indent=indent))
        self.format_object(data, DefaultOpt(indent=indent)) class SocketJsonYml(object):

    SOCKET_FIELDS = ['name', 'description', 'endpoints', 'dependencies']
    ENDPOINT_TYPES = ['script']
    HTTP_METHODS = ['GET', 'POST', 'DELETE', 'PUT', 'PATCH']
    DEPENDENCY_TYPES = {'scripts': DependencyTypeE.SCRIPT, 'classes': DependencyTypeE.CLASS}

    @classmethod
    def to_yml(cls, socket_object):
        """
        A method which transform CustomSocket object from Syncano Python LIB into yml representation.
        :param socket_object: the CustomSocket object;
        :param scope: how many attributes should be processed and showed in output;
        :return: the yml format of the CustomSocket;
        """
        yml_dict = {}
        files = []

        for socket_field in cls.SOCKET_FIELDS:
            if socket_field == 'endpoints':
                yml_dict[socket_field] = cls._yml_process_endpoints(socket_object.endpoints)
            elif socket_field == 'dependencies':
                yml_dict[socket_field], files = cls._yml_process_dependencies(socket_object.dependencies)
            else:
                # TODO: None for skip the description now;
                yml_dict[socket_field] = getattr(socket_object, socket_field, None)

        for meta_name, meta_data in six.iteritems(socket_object.metadata):
            yml_dict[meta_name] = meta_data

        return yaml.safe_dump(yml_dict, default_flow_style=False), files

    @classmethod
    def _yml_process_endpoints(cls, endpoints):
        yml_endpoints = {}
        for endpoint_name, endpoint_data in six.iteritems(endpoints):
            yml_endpoints[endpoint_name] = cls._yml_process_calls(endpoint_data['calls'])
            yml_endpoints.update(cls._yml_process_metadata(endpoint_data))
        return yml_endpoints

    @classmethod
    def _yml_process_metadata(cls, endpoint_data):
        return endpoint_data.get('metadata', {})  # some old Sockets do not have this field;

    @classmethod
    def _yml_process_calls(cls, data_calls):
        calls = {}
        for call in data_calls:
            if call['methods'] == ['*']:
                calls = {call['type']: call['name']}
            else:
                for method in call['methods']:
                    calls.update({method: {call['type']: call['name']}})
        return calls

    @classmethod
    def _yml_process_dependencies(cls, dependencies):
        yml_dependencies = {}
        scripts = {}
        files = []

        for dependency in dependencies:
            if dependency['type'] == 'script':
                file_name = '{name}{ext}'.format(name=dependency['name'],
                                                 ext=ALLOWED_RUNTIMES[dependency['runtime_name']])

                scripts[dependency['name']] = {
                    'runtime_name': dependency['runtime_name'],
                    'file': file_name
                }
                # create file list;
                files.append(
                    {
                        'source': dependency['source'],
                        'file_name': file_name
                    }
                )

        yml_dependencies['scripts'] = scripts

        return yml_dependencies, files

    @classmethod
    def get_dependency_handlers(cls):
        handlers = {}
        for yml_name, dependency_type in six.iteritems(cls.DEPENDENCY_TYPES):
            handlers[dependency_type] = getattr(cls, 'get_{}_dependency'.format(dependency_type))
        return handlers

    @classmethod
    def to_json(cls, socket_yml, directory=None):
        """
        A method which transform yml to the json. This json is used in API call (Syncano Python LIB)
        :param socket_yml: the yml file path;
        :return: the json format of the custom_socket;
        """
        metadata_dict = {}
        api_dict = {}
        for key, value in six.iteritems(socket_yml):
            if key not in cls.SOCKET_FIELDS:
                metadata_dict[key] = value
            else:
                if key == 'endpoints':
                    api_dict[key] = cls._json_process_endpoints(endpoints=value)
                elif key == 'dependencies':
                    api_dict[key] = cls._json_process_dependencies(dependencies=value, directory=directory)
                else:
                    api_dict[key] = value

        api_dict['metadata'] = metadata_dict
        return api_dict

    @classmethod
    def _json_process_endpoints(cls, endpoints):
        api_endpoints = {}

        for name, endpoint_data in six.iteritems(endpoints):
            api_endpoints[name] = {'calls': cls._get_calls(endpoint_data)}
            api_endpoints[name].update({'metadata': cls._get_metadata(endpoint_data)})

        return api_endpoints

    @classmethod
    def _get_metadata(cls, endpoint_data):
        metadata = defaultdict(dict)
        for data_key, data in six.iteritems(endpoint_data):
            if data_key in cls.HTTP_METHODS:
                for metadata_key, inner_data in six.iteritems(data):
                    if metadata_key in cls.ENDPOINT_TYPES:
                        continue
                    if metadata_key == 'parameters':
                        metadata[metadata_key][data_key] = inner_data
                    else:
                        metadata[metadata_key] = inner_data

            elif data_key not in cls.ENDPOINT_TYPES:
                if data_key == 'parameters':
                    metadata[data_key]['*'] = data
                else:
                    metadata[data_key] = data
                metadata[data_key] = data

        return metadata

    @classmethod
    def _get_calls(cls, endpoint_data):
        calls = []
        keys = set(endpoint_data.keys())
        if keys.intersection(set(cls.HTTP_METHODS)) and keys.intersection(set(cls.ENDPOINT_TYPES)):
            raise BadYAMLDefinitionInEndpointsException()

        for type_or_method in keys:
            data = endpoint_data[type_or_method]

            if type_or_method in cls.ENDPOINT_TYPES:
                calls.append({
                    'type': type_or_method,
                    'methods': ['*'],
                    'name': data,
                })

            elif type_or_method in cls.HTTP_METHODS:
                for call_type, name in six.iteritems(data):

                    if call_type in cls.ENDPOINT_TYPES:
                        calls.append({
                            'type': call_type,
                            'methods': [type_or_method],
                            'name': name
                        })
        return calls

    @classmethod
    def _json_process_dependencies(cls, dependencies, directory):
        api_dependencies = []
        dependency_handlers = cls.get_dependency_handlers()
        for dependencies_type, dependency in six.iteritems(dependencies):
            if dependencies_type in cls.DEPENDENCY_TYPES:
                for dependency_name, data in six.iteritems(dependency):
                    dependency_type = cls.DEPENDENCY_TYPES[dependencies_type]
                    base_dependency_data = {
                        'name': dependency_name,
                        'type': dependency_type
                    }
                    typed_dependency_data = dependency_handlers[dependency_type](data, directory=directory)
                    typed_dependency_data.update(base_dependency_data)
                    api_dependencies.append(typed_dependency_data)
        return api_dependencies

    @classmethod
    def get_script_dependency(cls, data, **kwargs):
        """
        Note: when definig new depenency processors use following pattern:
        get_{name}_dependency -> where {name} is one of the defined in DepedencyTypeE
        this allows to easily extend dependency handling;
        :param data:
        :param directory:
        :return:
        """
        directory = kwargs.get('directory')
        return {
            'runtime_name': data['runtime_name'],
            'source': cls._get_source(data['file'], directory),
        }

    @classmethod
    def get_class_dependency(cls, data, **kwargs):
        return {
            'schema': data['schema']
        }

    @classmethod
    def _get_source(cls, file_name, directory):
        with open(os.path.join(directory, '{}'.format(file_name)), 'r+') as source_file:
            return source_file.read() class SocketConfigParser(object):

    PROMPT_FIELD = 'prompt'
    VALUE_FIELD = 'value'

    def __init__(self, socket_yml):
        self.config = socket_yml.get('config', {})

    def is_valid(self):
        valid = True
        for field_name in [self.PROMPT_FIELD, self.VALUE_FIELD]:
            valid &= self._is_valid(field_name)
        if not valid:
            raise BadConfigFormatException()
        return valid

    def _is_valid(self, field_name):
        if field_name in self.config and not len(self.config[field_name]):
            return False
        return True

    def ask_for_config(self):
        provided_config = {}
        for config_var_name, config_metadata in six.iteritems(self.config):
            if config_metadata.get('prompt'):
                default_value = config_metadata.get('value')
                config_var_value = click.prompt(self.get_prompt_str(config_var_name), default=default_value)
                provided_config[config_var_name] = config_var_value
            else:
                provided_config[config_var_name] = config_metadata.get('value')

        return provided_config

    @staticmethod
    def get_prompt_str(config_var_name):
        return 'Provide value for {}'.format(config_var_name) SCRIPTS = {
    'scripts/custom_script.py': """static_data = {
    'title': 'Hunger games',
    'author': 'Suzzane Collins',
    'publish_year': 2008,
}

set_response(HttpResponse(status_code=200, content=static_data, content_type='application/json'))
""",
    'scripts/custom_script_1.py': "print('1')",
    'scripts/custom_script_2.py': "print('2')"
} SOCKET_YML = """
name: custom_socket_example
author:
  name: Info
  email: info@synano.com
icon:
  name: icon-name
  color: ffee11
config:
    some_key:
      value: some_value
    custom_api_key:
      prompt: true
description: Some custom integration
endpoints:
  custom_endpoint:
    script: custom_script

  custom_endpoint_1:
    POST:
      script: custom_script_1
    GET:
      script: custom_script_2

dependencies:
  scripts:
    custom_script:
      runtime_name: python_library_v5.0
      file: scripts/custom_script.py
    custom_script_1:
      runtime_name: python_library_v5.0
      file: scripts/custom_script_1.py
    custom_script_2:
      runtime_name: python_library_v5.0
      file: scripts/custom_script_2.py

  classes:
    country:
      schema:
        - name: name
          type: string
        - name: topLevelDomain
          type: string
        - name: capital
          type: string
        - name: alpha2Code
          type: string
        - name: alpha3Code
          type: string
"""
import os
import click
import requests
import six
import yaml
from syncano.exceptions import SyncanoRequestError
from syncano.models import CustomSocket, SocketEndpoint
from syncano_cli.base.command import BaseInstanceCommand
from syncano_cli.base.options import BottomSpacedOpt, SpacedOpt, TopSpacedOpt
from syncano_cli.custom_sockets.exceptions import (
    EndpointNotFoundException,
    SocketAPIException,
    SocketFileFetchException,
    SocketYMLParseException
)
from syncano_cli.custom_sockets.formatters import SocketFormatter
from syncano_cli.custom_sockets.jsonyaml import SocketJsonYml
from syncano_cli.custom_sockets.parsers import SocketConfigParser
from syncano_cli.custom_sockets.templates.socket_template import SCRIPTS, SOCKET_YML
from yaml.parser import ParserError
# -*- coding: utf-8 -*-




class SocketCommand(BaseInstanceCommand):

    SOCKET_FILE_NAME = 'socket.yml'

    def __init__(self, config):
        super(SocketCommand, self).__init__(config)
        self.socket_formatter = SocketFormatter()
        self.socket_processor = SocketJsonYml()

    def list(self):
        sockets = [cs for cs in CustomSocket.please.all(instance_name=self.instance.name)]
        self.socket_formatter.display_socket_list(socket_list=sockets, instance_name=self.instance.name)

    def details(self, socket_name):
        cs = CustomSocket.please.get(name=socket_name, instance_name=self.instance.name)
        self.socket_formatter.display_socket_details(cs, self.connection.connection().api_key)

    def recheck(self, socket_name):
        cs = CustomSocket.please.get(name=socket_name, instance_name=self.instance.name)
        cs.recheck()

    def config(self, socket_name):
        cs = CustomSocket.please.get(name=socket_name, instance_name=self.instance.name)
        self.formatter.write('Config for Socket `{}`'.format(cs.name), TopSpacedOpt())
        self.formatter.display_config(cs.config)

    def list_endpoints(self):
        endpoints = SocketEndpoint.get_all_endpoints(instance_name=self.instance.name)
        self.socket_formatter.display_all_endpoints(endpoints=endpoints, api_key=self.connection.connection().api_key)

    def delete(self, socket_name):
        custom_socket = CustomSocket.please.get(name=socket_name, instance_name=self.instance.name)
        custom_socket.delete()
        click.echo("INFO: Sockets {} deleted.".format(socket_name))

    def install_from_dir(self, dir_path):

        with open(os.path.join(dir_path, self.SOCKET_FILE_NAME)) as socket_file:
            yml_file = yaml.safe_load(socket_file)

        config = self.set_up_config(yml_file)

        api_data = self.socket_processor.to_json(socket_yml=yml_file, directory=dir_path)
        api_data.update({'instance_name': self.instance.name})
        api_data.update({'config': config})
        custom_socket = CustomSocket.please.create(**api_data)
        self.formatter.write('Sockets {} created.'.format(custom_socket.name))
        self._display_socket_status(custom_socket.name)

    def install_from_url(self, url_path, name):
        socket_yml = self.fetch_file(url_path)
        config = self.set_up_config(socket_yml)
        try:
            CustomSocket(name=name).install_from_url(url=url_path, instance_name=self.instance.name, config=config)
        except SyncanoRequestError as e:
            raise SocketAPIException(e.reason)

        self.formatter.write('Installing Sockets from url: {}.'.format(url_path), SpacedOpt())
        self._display_socket_status(name)

    def _display_socket_status(self, socket_name):
        cs = CustomSocket.please.get(name=socket_name, instance_name=self.instance.name)
        self.formatter.write(
            'Current status is: {} (syncano sockets details {} for refresh).'.format(cs.status, cs.name),

            BottomSpacedOpt())