#!/usr/bin/env python
# filename: s3.py


#
# Copyright (c) 2015 Bryan Briney
# License: The MIT license (http://opensource.org/licenses/MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#


import os
import subprocess as sp
import sys
import tarfile

from abutils.utils import log

if sys.version_info[0] > 2:
    raw_input = input


def compress_and_upload(data, compressed_file, s3_path, multipart_chunk_size_mb=500,
    method='gz', delete=False, access_key=None, secret_key=None):
    '''
    Compresses data and uploads to S3.

    S3 upload uses ``s3cmd``, so you must either:

        1) Manually configure ``s3cmd`` prior to use (typically using ``s3cmd --configure``).

        2) Configure ``s3cmd`` using ``s3.configure()``.

        3) Pass your access key and secret key to ``compress_and_upload``, which will automatically configure s3cmd.

    .. note:

        ``s3cmd`` configuration only needs to be done once per computer,
        which means that relaunching a cloud instance or Docker image will
        require re-configuration of ``s3cmd``.

    Args:

        data: Can be one of three things:

            1) Path to a single file

            2) Path to a directory

            3) A list of one or more paths to files or directories

        compressed_file (str): Path to the compressed file. Required.

        s3_path (str): The S3 path, with the filename omitted. The S3 filename
          will be the basename of the ``compressed_file``. For example::

            compress_and_upload(data='/path/to/data',
                                compressed_file='/path/to/compressed.tar.gz',
                                s3_path='s3://my_bucket/path/to/')

          will result in an uploaded S3 path of ``s3://my_bucket/path/to/compressed.tar.gz``

        method (str): Compression method. Options are ``'gz'`` (gzip) or ``'bz2'`` (bzip2).
            Default is ``'gz'``.

        delete (bool): If ``True``, the ``compressed_file`` will be deleted after upload
            to S3. Default is ``False``.

        access_key (str): AWS access key.

        secret_key (str): AWS secret key.
    '''
    logger = log.get_logger('s3')
    if all([access_key, secret_key]):
        configure(access_key=access_key, secret_key=secret_key, logger=logger)
    compress(data, compressed_file, fmt=method, logger=logger)
    put(compressed_file, s3_path, multipart_chunk_size_mb=multipart_chunk_size_mb, logger=logger)
    if delete:
        os.unlink(compressed_file)


def put(f, s3_path, multipart_chunk_size_mb=500, logger=None):
    '''
    Uploads a single file to S3, using s3cmd.

    Args:

        f (str): Path to a single file.

        s3_path (str): The S3 path, with the filename omitted. The S3 filename
            will be the basename of the ``f``. For example::

                put(f='/path/to/myfile.tar.gz', s3_path='s3://my_bucket/path/to/')

            will result in an uploaded S3 path of ``s3://my_bucket/path/to/myfile.tar.gz``
    '''
    if not logger:
        logger = log.get_logger('s3')
    fname = os.path.basename(f)
    target = os.path.join(s3_path, fname)
    s3cmd_cline = 's3cmd put {} {} --multipart-chunk-size-mb {}'.format(f,
                                                                        target,
                                                                        multipart_chunk_size_mb)
    print_put_info(fname, target, logger)
    s3cmd = sp.Popen(s3cmd_cline,
                     stdout=sp.PIPE,
                     stderr=sp.PIPE,
                     shell=True)
    stdout, stderr = s3cmd.communicate()


def print_put_info(fname, target, logger):
    logger.info('')
    logger.info('')
    logger.info('')
    logger.info('-' * 25)
    logger.info('UPLOADING TO S3')
    logger.info('-' * 25)
    logger.info('')
    logger.info('File: {}'.format(fname))
    logger.info('Target S3 location: {}'.format(target))



