# -*- coding: utf-8 -*-
"""
@file
@brief Starts an application.
"""
import os
from starlette.applications import Starlette
from starlette.staticfiles import StaticFiles
from starlette.responses import RedirectResponse, PlainTextResponse
# from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.templating import Jinja2Templates
from ..common import LogApp, AuthentificationAnswers
from ..display import DisplayQuestionChoiceHTML
from ...tests import get_game


class QCMApp(LogApp, AuthentificationAnswers):
    """
    Implements routes for a web application.

    .. faqref::
        :title: Which server to server starlette application?
        :lid: faq-server-app-starlette

        :epkg:`starlette` does not implement a webserver, it
        only provides a way to map urls to answers and to interect
        with the user. To launch a server serving :epkg:`starlette`
        applications, there is module :epkg:`uvicorn` but it does not
        implement a secured connection. There is :epkg:`hypercorn`
        which should support it. Other alternatives are described
        on `starlette/installation <https://www.starlette.io/#installation>`_.
    """

    def __init__(self,
                 # log parameters
                 secret_log=None, folder='.',
                 # authentification parameters
                 max_age=14 * 24 * 60 * 60, cookie_key=None,
                 cookie_name="mathenjeu", cookie_domain="127.0.0.1",
                 cookie_path="/",
                 # application parameters
                 title="Web Application MathEnJeu", short_title="MathEnJeu",
                 page_doc="http://www.xavierdupre.fr/app/mathenjeu/helpsphinx/",
                 secure=False, display=None, fct_game=None, games=None,
                 middles=None, debug=False, userpwd=None):
        """
        @param      secret_log      to encrypt log (None to ignore)
        @param      folder          folder where to write the logs (None to disable the logging)

        @param      max_age         cookie's duration in seconds
        @param      cookie_key      to encrypt information in the cookie (cannot be None)
        @param      cookie_name     name of the session cookie
        @param      cookie_domain   cookie is valid for this path only, also defines the
                                    domain of the web app (its url)
        @param      cookie_path     path of the cookie once storeds
        @param      secure          use secured connection for cookies

        @param      title           title
        @param      short_title     short application title
        @param      page_doc        documentation page
        @param      display         display such as @see cl DisplayQuestionChoiceHTML (default value)
        @param      fct_game        function *lambda name:* @see cl ActivityGroup
        @param      games           defines which games is available as a dictionary
                                    ``{ game_id: (game name, first page id) }``
        @param      middles         middles ware, list of couple ``[(class, **kwargs)]``
                                    where *kwargs* are the parameter constructor
        @param      userpwd         users are authentified with any alias but a common password
        @param      debug           display debug information (:epkg:`starlette` option)
        """
        if title is None:
            raise ValueError("title cannot be None.")
        if short_title is None:
            raise ValueError("short_title cannot be None.")
        if display is None:
            display = DisplayQuestionChoiceHTML()
        if fct_game is None:
            fct_game = get_game
        if games is None:
            games = dict(test_qcm1=('Maths', 0),
                         test_ml1=('ML', 0))

        this = os.path.abspath(os.path.dirname(__file__))
        templates = os.path.join(this, "templates")
        statics = os.path.join(this, "statics")
        if not os.path.exists(statics):
            raise FileNotFoundError("Unable to find '{0}'".format(statics))
        if not os.path.exists(templates):
            raise FileNotFoundError("Unable to find '{0}'".format(templates))

        login_page = "login.html"
        notauth_page = "notauthorized.html"
        auth_page = "authorized.html"
        redirect_logout = "/"
        app = Starlette(debug=debug)

        AuthentificationAnswers.__init__(self, app, login_page=login_page, auth_page=auth_page,
                                         notauth_page=notauth_page, redirect_logout=redirect_logout,
                                         max_age=max_age, cookie_name=cookie_name, cookie_key=cookie_key,
                                         cookie_domain=cookie_domain, cookie_path=cookie_path,
                                         page_context=self.page_context, userpwd=<PASSWORD>)
        LogApp.__init__(self, folder=folder, secret_log=secret_log,
                        fct_session=self.get_session)

        self.title = title
        self.short_title = short_title
        self.page_doc = page_doc
        self.display = display
        self.get_game = fct_game
        self.games = games
        self.templates = Jinja2Templates(directory=templates)

        if middles is not None:
            for middle, kwargs in middles:
                app.add_middleware(middle, **kwargs)
        app.add_middleware(TrustedHostMiddleware,
                           allowed_hosts=[cookie_domain])
        # app.add_middleware(HTTPSRedirectMiddleware)

        app.mount('/static', StaticFiles(directory=statics), name='static')
        app.add_route('/login', self.login)
        app.add_route('/logout', self.logout)
        app.add_route('/error', self.on_error)
        app.add_route('/authenticate', self.authenticate, methods=['POST'])
        app.add_route('/answer', self.answer, methods=['POST', 'GET'])
        app.add_exception_handler(404, self.not_found)
        app.add_exception_handler(500, self.server_error)
        app.add_event_handler("startup", self.startup)
        app.add_event_handler("shutdown", self.cleanup)
        app.add_route('/', self.main)
        app.add_route('/qcm', self.qcm, methods=['GET', 'POST'])
        app.add_route('/last', self.lastpage, methods=['GET', 'POST'])
        app.add_route('/event', self.event, methods=['GET', 'POST'])
        self.info("[QCMApp.create_app] create application", None)

    #########
    # common
    #########

    def page_context(self, **kwargs):
        """
        Returns the page context before applying any template.

        @param      kwargs      arguments
        @return                 parameters
        """
        res = dict(title=self.title, short_title=self.short_title,
                   page_doc=self.page_doc)
        res.update(kwargs)
        self.info('[QCMApp] page_context', str(res))
        return res

    def startup(self):
        """
        Startups.
        """
        self.info('[QCMApp] startup', None)

    def cleanup(self):
        """
        Cleans up.
        """
        self.info('[QCMApp] cleanup', None)

    def unlogged_response(self, request, session):
        """
        Returns an answer for somebody looking to access
        the questions without being authentified.
        """
        self.log_event("home-unlogged", request, session=session)
        context = {'request': request}
        context.update(self.page_context(**session))
        self.info('[QCMApp] unlogged_response', str(context))
        return self.templates.TemplateResponse('notlogged.html', context)

    def unknown_game(self, request, session):
        """
        Returns an answer for somebody looking to access
        the questions without being authentified.
        """
        self.log_event("home-nogame", request, session=session)
        context = {'request': request}
        context.update(self.page_context(**session))
        return self.templates.TemplateResponse('nogame.html', context)

    ########
    # route
    ########

    async def main(self, request):
        """
        Defines the main page.
        """
        session = self.get_session(request, notnone=True)
        if 'alias' in session:
            self.log_event("home-logged", request, session=session)
            context = {'request': request}
            context.update(self.page_context(games=self.games, **session))
            page = self.templates.TemplateResponse('index.html', context)
            self.info('[QCMApp] main', str(page))
            return page
        return self.unlogged_response(request, session)

    async def on_error(self, request):
        """
        An example error.
        """
        self.log_any('[error]', "?", request)
        raise RuntimeError("Oh no")

    async def not_found(self, request, exc):
        """
        Returns an :epkg:`HTTP 404` page.
        """
        context = {'request': request}
        context.update(self.page_context())
        return self.templates.TemplateResponse('404.html', context, status_code=404)

    async def server_error(self, request, exc):
        """
        Returns an :epkg:`HTTP 500` page.
        """
        context = {'request': request}
        context.update(self.page_context())
        return self.templates.TemplateResponse('500.html', context, status_code=500)

    async def qcm(self, request):
        """
        Defines the main page.
        """
        session = self.get_session(request, notnone=True)
        if 'alias' in session:
            game = request.query_params.get('game', None)
            if game is None:
                return self.unknown_game(request, session)
            obj_game = self.get_game(game)
            self.info('[QCMApp] qcm.1', str(obj_game))
            if isinstance(obj_game, str):
                raise RuntimeError(
                    "obj_game for '{0}' cannot be string".format(game))
            qn = request.query_params.get('qn', 0)
            data = dict(game=game, qn=qn)
            events = request.query_params.get('events', None)
            if events:
                data['events'] = events
            self.log_event("qcm", request, session=session, **data)
            disp = self.display
            context = disp.get_context(obj_game, qn)
            context.update(session)
            context['game'] = game
            if events:
                context['events'] = events
            context_req = {'request': request}
            context_req.update(self.page_context(**context))
            page = self.templates.TemplateResponse('qcm.html', context_req)
            return page
        return self.unlogged_response(request, session)

    async def answer(self, request):
        """
        Captures an answer.

        @param      request         request
        @return                     response
        """
        try:
            fo = await request.form()
        except Exception as e:
            raise RuntimeError(  # pylint: disable=W0707
                "Unable to read answer due to '{0}'".format(e))
        session = self.get_session(request, notnone=True)
        values = {k: v for k, v in fo.items()}  # pylint: disable=R1721
        ps = request.query_params
        values.update(ps)
        self.log_event("answer", request, session=session, data=values)
        if 'next' in values and 'game' in values and values['next'] in (None, 'None'):
            response = RedirectResponse(url='/last?game=' + values['game'])
        else:
            response = RedirectResponse(
                url='/qcm?game={0}&qn={1}'.format(values.get('game', ''), values.get('next', '')))
        return response

    async def lastpage(self, request):
        """
        Defines the last page.
        """
        session = self.get_session(request, notnone=True)
        ps = request.query_params
        self.log_event("finish", request, session=session, data=ps)
        context = {'request': request, 'alias': session.get('alias')}
        context.update(self.page_context())
        return self.templates.TemplateResponse('lastpage.html', context)

    #########
    # event route
    #########

    async def event(self, request):
        """
        This route does not return anything interesting except
        a blank page, but it logs
        """
        session = self.get_session(request, notnone=True)
        ps = request.query_params
        tostr = ','.join('{0}:{1}'.format(k, v) for k, v in sorted(ps.items()))
        self.log_event("event", request, session=session, events=[tostr])
        return PlainTextResponse("")

