# -*- coding: utf-8 -*-

"""Classes for S3 Buckets."""

from pathlib import Path
import mimetypes
import boto3
from botocore.exceptions import ClientError
from webotron import util
from hashlib import md5
from functools import reduce


class BucketManager:
    """Manage an S3 Bucket."""

    CHUNK_SIZE = 8388608

    def __init__(self, session):
        """Create a BucketManager object."""
        self.session = session
        self.s3 = self.session.resource('s3')
        self.transfer_config = boto3.s3.transfer.TransferConfig(
            multipart_chunksize=self.CHUNK_SIZE,
            multipart_threshold=self.CHUNK_SIZE
        )
        self.manifest = {}

    def get_bucket(self, bucket_name):
        """Get a bucket by name."""
        return self.s3.Bucket(bucket_name)

    def get_region_name(self, bucket):
        """Get the region name for this bucket."""
        bucket_client = self.s3.meta.client
        bucket_location = bucket_client.get_bucket_location(Bucket=bucket.name)
        return bucket_location['LocationConstraint'] or 'us-east-1'

    def get_bucket_url(self, bucket):
        """Get the wesite URL for this bucket."""
        return "http://{}.{}".format(
            bucket.name,
            util.get_endpoint(
                self.get_region_name(bucket)
                ).host
            )

    def all_buckets(self):
        """Get an iterator for all buckets."""
        return self.s3.buckets.all()

    def all_bucket_objects(self, bucket_name):
        """Get an iterator for all object in a bucket."""
        return self.s3.Bucket(bucket_name).objects.all()

    def init_bucket(self, bucket_name):
        """Initialize a bucket."""
        s3_bucket = None
        try:
            s3_bucket = self.s3.create_bucket(
                Bucket=bucket_name,
                CreateBucketConfiguration={
                    'LocationConstraint': self.session.region_name
                }
            )
        except ClientError as error:
            if error.response['Error']['Code'] == 'BucketAlreadyOwnedByYou':
                s3_bucket = self.s3.Bucket(bucket_name)
            else:
                raise error
        return s3_bucket

    def set_policy(self, bucket):
        """Set bucket policy to be readable by anyone."""
        policy = """
        {
          "Version":"2012-10-17",
          "Statement":[{
          "Sid":"PublicReadGetObject",
          "Effect":"Allow",
          "Principal": "*",
              "Action":["s3:GetObject"],
              "Resource":["arn:aws:s3:::%s/*"
              ]
            }
          ]
        }
        """ % bucket.name

        policy = policy.strip()
        pol = bucket.Policy()
        pol.put(Policy=policy)

    def configure_website(self, bucket):
        """Configure bucket as a website."""
        bucket.Website().put(
            WebsiteConfiguration={
                'ErrorDocument': {
                    'Key': 'error.html'
                },
                'IndexDocument': {
                    'Suffix': 'index.html'
                }
            }
        )

    def load_manifest(self, bucket):
        """Load manifest for caching purpose."""
        paginator = self.s3.meta.client.get_paginator('list_objects_v2')
        for page in paginator.paginate(Bucket=bucket.name):
            for obj in page.get('Contents', []):
                self.manifest[obj['Key']] = obj['ETag']

    @staticmethod
    def hash_data(data):
        """Generate md5 hash for data."""
        hash = md5()
        hash.update(data)
        return hash

    def gen_etag(self, path):
        """Generate etag for file."""
        hashes = []
        with open(path, 'rb') as f:
            while True:
                data = f.read(self.CHUNK_SIZE)
                if not data:
                    break
                hashes.append(self.hash_data(data))
        if not hashes:
            return
        elif len(hashes) == 1:
            return '"{}"'.format(hashes[0].hexdigest())
        else:
            hash = self.hash_data(
                                    reduce(
                                        lambda x, y: x+y,
                                        (
                                            h.digest() for h in hashes
                                        )
                                    )
                                )
            return '"{}-{}"'.format(hash.hexdigest(), len(hashes))

    def upload_file(self, bucket, path, key):
        """Upload path to S3 bucket with key."""
        content_type = mimetypes.guess_type(key)[0] or 'text/plain'
        etag = self.gen_etag(path)
        if self.manifest.get(key, '') == etag:
            print("Skipping {}, etag match".format(key))
            return
        print(
            'File has been successfully added : {}'.format(key)
        )
        return bucket.upload_file(
                path,
                key,
                ExtraArgs={
                    'ContentType': content_type
                    },
                Config=self.transfer_config
            )

    def delete_files(self, bucket, objects):
        """Delete files from given bucket."""
        response = bucket.delete_objects(
            Delete={
                    'Objects': objects
                },
            RequestPayer='Webotron'
            )
        for item in response['Deleted']:
            print(
                'File has been successfully deleted : {}'.format(item['Key'])
            )
        if 'Errors' in response.keys():
            for item in response['Errors']:
                print(
                    'Error in deleteFile : {} ,'
                    'ErrorCode: {} , ErrorMsg: {}'.format(
                        item['Key'], item['Code'], item['Message']
                    )
                )

    def sync(self, pathname, bucket_name):
        """Sync contents of path to bucket."""
        bucket = self.s3.Bucket(bucket_name)
        root = Path(pathname).expanduser().resolve()
        self.load_manifest(bucket)
        self.file_list_source = []

        def handle_directory(target):
            for p in target.iterdir():
                if p.is_dir():
                    handle_directory(p)
                if p.is_file():
                    self.file_list_source.append(
                        str(p.relative_to(root).as_posix())
                    )
                    self.upload_file(
                        bucket,
                        str(p),
                        str(p.relative_to(root).as_posix())
                    )

        def handle_bucket(directory_flist, bucket_keylist):
            files_delete = []
            for key in bucket_keylist.keys():
                if key not in directory_flist:
                    key_dic = {'Key': key}
                    files_delete.append(key_dic)
            if files_delete:
                self.delete_files(bucket, files_delete)

        handle_directory(root)
        handle_bucket(self.file_list_source, self.manifest)
