""" Store packages in S3 """
import calendar
import logging
import posixpath
import time
from contextlib import contextmanager
from hashlib import md5
from urllib import urlopen, quote

import boto.s3
from boto.cloudfront import Distribution
from boto.s3.key import Key
import boto.s3.connection
from pyramid.httpexceptions import HTTPFound
from pyramid.settings import asbool

from .base import IStorage
from pypicloud.models import Package
from pypicloud.util import parse_filename, getdefaults


LOG = logging.getLogger(__name__)

SUPPORTED_CALLING_FORMATS = {
    'SubdomainCallingFormat': boto.s3.connection.SubdomainCallingFormat,
    'VHostCallingFormat': boto.s3.connection.VHostCallingFormat,
    'OrdinaryCallingFormat': boto.s3.connection.OrdinaryCallingFormat,
    'ProtocolIndependentOrdinaryCallingFormat':
        boto.s3.connection.ProtocolIndependentOrdinaryCallingFormat
}


class S3Storage(IStorage):

    """ Storage backend that uses S3 """
    test = False

    def __init__(self, request=None, bucket=None, expire_after=None,
                 bucket_prefix=None, prepend_hash=None, redirect_urls=None,
                 **kwargs):
        super(S3Storage, self).__init__(request, **kwargs)
        self.bucket = bucket
        self.expire_after = expire_after
        self.bucket_prefix = bucket_prefix
        self.prepend_hash = prepend_hash
        self.redirect_urls = redirect_urls

    @classmethod
    def configure(cls, settings):
        kwargs = super(S3Storage, cls).configure(settings)
        kwargs['expire_after'] = int(getdefaults(
            settings, 'storage.expire_after', 'aws.expire_after', 60 * 60 *
            24))
        kwargs['bucket_prefix'] = getdefaults(
            settings, 'storage.prefix', 'aws.prefix', '')
        kwargs['prepend_hash'] = asbool(getdefaults(
            settings, 'storage.prepend_hash', 'aws.prepend_hash', True))
        access_key = getdefaults(settings, 'storage.access_key',
                                 'aws.access_key', None)
        secret_key = getdefaults(settings, 'storage.secret_key',
                                 'aws.secret_key', None)
        host = getdefaults(settings, 'storage.host',
                           'aws.host', boto.s3.connection.NoHostProvided)
        is_secure = getdefaults(settings, 'storage.is_secure',
                                'aws.is_secure', True)
        calling_format = settings.get('storage.calling_format',
                                      'SubdomainCallingFormat')
        kwargs['redirect_urls'] = asbool(settings.get('storage.redirect_urls',
                                                      False))

        if calling_format not in SUPPORTED_CALLING_FORMATS:
            raise ValueError("Only {0} are supported for calling_format"
                             .format(', '.join(SUPPORTED_CALLING_FORMATS)))

        # We used to always use boto.connect_s3 because it can look up buckets
        # in any region. New regions require AWS4-HMAC-SHA256, which boto can
        # only do with a region connection. So if the region is specified (and
        # it must be for new regions like eu-central-1), use a region
        # connection.
        location = settings.get('storage.region')
        if location is None:
            s3conn = boto.connect_s3(
                aws_access_key_id=access_key,
                aws_secret_access_key=secret_key,
                host=host,
                is_secure=asbool(is_secure),
                calling_format=SUPPORTED_CALLING_FORMATS[calling_format]())
        else:
            s3conn = boto.s3.connect_to_region(location,
                                               aws_access_key_id=access_key,
                                               aws_secret_access_key=secret_key)
        aws_bucket = getdefaults(settings, 'storage.bucket', 'aws.bucket',
                                 None)
        if aws_bucket is None:
            raise ValueError("You must specify the 'storage.bucket'")
        try:
            bucket = s3conn.get_bucket(aws_bucket)
        except boto.exception.S3ResponseError as e:
            if e.error_code != 'NoSuchBucket':
                if e.status == 301:
                    LOG.warn("Bucket found in different region. Check that "
                             "the S3 bucket specified in 'storage.bucket' is "
                             "in 'storage.region'")
                raise
            location = getdefaults(settings, 'storage.region', 'aws.region',
                                   boto.s3.connection.Location.DEFAULT)
            LOG.info("Creating S3 bucket %s in region %s", aws_bucket,
                     location)
            bucket = s3conn.create_bucket(aws_bucket, location=location)
        kwargs['bucket'] = bucket
        return kwargs

    def calculate_path(self, package):
        """ Calculates the path of a package """
        path = package.name + '/' + package.filename
        if self.prepend_hash:
            m = md5()
            m.update(package.filename)
            prefix = m.digest().encode('hex')[:4]
            path = prefix + '/' + path
        return path

    def get_path(self, package):
        """ Get the fully-qualified bucket path for a package """
        if 'path' not in package.data:
            filename = self.calculate_path(package)
            package.data['path'] = self.bucket_prefix + filename
        return package.data['path']

    def list(self, factory=Package):
        keys = self.bucket.list(self.bucket_prefix)
        for key in keys:
            # Boto doesn't send down metadata from bucket.list()
            # so we are forced to retrieve each key individually.
            key = self.bucket.get_key(key.key)
            filename = posixpath.basename(key.key)
            name = key.get_metadata('name')
            version = key.get_metadata('version')

            # We used to not store metadata. This is for backwards
            # compatibility
            if name is None or version is None:
                try:
                    name, version = parse_filename(filename)
                except ValueError:
                    LOG.warning("S3 file %s has no package name", key.key)
                    continue

            last_modified = boto.utils.parse_ts(key.last_modified)

            pkg = factory(name, version, filename, last_modified, path=key.key)

            yield pkg

    def _generate_url(self, package):
        """ Generate a signed url to the S3 file """
        key = Key(self.bucket, self.get_path(package))
        return key.generate_url(self.expire_after)

    def get_url(self, package):
        if self.redirect_urls:
            return super(S3Storage, self).get_url(package)
        else:
            return self._generate_url(package)

    def download_response(self, package):
        return HTTPFound(location=self._generate_url(package))

    def upload(self, package, data):
        key = Key(self.bucket)
        key.key = self.get_path(package)
        key.set_metadata('name', package.name)
        key.set_metadata('version', package.version)
        # S3 doesn't support uploading from a non-file stream, so we have to
        # read it into memory :(
        key.set_contents_from_string(data.read())

    def delete(self, package):
        path = self.get_path(package)
        key = Key(self.bucket)
        key.key = path
        key.delete()

    @contextmanager
    def open(self, package):
        url = self._generate_url(package)
        handle = urlopen(url)
        try:
            yield handle
        finally:
            handle.close()