# -*- coding: utf-8 -*-
"""
@file
@brief Starts an application.
"""
import os
from starlette.applications import Starlette
from starlette.staticfiles import StaticFiles
from starlette.responses import RedirectResponse, PlainTextResponse
# from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.templating import Jinja2Templates
from ..common import LogApp, AuthentificationAnswers
from ..display import DisplayQuestionChoiceHTML
from ...tests import get_game


class QCMApp(LogApp, AuthentificationAnswers):
    """
    Implements routes for a web application.

    .. faqref::
        :title: Which server to server starlette application?
        :lid: faq-server-app-starlette

        :epkg:`starlette` does not implement a webserver, it
        only provides a way to map urls to answers and to interect
        with the user. To launch a server serving :epkg:`starlette`
        applications, there is module :epkg:`uvicorn` but it does not
        implement a secured connection. There is :epkg:`hypercorn`
        which should support it. Other alternatives are described
        on `starlette/installation <https://www.starlette.io/#installation>`_.
    """

    def __init__(self,
                 # log parameters
                 secret_log=None, folder='.',
                 # authentification parameters
                 max_age=14 * 24 * 60 * 60, cookie_key=None,
                 cookie_name="mathenjeu", cookie_domain="127.0.0.1",
                 cookie_path="/",
                 # application parameters
                 title="Web Application MathEnJeu", short_title="MathEnJeu",
                 page_doc="http://www.xavierdupre.fr/app/mathenjeu/helpsphinx/",
                 secure=False, display=None, fct_game=None, games=None,
                 middles=None, debug=False, userpwd=None):
        """
        @param      secret_log      to encrypt log (None to ignore)
        @param      folder          folder where to write the logs (None to disable the logging)

        @param      max_age         cookie's duration in seconds
        @param      cookie_key      to encrypt information in the cookie (cannot be None)
        @param      cookie_name     name of the session cookie
        @param      cookie_domain   cookie is valid for this path only, also defines the
                                    domain of the web app (its url)
        @param      cookie_path     path of the cookie once storeds
        @param      secure          use secured connection for cookies

        @param      title           title
        @param      short_title     short application title
        @param      page_doc        documentation page
        @param      display         display such as @see cl DisplayQuestionChoiceHTML (default value)
        @param      fct_game        function *lambda name:* @see cl ActivityGroup
        @param      games           defines which games is available as a dictionary
                                    ``{ game_id: (game name, first page id) }``
        @param      middles         middles ware, list of couple ``[(class, **kwargs)]``
                                    where *kwargs* are the parameter constructor
        @param      userpwd         users are authentified with any alias but a common password
        @param      debug           display debug information (:epkg:`starlette` option)
        """
        if title is None:
            raise ValueError("title cannot be None.")
        if short_title is None:
            raise ValueError("short_title cannot be None.")
        if display is None:
            display = DisplayQuestionChoiceHTML()
        if fct_game is None:
            fct_game = get_game
        if games is None:
            games = dict(test_qcm1=('Maths', 0),
                         test_ml1=('ML', 0))

        this = os.path.abspath(os.path.dirname(__file__))
        templates = os.path.join(this, "templates")
        statics = os.path.join(this, "statics")
        if not os.path.exists(statics):
            raise FileNotFoundError("Unable to find '{0}'".format(statics))
        if not os.path.exists(templates):
            raise FileNotFoundError("Unable to find '{0}'".format(templates))

        login_page = "login.html"
        notauth_page = "notauthorized.html"
        auth_page = "authorized.html"
        redirect_logout = "/"
        app = Starlette(debug=debug)

        AuthentificationAnswers.__init__(self, app, login_page=login_page, auth_page=auth_page,
                                         notauth_page=notauth_page, redirect_logout=redirect_logout,
                                         max_age=max_age, cookie_name=cookie_name, cookie_key=cookie_key,
                                         cookie_domain=cookie_domain, cookie_path=cookie_path,
                                         page_context=self.page_context, userpwd=<PASSWORD>)
        LogApp.__init__(self, folder=folder, secret_log=secret_log,
                        fct_session=self.get_session)

        self.title = title
        self.short_title = short_title
        self.page_doc = page_doc
        self.display = display
        self.get_game = fct_game
        self.games = games
        self.templates = Jinja2Templates(directory=templates)

        if middles is not None:
            for middle, kwargs in middles:
                app.add_middleware(middle, **kwargs)
        app.add_middleware(TrustedHostMiddleware,
                           allowed_hosts=[cookie_domain])
        # app.add_middleware(HTTPSRedirectMiddleware)

        app.mount('/static', StaticFiles(directory=statics), name='static')
        app.add_route('/login', self.login)
        app.add_route('/logout', self.logout)
        app.add_route('/error', self.on_error)
        app.add_route('/authenticate', self.authenticate, methods=['POST'])
        app.add_route('/answer', self.answer, methods=['POST', 'GET'])
        app.add_exception_handler(404, self.not_found)
        app.add_exception_handler(500, self.server_error)
        app.add_event_handler("startup", self.startup)
        app.add_event_handler("shutdown", self.cleanup)
        app.add_route('/', self.main)
        app.add_route('/qcm', self.qcm, methods=['GET', 'POST'])
        app.add_route('/last', self.lastpage, methods=['GET', 'POST'])
        app.add_route('/event', self.event, methods=['GET', 'POST'])
        self.info("[QCMApp.create_app] create application", None)

    #########
    # common
    #########

    def page_context(self, **kwargs):
        """
        Returns the page context before applying any template.

        @param      kwargs      arguments
        @return                 parameters
        """
        res = dict(title=self.title, short_title=self.short_title,
                   page_doc=self.page_doc)
        res.update(kwargs)
        self.info('[QCMApp] page_context', str(res))
        return res

    def startup(self):
        """
        Startups.
        """
        self.info('[QCMApp] startup', None)

    def cleanup(self):
        """
        Cleans up.
        """
        self.info('[QCMApp] cleanup', None)

    def unlogged_response(self, request, session):
        """
        Returns an answer for somebody looking to access
        the questions without being authentified.
        """
        self.log_event("home-unlogged", request, session=session)
        context = {'request': request}
        context.update(self.page_context(**session))
        self.info('[QCMApp] unlogged_response', str(context))
        return self.templates.TemplateResponse('notlogged.html', context)

    def unknown_game(self, request, session):
        """
        Returns an answer for somebody looking to access
        the questions without being authentified.
        """
        self.log_event("home-nogame", request, session=session)
        context = {'request': request}
        context.update(self.page_context(**session))
        return self.templates.TemplateResponse('nogame.html', context)

    ########
    # route
    ########

    async def main(self, request):
        """
        Defines the main page.
        """
        session = self.get_session(request, notnone=True)
        if 'alias' in session:
            self.log_event("home-logged", request, session=session)
            context = {'request': request}
            context.update(self.page_context(games=self.games, **session))
            page = self.templates.TemplateResponse('index.html', context)
            self.info('[QCMApp] main', str(page))
            return page
        return self.unlogged_response(request, session)

    async def on_error(self, request):
        """
        An example error.
        """
        self.log_any('[error]', "?", request)
        raise RuntimeError("Oh no")

    async def not_found(self, request, exc):
        """
        Returns an :epkg:`HTTP 404` page.
        """
        context = {'request': request}
        context.update(self.page_context())
        return self.templates.TemplateResponse('404.html', context, status_code=404)

    async def server_error(self, request, exc):
        """
        Returns an :epkg:`HTTP 500` page.
        """
        context = {'request': request}
        context.update(self.page_context())
        return self.templates.TemplateResponse('500.html', context, status_code=500)

    async def qcm(self, request):
        """
        Defines the main page.
        """
        session = self.get_session(request, notnone=True)
        if 'alias' in session:
            game = request.query_params.get('game', None)
            if game is None:
                return self.unknown_game(request, session)
            obj_game = self.get_game(game)
            self.info('[QCMApp] qcm.1', str(obj_game))
            if isinstance(obj_game, str):
                raise RuntimeError(
                    "obj_game for '{0}' cannot be string".format(game))
            qn = request.query_params.get('qn', 0)
            data = dict(game=game, qn=qn)
            events = request.query_params.get('events', None)
            if events:
                data['events'] = events
            self.log_event("qcm", request, session=session, **data)
            disp = self.display
            context = disp.get_context(obj_game, qn)
            context.update(session)
            context['game'] = game
            if events:
                context['events'] = events
            context_req = {'request': request}
            context_req.update(self.page_context(**context))
            page = self.templates.TemplateResponse('qcm.html', context_req)
            return page
        return self.unlogged_response(request, session)

    async def answer(self, request):
        """
        Captures an answer.

        @param      request         request
        @return                     response
        """
        try:
            fo = await request.form()
        except Exception as e:
            raise RuntimeError(  # pylint: disable=W0707
                "Unable to read answer due to '{0}'".format(e))
        session = self.get_session(request, notnone=True)
        values = {k: v for k, v in fo.items()}  # pylint: disable=R1721
        ps = request.query_params
        values.update(ps)
        self.log_event("answer", request, session=session, data=values)
        if 'next' in values and 'game' in values and values['next'] in (None, 'None'):
            response = RedirectResponse(url='/last?game=' + values['game'])
        else:
            response = RedirectResponse(
                url='/qcm?game={0}&qn={1}'.format(values.get('game', ''), values.get('next', '')))
        return response

    async def lastpage(self, request):
        """
        Defines the last page.
        """
        session = self.get_session(request, notnone=True)
        ps = request.query_params
        self.log_event("finish", request, session=session, data=ps)
        context = {'request': request, 'alias': session.get('alias')}
        context.update(self.page_context())
        return self.templates.TemplateResponse('lastpage.html', context)

    #########
    # event route
    #########

    async def event(self, request):
        """
        This route does not return anything interesting except
        a blank page, but it logs
        """
        session = self.get_session(request, notnone=True)
        ps = request.query_params
        tostr = ','.join('{0}:{1}'.format(k, v) for k, v in sorted(ps.items()))
        self.log_event("event", request, session=session, events=[tostr])
        return PlainTextResponse("")

