#  2015-07-09 Cornelius Kölbel <cornelius.koelbel@netknights.it>
#             Fix mOTP reuse of OTP values
#  2015-01-27 Rewrite due to flask migration
#             Cornelius Kölbel <cornelius@privacyidea.org>
#  2014-10-03 Add getInitDetail
#             Cornelius Kölbel <cornelius@privacyidea.org>
#
#  May 08, 2014 Cornelius Kölbel
#  Sept 16, 2014 Cornelius Kölbel, added key generation for Token2
#
#  License:  AGPLv3
#  contact:  http://www.privacyidea.org
#
#
#  privacyIDEA is a fork of LinOTP
#  Copyright (C) 2010 - 2014 LSE Leading Security Experts GmbH
#  License:  LSE
#  contact:  http://www.linotp.org
#            http://www.lsexperts.de
#            linotp@lsexperts.de
#
# This code is free software; you can redistribute it and/or
# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
# License as published by the Free Software Foundation; either
# version 3 of the License, or any later version.
#
# This code 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 this program.  If not, see <http://www.gnu.org/licenses/>.
#
"""This code implements the motp one time password algorithm
described in motp.sourceforge.net.

The code is tested in tests/test_lib_tokens_motp
"""
from .mOTP import mTimeOtp
from privacyidea.lib.apps import create_motp_url
from privacyidea.lib.tokenclass import TokenClass
from privacyidea.lib.log import log_with
from privacyidea.lib.utils import create_img, is_true
from privacyidea.api.lib.utils import getParam
from privacyidea.lib.crypto import generate_otpkey
from privacyidea.lib.decorators import check_token_locked
import traceback
import logging
from privacyidea.lib import _
from privacyidea.lib.policy import SCOPE, ACTION, GROUP

optional = True
required = False
log = logging.getLogger(__name__)


class MotpTokenClass(TokenClass):

    @staticmethod
    def get_class_type():
        return "motp"

    @staticmethod
    def get_class_prefix():
        return "PIMO"

    @staticmethod
    def get_class_info(key=None, ret='all'):
        """
        returns a subtree of the token definition
        Is used by lib.token.get_token_info

        :param key: subsection identifier
        :type key: string
        :param ret: default return value, if nothing is found
        :type ret: user defined
        :return: subsection if key exists or user defined
        :rtype : dict or string
        """

        res = {'type': 'motp',
               'title': 'mOTP Token',
               'description': _('mOTP: Classical mobile One Time Passwords.'),
               'init': {'page': {'html': 'motptoken.mako',
                                 'scope': 'enroll', },
                        'title': {'html': 'motptoken.mako',
                                  'scope': 'enroll.title'},
                        },
               'config': {'page': {'html': 'motptoken.mako',
                                   'scope': 'config'},
                          'title': {'html': 'motptoken.mako',
                                    'scope': 'config.title', },
                          },
               'user': ['enroll'],
               # This tokentype is enrollable in the UI for...
               'ui_enroll': ["admin", "user"],
               'policy': {
                   SCOPE.ENROLL: {
                       ACTION.MAXTOKENUSER: {
                           'type': 'int',
                           'desc': _("The user may only have this maximum number of mOTP tokens assigned."),
                           'group': GROUP.TOKEN
                       },
                       ACTION.MAXACTIVETOKENUSER: {
                           'type': 'int',
                           'desc': _("The user may only have this maximum number of active mOTP tokens assigned."),
                           'group': GROUP.TOKEN
                       }
                   }
               }
               }

        if key:
            ret = res.get(key, {})
        else:
            if ret == 'all':
                ret = res

        return ret

    @log_with(log)
    def __init__(self, db_token):
        """
        constructor - create a token object

        :param db_token: instance of the orm db object
        :type db_token:  orm object
        """
        TokenClass.__init__(self, db_token)
        self.set_type("motp")
        self.hKeyRequired = True
        return

    @log_with(log)
    def get_init_detail(self, params=None, user=None):
        """
        to complete the token normalisation, the response of the initialization
        should be built by the token specific method, the getInitDetails
        """
        response_detail = TokenClass.get_init_detail(self, params, user)
        otpkey = self.init_details.get('otpkey')
        if otpkey:
            if user is not None:
                try:
                    motp_url = create_motp_url(otpkey,
                                               user.login, user.realm,
                                               serial=self.get_serial())
                    response_detail["motpurl"] = {"description": _("URL for mOTP "
                                                                   "token"),
                                                  "value": motp_url,
                                                  "img": create_img(motp_url)
                                                  }
                except Exception as ex:   # pragma: no cover
                    log.debug("{0!s}".format(traceback.format_exc()))
                    log.error('failed to set motp url: {0!r}'.format(ex))

        return response_detail

    @log_with(log)
    def update(self, param, reset_failcount=True):
        """
        update - process initialization parameters

        :param param: dict of initialization parameters
        :type param: dict

        :return: nothing
        """
        if self.hKeyRequired is True:
            genkey = is_true(getParam(param, "genkey", optional))
            if not param.get('keysize'):
                param['keysize'] = 16
            if genkey:
                otpKey = generate_otpkey(param['keysize'])
                del param['genkey']
            else:
                # genkey not set: check otpkey is given
                # this will raise an exception if otpkey is not present
                otpKey = getParam(param, "otpkey", required)

            param['otpkey'] = otpKey

        # motp token specific
        mOTPPin = getParam(param, "motppin", required)
        self.token.set_user_pin(mOTPPin)

        TokenClass.update(self, param, reset_failcount)

        return

    @log_with(log)
    @check_token_locked
    def check_otp(self, anOtpVal, counter=None, window=None, options=None):
        """
        validate the token otp against a given otpvalue

        :param anOtpVal: the to be verified otpvalue
        :type anOtpVal:  str
        :param counter: the counter state, that should be verified
        :type counter: int
        :param window: the counter +window, which should be checked
        :type window: int
        :param options: the dict, which could contain token specific info
        :type options: dict
        :return: the counter state or -1
        :rtype: int
        """
        otplen = self.token.otplen

        # otime contains the previous verification time
        # the new one must be newer than this!
        oCount = self.get_otp_count()
        secretHOtp = self.token.get_otpkey()
        window = self.token.count_window
        secretPin = self.token.get_user_pin()

        log.debug("original counter %s", oCount)

        mtimeOtp = mTimeOtp(secretHOtp, secretPin, oCount, otplen)
        res = mtimeOtp.checkOtp(anOtpVal, window, options=options)

        if res != -1 and oCount != 0 and res <= oCount:
            log.warning("a previous OTP value was used again! former "
                        "tokencounter: %i, presented counter %i" %
                        (oCount, res))
            res = -1
            return res

        if res != -1:
            # on success, we have to save the last attempt
            self.set_otp_count(res)

        return res