def compress(d, output, fmt='gz', logger=None):
    '''
    Creates a compressed/uncompressed tar file.

    Args:

        d: Can be one of three things:

            1. the path to a single file, as a string

            2. the path to a single directory, as a string

            3. an iterable of file or directory paths

        output (str): Output file path.

        fmt: Compression method. Options are ``'gz'`` (gzip),
            ``'bz2'`` (bzip2) and ``'none'`` (uncompressed). Default is ``'gz'``.
    '''
    if not logger:
        logger = log.get_logger('s3')
    if type(d) not in [list, tuple]:
        d = [d, ]
    d = [os.path.expanduser(_d) for _d in d]
    print_compress_info(d, output, compress, logger)
    if fmt.lower() == 'none':
        fmt = ''
    elif fmt.lower() not in ['gz', 'bz2']:
        logger.info('Compression option ("{}") is invalid.\nFalling back to uncompressed.'.format(fmt))
        fmt = ''
    output = os.path.expanduser(output)
    tar = tarfile.open(output, 'w:{}'.format(fmt))
    for obj in d:
        tar.add(obj)
    tar.close()
    return output


def print_compress_info(d, output, compress, logger):
    if not logger:
        logger = log.get_logger('s3')
    dirs = [obj for obj in d if os.path.isdir(obj)]
    files = [obj for obj in d if os.path.isfile(obj)]
    logger.info('')
    logger.info('')
    logger.info('')
    logger.info('-' * 25)
    logger.info('COMPRESSING DATA')
    logger.info('-' * 25)
    logger.info('')
    logger.info('Ouptut file: {}'.format(output))
    logger.info('Compression: {}'.format(compress.lower()))
    if dirs:
        d = 'directories' if len(dirs) > 1 else 'directory'
        logger.info('Found {} {} to compress: {}'.format(len(dirs), d,
                                                         ', '.join(dirs)))
    if files:
        f = 'files' if len(files) > 1 else 'file'
        logger.info('Found {} {} to compress: {}'.format(len(files), f,
                                                         ', '.join(files)))


def configure(access_key=None, secret_key=None, logger=None):
    '''
    Configures s3cmd prior to first use.

    If no arguments are provided, you will be prompted to enter
    the access key and secret key interactively.

    Args:

        access_key (str): AWS access key

        secret_key (str): AWS secret key
    '''
    if not logger:
        logger = log.get_logger('s3')
    if not all([access_key, secret_key]):
        logger.info('')
        access_key = input('AWS Access Key: ')
        secret_key = input('AWS Secret Key: ')
    _write_config(access_key, secret_key)
    logger.info('')
    logger.info('Completed writing S3 config file.')
    logger.info('')


def _write_config(access_key, secret_key):
    cfg_string = '[default]\n'
    cfg_string += 'access_key = {}\n'.format(access_key)
    cfg_string += 'secret_key = {}\n'.format(secret_key)
    cfg_string += CONFIG_DEFAULTS
    cfg_file = os.path.expanduser('~/.s3cfg')
    open(cfg_file, 'w').write(cfg_string)


CONFIG_DEFAULTS = '''
access_token =
add_encoding_exts =
add_headers =
bucket_location = US
cache_file =
cloudfront_host = cloudfront.amazonaws.com
default_mime_type = binary/octet-stream
delay_updates = False
delete_after = False
delete_after_fetch = False
delete_removed = False
dry_run = False
enable_multipart = True
encoding = UTF-8
encrypt = False
expiry_date =
expiry_days =
expiry_prefix =
follow_symlinks = False
force = False
get_continue = False
gpg_command = /usr/bin/gpg
gpg_decrypt = %(gpg_command)s -d --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s
gpg_encrypt = %(gpg_command)s -c --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s
gpg_passphrase =
guess_mime_type = True
host_base = s3.amazonaws.com
host_bucket = %(bucket)s.s3.amazonaws.com
human_readable_sizes = False
ignore_failed_copy = False
invalidate_default_index_on_cf = False
invalidate_default_index_root_on_cf = True
invalidate_on_cf = False
list_md5 = False
log_target_prefix =
max_delete = -1
mime_type =
multipart_chunk_size_mb = 250
preserve_attrs = True
progress_meter = True
proxy_host =
proxy_port = 0
put_continue = False
recursive = False
recv_chunk = 4096
reduced_redundancy = False
restore_days = 1
send_chunk = 4096
server_side_encryption = False
simpledb_host = sdb.amazonaws.com
skip_existing = False
socket_timeout = 300
urlencoding_mode = normal
use_https = True
use_mime_magic = True
verbosity = WARNING
website_endpoint = http://%(bucket)s.s3-website-% (location)s.amazonaws.com/
website_error =
website_index = index.html
'''
