"""
Stack-In-A-Box: Service Interface
"""
import logging
import re
import uuid

import six

from stackinabox.services import exceptions
from stackinabox.services import router


logger = logging.getLogger(__name__)


class StackInABoxService(object):
    """Stack-In-A-Box Service interface.

    StackInABoxService provides the functionality and interface to build a
    service that is compliant with the requirements of Stack-In-A-Box.
    """

    DELETE = 'DELETE'
    GET = 'GET'
    HEAD = 'HEAD'
    OPTIONS = 'OPTIONS'
    PATCH = 'PATCH'
    POST = 'POST'
    PUT = 'PUT'
    TRACE = 'TRACE'
    METHODS = [
        DELETE,
        GET,
        HEAD,
        OPTIONS,
        PATCH,
        POST,
        PUT,
        TRACE
    ]

    def __init__(self, name):
        """Initialize the service.

        :param name: name of the service used for the URI
        """
        self.__base_url = '/{0}'.format(name)
        self.__id = uuid.uuid4()
        self.name = name
        self.routes = {
        }
        logger.debug('StackInABoxService ({0}): Hosting Service {1}'
                     .format(self.__id, self.name))

    @staticmethod
    def is_regex(uri):
        """Is the provided URI a regex?

        :returns: boolean
        """
        regex_type = type(re.compile(''))
        return isinstance(uri, regex_type)

    @staticmethod
    def validate_regex(regex, sub_service):
        """Is the regex valid for StackInABox routing?

        :param regex: Python regex object to match the URI
        :param sub_service: boolean for whether or not the regex is for
                            a sub-service

        :raises: InvalidRouteRegexError if the regex does not meet the
                 requirements.
        """
        # The regex generated by stackinabox starts with ^
        # and ends with $. Enforce that the provided regex does the same.

        if regex.pattern.startswith('^') is False:
            logger.debug('StackInABoxService: Pattern must start with ^')
            raise exceptions.InvalidRouteRegexError(
                'Pattern must start with ^'
            )

        # Note: pattern may end with $ even if sub_service is True
        if regex.pattern.endswith('$') is False and sub_service is False:
            logger.debug('StackInABoxService: Pattern must end with $')
            raise exceptions.InvalidRouteRegexError('Pattern must end with $')

        # Enforce that if the pattern does not end with $ that it is a service
        if regex.pattern.endswith('$') is True and sub_service is True:
            logger.debug(
                'StackInABoxService: Sub-Service RegEx Pattern must not '
                'end with $')
            raise exceptions.InvalidRouteRegexError('Pattern must end with $')

    @staticmethod
    def get_service_regex(base_url, service_url, sub_service):
        """Get the regex for a given service.

        :param base_url: string - Base URI
        :param service_url: string - Service URI under the Base URI
        :param sub_service: boolean - is the Service URI for a sub-service?

        :returns: Python Regex object containing the regex for the Service
        """
        # if the specified service_url is already a regex
        # then just use. Otherwise create what we need
        if StackInABoxService.is_regex(service_url):
            logger.debug('StackInABoxService: Received regex {0} for use...'
                         .format(service_url.pattern))

            # Validate the regex against StackInABoxService requirement
            StackInABoxService.validate_regex(service_url, sub_service)

            return service_url
        else:
            regex = '^{0}{1}$'.format('', service_url)
            logger.debug('StackInABoxService: {0} + {1} -> {2}'
                         .format(base_url, service_url, regex))
            return re.compile(regex)

    @property
    def base_url(self):
        """Base URI utilized for anything managed by this instance."""
        return self.__base_url

    @base_url.setter
    def base_url(self, value):
        """Set the Base URI value.

        :param value: the new URI to use for the Base URI
        """
        logger.debug('StackInABoxService ({0}:{1}) Updating Base URL '
                     'from {2} to {3}'
                     .format(self.__id,
                             self.name,
                             self.__base_url,
                             value))
        self.__base_url = value
        for k, v in six.iteritems(self.routes):
            v['regex'] = StackInABoxService.get_service_regex(
                value,
                v['uri'],
                v['handlers'].is_subservice)

    def reset(self):
        """Reset the service to its' initial state."""
        logger.debug('StackInABoxService ({0}): Reset'
                     .format(self.__id, self.name))
        self.base_url = '/{0}'.format(self.name)
        logger.debug('StackInABoxService ({0}): Hosting Service {1}'
                     .format(self.__id, self.name))

    def try_handle_route(self, route_uri, method, request, uri, headers):
        """Try to handle the supplied request on the specified routing URI.

        :param route_uri: string - URI of the request
        :param method: string - HTTP Verb
        :param request: request object describing the HTTP request
        :param uri: URI of the reuqest
        :param headers: case-insensitive headers dict

        :returns: tuple - (int, dict, string) containing:
                          int - the http response status code
                          dict - the headers for the http response
                          string - http string response
        """
        uri_path = route_uri
        if '?' in uri:
            logger.debug('StackInABoxService ({0}:{1}): Found query string '
                         'removing for match operation.'
                         .format(self.__id, self.name))
            uri_path, uri_qs = uri.split('?')
            logger.debug('StackInABoxService ({0}:{1}): uri =  "{2}", '
                         'query = "{3}"'
                         .format(self.__id, self.name, uri_path, uri_qs))

        for k, v in six.iteritems(self.routes):
            logger.debug('StackInABoxService ({0}:{1}): Checking if '
                         'route {2} handles...'
                         .format(self.__id, self.name, v['uri']))
            logger.debug('StackInABoxService ({0}:{1}): ...using regex '
                         'pattern {2} against {3}'
                         .format(self.__id,
                                 self.name,
                                 v['regex'].pattern,
                                 uri_path))
            if v['regex'].match(uri_path):
                logger.debug('StackInABoxService ({0}:{1}): Checking if '
                             'route {2} handles method {2}...'
                             .format(self.__id, self.name, v['uri'], method))
                return v['handlers'](method,
                                     request,
                                     uri,
                                     headers)
        return (595, headers, 'Route ({0}) Not Handled'.format(uri))

    def request(self, method, request, uri, headers):
        """Handle the supplied request on the specified routing URI.

        :param method: string - HTTP Verb
        :param request: request object describing the HTTP request
        :param uri: URI of the reuqest
        :param headers: case-insensitive headers dict

        :returns: tuple - (int, dict, string) containing:
                          int - the http response status code
                          dict - the headers for the http response
                          string - http string response
        """
        logger.debug('StackInABoxService ({0}:{1}): Request Received {2} - {3}'
                     .format(self.__id, self.name, method, uri))
        return self.try_handle_route(uri, method, request, uri, headers)

    def sub_request(self, method, request, uri, headers):
        """Handle the supplied sub-service request on the specified routing URI

        :param method: string - HTTP Verb
        :param request: request object describing the HTTP request
        :param uri: URI of the reuqest
        :param headers: case-insensitive headers dict

        :returns: tuple - (int, dict, string) containing:
                          int - the http response status code
                          dict - the headers for the http response
                          string - http string response
        """
        logger.debug('StackInABoxService ({0}:{1}): Sub-Request Received '
                     '{2} - {3}'
                     .format(self.__id, self.name, method, uri))
        return self.request(method, request, uri, headers)

    def create_route(self, uri, sub_service):
        """Create the route for the URI.

        :param uri: string - URI to be routed
        :param sub_service: boolean - is the URI for a sub-service

        :returns: n/a
        """
        if uri not in self.routes.keys():
            logger.debug('Service ({0}): Creating routes'
                         .format(self.name))
            self.routes[uri] = {
                'regex': StackInABoxService.get_service_regex(
                    self.base_url,
                    uri,
                    sub_service
                ),
                'uri': uri,
                'handlers': router.StackInABoxServiceRouter(
                    self.name,
                    uri,
                    None,
                    self
                )
            }

    def register(self, method, uri, call_back):
        """Register a class instance function to handle a request.

        :param method: string - HTTP Verb
        :param uri: string - URI for the request
        :param call_back: class instance function that handles the request

        :returns: n/a
        """
        found = False

        self.create_route(uri, False)
        self.routes[uri]['handlers'].register_method(method,
                                                     call_back)

    def register_subservice(self, uri, service):
        """Register a class instance to handle a URI.

        :param uri: string - URI for the request
        :param service: StackInABoxService object instance that handles
                        the request

        :returns: n/a
        """
        found = False

        self.create_route(uri, True)
        self.routes[uri]['handlers'].set_subservice(service)
