#!/usr/bin/env python

import boto
from boto.s3.acl import ACL
import sys
import logging

from umobj.utils import umobj_logging, umobj_init, \
    umobj_get_bucket_key_pair_from_string, parse_key_value_pair
from umobj.obj import Obj
from umobj.acl import split_acl, make_private
from umobj.options import umobj_parser, get_logging_level
from umobj.key import add_metadata, set_metadata, \
    remove_metadata_by_key_value_pair


if __name__ == "__main__":
    umobj_init()

    description = 'Change Object - ' + \
                  'Modify Access Control lists for a set of buckets ' + \
                  'and/or keys'
    description_epilog = '''  POLICY      - username:ACL where ACL is one of
                FULL_CONTROL, READ, WRITE, READ_ACL, WRITE_ACL'''
    parser = umobj_parser(description=description,
                          description_epilog=description_epilog)
    parser.add_recursive(help='Recursively apply ACL to keys')
    parser.add_mode(
        choices=[
            'add',
            'remove',
            'bucket',
            'public',
            'private',
            'clear'],
        help='Mode of operation.  Specifying "clear" will create an empty ' +
             'ACL to be applied.  However, if you specify policies in the ' +
             'same command, the ACL will contain those policies.')
    parser.add_no_bucket_changes()
    parser.add_policy()
    parser.add_metadata()
    parser.add_s3path(number=1)
    args = parser.parse_args()

    umobj_logging(get_logging_level(args))  # set up logging

    logging.info("Running %s" % sys.argv)

    S3PATH = args.S3PATH
    recursive = args.recursive
    mode = args.mode
    public = mode == 'public'
    private = mode == 'private'
    no_bucket_changes = args.no_bucket_changes
    push_bucket_acls = mode == 'bucket'
    policies = args.policies
    metadata = args.metadata

    if policies is None:
        policies = []
    if metadata is None:
        metadata = []

    # Bucket changes are only possible if the user does not not specify
    # --no-bucket-changes and also does not specify --push-bucket-acls.
    # Whether or not the bucket is included will also depend on whether or
    # not a key_prefix is present.
    bucket_changes_possible = not no_bucket_changes and not push_bucket_acls

    # --- guard clasuses ---

    # failure conditions by mode:
    #   - add
    #   - remove
    #   - bucket
    #   - public
    #   - private
    #   - clear

    # ensure a successful connection to the object store
    if not Obj.connect(host=args.host,
                       port=args.port,
                       access_key=args.access_key,
                       secret_key=args.secret_key):
        logging.error('Unable to contact object store.')
        sys.exit(1)

    # make sure we know what objects to change
    if len(S3PATH) == 0:
        parser.print_help()
        sys.exit(1)
    # --- end guard claseses ---

    if push_bucket_acls and no_bucket_changes:
        logging.warning('--no-bucket-changes cannot possibly have any ' +
                        'effect when pushing bucket ACLs.')

    for bucket_name in S3PATH:
        bucket_name, key_name_prefix = \
            umobj_get_bucket_key_pair_from_string(bucket_name)
        try:
            bucket = Obj.conn.get_bucket(bucket_name)
        except boto.exception.S3ResponseError as e:
            logging.error("Can not access bucket %s, %s." %
                          (bucket_name, e.error_code))
            continue

        # get objects that we are targeting
        objects = []
        if key_name_prefix:
            for key in bucket.list(prefix=key_name_prefix):
                objects.append(key)
        else:
            if bucket_changes_possible:
                objects.append(bucket)
            if recursive:
                for key in bucket.get_all_keys():
                    objects.append(key)

        if push_bucket_acls:
            bucket_policy = bucket.get_acl()

        for object in objects:
            if public:
                logging.debug('Making %s public' % object.name)
                object.make_public()
            elif private:
                logging.debug('Making %s private' % object.name)
                make_private(object)
            elif push_bucket_acls:
                logging.debug('Pushing bucket acls to %s' % object.name)
                object.set_acl(bucket_policy)
            else:
                policy = object.get_acl()
                if mode == 'add':
                    for p in policies:
                        u, g = split_acl(p)
                        policy.acl.add_user_grant(g, u)
                    for meta in metadata:
                        k, v = parse_key_value_pair(meta)
                        add_metadata(bucket, object.name, k, v)
                elif mode == 'remove':
                    grants_to_remove = []
                    for grant in policy.acl.grants:
                        for p in policies:
                            u, g = split_acl(p)
                            if grant.permission == g and grant.id == u:
                                grants_to_remove.append(grant)
                    policy.acl.grants = [gr for gr in policy.acl.grants
                                         if gr not in grants_to_remove]
                    for meta in metadata:
                        remove_metadata_by_key_value_pair(
                            bucket,
                            object.name,
                            meta)
                elif mode == 'clear':
                    acl = ACL()
                    for p in policies:
                        u, g = split_acl(p)
                        acl.add_user_grant(g, u)
                    else:
                        # clear shouldn't *fully* clear.  We still want the
                        # object's owner to have FULL_CONTROL
                        acl.add_user_grant('FULL_CONTROL', object.owner.id)
                    policy.acl = acl
                    set_metadata(bucket, object.name, {})
                try:
                    object.set_acl(policy)
                except boto.exception.S3ResponseError:
                    pass
