#
# Copyright (c) 2015 Matthew Bentley
#
#
# 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 hashlib

import duplicity.backend
from duplicity.errors import BackendException, FatalBackendException
from duplicity import log

import json
import urllib2
import base64


class B2Backend(duplicity.backend.Backend):
    """
    Backend for BackBlaze's B2 storage service
    """

    def __init__(self, parsed_url):
        """
        Authorize to B2 api and set up needed variables
        """
        duplicity.backend.Backend.__init__(self, parsed_url)

        # for prettier password prompt only
        self.parsed_url.hostname = 'B2'

        self.account_id = parsed_url.username
        account_key = self.get_password()

        self.url_parts = [
            x for x in parsed_url.path.replace("@", "/").split('/') if x != ''
        ]
        if self.url_parts:
            self.username = self.url_parts.pop(0)
            self.bucket_name = self.url_parts.pop(0)
        else:
            raise BackendException("B2 requires a bucket name")
        self.path = "/".join(self.url_parts)

        self.id_and_key = self.account_id + ":" + account_key
        self._authorize()

        try:
            self.find_or_create_bucket(self.bucket_name)
        except urllib2.HTTPError:
            raise FatalBackendException("Bucket cannot be created")

    def _authorize(self):
        basic_auth_string = 'Basic ' + base64.b64encode(self.id_and_key)
        headers = {'Authorization': basic_auth_string}

        request = urllib2.Request(
            'https://api.backblazeb2.com/b2api/v1/b2_authorize_account',
            headers=headers
        )

        response = urllib2.urlopen(request)
        response_data = json.loads(response.read())
        response.close()

        self.auth_token = response_data['authorizationToken']
        self.api_url = response_data['apiUrl']
        self.download_url = response_data['downloadUrl']

    def _get(self, remote_filename, local_path):
        """
        Download remote_filename to local_path
        """
        log.Log("Getting file %s" % remote_filename, 9)
        remote_filename = self.full_filename(remote_filename)
        url = self.download_url + \
            '/file/' + self.bucket_name + '/' + \
            remote_filename
        resp = self.get_or_post(url, None)

        to_file = open(local_path.name, 'wb')
        to_file.write(resp)
        to_file.close()

    def _put(self, source_path, remote_filename):
        """
        Copy source_path to remote_filename
        """
        log.Log("Putting file to %s" % remote_filename, 9)
        self._delete(remote_filename)
        digest = self.hex_sha1_of_file(source_path)
        content_type = 'application/pgp-encrypted'
        remote_filename = self.full_filename(remote_filename)

        info = self.get_upload_info(self.bucket_id)
        url = info['uploadUrl']

        headers = {
            'Authorization': info['authorizationToken'],
            'X-Bz-File-Name': remote_filename,
            'Content-Type': content_type,
            'X-Bz-Content-Sha1': digest,
            'Content-Length': str(os.path.getsize(source_path.name)),
        }
        data_file = source_path.open()
        self.get_or_post(url, None, headers, data_file=data_file)

    def _list(self):
        """
        List files on remote server
        """
        log.Log("Listing files", 9)
        endpoint = 'b2_list_file_names'
        url = self.formatted_url(endpoint)
        params = {
            'bucketId': self.bucket_id,
            'maxFileCount': 1000,
        }
        try:
            resp = self.get_or_post(url, params)
        except urllib2.HTTPError:
            return []

        files = [x['fileName'].split('/')[-1] for x in resp['files']
                 if os.path.dirname(x['fileName']) == self.path]

        next_file = resp['nextFileName']
        while next_file:
            log.Log("There are still files, getting next list", 9)
            params['startFileName'] = next_file
            try:
                resp = self.get_or_post(url, params)
            except urllib2.HTTPError:
                return files

            files += [x['fileName'].split('/')[-1] for x in resp['files']
                      if os.path.dirname(x['fileName']) == self.path]
            next_file = resp['nextFileName']

        return files

    def _delete(self, filename):
        """
        Delete filename from remote server
        """
        log.Log("Deleting file %s" % filename, 9)
        endpoint = 'b2_delete_file_version'
        url = self.formatted_url(endpoint)
        fileid = self.get_file_id(filename)
        if fileid is None:
            return
        filename = self.full_filename(filename)
        params = {'fileName': filename, 'fileId': fileid}
        try:
            self.get_or_post(url, params)
        except urllib2.HTTPError as e:
            if e.code == 400:
                return
            else:
                raise e

    def _query(self, filename):
        """
        Get size info of filename
        """
        log.Log("Querying file %s" % filename, 9)
        info = self.get_file_info(filename)
        if not info:
            return {'size': -1}

        return {'size': info['size']}

    def _error_code(self, operation, e):
        if isinstance(e, urllib2.HTTPError):
            if e.code == 400:
                return log.ErrorCode.bad_request
            if e.code == 500:
                return log.ErrorCode.backend_error
            if e.code == 403:
                return log.ErrorCode.backend_permission_denied

    def find_or_create_bucket(self, bucket_name):
        """
        Find a bucket with name bucket_name and save its id.
        If it doesn't exist, create it
        """
        endpoint = 'b2_list_buckets'
        url = self.formatted_url(endpoint)

        params = {'accountId': self.account_id}
        resp = self.get_or_post(url, params)

        bucket_names = [x['bucketName'] for x in resp['buckets']]

        if bucket_name not in bucket_names:
            self.create_bucket(bucket_name)
        else:
            self.bucket_id = {
                x[
                    'bucketName'
                ]: x['bucketId'] for x in resp['buckets']
            }[bucket_name]

    def create_bucket(self, bucket_name):
        """
        Create a bucket with name bucket_name and save its id
        """
        endpoint = 'b2_create_bucket'
        url = self.formatted_url(endpoint)
        params = {
            'accountId': self.account_id,
            'bucketName': bucket_name,
            'bucketType': 'allPrivate'
        }
        resp = self.get_or_post(url, params)

        self.bucket_id = resp['bucketId']

    def formatted_url(self, endpoint):
        """
        Return the full api endpoint from just the last part
        """
        return '%s/b2api/v1/%s' % (self.api_url, endpoint)

    def get_upload_info(self, bucket_id):
        """
        Get an upload url for a bucket
        """
        endpoint = 'b2_get_upload_url'
        url = self.formatted_url(endpoint)
        return self.get_or_post(url, {'bucketId': bucket_id})

    def get_or_post(self, url, data, headers=None, data_file=None):
        """
        Sends the request, either get or post.
        If data and data_file are None, send a get request.
        data_file takes precedence over data.
        If headers are not supplied, just send with an auth key
        """
        if headers is None:
            if self.auth_token is None:
                self._authorize()
            headers = {'Authorization': self.auth_token}
        if data_file is not None:
            data = data_file
        else:
            data = json.dumps(data) if data else None

        encoded_headers = dict(
            (k, urllib2.quote(v.encode('utf-8')))
            for (k, v) in headers.iteritems()
        )

        try:
            with OpenUrl(url, data, encoded_headers) as resp:
                out = resp.read()
            try:
                return json.loads(out)
            except ValueError:
                return out
        except urllib2.HTTPError as e:
            if e.code == 401:
                self.auth_token = None
                log.Warn("Authtoken expired, will reauthenticate with next attempt")
            raise e

    def get_file_info(self, filename):
        """
        Get a file info from filename
        """
        endpoint = 'b2_list_file_names'
        url = self.formatted_url(endpoint)
        filename = self.full_filename(filename)
        params = {
            'bucketId': self.bucket_id,
            'maxFileCount': 1,
            'startFileName': filename,
        }
        resp = self.get_or_post(url, params)

        try:
            return resp['files'][0]
        except IndexError:
            return None
        except TypeError:
            return None

    def get_file_id(self, filename):
        """
        Get a file id form filename
        """
        try:
            return self.get_file_info(filename)['fileId']
        except IndexError:
            return None
        except TypeError:
            return None

    def full_filename(self, filename):
        if self.path:
            return self.path + '/' + filename
        else:
            return filename

    @staticmethod
    def hex_sha1_of_file(path):
        """
        Calculate the sha1 of a file to upload
        """
        f = path.open()
        block_size = 1024 * 1024
        digest = hashlib.sha1()
        while True:
            data = f.read(block_size)
            if len(data) == 0:
                break
            digest.update(data)
        f.close()
        return digest.hexdigest()


class OpenUrl(object):
    """
    Context manager that handles an open urllib2.Request, and provides
    the file-like object that is the response.
    """

    def __init__(self, url, data, headers):
        log.Log("Getting %s" % url, 9)
        self.url = url
        self.data = data
        self.headers = headers
        self.file = None

    def __enter__(self):
        request = urllib2.Request(self.url, self.data, self.headers)
        self.file = urllib2.urlopen(request)
        log.Log("Request of %s returned with status %s" %
                (self.url, self.file.code), 9)
        return self.file

    def __exit__(self, exception_type, exception, traceback):
        if self.file is not None:
            self.file.close()


duplicity.backend.register_backend("b2", B2Backend)
