#!/usr/bin/python
# api_web_socket.py
#
# Copyright (C) 2008 Veselin Penev, https://bitdust.io
#
# This file (api_web_socket.py) is part of BitDust Software.
#
# BitDust is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BitDust Software 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with BitDust Software.  If not, see <http://www.gnu.org/licenses/>.
#
# Please contact us if you have any questions at bitdust.io@gmail.com
#
#
#
#

"""
..

module:: api_web_socket
"""

#------------------------------------------------------------------------------

from __future__ import absolute_import

#------------------------------------------------------------------------------

_Debug = False
_DebugLevel = 10

_APILogFileEnabled = False

#------------------------------------------------------------------------------

from twisted.application.strports import listen
from twisted.internet.defer import Deferred
from twisted.internet.protocol import Protocol, Factory
from twisted.python.failure import Failure

#------------------------------------------------------------------------------

from logs import lg

from lib import txws
from lib import serialization

from system import local_fs

from main import events
from main import settings

from interface import api

#------------------------------------------------------------------------------

_WebSocketListener = None
_WebSocketTransports = {}
_AllAPIMethods = []
_APISecret = None

#------------------------------------------------------------------------------

def init(port=None):
    global _WebSocketListener
    global _AllAPIMethods
    global _APILogFileEnabled
    _APILogFileEnabled = settings.config.conf().getBool('logs/api-enabled')
    if _WebSocketListener is not None:
        lg.warn('_WebSocketListener already initialized')
        return
    if not port:
        port = settings.DefaultWebSocketPort()
    try:
        ws = BitDistWrappedWebSocketFactory(BitDustWebSocketFactory())
        _WebSocketListener = listen("tcp:%d" % port, ws)
    except:
        lg.exc()
        return
    _AllAPIMethods = set(dir(api))
    _AllAPIMethods.difference_update([
        # TODO: keep that list up to date when changing the api
        'on_api_result_prepared', 'Deferred', 'ERROR', 'Failure', 'OK', 'RESULT', '_Debug', '_DebugLevel',
        'strng', 'sys', 'time', 'gc', 'map', 'os',
        '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__',
        'absolute_import', 'driver', 'filemanager', 'jsn', 'lg',
        'event_listen', 'message_receive',
    ])
    if _Debug:
        lg.out(_DebugLevel, 'api_web_socket.init  _WebSocketListener=%r with %d methods' % (
            _WebSocketListener, len(_AllAPIMethods)))
    read_api_secret()
    events.add_subscriber(on_event, event_id='*')


def shutdown():
    global _WebSocketListener
    events.remove_subscriber(on_event, event_id='*')
    if _WebSocketListener:
        if _Debug:
            lg.out(_DebugLevel, 'api_web_socket.shutdown calling _WebSocketListener.stopListening()')
        _WebSocketListener.stopListening()
        del _WebSocketListener
        _WebSocketListener = None
        if _Debug:
            lg.out(_DebugLevel, '    _WebSocketListener destroyed')
    else:
        lg.warn('_WebSocketListener is None')

#------------------------------------------------------------------------------

def read_api_secret():
    global _APISecret
    _APISecret = local_fs.ReadTextFile(settings.APISecretFile())

#------------------------------------------------------------------------------

class BitDustWrappedWebSocketProtocol(txws.WebSocketProtocol):

    def validateHeaders(self):
        global _APISecret
        if _APISecret:
            _, _, api_secret_parameter = self.location.partition('?')
            _, _, api_secret_parameter = api_secret_parameter.partition('=')
            if api_secret_parameter != _APISecret:
                events.send('web-socket-access-denied', data=dict())
                self.loseConnection()
                return
        return txws.WebSocketProtocol.validateHeaders(self)


class BitDistWrappedWebSocketFactory(txws.WebSocketFactory):

    protocol = BitDustWrappedWebSocketProtocol


#------------------------------------------------------------------------------