class CloudFrontS3Storage(S3Storage):

    """ Storage backend that uses S3 and CloudFront """
    def __init__(self, request=None, bucket=None, expire_after=None, bucket_prefix=None,
                 prepend_hash=None, cloud_front_domain=None, cloud_front_key_file=None,
                 cloud_front_key_string=None, cloud_front_key_id=None, **kwargs):
        super(CloudFrontS3Storage, self).__init__(request, bucket, expire_after, bucket_prefix, prepend_hash, **kwargs)
        self.cloud_front_domain = cloud_front_domain
        self.cloud_front_key_file = cloud_front_key_file
        self.cloud_front_key_id = cloud_front_key_id
        self.cloud_front_key_string = cloud_front_key_string

        self.distribution = Distribution()

    @classmethod
    def configure(cls, settings):
        kwargs = super(CloudFrontS3Storage, cls).configure(settings)
        kwargs['cloud_front_domain'] = getdefaults(
            settings, 'storage.cloud_front_domain', 'aws.cloud_front_domain', '')
        kwargs['cloud_front_key_file'] = getdefaults(
            settings, 'storage.cloud_front_key_file', 'aws.cloud_front_key_file', None)
        kwargs['cloud_front_key_string'] = getdefaults(
            settings, 'storage.cloud_front_key_string', 'aws.cloud_front_key_string', None)
        kwargs['cloud_front_key_id'] = getdefaults(
            settings, 'storage.cloud_front_key_id', 'aws.cloud_front_key_id', '')

        return kwargs

    def _generate_url(self, package):
        """ Get the fully-qualified CloudFront path for a package """
        path = self.calculate_path(package)
        url = self.cloud_front_domain + '/' + quote(path)

        if self.cloud_front_key_file or self.cloud_front_key_string:
            expire_time = int(time.time() + self.expire_after)
            url = self.distribution.create_signed_url(
                url, self.cloud_front_key_id, expire_time, private_key_file=self.cloud_front_key_file,
                private_key_string=self.cloud_front_key_string)

        return url
