# The MIT License (MIT)
#
# Copyright (c) 2016 Thorsten Simons (sw@snomis.de)
#
# 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 sys
import os
import time
from textwrap import wrap
import boto3
from boto3.s3.transfer import S3Transfer
import botocore
from botocore.utils import fix_s3_host
from botocore.vendored.requests.packages.urllib3 import disable_warnings
import cmd

from .init import INTRO, no_redir_cmds
from .conf import readconf
from .parse import parse_acl, BUCKET, GRANT, OBJECT, SET, paramcheck

from pprint import pprint

PIPE = '|'          # output shall be piped
OUTFILE = '>'       # output shall be written to a file
EXTENDFILE = '>>'   # output shall extend a file


class HS3shell(cmd.Cmd):
    intro = INTRO

    prompt = '--> '

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.endpoint = None     # the aws region or the target HCP Tenant
        self.mode = None         # 'aws' or 'compatible'
        self.ssl = True          # if https shall be used
        self.s3 = None           # the connection
        self.bucket = None       # the bucket object to work with
        self.bucketname = None   # its name
        self.encrypted = False   # if objects shall be server side encrypted
        self.profiles = None     # a dict of dicts per profile in .hs3sh.conf
        self.profile = None      # the profile in use

    def preloop(self):
        disable_warnings()
        # read the configuration file(s)
        try:
            self.profiles = readconf()
        except Exception as e:
            sys.exit('error: no config file loaded...\nhint: {}'.format(e))

        # if we have a ~/.hs3shrc file, read it and execute the commands...
        try:
            startupfile = os.path.join(os.path.expanduser("~"), ".hs3shrc")
            with open(startupfile, 'r') as inhdl:
                for cmnd in inhdl.readlines():
                    self.cmdqueue.append(cmnd.strip())
        except Exception:
            pass

    def emptyline(self):
        'Disable repetition of last command by pressing Enter'
        pass

    def do_lsp(self, arg):
        'lsp\n'\
        '    show the loaded profiles'
        print('{:1} {:26} {:>8}  {}'.format('C', 'profile', 'tag', 'value'))
        print('{:1} {:26} {:8}- {}'.format('-', '-'*26, '-'*8, '-'*39))
        for p in sorted(self.profiles.keys()):
            for tag in ['comment', 'endpoint', 'region', 'https']:
                if self.profiles[p][tag] or tag == 'https':
                    print('{:1} {:26} {:>8}: {}'
                          .format('|' if p == self.profile else '',
                                  p if tag == 'comment' else '',
                                  tag,
                                  self.profiles[p][tag]))

    def do_connect(self, arg):
        'connect <profile_name>\n'\
        '    Connect to an S3 endpoint using profile <profile_name>\n'\
        '    from ~/.hs3sh.conf or ./.hs3sh.conf'
        try:
            para = paramcheck(arg, flags='')
        except Exception as e:
            print('error: parameters invalid...\nhint: {}'.format(e),
                  file=sys.stderr)
            return
        else:
            if len(para.args) != 1:
                print('error: a profile_name as explicit parameter is required',
                      file=sys.stderr)
                return
            try:
                self.mode = 'aws' if self.profiles[para.args[0]]['region'] else 'compatible'
            except KeyError:
                print('error: profile {} unavailable...'.format(para.args[0]),
                      file=sys.stderr)
                return
            else:
                self.profile = para.args[0]

            try:
                if self.mode == 'aws':
                    d = boto3.session.Session(aws_access_key_id=self.profiles[self.profile]['aws_access_key_id'],
                                              aws_secret_access_key=self.profiles[self.profile]['aws_secret_access_key'],
                                              region_name=self.profiles[self.profile]['region'])
                    self.s3 = d.resource('s3',
                                         config=boto3.session.Config(signature_version='s3v4'))
                else:
                    endpoint = ('https://' if self.profiles[self.profile]['https'] else 'http://') + \
                               self.profiles[self.profile]['endpoint']
                    d = boto3.session.Session(aws_access_key_id=self.profiles[self.profile]['aws_access_key_id'],
                                              aws_secret_access_key=self.profiles[self.profile]['aws_secret_access_key'])
                    self.s3 = d.resource('s3', endpoint_url=endpoint,
                                         verify=False)
                    self.s3.meta.client.meta.events.unregister('before-sign.s3',
                                                               fix_s3_host)
            except Exception as e:
                print('error: connect failed\nhint: {}'.format(e),
                      file=sys.stderr)
            else:
                self.bucket = self.bucketname = None

    def do_bucket(self, arg):
        'bucket [-c|-cv|-r|-v] bucketname\n'\
        '    create or remove a bucket\n'\
        '    -c create <bucketname>\n'\
        '    -r remove bucket (needs to be empty)\n'\
        '    -v toggle versioning\n'\
        '    without any flags, the bucket\'s versioning status is shown'
        if not self.profile:
            print('error: you need to connect, first...', file=sys.stderr)
            return

        try:
            para = paramcheck(arg, flags='cvr')
        except Exception as e:
            print('error: while parsing parameters...\nhint: {}'.format(e),
                  file=sys.stderr)
            return
        else:
            if not len(para.args):
                print('error: no bucketname specified...', file=sys.stderr)
                return
            else:
                bucket = para.args[0]
            if 'v' in para.flags and 'r' in para.flags:
                print('error: -v not possible with -r', file=sys.stderr)
                return

            if 'c' in para.flags:                   # create bucket
                try:
                    bkt = self.s3.Bucket(bucket)
                    bkt.create(CreateBucketConfiguration={
                        'LocationConstraint': self.profiles[self.profile][
                                                  'region'] or ''})
                except Exception as e:
                    _print('error: create bucket failed...\nhint: {}'
                           .format(e), file=sys.stderr)
                else:
                    if 'v' in para.flags:
                        try:
                            bv = bkt.Versioning()
                            bv.enable()
                        except Exception as f:
                            _print('error: enabling versioning failed...\nhint: {}'
                               .format(f), file=sys.stderr)
                return
            if 'r' in para.flags:                   # remove bucket
                try:
                    bkt = self.s3.Bucket(bucket)
                    bkt.delete()
                except Exception as e:
                    _print('error: delete bucket failed...\nhint: {}'.format(e),
                           file=sys.stderr)
                return

            # get info about versioning status
            try:
                bkt = self.s3.Bucket(bucket)
                bv = bkt.Versioning()
            except Exception as e:
                _print('error: can\'t get versioning status...\nhint: {}'
                   .format(e), file=sys.stderr)
                return

            if 'v' in para.flags and not 'c' in para.flags: # toggle versioning
                if bv.status == 'Enabled':
                    bv.suspend()
                    bv.reload()
                    print('versioning is now {}'.format(bv.status.lower() if bv.status else 'disabled'))
                else:
                    bv.enable()
                    bv.reload()
                    print('versioning is now {}'.format(bv.status.lower() if bv.status else 'disabled'))
                return
            if not para.flags:                      # show bucket status
                try:
                    print('versioning status: {}'.format(bv.status.lower() if bv.status else 'disabled'))
                except Exception as e:
                    _print('error: can\'t get versioning status...\nhint: {}'
                       .format(e), file=sys.stderr)
                return
        print('error: invalid parameters...', file=sys.stderr)
        return

    def do_acl(self, arg):
        'acl -b|-o name [-g user permission [, user permission]* [-s canned ACL]\n'\
        '    -b name = bucket\n'\
        '    -o name = object\n'\
        '    -g set per-user permission(s)\n'\
        '    -s set canned ACL\n'\
        '* If neither -g nor -s is given, list the actual ACLs\n'\
        '* If any specifier holds characters outside the ascii alphabet and\n'\
        '  the underscore, surrond it with "quotation marks"'
        if not self.profile:
            print('error: you need to connect, first...', file=sys.stderr)
            return

        try:
            para = parse_acl(arg)
        except Exception as e:
            print('error: while parsing parameters...\nhint: {}'.format(e),
                  file=sys.stderr)
            return
        else:
            if OBJECT in para.flags and not self.bucket:
                print('error: you need to attach a bucket, first...',
                      file=sys.stderr)
                return

        if para.isbucket:   # working on a bucket
            try:
                acl = self.s3.Bucket(para.name).Acl()
                acl.load()
            except Exception as e:
                _print('error: unable to access bucket ACLs...\nhint: {}'
                       .format(e), file=sys.stderr)
                return
            else:
                                            # we just list what we have
                if GRANT not in para.flags and not para.canned:
                    print('ACLs for bucket {}'.format(para.name))
                    out = {'Owner': acl.owner, 'Grants': acl.grants}
                    pprint(out)
                else:                       # set ACLs
                    # set canned ACL
                    if para.canned:
                        try:
                            acl.put(ACL=para.canned)
                        except Exception as e:
                            _print('error: setting a canned bucket ACL failed...\nhint: {}'
                                   .format(e), file=sys.stderr)

                    if para.pairs:
                        newgrants = []
                        try:
                            for g, p in para.pairs:
                                newgrants.append({'Grantee': {'EmailAddress': g,
                                                               # 'ID': '',
                                                               'Type': 'AmazonCustomerByEmail',
                                                               },
                                                   'Permission': p})

                            print('BEFORE acl.put(): ACLs for bucket {}'.format(para.name))
                            out = {'Owner': acl.owner, 'Grants': newgrants}
                            pprint(out)

                            acl.put(AccessControlPolicy={'Owner': acl.owner,
                                                         'Grants': newgrants,
                                                         })
                        except Exception as e:
                            _print('error: setting per-user bucket ACL failed...\nhint: {}'
                                   .format(e), file=sys.stderr)


        else:               # working on an object
            print('not yet implemented, sorry...', file=sys.stderr)
            return
            pass




    def do_attach(self, bucket):
        'attach <bucket_name>\n' \
        '    Attaches the bucket to be used by further commands. Think of\n'\
        '    change directory...'
        if not self.profile:
            print('error: you need to connect, first...', file=sys.stderr)
            return

        try:
            self.bucket = self.s3.Bucket(bucket)
            # switch this of, as it causes an error (404) when attaching a
            # bucket which is not owned by this user
            # self.bucket.load()
        except Exception as e:
            _print('error: attach of bucket "{}" failed\nhint: {}'
                  .format(bucket, e), file=sys.stderr)
        else:
            self.bucketname = bucket

    def do_lsb(self, arg):
        'lsb [-a]\n' \
        '    List the buckets available through the connected endpoint.\n'\
        '    -a shows ACLs as well.'
        if not self.profile:
            print('error: you need to connect, first...', file=sys.stderr)
            return

        try:
            para = paramcheck(arg, flags='a')
        except Exception as e:
            _print('error: parameters invalid...\nhint: {}'.format(e),
                   file=sys.stderr)
            return

        _acls = True if 'a' in para.flags else False

        # test to get access to the region in AWS
        if self.mode == 'aws':
            try:
                cl = botocore.session.Session().create_client('s3',
                                                              aws_access_key_id=
                                                              self.profiles[
                                                                  self.profile][
                                                                  'aws_access_key_id'],
                                                              aws_secret_access_key=
                                                              self.profiles[
                                                                  self.profile][
                                                                  'aws_secret_access_key'],
                                                              region_name=
                                                              self.profiles[
                                                                  self.profile][
                                                                  'region'])
            except Exception as e:
                _print('error: lsb failed...\nhint: {}'.format(e),
                       file=sys.stderr)
                return

        print('{:>17} {:30} {:14} {} {:38}'
              .format('created',
                      'owner|ID' + (' '*15+'grantee' if _acls else ''),
                      'region', 'V',
                      'bucket name' + (' '*25+'ACLs' if _acls else ''),
                      ))
        print('-'*17, '-'*30, '-'*14, '-', '-'*38)

        try:
            for b in self.s3.buckets.all():
                location = 'compatible' if self.mode != 'aws' else \
                    cl.get_bucket_location(Bucket=b.name)['LocationConstraint']

                # get versioning status
                try:
                    bv = b.Versioning()
                    version_state = 'X' if bv.status == 'Enabled' else ' '
                except Exception:
                    version_state = '?'

                try:
                    acl = b.Acl()
                    acl.load()
                    try:
                        owner = acl.owner['DisplayName']
                    except Exception:
                        owner = acl.owner['ID']
                    print('{:17} {:30} {:14} {} {:38}'
                          .format(b.creation_date.strftime('%y/%m/%d-%H:%M:%S'),
                                  _(owner, 30), location,
                                  version_state,
                                  b.name))
                except botocore.exceptions.ClientError as e:
                    acl = None
                    if self.mode == 'aws':
                        print('{:17} {:30} {:14} {} {:38}'
                              .format(b.creation_date.strftime('%y/%m/%d-%H:%M:%S'),
                                      '**not avail. through region**', location,
                                      version_state, b.name))
                    else:
                        print('{:17} {:30} {:14} {} {:38}'
                              .format(b.creation_date.strftime('%y/%m/%d-%H:%M:%S'),
                                      '**GetBucketAcl not impl.**', location,
                                      version_state, b.name))


                if _acls and acl:
                    for g in acl.grants:
                        if g['Grantee']['Type'] == 'CanonicalUser':
                            print(' ' * 17, '{:>30} {:14} {:>40}'
                                  .format(
                                g['Grantee']['DisplayName'] if 'DisplayName' in
                                                               g['Grantee'].keys() else
                                                               _(g['Grantee']['ID'],30),
                                '', g['Permission']))
                        elif g['Grantee']['Type'] == 'Group':
                            print(' '*17, '{:>30} {:14} {:>40}'
                                  .format(g['Grantee']['URI'].split('/')[-1],
                                          '', g['Permission']))
                        else:
                            print(' '*17, 'unknown')

        except Exception as e:
            _print('error: listing buckets failed...\nhint: {}'.format(e),
                   file=sys.stderr)

    def do_ls(self, arg):
        'ls [-amv] [prefix]\n' \
        '    List the objects within the active bucket.\n'\
        '    -a print object acls,\n'\
        '    -m prints metadata pairs per object, if available,\n'\
        '    -v prints each object\'s versions, if there are some.\n'\
        '    If prefix is given, list objects starting with the prefifx, only.'
        if not self.profile:
            print('error: you need to connect, first...', file=sys.stderr)
            return

        if not self.bucket:
            print('error: you need to attach to a bucket, first...',
                  file=sys.stderr)
            return

        try:
            para = paramcheck(arg, flags='amv')
        except Exception as e:
            _print('error: parameters invalid...\nhint: {}'.format(e), file=sys.stderr)
            return

        if len(para.args) > 1:
            print('error: maximal one parameter is required', file=sys.stderr)
            return
        else:
            prefix = '' if not len(para.args) else para.args[0]

        _acl = True if 'a' in para.flags else False
        _meta = True if 'm' in para.flags else False
        _versions = True if 'v' in para.flags else False

        try:
            if _versions:
                if _acl:
                    print('{:>17} {:>15} {:32} {} {}'
                      .format('last modified', 'size',
                              'version_id               grantee', 'A',
                              'object name/metadata    ACLs' if _meta else 'object name             ACLs'))
                else:
                    print('{:>17} {:>15} {:32} {} {}'
                          .format('last modified', 'size', 'version_id', 'A',
                                  'object name/metadata' if _meta else 'object name'))
                print('-'*17, '-'*15, '-'*32, '-', '-'*28)
            else:
                if _acl:
                    print('{:>17} {:>15} {:32} {}'
                          .format('last modified', 'size', 'version_id',
                                  'object name/metadata    ACLs' if _meta else 'object name             ACLs'))
                else:
                    print('{:>17} {:>15} {:32} {}'
                          .format('last modified', 'size', 'version_id',
                                  'object name/metadata' if _meta else 'object name'))
                print('-'*17, '-'*15, '-'*32, '-'*28)

            query = self.bucket.objects.filter if not _versions else self.bucket.object_versions.filter

            for obj in query(Prefix=prefix):
                if not _versions:
                    obj = obj.Object()
                    obj.load()
                    print('{:>17} {:>15,} {:32} {}'
                          .format(obj.last_modified.strftime('%y/%m/%d-%H:%M:%S'),
                                  obj.size or 0 if _versions else obj.content_length,
                                  _(obj.version_id,32) or 'null',
                                  obj.key))
                else:
                    print('{:>17} {:>15,} {:32} {} {}'
                          .format(obj.last_modified.strftime('%y/%m/%d-%H:%M:%S'),
                                  obj.size or 0 if _versions else obj.content_length,
                                  _(obj.version_id,32),
                                  ' ' if not _versions else 'X' if obj.is_latest else ' ',
                                  obj.key))
                if _acl:
                    if _versions:
                        acl = obj.Object().Acl()
                    else:
                        acl = obj.Acl()
                    acl.load()
                    for g in acl.grants:
                        if g['Grantee']['Type'] == 'CanonicalUser':
                            print(' ' * 17, '{:15} {:>32} {}{:>28}'
                                  .format('',
                                g['Grantee']['DisplayName'] if 'DisplayName' in
                                                               g['Grantee'].keys() else
                                                               _(g['Grantee']['ID'],30),
                                '  ' if _versions else '',
                                g['Permission']))
                        elif g['Grantee']['Type'] == 'Group':
                            print(' '*17, '{:15} {:>32} {}{:28}'
                                  .format('', g['Grantee']['URI'].split('/')[-1],
                                          '  ' if _versions else '',
                                          g['Permission']))
                        else:
                            print(' '*17, 'unknown')
                try:
                    if _meta:
                        if _versions:
                            resp = obj.head()
                            if resp['Metadata']:
                                print('{:>68} {}'.format('', resp['Metadata']))
                        else:
                            if obj.metadata:
                                print('{:>66} {}'.format('', obj.metadata))
                except Exception as f:
                    print(f, file=sys.stderr)
                    pass

        except Exception as e:
            _print('error: ls failed\nhint: {}'.format(e), file=sys.stderr)

    def do_copy(self, arg):
        'copy [-v version_id] source target ["metakey:metavalue"]*\n' \
        '    Request HCP to perform a server-side copy of (a defined\n'\
        '    version_id of) source to target object, replacing eventually\n'\
        '    existing source metadata pairs with the named metadata pairs,\n'\
        '    if given; else copy the existing metadata pairs, along with the\n'\
        '    object.\n\n'\
        '    You can use the copy command to copy the source object to\n'\
        '    itself to create a new version of the source object with\n'\
        '    changed metadata pairs.'
        if not self.profile:
            print('error: you need to connect, first...', file=sys.stderr)
            return

        if not self.bucket:
            print('error: you need to attach to a bucket, first...',
                  file=sys.stderr)
            return

        try:
            para = paramcheck(arg, flags='v', meta=True)
        except Exception as e:
            _print('error: parameters invalid...\nhint: {}'.format(e),
                   file=sys.stderr)
            return
        else:
            _version = True if 'v' in para.flags else False
            if len(para.args) < (3 if _version else 2):
                print('error: at least {} parameter are required'
                      .format('three' if _version else 'two'), file=sys.stderr)
                return
            else:
                if 'v' in para.flags:
                    _ver = para.args[0]
                    _src = para.args[1]
                    _tgt = para.args[2]
                else:
                    _ver = None
                    _src = para.args[0]
                    _tgt = para.args[1]

        try:
            obj = self.s3.Object(self.bucketname, _tgt)
            if para.metadict:
                response = obj.copy_from(CopySource={'Bucket': self.bucketname,
                                                     'Key': _src,
                                                     'VersionId': _ver},
                                         Metadata=para.metadict,
                                         MetadataDirective='REPLACE')
            else:
                response = obj.copy_from(CopySource={'Bucket': self.bucketname,
                                                     'Key': _src,
                                                     'VersionId': _ver},
                                         MetadataDirective='COPY')
            if 'CopySourceVersionId' in response:
                print('CopySourceVersionId: {}'
                      .format(response['CopySourceVersionId']))
            if 'VersionId' in response:
                print('          VersionId: {}'.format(response['VersionId']))
        except Exception as f:
            _print('error: copy_from failed...\nhint: {}'.format(f),
                   file=sys.stderr)

    def do_put(self, arg):
        'put [-m] localfile object ["metakey:metavalue"]*\n' \
        '    Put (store) localfile as object into the attached bucket,\n'\
        '    adding metadata pairs, if specified.\n'\
        '    -m will try to do a multi-part put.'
        if not self.profile:
            print('error: you need to connect, first...', file=sys.stderr)
            return

        if not self.bucket:
            print('error: you need to attach to a bucket, first...',
                  file=sys.stderr)
            return

        try:
            para = paramcheck(arg, flags='m', meta=True)
        except Exception as e:
            _print('error: parameters invalid...\nhint: {}'.format(e),
                   file=sys.stderr)
            return
        else:
            if len(para.args) != 2:
                print('error: at least 2 parameters are required...',
                      file=sys.stderr)
                return

            _src = para.args[0]
            _tgt = para.args[1]
            _metadict = para.metadict

            if 'm' in para.flags:                           # multipart upload
                try:
                    if self.mode == 'aws':
                        cl = botocore.session.Session().create_client('s3',
                                                                      aws_access_key_id=
                                                                      self.profiles[
                                                                          self.profile][
                                                                          'aws_access_key_id'],
                                                                      aws_secret_access_key=
                                                                      self.profiles[
                                                                          self.profile][
                                                                          'aws_secret_access_key'],
                                                                      region_name=
                                                                      self.profiles[
                                                                          self.profile][
                                                                          'region'])
                    else:
                        endpoint = ('https://' if self.profiles[self.profile][
                            'https'] else 'http://') + \
                                   self.profiles[self.profile]['endpoint']
                        cl = botocore.session.Session().create_client('s3',
                                                                      aws_access_key_id=
                                                                      self.profiles[
                                                                          self.profile][
                                                                          'aws_access_key_id'],
                                                                      aws_secret_access_key=
                                                                      self.profiles[
                                                                          self.profile][
                                                                          'aws_secret_access_key'],
                                                                      endpoint_url=endpoint)
                        cl.meta.events.unregister('before-sign.s3',fix_s3_host)
                except Exception as e:
                    _print('error: put -m failed...\nhint: {}'.format(e),
                           file=sys.stderr)
                    return
                try:
                    transfer = S3Transfer(cl)
                    transfer.upload_file(_src, self.bucketname, _tgt)
                except Exception as e:
                    _print('error: transfer failed...\nhint: {}'.format(e),
                           file=sys.stderr)
                    return
            else:                                           # standard upload
                try:
                    with open(_src, 'br') as inhdl:
                        obj = self.s3.Object(self.bucketname, _tgt)
                        response = obj.put(Body=inhdl.read(), Metadata=_metadict)
                        if 'VersionId' in response:
                            print('VersionId = {}'.format(response['VersionId']))
                except Exception as f:
                    _print('error: put failed...\nhint: {}'.format(f),
                           file=sys.stderr)

    def do_get(self, arg):
        'get [-v version_id] object [localfile]\n' \
        '    Get (read) the latest (or a named version_id) of object and\n'\
        '    print it. If localfile is specified, write the object to it.'
        if not self.profile:
            print('error: you need to connect, first...', file=sys.stderr)
            return

        if not self.bucket:
            print('error: you need to attach to a bucket, first...',
                  file=sys.stderr)
            return

        try:
            para = paramcheck(arg, flags='v', meta=False)
        except Exception as e:
            _print('error: parameters invalid...\nhint: {}'.format(e),
                   file=sys.stderr)
            return
        else:
            _version = True if 'v' in para.flags else False
            if len(para.args) < (2 if _version else 1):
                print('error: at least {} parameter are required'
                      .format('two' if _version else 'one'), file=sys.stderr)
                return
            else:
                if 'v' in para.flags:
                    _ver = para.args[0]
                    _src = para.args[1]
                    _tgt = para.args[2] if len(para.args) > 2 else None
                else:
                    _ver = None
                    _src = para.args[0]
                    _tgt = para.args[1] if len(para.args) > 1 else None

        try:
            obj = self.s3.Object(self.bucketname, _src) if not _ver \
                else self.s3.ObjectVersion(self.bucketname, _src, _ver)
            response = obj.get()
            if _tgt:
                with open(_tgt, 'wb') as outhdl:
                    outhdl.write(response['Body'].read())
            else:
                print('-'*79)
                print(response['Body'].read().decode())
                print('-'*79)
        except Exception as e:
            _print('error: get failed...\nhint: {}'.format(e), file=sys.stderr)

    def do_rm(self, arg):
        'rm object [version_id]\n' \
        '    Delete object (or it\'s version_id) from the attached bucket.'
        if not self.profile:
            print('error: you need to connect, first...', file=sys.stderr)
            return

        if not arg:
            print('error: at least one parameter is required...',
                  file=sys.stderr)
            return

        if len(arg.split()) == 1:
            object = arg
            version = None
            try:
                obj = self.s3.Object(self.bucketname, object)
                response = obj.delete()
                print('deleted "{}", version_id {}'
                      .format(object,
                              response['VersionId'] if 'VersionId' in response else 'null'))
            except Exception as e:
                _print('error: delete failed...\nhint: {}'.format(e),
                       file=sys.stderr)
        else:
            try:
                object, version = arg.split()
            except Exception as e:
                print('error: one or two parameters are required...\nhint: {}'
                      .format(e), file=sys.stderr)
            else:
                try:
                    obj = self.s3.Object(self.bucketname, object)
                    response = obj.delete(VersionId=version)
                    print('deleted "{}", version_id {}'
                          .format(object,
                                  response['VersionId'] if 'VersionId' in response else 'null'))
                except Exception as e:
                    _print('error: delete failed...\nhint: {}'.format(e),
                           file=sys.stderr)

    def do_status(self, arg):
        'status\n' \
        '    Show the session status (the connected endpoint and the\n'\
        '    attached bucket)'
        print('{:>20} {}'.format('config item', 'value'))
        print('-'*20, '-'*58)

        print('{:>20} {}'.format('mode', self.mode or 'not set'))
        print('{:>20} {}'.format('profile', self.profile or 'not set'))
        if self.profile:
            print('{:>20} {}'.format('profile comment',
                                     '  '+self.profiles[self.profile]['comment']))
            print('{:>20} {}'.format('session mode',
                                     '  '+'secure (https)' if  self.profiles[self.profile]['https'] else 'insecure (http)'))
            print('{:>20} {}'.format('endpoint',
                                     '  '+(self.profiles[self.profile]['endpoint'] or 'Amazon S3')))
            print('{:>20} {}'.format('region',
                                     '  '+(self.profiles[self.profile]['region'] or 'n/a')))
        print('{:>20} {}'.format('attached bucket', self.bucketname or 'not set'))
        print()


    def do_time(self, arg):
        'time <command args>\n'\
        '    measure the time <command> takes to complete'
        p = arg.split(maxsplit=1)
        cmd, params = p if len(p) > 1 else (arg, '')

        st = time.time()
        result = eval('self.do_{}("{}")'.format(cmd, params))
        print('[time: {}]'.format(calcTime(time.time() - st)), file=sys.stderr,
              flush=True)
        return result

    # def do_test(self, arg):
    #     'test [-flags] [args]*\n'\
    #     '    test the argument processor'
    #     # try:
    #     #     para = paramcheck(arg, flags='abc', meta=True)
    #     # except Exception as e:
    #     #     _print('error: parameters invalid...\nhint: {}'.format(e))
    #     #     return
    #     # print('flags: {}'.format(para.flags))
    #     # print(' args: {}'.format(para.args))
    #     # print(' meta: {}'.format(para.metadict))
    #
    #     try:
    #         para = paramcheck(arg, flags='abcdef', meta=False)
    #     except Exception as e:
    #         _print('error: arg parsing failed...\nhint: {}'.format(e),
    #                file=sys.stderr)
    #     else:
    #         print(para)

    def do_url(self, arg):
        'url [-e minutes] object\n' \
        '    Generate a pre-signed URL to access object\n' \
        '    -v set the expiration time for the URL to minutes\n' \
        '       (defaults to 60 minutes)'
        if not self.profile:
            print('error: you need to connect, first...', file=sys.stderr)
            return

        if not self.bucket:
            print('error: you need to attach to a bucket, first...',
                  file=sys.stderr)
            return

        try:
            para = paramcheck(arg, flags='e')
        except Exception as e:
            _print('error: parameters invalid...\nhint: {}'.format(e),
                   file=sys.stderr)
            return
        else:
            if 'e' in para.flags and len(para.args) != 2:
                print('error: exactly 2 parameters are required...',
                      file=sys.stderr)
                return
            elif 'e' not in para.flags and len(para.args) != 1:
                print('error: exactly 1 parameter is required...',
                      file=sys.stderr)
                return

            try:
                _expiresin = int(para.args[0]) if 'e' in para.flags else 3600
            except Exception as e:
                _print('error: expiration time is invalid...\nhint: {}'.format(e),
                       file=sys.stderr)
                return
            _key = para.args[1] if 'e' in para.flags else para.args[0]

            try:
                if self.mode == 'aws':
                    cl = botocore.session.Session().create_client('s3',
                                                                  aws_access_key_id=
                                                                  self.profiles[
                                                                      self.profile][
                                                                      'aws_access_key_id'],
                                                                  aws_secret_access_key=
                                                                  self.profiles[
                                                                      self.profile][
                                                                      'aws_secret_access_key'],
                                                                  region_name=
                                                                  self.profiles[
                                                                      self.profile][
                                                                      'region'])
                else:
                    endpoint = ('https://' if self.profiles[self.profile][
                        'https'] else 'http://') + \
                               self.profiles[self.profile]['endpoint']
                    cl = botocore.session.Session().create_client('s3',
                                                                  aws_access_key_id=
                                                                  self.profiles[
                                                                      self.profile][
                                                                      'aws_access_key_id'],
                                                                  aws_secret_access_key=
                                                                  self.profiles[
                                                                      self.profile][
                                                                      'aws_secret_access_key'],
                                                                  endpoint_url=endpoint)
                    #cl.meta.events.unregister('before-sign.s3', fix_s3_host)
            except Exception as e:
                _print('error: generation of pre-signed URL failed...\nhint: {}'
                       .format(e), file=sys.stderr)
                return
            try:
                url = cl.generate_presigned_url('get_object',
                                                Params={'Bucket': self.bucketname,
                                                        'Key': _key},
                                                ExpiresIn=_expiresin*60,
                                                HttpMethod=None)
                if self.mode != 'aws':  # fix the URL if not AWS...
                    url = url.replace('s3.amazonaws.com',
                                      self.profiles[self.profile]['endpoint'],
                                      1)
                print(url)
            except Exception as e:
                _print('error: generation of pre-signed URL failed...\nhint: {}'
                       .format(e), file=sys.stderr)
                return

    def do_quit(self, arg):
        'Exit hs3sh gracefully.'
        print('Ending gracefully...')
        return True

    def precmd(self, arg):
        '''
        Strip off redirections from a command and map print accordingly.

        :param arg:     the paramaters given with the command
        :return:        the command w/o the redirection or an empty string
                        if parsing failed
        '''

        # first let's see if we need to look for pipe/outfile
        redir_type = redir_arg = None
        try:
            if arg.find(EXTENDFILE) != -1:
                redir_type = EXTENDFILE
                arg, redir_arg = arg.split(EXTENDFILE)
                redir_arg = redir_arg.strip()
            elif arg.find(OUTFILE) != -1:
                redir_type = OUTFILE
                arg, redir_arg = arg.split(OUTFILE)
                redir_arg = redir_arg.strip()
            elif arg.find(PIPE) != -1:
                redir_type = PIPE
                arg, redir_arg = arg.split(PIPE)
                redir_arg = redir_arg.strip()
        except Exception as e:
            _print('parsing redirction failed...\nhint: {}'.format(e))
            return ''

        if redir_type and arg.split()[0] in no_redir_cmds:
            print('error: no redirection for command "{}"...'
                  .format(arg.split()[0]))
            return ''


        if redir_type and not redir_arg:
            print('error: redirection without arguments...')
            return ''

        if redir_type == PIPE:
            try:
                sys.stdout = os.popen(redir_arg, 'w')
            except Exception as e:
                _print('redirection error...\nhint: {}'.format(e))
                return ''
        elif redir_type == OUTFILE:
            try:
                sys.stdout = open(redir_arg, 'w')
            except Exception as e:
                _print('redirection error...\nhint: {}'.format(e))
                return ''
        elif redir_type == EXTENDFILE:
            try:
                sys.stdout = open(redir_arg, 'a')
            except Exception as e:
                _print('redirection error...\nhint: {}'.format(e))
                return ''

        return arg

    def postcmd(self, stop, line):
        'Reset sys.stdout.'
        print('', end='', flush=True)
        if sys.stdout != sys.__stdout__:
            sys.stdout.close()
            sys.stdout = sys.__stdout__
        return stop

def calcTime(t):
    '''
    Calculate a string 'H:M:S.ms' out of a given no. of seconds.
    '''
    msec    = int("{0:.2f}".format(t%1)[2:])
    minute  = int(t//60)
    sec     = int(t%60)
    hour    = int(minute//60)
    minute  = int(minute%60)
    return("{0:02}:{1:02}:{2:02}.{3}".format(hour, minute, sec, msec))

def _print(string, **kwargs):
    '''
    Print a string, first line as it is, following lines wrapped to 78 chars.

    :param string:  the string to print
    '''

    strings = string.split('\n')
    print(strings[0], **kwargs)
    lines = wrap('\n'.join(strings[1:]),
                 initial_indent='    ', subsequent_indent='    ')
    for l in lines:
        print(l, **kwargs)

def _(string, width):
    '''
    Return the string cut to num characters.

    :param string:  the string to work on
    :param num:     the number of characters wanted
    :return:        the cut string
    '''
    string = string or ''
    return string if len(string) <= width else string[:width-3]+'...'


