# w.c.s. - web application for online forms
# Copyright (C) 2005-2013  Entr'ouvert
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, see <http://www.gnu.org/licenses/>.

import base64
import hmac
import hashlib
import datetime
import random
import os
import errno
import calendar

from django.utils import six
from django.utils.encoding import force_bytes, force_text
from django.utils.six.moves.urllib import parse as urllib
from django.utils.six.moves.urllib import parse as urlparse

from quixote import get_request, get_publisher
from .qommon.errors import (AccessForbiddenError, UnknownNameIdAccessForbiddenError)
import qommon.misc

DEFAULT_DURATION = 30


def is_url_signed(utcnow=None, duration=DEFAULT_DURATION):
    if get_request().signed:
        return True
    query_string = get_request().get_query()
    if not query_string:
        return False
    signature = get_request().form.get('signature')
    if not isinstance(signature, six.string_types):
        return False
    signature = force_bytes(signature)
    # verify signature
    orig = get_request().form.get('orig')
    if not isinstance(orig, six.string_types):
        raise AccessForbiddenError('missing/multiple orig field')
    key = get_publisher().get_site_option(orig, 'api-secrets')
    if not key:
        raise AccessForbiddenError('invalid orig')
    algo = get_request().form.get('algo')
    if not isinstance(algo, six.string_types):
        raise AccessForbiddenError('missing/multiple algo field')
    try:
        algo = getattr(hashlib, algo)
    except AttributeError:
        raise AccessForbiddenError('invalid algo')
    if signature != base64.standard_b64encode(
            hmac.new(force_bytes(key), force_bytes(query_string[:query_string.find('&signature=')]), algo).digest()):
        raise AccessForbiddenError('invalid signature')
    timestamp = get_request().form.get('timestamp')
    if not isinstance(timestamp, six.string_types):
        raise AccessForbiddenError('missing/multiple timestamp field')
    try:
        timestamp = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
    except ValueError as e:
        raise AccessForbiddenError('invalid timestamp field; %s' % e)
    delta = (utcnow or datetime.datetime.utcnow()).replace(tzinfo=None) - timestamp
    if abs(delta) > datetime.timedelta(seconds=duration):
        raise AccessForbiddenError('timestamp delta is more than %s seconds: %s'
                                   % (duration, delta))
    # check nonce
    nonce = get_request().form.get('nonce')
    if nonce:
        # compute end date of the nonce as an unix timestamp
        until = timestamp + datetime.timedelta(seconds=duration)
        until = calendar.timegm(until.timetuple())
        # normalize nonce
        nonce = nonce[:128]
        nonce = qommon.misc.simplify(nonce).replace('/', '-')
        nonce_dir = os.path.join(get_publisher().app_dir, 'nonces')
        nonce_path = os.path.join(nonce_dir, nonce)
        try:
            try:
                if not os.path.exists(nonce_dir):
                    os.makedirs(nonce_dir)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    raise
            os.close(os.open(nonce_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY))
            # touch file in the future for cleaning_nonces
            os.utime(nonce_path, (until, until))
        except OSError as e:
            if e.errno != errno.EEXIST:
                raise
            raise AccessForbiddenError('nonce already used')
    get_request().signed = True
    return True


def get_user_from_api_query_string(api_name=None):
    auth_header = get_request().get_header('Authorization', '')
    if auth_header and api_name:
        if not auth_header.startswith('Basic '):
            # we do not handle other authentication schemes
            raise AccessForbiddenError('unhandled authorization header')
        auth_header = auth_header.split(' ', 1)[1]
        username, password = force_text(base64.decodestring(force_bytes(auth_header))).split(':', 1)
        configured_password = get_publisher().get_site_option(
                username, section='api-http-auth-%s' % api_name)
        if configured_password != password:
            raise AccessForbiddenError('invalid authorization')
    elif not is_url_signed():
        return None
    # Signature or auth header are ok.
    # Look for the user, by email/NameID.
    user = None
    if get_request().form.get('email'):
        email = get_request().form.get('email')
        if not isinstance(email, six.string_types):
            raise AccessForbiddenError('multiple email field')
        users = list(get_publisher().user_class.get_users_with_email(email))
        if users:
            user = users[0]
        else:
            raise AccessForbiddenError('unknown email')
    elif get_request().form.get('NameID'):
        ni = get_request().form.get('NameID')
        if not isinstance(ni, six.string_types):
            raise AccessForbiddenError('multiple NameID field')
        users = list(get_publisher().user_class.get_users_with_name_identifier(ni))
        if users:
            user = users[0]
        else:
            raise UnknownNameIdAccessForbiddenError('unknown NameID')
    elif 'email' in get_request().form or 'NameID' in get_request().form:
        # email or NameID were given as empty to the query string, this maps
        # the anonymous user case.
        return False

    return user


def sign_url(url, key, algo='sha256', timestamp=None, nonce=None):
    parsed = urlparse.urlparse(url)
    new_query = sign_query(parsed.query, key, algo, timestamp, nonce)
    return urlparse.urlunparse(parsed[:4] + (new_query,) + parsed[5:])


def sign_query(query, key, algo='sha256', timestamp=None, nonce=None):
    if timestamp is None:
        timestamp = datetime.datetime.utcnow()
    timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%SZ')
    if nonce is None:
        nonce = hex(random.getrandbits(128))[2:-1]
    new_query = query
    if new_query:
        new_query += '&'
    new_query += urllib.urlencode((
        ('algo', algo),
        ('timestamp', timestamp),
        ('nonce', nonce)))
    signature = base64.b64encode(sign_string(new_query, key, algo=algo))
    new_query += '&signature=' + urllib.quote(signature)
    return new_query


def sign_string(s, key, algo='sha256', timedelta=30):
    digestmod = getattr(hashlib, algo)
    hash = hmac.HMAC(force_bytes(key), digestmod=digestmod, msg=force_bytes(s))
    return hash.digest()


class MissingSecret(Exception):
    pass


def get_secret_and_orig(url):
    frontoffice_url = get_publisher().get_frontoffice_url()
    orig = urlparse.urlparse(frontoffice_url).netloc.rsplit('@', 1)[-1].rsplit(':', 1)[0]
    target_orig = urlparse.urlparse(url).netloc.rsplit('@', 1)[-1].rsplit(':', 1)[0]
    secret = get_publisher().get_site_option(target_orig, 'wscall-secrets')
    if not secret:
        raise MissingSecret()
    return secret, orig

def sign_url_auto_orig(url):
    try:
        signature_key, orig = get_secret_and_orig(url)
    except MissingSecret:
        return url
    parsed = urlparse.urlparse(url)
    querystring = urlparse.parse_qsl(parsed.query)
    querystring.append(('orig', orig))
    querystring = urllib.urlencode(querystring)
    url = urlparse.urlunparse(parsed[:4] + (querystring,) + parsed[5:6])
    return sign_url(url, signature_key)


def get_query_flag(flag, default=False):
    value = get_request().form.get(flag)
    if value in (True, 'True', 'true', 'on', '1'):
        return True
    if value in (False, 'False', 'false', 'off', '0'):
        return False
    return default
