"""
Contains a CLICommand that can issue PUT requests.

Uses the following from :py:class:`swiftly.cli.context.CLIContext`:

===============  ====================================================
cdn              True if the CDN Management URL should be used
                 instead of the Storage URL.
client_manager   For connecting to Swift.
concurrency      The number of concurrent actions that can be
                 performed.
different        Set to True to check if the local file is different
                 than an existing object before uploading.
empty            Set to True if you wish to send an empty body with
                 the PUT rather than reading from the io_manager's
                 stdin.
headers          A dict of headers to send.
input\_          A string representing where input should be obtained
                 from. If None, the io_manager's stdin will be used.
                 If a directory path is specified, a set of PUTs will
                 be generated for each item in the directory
                 structure. If a file path is specified, that single
                 file will be used as input.
io_manager       For directing output and obtaining input if needed.
newer            Set to True to check if the local file is newer than
                 an existing object before uploading.
query            A dict of query parameters to send.
seek             Where to seek to in the input\_ before uploading;
                 usually just used by recursive calls with segmented
                 objects.
segment_size     The max size of a file before switching to a
                 segmented object and the max size of each object
                 segment.
static_segments  Set to True to use static large object support
                 instead of dynamic large object support.
===============  ====================================================
"""
"""
Copyright 2011-2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
import os

from swiftly.cli.command import CLICommand, ReturnCode
from swiftly.concurrency import Concurrency
from swiftly.dencrypt import AES256CBC, aes_encrypt
from swiftly.filelikeiter import FileLikeIter


def cli_put_directory_structure(context, path):
    """
    Performs PUTs rooted at the path using a directory structure
    pointed to by context.input\_.

    See :py:mod:`swiftly.cli.put` for context usage information.

    See :py:class:`CLIPut` for more information.
    """
    if not context.input_:
        raise ReturnCode(
            'called cli_put_directory_structure without context.input_ set')
    if not os.path.isdir(context.input_):
        raise ReturnCode(
            '%r is not a directory' % context.input_)
    if not path:
        raise ReturnCode(
            'uploading a directory structure requires at least a container '
            'name')
    new_context = context.copy()
    new_context.input_ = None
    container = path.split('/', 1)[0]
    cli_put_container(new_context, container)
    ilen = len(context.input_)
    if not context.input_.endswith(os.sep):
        ilen += 1
    conc = Concurrency(context.concurrency)
    for (dirpath, dirnames, filenames) in os.walk(context.input_):
        if not dirnames and not filenames:
            new_context = context.copy()
            new_context.headers = dict(context.headers)
            new_context.headers['content-type'] = 'text/directory'
            new_context.headers['x-object-meta-mtime'] = \
                '%f' % os.path.getmtime(context.input_)
            new_context.input_ = None
            new_context.empty = True
            new_path = path
            if path[-1] != '/':
                new_path += '/'
            new_path += dirpath[ilen:]
            for (exc_type, exc_value, exc_tb, result) in \
                    conc.get_results().itervalues():
                if exc_value:
                    conc.join()
                    raise exc_value
            conc.spawn(new_path, cli_put_object, new_context, new_path)
        else:
            for fname in filenames:
                new_context = context.copy()
                new_context.input_ = os.path.join(dirpath, fname)
                new_path = path
                if path[-1] != '/':
                    new_path += '/'
                if dirpath[ilen:]:
                    new_path += dirpath[ilen:] + '/'
                new_path += fname
                for (exc_type, exc_value, exc_tb, result) in \
                        conc.get_results().itervalues():
                    if exc_value:
                        conc.join()
                        raise exc_value
                conc.spawn(new_path, cli_put_object, new_context, new_path)
    conc.join()
    for (exc_type, exc_value, exc_tb, result) in \
            conc.get_results().itervalues():
        if exc_value:
            raise exc_value


def cli_put_account(context):
    """
    Performs a PUT on the account.

    See :py:mod:`swiftly.cli.put` for context usage information.

    See :py:class:`CLIPut` for more information.
    """
    body = None
    if context.input_:
        if context.input_ == '-':
            body = context.io_manager.get_stdin()
        else:
            body = open(context.input_, 'rb')
    with context.client_manager.with_client() as client:
        status, reason, headers, contents = client.put_account(
            headers=context.headers, query=context.query, cdn=context.cdn,
            body=body)
        if hasattr(contents, 'read'):
            contents.read()
    if status // 100 != 2:
        raise ReturnCode('putting account: %s %s' % (status, reason))


def cli_put_container(context, path):
    """
    Performs a PUT on the container.

    See :py:mod:`swiftly.cli.put` for context usage information.

    See :py:class:`CLIPut` for more information.
    """
    path = path.rstrip('/')
    if '/' in path:
        raise ReturnCode('called cli_put_container with object %r' % path)
    body = None
    if context.input_:
        if context.input_ == '-':
            body = context.io_manager.get_stdin()
        else:
            body = open(context.input_, 'rb')
    with context.client_manager.with_client() as client:
        status, reason, headers, contents = client.put_container(
            path, headers=context.headers, query=context.query,
            cdn=context.cdn, body=body)
        if hasattr(contents, 'read'):
            contents.read()
    if status // 100 != 2:
        raise ReturnCode(
            'putting container %r: %s %s' % (path, status, reason))


def cli_put_object(context, path):
    """
    Performs a PUT on the object.

    See :py:mod:`swiftly.cli.put` for context usage information.

    See :py:class:`CLIPut` for more information.
    """
    if context.different and context.encrypt:
        raise ReturnCode(
            'context.different will not work properly with context.encrypt '
            'since encryption may change the object size')
    put_headers = dict(context.headers)
    if context.empty:
        body = ''
        put_headers['content-length'] = '0'
    elif not context.input_ or context.input_ == '-':
        body = context.io_manager.get_stdin()
    elif context.seek is not None:
        if context.encrypt:
            raise ReturnCode(
                'putting object %r: Cannot use encryption and context.seek' %
                path)
        body = open(context.input_, 'rb')
        body.seek(context.seek)
    else:
        l_mtime = os.path.getmtime(context.input_)
        l_size = os.path.getsize(context.input_)
        put_headers['content-length'] = str(l_size)
        if context.newer or context.different:
            r_mtime = None
            r_size = None
            with context.client_manager.with_client() as client:
                status, reason, headers, contents = client.head_object(
                    *path.split('/', 1), headers=context.headers,
                    query=context.query, cdn=context.cdn)
                if hasattr(contents, 'read'):
                    contents.read()
            if status // 100 == 2:
                r_mtime = headers.get('x-object-meta-mtime')
                if r_mtime:
                    try:
                        r_mtime = float(r_mtime)
                    except ValueError:
                        r_mtime = None
                r_size = headers.get('content-length')
                if r_size:
                    try:
                        r_size = int(r_size)
                    except ValueError:
                        r_size = None
            elif status != 404:
                raise ReturnCode(
                    'could not head %r for conditional check; skipping put: '
                    '%s %s' % (path, status, reason))
            if context.newer and r_mtime is not None or l_mtime <= r_mtime:
                return
            if context.different and r_mtime is not None and \
                    l_mtime == r_mtime and r_size is not None and \
                    l_size == r_size:
                return
        put_headers['x-object-meta-mtime'] = '%f' % l_mtime
        size = os.path.getsize(context.input_)
        if size > context.segment_size:
            if context.encrypt:
                raise ReturnCode(
                    'putting object %r: Cannot use encryption for objects '
                    'greater than the segment size' % path)
            new_context = context.copy()
            new_context.input_ = None
            new_context.headers = None
            new_context.query = None
            container = path.split('/', 1)[0] + '_segments'
            cli_put_container(new_context, container)
            prefix = container + '/' + path.split('/', 1)[1]
            prefix = '%s/%s/%s/' % (prefix, l_mtime, size)
            conc = Concurrency(context.concurrency)
            start = 0
            segment = 0
            path2info = {}
            while start < size:
                new_context = context.copy()
                new_context.headers = dict(context.headers)
                new_context.headers['content-length'] = str(min(
                    size - start, context.segment_size))
                new_context.seek = start