"""
Plugin for providing static asset management with AWS' S3 (with or without a
CDN that works with S3, such as CloudFront.)
"""

from __future__ import absolute_import, division, print_function, with_statement, unicode_literals

try:
    from boto.s3.connection import S3Connection
except ImportError:
    S3Connection = None

import os
import shutil
import hashlib
import mimetypes
from tornado import escape

from .actions import *
from .middleware import *
from .options import *
from .uimodules import *


def full_path(path, s3_prefix=None):
    """Return `path` with the s3_prefix applied."""
    prefix = s3_prefix or oz.settings["s3_prefix"]
    if prefix:
        path = "%s/%s" % (prefix, path)
    return path

def static_url(redis, path):
    """Gets the static path for a file"""
    file_hash = get_cache_buster(redis, path)
    return "%s/%s?v=%s" % (oz.settings["static_host"], path, file_hash)

def get_cache_buster(redis, path):
    """Gets the cache buster value for a given file path"""
    return escape.to_unicode(redis.hget("cache-buster:{}:v3".format(oz.settings["s3_bucket"]), full_path(path)))

def set_cache_buster(redis, path, hash):
    """Sets the cache buster value for a given file path"""
    redis.hset("cache-buster:{}:v3".format(oz.settings["s3_bucket"]), full_path(path), hash)

def remove_cache_buster(redis, path):
    """Removes the cache buster for a given file"""
    redis.hdel("cache-buster:{}:v3".format(oz.settings["s3_bucket"]), full_path(path))

def get_bucket(s3_bucket=None, validate=False):
    """Gets a bucket from specified settings"""
    global S3Connection

    if S3Connection != None:
        settings = oz.settings
        s3_bucket = s3_bucket or settings["s3_bucket"]
        opts = {}
        if settings["s3_host"]:
            opts["host"] = settings["s3_host"]
        if settings["aws_access_key"] and settings["aws_secret_key"]:
            opts["aws_access_key_id"] = settings["aws_access_key"]
            opts["aws_secret_access_key"] = settings["aws_secret_key"]
        return S3Connection(**opts).get_bucket(s3_bucket, validate=validate)
    else:
        raise Exception("S3 not supported in this environment as boto is not installed")

def get_file(path, s3_bucket=None, s3_prefix=None):
    """Gets a file"""

    bucket_name = s3_bucket or oz.settings["s3_bucket"]

    if bucket_name:
        bucket = get_bucket(bucket_name)
        path = full_path(path, s3_prefix)
        key = bucket.get_key(path)
        if not key:
            key = bucket.new_key(path)
        return S3File(key)
    else:
        return LocalFile(oz.settings["static_path"], path)

class CDNFile(object):
    """File spec for a CDN/S3-hosted file"""

    def __hash__(self):
        return hash("%s:%s" % (self.__class__.__name__, self.path()))

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.path() == other.path()
        else:
            return False

    def hash(self):
        """Creates a cache buster value for the file"""
        return hashlib.sha256(self.contents()).hexdigest()

    def path(self):
        """Gets the path of the file"""
        raise NotImplementedError()

    def upload(self, contents, replace=False):
        """
        Uploads the file to its path with the given `content`, adding the
        appropriate parent directories when needed. If the path already exists
        and `replace` is `False`, the file will not be uploaded.
        """
        raise NotImplementedError()

    def contents(self):
        """Gets the contents of the file"""
        raise NotImplementedError()

    def exists(self):
        """Returns whether the path exists"""
        raise NotImplementedError()

    def remove(self):
        """Removes the given file"""
        raise NotImplementedError()


class LocalFile(CDNFile):
    """
    Specifies a file stored locally and hosted through Tornado's static asset
    engine
    """

    def __init__(self, static_path, file_path):
        self.static_path = static_path
        self.file_path = file_path
        self.full_path = os.path.join(self.static_path, self.file_path)

    def path(self):
        return self.file_path

    def upload(self, contents, replace=False):
        if replace or not os.path.exists(self.full_path):
            try:
                os.makedirs(os.path.dirname(self.full_path))
            except:
                pass

            with open(self.full_path, "wb") as f:
                f.write(contents)

    def contents(self):
        with open(self.full_path, "rb") as f:
            return f.read()

    def exists(self):
        return os.path.exists(self.full_path)

    def remove(self):
        os.remove(self.full_path)

    def copy(self, new_path, replace=False):
        """ Uses shutil to copy a file over """
        new_full_path = os.path.join(self.static_path, new_path)
        if replace or not os.path.exists(new_full_path):
            shutil.copy2(self.full_path, new_full_path)
            return True
        return False


class S3File(CDNFile):
    """Specifies a file stored on Amazon S3"""

    def __init__(self, key):
        self.key = key

    def path(self):
        return self.key.name

    def upload(self, contents, replace=False):
        if replace or not self.key.exists():
            guessed_type = mimetypes.guess_type(self.path())[0]

            self.key.set_contents_from_string(contents, {
                "Content-Type": guessed_type or "binary/octet-stream",
                "Cache-Control": "max-age=155520000, public",
                "Expires": "Sat, 29 Apr 2017 13:31:45-0000 GMT"
            })

    def contents(self):
        return self.key.get_contents_as_string()

    def exists(self):
        return self.key.exists()

    def remove(self):
        self.key.delete()

    def copy(self, new_path, replace=False):
        """Uses boto to copy the file to the new path instead of uploading another file to the new key"""
        if replace or not get_file(new_path).exists():
            self.key.copy(self.key.bucket, new_path)
            return True
        return False
