# -*- coding: utf-8 -*-
# (c) 2015 Tuomas Airaksinen
#
# This file is part of automate-webui.
#
# automate-webui 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 3 of the License, or
# (at your option) any later version.
#
# automate-webui 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 automate-webui.  If not, see <http://www.gnu.org/licenses/>.
#
# ------------------------------------------------------------------
#
# If you like Automate, please take a look at this page:
# http://evankelista.net/automate/

"""
    Web interface module for Automate
"""
from __future__ import unicode_literals

import json
import datetime
import time
import os

from django.template.loader import render_to_string
import tornado.web
from tornado.websocket import WebSocketHandler, WebSocketClosedError

from traits.api import CBool, Tuple, Int, Str, CSet, List, CInt, Dict, Unicode

from automate.extensions.webui.djangoapp.views import set_globals
from automate.statusobject import StatusObject
from django.core.wsgi import get_wsgi_application
from automate.extensions.wsgi import TornadoService


class WebService(TornadoService):

    """
    Web User Interface Service for Automate
    """

    #: Restrict usage to only monitoring statuses (default: ``True``).
    #: If WebService is not in read_only mode, it is possible to run arbitrary Python commands
    #: through eval/exec via web browser. This is, of course, a severe security issue.
    #: Secure SSL configuration HIGHLY recommended, if not operating in ``read_only`` mode.
    read_only = CBool(True)

    #: Default view that is displayed when entering the server. Can be the name of any view in views.py
    default_view = Str('system')

    #: Below Actuator row, show active Programs that are controlling Actuator
    show_actuator_details = CBool(True)

    #: HTTP port to listen
    http_port = Int(8080)

    #: Authentication for logging into web server. (user,password) pairs in a tuple.
    http_auth = Tuple

    #: Let websocket connection die after ``websocket_timeout`` time of no ping reply from client.
    websocket_timeout = CInt(60 * 5)

    #: Tags that are shown in user defined view
    user_tags = CSet(trait=Str, value={'user'})

    #: Django debugging mode (slower, more info shown when error occurs)
    debug = CBool(False)

    #: User-defined custom pages as a dictionary of form ``{name: template_content}``
    custom_pages = Dict(key_trait=Unicode, value_trait=Unicode)

    #: set to True, if you want to launch multiple servers with same system. Authentication and
    #: other settings are then taken from master service. Only web server settings (http host/port)
    #: are used from slave.
    slave = CBool(False)

    #: In this dictionary you can define your custom Django settings which will override the default ones
    django_settings = Dict()

    #: From /set/object/value and /toggle/object, redirect to /set_ready/object/value after after executing action
    redirect_from_setters = CBool(True)

    #: Show hidden objects in web UI (those prefixed with _)
    show_hidden = CBool(False)

    #: Full exposed url root
    server_url = Unicode()

    _sockets = List

    def get_filehandler_class(service):
        class MyFileHandler(tornado.web.StaticFileHandler):

            def validate_absolute_path(self, *args, **kwargs):
                session_id = getattr(self.request.cookies.get('sessionid', None), 'value', None)
                from django.contrib.sessions.middleware import SessionMiddleware
                mw = SessionMiddleware()
                session_data = mw.SessionStore(session_id)
                if not session_data.get('logged_in', False):
                    raise tornado.web.HTTPError(403, 'not logged in')

                return super(MyFileHandler, self).validate_absolute_path(*args, **kwargs)

            def check_etag_header(self):
                """ Disable etag_header checking (checks only modified time). Due to etag caching
                    file changes were not detected at all. """
                return False
        return MyFileHandler

    def get_tornado_handlers(self):
        if self.slave:
            return self.system.request_service('WebService').get_tornado_handlers()
        super_handlers = super(WebService, self).get_tornado_handlers()
        path = os.path.join(os.path.dirname(__file__), 'static')
        static = [('/static/(.*)', tornado.web.StaticFileHandler, {'path': path})]
        return static + super_handlers

    def setup(self):
        if not self.slave:
            os.environ['DJANGO_SETTINGS_MODULE'] = 'automate.extensions.webui.settings'
            from django.conf import settings
            settings.DEBUG = self.debug
            if not 'SECRET_KEY' in self.django_settings:
                self.logger.warning('Insecure settings! Please set proper SECRET_KEY in django_settings!')
            if not 'TIME_ZONE' in self.django_settings:
                os.environ.pop('TZ')  # Django uses America/Chicago as default timezone. Let's clean this up.
                time.tzset()
            if self.server_url:
                self.django_settings['SERVER_URL'] = self.server_url
            for key, value in self.django_settings.items():
                setattr(settings, key, value)

            set_globals(self, self.system)

        super(WebService, self).setup()
        if not self.slave:
            self.system.request_service('LogStoreService').on_trait_change(self.push_log, 'most_recent_line')

            self.system.on_trait_change(self.update_sockets, 'objects.status, objects.changing, objects.active, '
                                        'objects.program_status_items')

    def get_websocket(service):
        if service.slave:
            return service.system.request_service('WebService').get_websocket()

        class WebSocket(WebSocketHandler):

            def data_received(self, chunk):
                pass

            def __init__(self, application, request, **kwargs):
                self.log_requested = False
                self.subscribed_objects = set()
                self.last_message = None
                self.logged_in = False

                super(WebSocket, self).__init__(application, request, **kwargs)

            def check_origin(self, origin):
                return True

            def write_json(self, **kwargs):
                msg = json.dumps(kwargs)
                service.logger.debug('Sending to client %s', msg)
                self.write_message(msg)

            def open(self):
                self.session_id = session_id = getattr(self.request.cookies.get('sessionid', None), 'value', None)
                from django.contrib.sessions.middleware import SessionMiddleware
                mw = SessionMiddleware()
                session_data = mw.SessionStore(session_id)

                if session_data.get('logged_in', False) or not service.http_auth:
                    self.logged_in = True
                else:
                    service.logger.warning("Not (yet) logged in %s", session_id)

                service.logger.debug("WebSocket opened for session %s", session_id)
                service._sockets.append(self)

            def _authenticate(self, username, password):
                if (username, password) == service.http_auth:
                    self.logged_in = True
                    service.logger.debug('Websocket user %s logged in.', username)
                else:
                    service.logger.warning('Authentication failure: user %s with passwd %s != %s ',
                                           username, password, service.http_auth)
                    self.close()

            def _ping(self):
                pass

            def _set_status(self, name, status):
                if service.read_only:
                    service.logger.warning("Could not perform operation: read only mode enabled")
                    return
                obj = service.system.namespace.get(name, None)
                if obj:
                    obj.status = status

            def _subscribe(self, objects):
                self.subscribed_objects.update(objects)

            def _unsubscribe(self, objects):
                self.subscribed_objects -= set(objects)

            def _clear_subscriptions(self):
                self.subscribed_objects.clear()

            def _send_command(self, command):
                if not service.read_only:
                    service.system.cmd_exec(command)
                else:
                    service.logger.warning("Could not perform operation: read only mode enabled")

            def _fetch_objects(self):
                data = [(i.name, i.get_as_datadict()) for i in service.system.objects_sorted]
                self.write_json(action='return', rv=data)

            def _request_log(self):
                self.log_requested = True

            def on_message(self, json_message):
                """
                    Message format: {'action': 'action_name', other_kwargs...)
                """

                message = json.loads(json_message)
                service.logger.debug('Message received from client: %s', message)

                action = message.pop('action', '')

                if self.logged_in:
                    action_func = getattr(self, '_' + action, None)
                    if action_func:
                        service.logger.debug('Running websocket action %s', action)
                        action_func(**message)
                    else:
                        service.logger.error('Not logged in or unknown message %s', message)
                elif action == 'authenticate':
                    return self._authenticate(**message)

                self.last_message = datetime.datetime.now()

            def on_close(self):
                service.logger.debug("WebSocket closed for session %s", self.session_id)
                service._sockets.remove(self)

        return WebSocket

    def push_log(self, new):
        for s in self._sockets:
            if s.log_requested:
                try:
                    s.write_json(action='log', data=new)
                except WebSocketClosedError:
                    pass

    def update_sockets(self, obj, attribute, old, new):
        if isinstance(obj, StatusObject):
            self.logger.debug('Update_sockets %s %s %s %s', obj, attribute, old, new)
            for s in self._sockets:
                if s.last_message and (s.last_message < datetime.datetime.now()
                                       - datetime.timedelta(seconds=self.websocket_timeout)):
                    self.logger.info('Closing connection %s due to timeout', s.session_id)
                    s.on_close()
                    s.close(code=1000, reason='Timeout')
                    continue
                if obj.name in s.subscribed_objects:
                    if attribute == 'program_status_items' and self.show_actuator_details:
                        new_actuator_html = render_to_string(
                            'rows/actuator_row.html', dict(actuator=obj, source='__SOURCE__', service=self))
                        s.write_json(action='update_actuator', name=obj.name, html=new_actuator_html)
                    elif attribute == 'active':
                        s.write_json(action='program_active', name=obj.name, active=obj.active)
                    elif attribute in ['status', 'changing']:
                        s.write_json(action='object_status',
                                     name=obj.name,
                                     status=obj.status,
                                     display=obj.get_status_display(),
                                     changing=obj.changing)

    def get_wsgi_application(self):
        return get_wsgi_application()
