# -*- coding: utf-8 -*-
#
# This file is part of Gajim.
#
# Gajim 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; version 3 only.
#
# Gajim 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 Gajim.  If not, see <http://www.gnu.org/licenses/>.

import os
import sys
import threading
import ssl
import urllib
from urllib.request import Request, urlopen
from urllib.parse import urlparse, quote
import io
import mimetypes
import logging

import nbxmpp
from nbxmpp import NS_HTTPUPLOAD
from gi.repository import GLib

from gajim.common import app
from gajim.common import ged
from gajim.common.connection_handlers_events import InformationEvent
from gajim.common.connection_handlers_events import HTTPUploadProgressEvent
from gajim.common.connection_handlers_events import MessageOutgoingEvent
from gajim.common.connection_handlers_events import GcMessageOutgoingEvent

if sys.platform in ('win32', 'darwin'):
    import certifi

log = logging.getLogger('gajim.c.httpupload')

NS_HTTPUPLOAD_0 = NS_HTTPUPLOAD + ':0'

class ConnectionHTTPUpload:
    """
    Implement HTTP File Upload
    (XEP-0363, https://xmpp.org/extensions/xep-0363.html)
    """
    def __init__(self):
        self.httpupload = False
        self.encrypted_upload = False
        self.component = None
        self.httpupload_namespace = None
        self._allowed_headers = ['Authorization', 'Cookie', 'Expires']
        self.max_file_size = None  # maximum file size in bytes

        app.ged.register_event_handler('agent-info-received',
                                       ged.GUI1,
                                       self.handle_agent_info_received)
        app.ged.register_event_handler('stanza-message-outgoing',
                                       ged.OUT_PREGUI,
                                       self.handle_outgoing_stanza)
        app.ged.register_event_handler('gc-stanza-message-outgoing',
                                       ged.OUT_PREGUI,
                                       self.handle_outgoing_stanza)

        self.messages = []

    def cleanup(self):
        app.ged.remove_event_handler('agent-info-received',
                                     ged.GUI1,
                                     self.handle_agent_info_received)
        app.ged.remove_event_handler('stanza-message-outgoing',
                                     ged.OUT_PREGUI,
                                     self.handle_outgoing_stanza)
        app.ged.remove_event_handler('gc-stanza-message-outgoing',
                                     ged.OUT_PREGUI,
                                     self.handle_outgoing_stanza)

    def handle_agent_info_received(self, event):
        account = event.conn.name
        if account != self.name:
            return

        if not app.jid_is_transport(event.jid):
            return

        if not event.id_.startswith('Gajim_'):
            return

        if NS_HTTPUPLOAD_0 in event.features:
            self.httpupload_namespace = NS_HTTPUPLOAD_0
        elif NS_HTTPUPLOAD in event.features:
            self.httpupload_namespace = NS_HTTPUPLOAD
        else:
            return

        self.component = event.jid

        for form in event.data:
            form_dict = form.asDict()
            if form_dict.get('FORM_TYPE', None) != self.httpupload_namespace:
                continue
            size = form_dict.get('max-file-size', None)
            if size is not None:
                self.max_file_size = int(size)
                break

        if self.max_file_size is None:
            log.warning('%s does not provide maximum file size', account)
        else:
            log.info('%s has a maximum file size of: %s MiB',
                     account, self.max_file_size/(1024*1024))

        self.httpupload = True
        for ctrl in app.interface.msg_win_mgr.get_controls(acct=self.name):
            ctrl.update_actions()

    def handle_outgoing_stanza(self, event):
        if event.conn.name != self.name:
            return
        message = event.msg_iq.getTagData('body')
        if message and message in self.messages:
            self.messages.remove(message)
            # Add oob information before sending message to recipient,
            #  to distinguish HTTP File Upload Link from pasted URL
            oob = event.msg_iq.addChild('x', namespace=nbxmpp.NS_X_OOB)
            oob.addChild('url').setData(message)
            if 'gajim' in event.additional_data:
                event.additional_data['gajim']['oob_url'] = message
            else:
                event.additional_data['gajim'] = {'oob_url': message}

    def check_file_before_transfer(self, path, encryption, contact, session,
                                   groupchat=False):
        if not path or not os.path.exists(path):
            return

        invalid_file = False
        stat = os.stat(path)

        if os.path.isfile(path):
            if stat[6] == 0:
                invalid_file = True
                msg = _('File is empty')
        else:
            invalid_file = True
            msg = _('File does not exist')

        if self.max_file_size is not None and \
                stat.st_size > self.max_file_size:
            invalid_file = True
            size = GLib.format_size_full(self.max_file_size,
                                         GLib.FormatSizeFlags.IEC_UNITS)
            msg = _('File is too large, '
                    'maximum allowed file size is: %s') % size

        if invalid_file:
            self.raise_information_event('open-file-error2', msg)
            return

        mime = mimetypes.MimeTypes().guess_type(path)[0]
        if not mime:
            mime = 'application/octet-stream'  # fallback mime type
        log.info("Detected MIME type of file: %s", mime)

        try:
            file = File(path, contact, mime=mime, encryption=encryption,
                        update_progress=self.raise_progress_event,
                        session=session, groupchat=groupchat)
            app.interface.show_httpupload_progress(file)
        except Exception as error:
            log.exception('Error while loading file')
            self.raise_information_event('open-file-error2', str(error))
            return

        if encryption is not None:
            app.interface.encrypt_file(file, self.request_slot)
        else:
            self.request_slot(file)

    @staticmethod
    def raise_progress_event(status, file, seen=None, total=None):
        app.nec.push_incoming_event(HTTPUploadProgressEvent(
            None, status=status, file=file, seen=seen, total=total))

    @staticmethod
    def raise_information_event(dialog_name, args=None):
        app.nec.push_incoming_event(InformationEvent(
            None, dialog_name=dialog_name, args=args))

    def request_slot(self, file):
        GLib.idle_add(self.raise_progress_event, 'request', file)
        iq = self._build_request(file)
        log.info("Sending request for slot")
        app.connections[self.name].connection.SendAndCallForResponse(
            iq, self.received_slot, {'file': file})
        
    def _build_request(self, file):
        iq = nbxmpp.Iq(typ='get', to=self.component)
        id_ = app.get_an_id()
        iq.setID(id_)
        if self.httpupload_namespace == NS_HTTPUPLOAD:
            # experimental namespace
            request = iq.setTag(name="request",
                                namespace=self.httpupload_namespace)
            request.addChild('filename', payload=os.path.basename(file.path))
            request.addChild('size', payload=file.size)
            request.addChild('content-type', payload=file.mime)
        else:
            attr = {'filename': os.path.basename(file.path),
                    'size': file.size,
                    'content-type': file.mime}
            iq.setTag(name="request",
                      namespace=self.httpupload_namespace,
                      attrs=attr)
        return iq

    @staticmethod
    def get_slot_error_message(stanza):
        tmp = stanza.getTag('error').getTag('file-too-large')

        if tmp is not None:
            max_file_size = int(tmp.getTag('max-file-size').getData())
            return _('File is too large, maximum allowed file size is: %s') % \
                GLib.format_size_full(max_file_size,
                                      GLib.FormatSizeFlags.IEC_UNITS)

        return stanza.getErrorMsg()

    def received_slot(self, conn, stanza, file):
        log.info("Received slot")
        if stanza.getType() == 'error':
            self.raise_progress_event('close', file)
            self.raise_information_event('request-upload-slot-error',
                                         self.get_slot_error_message(stanza))
            log.error(stanza)
            return

        try:
            if self.httpupload_namespace == NS_HTTPUPLOAD:
                file.put = stanza.getTag('slot').getTag('put').getData()
                file.get = stanza.getTag('slot').getTag('get').getData()
            else:
                slot = stanza.getTag('slot')
                file.put = slot.getTagAttr('put', 'url')
                file.get = slot.getTagAttr('get', 'url')
                for header in slot.getTag('put').getTags('header'):
                    name = header.getAttr('name')
                    if name not in self._allowed_headers:
                        raise ValueError('Not allowed header')
                    data = header.getData()
                    if '\n' in data:
                        raise ValueError('Newline in header data')
                    file.headers[name] = data
        except Exception:
            log.error("Got invalid stanza: %s", stanza)
            log.exception('Error')
            self.raise_progress_event('close', file)
            self.raise_information_event('request-upload-slot-error2')
            return

        if (urlparse(file.put).scheme != 'https' or
                urlparse(file.get).scheme != 'https'):
            self.raise_progress_event('close', file)
            self.raise_information_event('unsecure-error')
            return

        try:
            file.stream = StreamFileWithProgress(file)
        except Exception:
            log.exception('Error')
            self.raise_progress_event('close', file)
            self.raise_information_event('open-file-error')
            return

        log.info('Uploading file to %s', file.put)
        log.info('Please download from %s', file.get)

        thread = threading.Thread(target=self.upload_file, args=(file,))
        thread.daemon = True
        thread.start()

    def upload_file(self, file):
        GLib.idle_add(self.raise_progress_event, 'upload', file)
        try:
            file.headers['User-Agent'] = 'Gajim %s' % app.version
            file.headers['Content-Type'] = file.mime
            file.headers['Content-Length'] = file.size

            request = Request(
                file.put, data=file.stream, headers=file.headers, method='PUT')
            log.info("Opening Urllib upload request...")

            if not app.config.get_per('accounts', self.name, 'httpupload_verify'):
                context = ssl.create_default_context()
                context.check_hostname = False
                context.verify_mode = ssl.CERT_NONE
                log.warning('CERT Verification disabled')
                transfer = urlopen(request, timeout=30, context=context)
            else:
                if sys.platform in ('win32', 'darwin'):
                    transfer = urlopen(
                        request, cafile=certifi.where(), timeout=30)
                else:
                    transfer = urlopen(request, timeout=30)
            file.stream.close()
            log.info('Urllib upload request done, response code: %s',
                     transfer.getcode())
            GLib.idle_add(self.upload_complete, transfer.getcode(), file)
            return
        except UploadAbortedException as exc:
            log.info(exc)
            error_msg = exc
        except urllib.error.URLError as exc:
            if isinstance(exc.reason, ssl.SSLError):
                error_msg = exc.reason.reason
                if error_msg == 'CERTIFICATE_VERIFY_FAILED':
                    log.exception('Certificate verify failed')
            else:
                log.exception('URLError')
                error_msg = exc.reason
        except Exception as exc:
            log.exception("Exception during upload")
            error_msg = exc
        GLib.idle_add(self.raise_progress_event, 'close', file)
        GLib.idle_add(self.on_upload_error, file, error_msg)

    def upload_complete(self, response_code, file):
        self.raise_progress_event('close', file)
        if 200 <= response_code < 300:
            log.info("Upload completed successfully")
            message = file.get
            if file.user_data:
                message += '#' + file.user_data
                message = self.convert_to_aegscm(message)
            else:
                self.messages.append(message)

            if file.groupchat:
                app.nec.push_outgoing_event(GcMessageOutgoingEvent(
                    None, account=self.name, jid=file.contact.jid,
                    message=message, automatic_message=False,
                    session=file.session))
            else:
                app.nec.push_outgoing_event(MessageOutgoingEvent(
                    None, account=self.name, jid=file.contact.jid,
                    message=message, keyID=file.keyID, type_='chat',
                    automatic_message=False, session=file.session))

        else:
            log.error('Got unexpected http upload response code: %s',
                      response_code)
            self.raise_information_event('httpupload-response-error',
                                         response_code)

    def on_upload_error(self, file, reason):
        self.raise_progress_event('close', file)
        self.raise_information_event('httpupload-error', str(reason))

    @staticmethod
    def convert_to_aegscm(url):
        return 'aesgcm' + url[5:]