# -*- coding: utf-8 -*- @file @brief Starts an application. # from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware Implements routes for a web application.      .. faqref::         :title: Which server to server starlette application?         :lid: faq-server-app-starlette          :epkg:`starlette` does not implement a webserver, it         only provides a way to map urls to answers and to interect         with the user. To launch a server serving :epkg:`starlette`         applications, there is module :epkg:`uvicorn` but it does not         implement a secured connection. There is :epkg:`hypercorn`         which should support it. Other alternatives are described         on `starlette/installation <https://www.starlette.io/#installation>`_. # log parameters # authentification parameters # application parameters @param      secret_log      to encrypt log (None to ignore)         @param      folder          folder where to write the logs (None to disable the logging)          @param      max_age         cookie's duration in seconds         @param      cookie_key      to encrypt information in the cookie (cannot be None)         @param      cookie_name     name of the session cookie         @param      cookie_domain   cookie is valid for this path only, also defines the                                     domain of the web app (its url)         @param      cookie_path     path of the cookie once storeds         @param      secure          use secured connection for cookies          @param      title           title         @param      short_title     short application title         @param      page_doc        documentation page         @param      display         display such as @see cl DisplayQuestionChoiceHTML (default value)         @param      fct_game        function *lambda name:* @see cl ActivityGroup         @param      games           defines which games is available as a dictionary                                     ``{ game_id: (game name, first page id) }``         @param      middles         middles ware, list of couple ``[(class, **kwargs)]``                                     where *kwargs* are the parameter constructor         @param      userpwd         users are authentified with any alias but a common password         @param      debug           display debug information (:epkg:`starlette` option) # app.add_middleware(HTTPSRedirectMiddleware) ######### # common ######### Returns the page context before applying any template.          @param      kwargs      arguments         @return                 parameters Startups. Cleans up. Returns an answer for somebody looking to access         the questions without being authentified. Returns an answer for somebody looking to access         the questions without being authentified. ######## # route ######## Defines the main page. An example error. Returns an :epkg:`HTTP 404` page. Returns an :epkg:`HTTP 500` page. Defines the main page. Captures an answer.          @param      request         request         @return                     response # pylint: disable=W0707 # pylint: disable=R1721 Defines the last page. ######### # event route ######### This route does not return anything interesting except         a blank page, but it logs