import logging
import urllib

from urlparse import urlparse
from flask import abort, request
from jsonschema import validate, ValidationError

from util.security.registry_jwt import (generate_bearer_token, decode_bearer_token,
                                        InvalidBearerTokenException)

logger = logging.getLogger(__name__)


PROXY_STORAGE_MAX_LIFETIME_S = 30 # Seconds
STORAGE_PROXY_SUBJECT = 'storageproxy'
STORAGE_PROXY_ACCESS_TYPE = 'storageproxy'

ACCESS_SCHEMA = {
  'type': 'array',
  'description': 'List of access granted to the subject',
  'items': {
    'type': 'object',
    'required': [
      'type',
      'scheme',
      'host',
      'uri',
    ],
    'properties': {
      'type': {
        'type': 'string',
        'description': 'We only allow storage proxy permissions',
        'enum': [
          'storageproxy',
        ],
      },
      'scheme': {
        'type': 'string',
        'description': 'The scheme for the storage URL being proxied'
      },
      'host': {
        'type': 'string',
        'description': 'The hostname for the storage URL being proxied'
      },
      'uri': {
        'type': 'string',
        'description': 'The URI path for the storage URL being proxied'
      },
    },
  },
}


class DownloadProxy(object):
  """ Helper class to enable proxying of direct download URLs for storage via the registry's
      local NGINX.
  """
  def __init__(self, app, instance_keys):
    self.app = app
    self.instance_keys = instance_keys

    app.add_url_rule('/_storage_proxy_auth', '_storage_proxy_auth', self._validate_proxy_url)

  def proxy_download_url(self, download_url):
    """ Returns a URL to proxy the specified blob download URL.
    """
    # Parse the URL to be downloaded into its components (host, path, scheme).
    parsed = urlparse(download_url)

    path = parsed.path
    if parsed.query:
      path = path + '?' + parsed.query

    if path.startswith('/'):
      path = path[1:]

    access = {
      'type': STORAGE_PROXY_ACCESS_TYPE,
      'uri': path,
      'host': parsed.netloc,
      'scheme': parsed.scheme,
    }

    # Generate a JWT that signs access to this URL. This JWT will be passed back to the registry
    # code when the download commences. Note that we don't add any context here, as it isn't
    # needed.
    server_hostname = self.app.config['SERVER_HOSTNAME']
    token = generate_bearer_token(server_hostname, STORAGE_PROXY_SUBJECT, {}, [access],
                                  PROXY_STORAGE_MAX_LIFETIME_S, self.instance_keys)

    url_scheme = self.app.config['PREFERRED_URL_SCHEME']
    server_hostname = self.app.config['SERVER_HOSTNAME']

    # The proxy path is of the form:
    # http(s)://registry_server/_storage_proxy/{token}/{scheme}/{hostname}/rest/of/path/here
    encoded_token = urllib.quote(token)
    proxy_url = '%s://%s/_storage_proxy/%s/%s/%s/%s' % (url_scheme, server_hostname, encoded_token,
                                                        parsed.scheme, parsed.netloc, path)
    logger.debug('Proxying via URL %s', proxy_url)
    return proxy_url


  def _validate_proxy_url(self):
    original_uri = request.headers.get('X-Original-URI', None)
    if not original_uri:
      logger.error('Missing original URI: %s', request.headers)
      abort(401)

    if not original_uri.startswith('/_storage_proxy/'):
      logger.error('Unknown storage proxy path: %s', original_uri)
      abort(401)

    # The proxy path is of the form:
    # /_storage_proxy/{token}/{scheme}/{hostname}/rest/of/path/here
    without_prefix = original_uri[len('/_storage_proxy/'):]
    parts = without_prefix.split('/', 3)
    if len(parts) != 4:
      logger.error('Invalid storage proxy path (found %s parts): %s', len(parts), without_prefix)
      abort(401)

    encoded_token, scheme, host, uri = parts
    token = urllib.unquote(encoded_token)

    logger.debug('Got token %s for storage proxy auth request %s with parts %s', token,
                 original_uri, parts)

    # Decode the bearer token.
    try:
      decoded = decode_bearer_token(token, self.instance_keys, self.app.config)
    except InvalidBearerTokenException:
      logger.exception('Invalid token for storage proxy')
      abort(401)

    # Ensure it is for the proxy.
    if decoded['sub'] != STORAGE_PROXY_SUBJECT:
      logger.exception('Invalid subject %s for storage proxy auth', decoded['subject'])
      abort(401)

    # Validate that the access matches the token format.
    access = decoded.get('access', {})
    try:
      validate(access, ACCESS_SCHEMA)
    except ValidationError:
      logger.exception('We should not be minting invalid credentials: %s', access)
      abort(401)

    # For now, we only expect a single access credential.
    if len(access) != 1:
      logger.exception('We should not be minting invalid credentials: %s', access)
      abort(401)

    # Ensure the signed access matches the requested URL's pieces.
    granted_access = access[0]
    if granted_access['scheme'] != scheme:
      logger.exception('Mismatch in scheme. %s expected, %s found', granted_access['scheme'],
                       scheme)
      abort(401)

    if granted_access['host'] != host:
      logger.exception('Mismatch in host. %s expected, %s found', granted_access['host'], host)
      abort(401)

    if granted_access['uri'] != uri:
      logger.exception('Mismatch in uri. %s expected, %s found', granted_access['uri'], uri)
      abort(401)

    return 'OK'