# w.c.s. - web application for online forms
# Copyright (C) 2005-2020  Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.

import base64
import os

from quixote import get_publisher
from quixote.http_request import Upload

from django.utils.encoding import force_text
from django.utils.module_loading import import_string
from django.utils.six import BytesIO

from .errors import ConnectionError
from .misc import json_loads, file_digest, can_thumbnail
from .storage import atomic_write


class PicklableUpload(Upload):
    def __getstate__(self):
        odict = self.__dict__.copy()
        if 'fp' in odict:
            del odict['fp']
        get_storage_object(getattr(self, 'storage', None)).save(self)
        odict['qfilename'] = getattr(self, 'qfilename', None)
        return odict

    def get_file_pointer(self):
        if 'fp' in self.__dict__ and self.__dict__.get('fp') is not None:
            return self.__dict__.get('fp')
        elif hasattr(self, 'qfilename'):
            basedir = os.path.join(get_publisher().app_dir, 'uploads')
            self.fp = open(os.path.join(basedir, self.qfilename), 'rb')
            return self.fp
        return None

    def __setstate__(self, dict):
        self.__dict__.update(dict)
        if hasattr(self, 'data'):
            # backward compatibility with older w.c.s. version
            self.fp = BytesIO(self.data)
            del self.data

    def get_file(self):
        # quack like UploadedFile
        return self.get_file_pointer()

    def get_filename(self):
        if not hasattr(self, 'qfilename'):
            raise AttributeError('filename')
        basedir = os.path.join(get_publisher().app_dir, 'uploads')
        return os.path.join(basedir, self.qfilename)

    def get_content(self):
        if hasattr(self, 'storage_attrs'):  # alternative storage
            return b''
        if hasattr(self, 'qfilename'):
            filename = os.path.join(get_publisher().app_dir, 'uploads', self.qfilename)
            return open(filename, 'rb').read()
        return None

    def get_base64_content(self):
        content = self.get_content()
        if content:
            return base64.encodestring(content)
        return b''

    def get_json_value(self):
        return get_storage_object(getattr(self, 'storage', None)).get_json_value(self)

    def can_thumbnail(self):
        return get_storage_object(getattr(self, 'storage', None)).can_thumbnail(self)

    def has_redirect_url(self):
        return get_storage_object(getattr(self, 'storage', None)).has_redirect_url(self)

    def get_redirect_url(self, backoffice=False):
        return get_storage_object(getattr(self, 'storage', None)).get_redirect_url(self,
                backoffice=backoffice)


class UploadStorageError(Exception):
    pass


class UploadStorage(object):
    def save_tempfile(self, upload):
        upload.__class__ = PicklableUpload
        dirname = os.path.join(get_publisher().app_dir, 'tempfiles')
        filename = os.path.join(dirname, upload.token)
        fd = open(filename, 'wb')
        upload.get_file_pointer().seek(0)
        fd.write(upload.get_file_pointer().read())
        upload.size = fd.tell()
        fd.close()

    def get_tempfile(self, temp_data):
        value = PicklableUpload(
                temp_data['orig_filename'],
                temp_data['content_type'],
                temp_data['charset'])
        value.storage = temp_data.get('storage')
        dirname = os.path.join(get_publisher().app_dir, 'tempfiles')
        filename = os.path.join(dirname, temp_data['unsigned_token'])
        value.token = temp_data['token']
        value.file_size = os.path.getsize(filename)
        value.fp = open(filename, 'rb')
        return value

    def save(self, upload):
        basedir = os.path.join(get_publisher().app_dir, 'uploads')
        if not os.path.exists(basedir):
            os.mkdir(basedir)
        if getattr(upload, 'qfilename', None):
            filepath = os.path.join(basedir, upload.qfilename)
        else:
            upload.qfilename = file_digest(upload.fp)
            filepath = os.path.join(basedir, upload.qfilename)

        if getattr(upload, 'fp', None) and not upload.fp.closed:
            upload.fp.seek(0)
            atomic_write(filepath, upload.fp, async_op=False)

    def get_json_value(self, upload):
        return {
            'filename': upload.base_filename,
            'content_type': upload.content_type or 'application/octet-stream',
            'content': force_text(base64.b64encode(upload.get_content())),
        }

    def can_thumbnail(self, upload):
        return can_thumbnail(upload.content_type)

    def has_redirect_url(self, upload):
        return False

    def get_redirect_url(self, upload, backoffice=False):
        # should never be called, has_redirect_url is False
        raise AssertionError('no get_redirect_url on UploadStorage object')