class BitDustWebSocketProtocol(Protocol):

    _key = None

    def dataReceived(self, data):
        try:
            json_data = serialization.BytesToDict(data, keys_to_text=True, values_to_text=True, encoding='utf-8')
        except:
            lg.exc()
            return
        if _Debug:
            lg.dbg(_DebugLevel, 'received %d bytes from web socket: %r' % (len(data), json_data))
        if not do_process_incoming_message(json_data):
            lg.warn('failed processing incoming message from web socket: %r' % json_data)

    def connectionMade(self):
        global _WebSocketTransports
        Protocol.connectionMade(self)
        peer = self.transport.getPeer()
        self._key = (peer.type, peer.host, peer.port, )
        peer = '%s://%s:%s' % (self._key[0], self._key[1], self._key[2])
        _WebSocketTransports[self._key] = self.transport
        if _Debug:
            lg.args(_DebugLevel, key=self._key, ws_connections=len(_WebSocketTransports))
        events.send('web-socket-connected', data=dict(peer=peer))

    def connectionLost(self, *args, **kwargs):
        global _WebSocketTransports
        if _Debug:
            lg.args(_DebugLevel, key=self._key, ws_connections=len(_WebSocketTransports))
        Protocol.connectionLost(self, *args, **kwargs)
        _WebSocketTransports.pop(self._key)
        peer = '%s://%s:%s' % (self._key[0], self._key[1], self._key[2])
        self._key = None
        events.send('web-socket-disconnected', data=dict(peer=peer))

#------------------------------------------------------------------------------

class BitDustWebSocketFactory(Factory):

    protocol = BitDustWebSocketProtocol

    def buildProtocol(self, addr):
        """
        Only accepting connections from local machine!
        """
        if addr.host != '127.0.0.1':
            lg.err('refused connection from remote host: %r' % addr.host)
            return None
        proto = Factory.buildProtocol(self, addr)
        return proto

#------------------------------------------------------------------------------

def do_process_incoming_message(json_data):
    global _AllAPIMethods
    command = json_data.get('command')
    if command == 'api_call':
        method = json_data.get('method', None)
        kwargs = json_data.get('kwargs', {})
        call_id = json_data.get('call_id', None)

        if not method:
            lg.warn('api method name was not provided')
            return push({
                    'type': 'api_call',
                    'payload': {
                        'call_id': call_id,
                        'errors': ['api method name was not provided', ],
                    },
                })

        if method not in _AllAPIMethods:
            lg.warn('invalid api method name: %r' % method)
            return push({
                    'type': 'api_call',
                    'payload': {
                        'call_id': call_id,
                        'errors': ['invalid api method name', ],
                    },
                })

        if _Debug:
            lg.out(_DebugLevel, '*** %s  API WS IN  %s(%r)' % (
                call_id, method, kwargs))

        if _APILogFileEnabled:
            lg.out(0, '*** %s  WS IN  %s(%r)' % (
                call_id, method, kwargs), log_name='api', showtime=True)

        func = getattr(api, method)
        try:
            response = func(**kwargs)
        except Exception as err:
            lg.err(f'{method}({kwargs}) : {err}')
            return push({
                    'type': 'api_call',
                    'payload': {
                        'call_id': call_id,
                        'errors': [str(err), ],
                    },
                })

        if isinstance(response, Deferred):

            def _cb(r):
                return push({
                    'type': 'api_call',
                    'payload': {
                        'call_id': call_id,
                        'response': r,
                    },
                })

            def _eb(err):
                err_msg = err.getErrorMessage() if isinstance(err, Failure) else str(err)
                return push({
                    'type': 'api_call',
                    'payload': {
                        'call_id': call_id,
                        'errors': [err_msg, ],
                    },
                })

            response.addCallback(_cb)
            response.addErrback(_eb)
            return True

        return push({
            'type': 'api_call',
            'payload': {
                'call_id': call_id,
                'response': response,
            },
        })

    return False

#------------------------------------------------------------------------------

def on_event(evt):
    return push({
        'type': 'event',
        'payload': {
            'event_id': evt.event_id,
            'data': evt.data,
        },
    })


def on_stream_message(message_json):
    return push({
        'type': 'stream_message',
        'payload': message_json,
    })


def on_online_status_changed(status_info):
    return push({
        'type': 'online_status',
        'payload': status_info,
    })

#------------------------------------------------------------------------------

def push(json_data):
    global _WebSocketTransports
    if not _WebSocketTransports:
        return False
    raw_bytes = serialization.DictToBytes(json_data, encoding='utf-8')
    for _key, transp in _WebSocketTransports.items():
        transp.write(raw_bytes)
        if _Debug:
            lg.dbg(_DebugLevel, 'sent %d bytes to web socket %s' % (
                len(raw_bytes), '%s://%s:%s' % (_key[0], _key[1], _key[2]), ))
    if _Debug:
        lg.out(_DebugLevel, '***   API WS PUSH  %d bytes' % len(raw_bytes))
    if _APILogFileEnabled:
        lg.out(0, '*** WS PUSH  %d bytes : %r' % (len(raw_bytes), json_data, ), log_name='api', showtime=True)
    return True