class File:
    def __init__(self, path, contact, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)
        self.encrypted = False
        self.contact = contact
        self.keyID = None
        if hasattr(contact, 'keyID'):
            self.keyID = contact.keyID
        self.stream = None
        self.path = path
        self.put = None
        self.get = None
        self.data = None
        self.user_data = None
        self.size = None
        self.headers = {}
        self.event = threading.Event()
        self.load_data()

    def load_data(self):
        with open(self.path, 'rb') as content:
            self.data = content.read()
        self.size = len(self.data)

    def get_data(self, full=False):
        if full:
            return io.BytesIO(self.data).getvalue()
        return io.BytesIO(self.data)


class StreamFileWithProgress:
    def __init__(self, file):
        self.file = file
        self.event = file.event
        self.backing = file.get_data()
        self.backing.seek(0, os.SEEK_END)
        self._total = self.backing.tell()
        self.backing.seek(0)
        self._callback = file.update_progress
        self._seen = 0

    def __len__(self):
        return self._total

    def read(self, size):
        if self.event.isSet():
            raise UploadAbortedException

        data = self.backing.read(size)
        self._seen += len(data)
        if self._callback:
            GLib.idle_add(self._callback, 'update',
                          self.file, self._seen, self._total)
        return data

    def close(self):
        return self.backing.close()


class UploadAbortedException(Exception):
    def __str__(self):
        return "Upload Aborted"