class RemoteOpaqueUploadStorage(object):
    def __init__(self, ws, frontoffice_redirect='true', backoffice_redirect='true', **kwargs):
        self.ws = ws
        self.frontoffice_redirect = bool(frontoffice_redirect == 'true')
        self.backoffice_redirect = bool(backoffice_redirect == 'true')

    def save_tempfile(self, upload):
        if getattr(upload, 'storage_attrs', None):
            # upload is already a remote PicklableUpload, it does not
            # have content. We are certainly restoring a draft.
            return

        upload.__class__ = PicklableUpload
        upload.get_file_pointer().seek(0)
        content = upload.get_file_pointer().read()
        base64content = base64.b64encode(content)
        file_size = len(content)

        post_data = {
                'file': {
                    'filename': upload.base_filename,
                    'content_type': upload.content_type or 'application/octet-stream',
                    'content': base64content,
                    }
                }
        try:
            from wcs.wscalls import call_webservice
            response, status, data = call_webservice(self.ws, method='POST', post_data=post_data)
        except ConnectionError as e:
            raise UploadStorageError('remote storage connection error (%r)' % e)
        if status not in (200, 201):
            raise UploadStorageError('remote storage returned status %s' % status)
        try:
            ws_result = json_loads(data)
        except (ValueError, TypeError) as e:
            raise UploadStorageError('remote storage returned invalid JSON')
        if not isinstance(ws_result, dict):
            raise UploadStorageError('remote storage returned non-dict JSON')
        if ws_result.get('err') != 0:
            raise UploadStorageError('remote storage returned err = %s' % ws_result.get('err'))
        ws_result_data = ws_result.get('data', {})
        if not ws_result_data.get('redirect_url'):
            raise UploadStorageError('remote storage returned data.redirect_url= %s' %
                    ws_result_data.get('redirect_url'))

        upload.storage_attrs = ws_result_data
        upload.storage_attrs['file_size'] = file_size

    def get_tempfile(self, temp_data):
        value = PicklableUpload(
                temp_data['orig_filename'],
                temp_data['content_type'],
                temp_data['charset'])
        value.storage = temp_data.get('storage')
        value.storage_attrs = temp_data['storage-attrs']
        value.token = temp_data['token']
        value.fp = None
        value.file_size = value.storage_attrs['file_size']
        return value

    def save(self, upload):
        pass

    def get_json_value(self, upload):
        return {
            'filename': upload.base_filename,
            'content_type': upload.content_type or 'application/octet-stream',
            'content': '',
            'storage': upload.storage,
            'storage_attrs': upload.storage_attrs,
        }

    def can_thumbnail(self, upload):
        return False

    def has_redirect_url(self, upload):
        return True

    def get_redirect_url(self, upload, backoffice=False):
        if backoffice:
            if not self.backoffice_redirect:
                return None
            if 'backoffice_redirect_url' in upload.storage_attrs:
                return upload.storage_attrs['backoffice_redirect_url']
        else:
            if not self.frontoffice_redirect:
                return None
            if 'frontoffice_redirect_url' in upload.storage_attrs:
                return upload.storage_attrs['frontoffice_redirect_url']
        return upload.storage_attrs.get('redirect_url')


def get_storage_object(storage):
    if not storage or storage == 'default':
        return UploadStorage()
    storage_cfg = get_publisher().get_site_storages().get(storage)
    if not storage_cfg:
        raise UploadStorageError('unknown storage %s' % storage)
    try:
        storage_class = import_string(storage_cfg['class'])
    except ImportError:
        raise UploadStorageError('failed to import storage class %s' % storage_class)
    return storage_class(**storage_cfg)
