# encoding: utf8

from __future__ import absolute_import, division, print_function, unicode_literals

from base64 import urlsafe_b64encode, urlsafe_b64decode
from datetime import datetime, timedelta

from aspen import Response, json
from aspen.utils import to_rfc822, utcnow
from dependency_injection import resolve_dependencies
from postgres.cursors import SimpleCursorBase

import gratipay


BEGINNING_OF_EPOCH = to_rfc822(datetime(1970, 1, 1)).encode('ascii')

# Difference between current time and credit card expiring date when
# card is considered as expiring
EXPIRING_DELTA = timedelta(days = 30)


def dict_to_querystring(mapping):
    if not mapping:
        return u''

    arguments = []
    for key, values in mapping.iteritems():
        for val in values:
            arguments.append(u'='.join([key, val]))

    return u'?' + u'&'.join(arguments)


def use_tildes_for_participants(website, request):
    if request.path.raw.startswith('/~/'):
        to = '/~' + request.path.raw[3:]
        if request.qs.raw:
            to += '?' + request.qs.raw
        website.redirect(to)
    elif request.path.raw.startswith('/~'):
        request.path.__init__('/~/' + request.path.raw[2:])


def canonicalize(redirect, path, base, canonical, given, arguments=None):
    if given != canonical:
        assert canonical.lower() == given.lower()  # sanity check
        remainder = path[len(base + given):]

        if arguments is not None:
            arguments = dict_to_querystring(arguments)

        newpath = base + canonical + remainder + arguments or ''
        redirect(newpath)


def get_participant(state, restrict=True, resolve_unclaimed=True):
    """Given a Request, raise Response or return Participant.

    If restrict is True then we'll restrict access to owners and admins.

    """
    redirect = state['website'].redirect
    request = state['request']
    user = state['user']
    slug = request.line.uri.path['username']
    qs = request.line.uri.querystring
    _ = state['_']

    if restrict:
        if user.ANON:
            raise Response(403, _("You need to log in to access this page."))

    from gratipay.models.participant import Participant  # avoid circular import
    participant = Participant.from_username(slug)

    if participant is None:
        raise Response(404)

    canonicalize(redirect, request.line.uri.path.raw, '/~/', participant.username, slug, qs)

    if participant.is_closed:
        if user.ADMIN:
            return participant
        raise Response(410)

    if participant.claimed_time is None and resolve_unclaimed:
        to = participant.resolve_unclaimed()
        if to:
            # This is a stub account (someone on another platform who hasn't
            # actually registered with Gratipay yet)
            redirect(to)
        else:
            # This is an archived account (result of take_over)
            if user.ADMIN:
                return participant
            raise Response(404)

    if restrict:
        if participant != user.participant:
            if not user.ADMIN:
                raise Response(403, _("You are not authorized to access this page."))

    return participant


def get_team(state):
    """Given a Request, raise Response or return Team.
    """
    redirect = state['website'].redirect
    request = state['request']
    user = state['user']
    slug = request.line.uri.path['team']
    qs = request.line.uri.querystring

    from gratipay.models.team import Team  # avoid circular import
    team = Team.from_slug(slug)

    if team is None:
        # Try to redirect to a Participant.
        from gratipay.models.participant import Participant # avoid circular import
        participant = Participant.from_username(slug)
        if participant is not None:
            qs = '?' + request.qs.raw if request.qs.raw else ''
            redirect('/~' + request.path.raw[1:] + qs)
        raise Response(404)

    canonicalize(redirect, request.line.uri.path.raw, '/', team.slug, slug, qs)

    if team.is_closed and not user.ADMIN:
        raise Response(410)

    return team


def encode_for_querystring(s):
    """Given a unicode, return a unicode that's safe for transport across a querystring.
    """
    if not isinstance(s, unicode):
        raise TypeError('unicode required')
    return urlsafe_b64encode(s.encode('utf8')).replace(b'=', b'~').decode('ascii')


def decode_from_querystring(s, **kw):
    """Given a unicode computed by encode_for_querystring, return the inverse.

    We raise Response(400) if the input value can't be decoded (i.e., it's not
    ASCII, not padded properly, or not decodable as UTF-8 once Base64-decoded).

    """
    if not isinstance(s, unicode):
        raise TypeError('unicode required')
    try:
        return urlsafe_b64decode(s.encode('ascii').replace(b'~', b'=')).decode('utf8')
    except:
        if 'default' in kw:
            # Enable callers to handle errors without using try/except.
            return kw['default']
        raise Response(400, "invalid input")


def update_cta(website):
    nusers = website.db.one("""
        SELECT nusers FROM paydays
        ORDER BY ts_end DESC LIMIT 1
    """, default=0)
    nreceiving_from = website.db.one("""
        SELECT nreceiving_from
          FROM teams
         WHERE slug = 'Gratipay'
    """, default=0)
    website.support_current = cur = int(round(nreceiving_from / nusers * 100)) if nusers else 0
    if cur < 10:    goal = 20
    elif cur < 15:  goal = 30
    elif cur < 25:  goal = 40
    elif cur < 35:  goal = 50
    elif cur < 45:  goal = 60
    elif cur < 55:  goal = 70
    elif cur < 65:  goal = 80
    elif cur > 70:  goal = None
    website.support_goal = goal


def _execute(this, sql, params=[]):
    print(sql.strip(), params)
    super(SimpleCursorBase, this).execute(sql, params)

def log_cursor(f):
    "Prints sql and params to stdout. Works globaly so watch for threaded use."
    def wrapper(*a, **kw):
        try:
            SimpleCursorBase.execute = _execute
            ret = f(*a, **kw)
        finally:
            del SimpleCursorBase.execute
        return ret
    return wrapper


def format_money(money):
    format = '%.2f' if money < 1000 else '%.0f'
    return format % money


def excerpt_intro(text, length=175, append=u'…'):
    if not text:
        return ''
    if len(text) > length:
        return text[:length] + append
    return text


def is_card_expiring(expiration_year, expiration_month):
    now = datetime.utcnow()
    expiring_date = datetime(expiration_year, expiration_month, 1)
    delta = expiring_date - now
    return delta < EXPIRING_DELTA


def set_cookie(cookies, key, value, expires=None, httponly=True, path=b'/'):
    cookies[key] = value
    cookie = cookies[key]
    if expires:
        if isinstance(expires, timedelta):
            expires += utcnow()
        if isinstance(expires, datetime):
            expires = to_rfc822(expires).encode('ascii')
        cookie[b'expires'] = expires
    if httponly:
        cookie[b'httponly'] = True
    if path:
        cookie[b'path'] = path
    if gratipay.use_secure_cookies:
        cookie[b'secure'] = True


def erase_cookie(cookies, key, **kw):
    set_cookie(cookies, key, '', BEGINNING_OF_EPOCH, **kw)


def filter_profile_nav(user, participant, pages):
    out = []
    for foo, bar, show_them, show_others in pages:
        if (user.participant == participant and show_them) \
        or (user.participant != participant and show_others) \
        or user.ADMIN:
            out.append((foo, bar, show_them, show_others))
    return out


def to_javascript(obj):
    """For when you want to inject an object into a <script> tag.
    """
    return json.dumps(obj).replace('</', '<\\/')


class LazyResponse(Response):

    def __init__(self, code, lazy_body, **kw):
        Response.__init__(self, code, '', **kw)
        self.lazy_body = lazy_body

    def render_body(self, state):
        f = self.lazy_body
        self.body = f(*resolve_dependencies(f, state).as_args)

gratipay/utils/__init__.py
# encoding: utf8

from __future__ import absolute_import, division, print_function, unicode_literals

from base64 import urlsafe_b64encode, urlsafe_b64decode
from datetime import datetime, timedelta

from aspen import Response, json
from aspen.utils import to_rfc822, utcnow
from dependency_injection import resolve_dependencies
from postgres.cursors import SimpleCursorBase

import gratipay


BEGINNING_OF_EPOCH = to_rfc822(datetime(1970, 1, 1)).encode('ascii')

# Difference between current time and credit card expiring date when
# card is considered as expiring
EXPIRING_DELTA = timedelta(days = 30)


def dict_to_querystring(mapping):
    if not mapping:
        return u''

    arguments = []
    for key, values in mapping.iteritems():
        for val in values:
            arguments.append(u'='.join([key, val]))

    return u'?' + u'&'.join(arguments)


def use_tildes_for_participants(website, request):
    if request.path.raw.startswith('/~/'):
        to = '/~' + request.path.raw[3:]
        if request.qs.raw:
            to += '?' + request.qs.raw
        website.redirect(to)
    elif request.path.raw.startswith('/~'):
        request.path.__init__('/~/' + request.path.raw[2:])


def canonicalize(redirect, path, base, canonical, given, arguments=None):
    if given != canonical:
        assert canonical.lower() == given.lower()  # sanity check
        remainder = path[len(base + given):]

        if arguments is not None:
            arguments = dict_to_querystring(arguments)

        newpath = base + canonical + remainder + arguments or ''
        redirect(newpath)


def get_participant(state, restrict=True, resolve_unclaimed=True):
    """Given a Request, raise Response or return Participant.

    If restrict is True then we'll restrict access to owners and admins.

    """
    redirect = state['website'].redirect
    request = state['request']
    user = state['user']
    slug = request.line.uri.path['username']
    qs = request.line.uri.querystring
    _ = state['_']

    if restrict:
        if user.ANON:
            raise Response(401, _("You need to log in to access this page."))

    from gratipay.models.participant import Participant  # avoid circular import
    participant = Participant.from_username(slug)

    if participant is None:
        raise Response(404)

    canonicalize(redirect, request.line.uri.path.raw, '/~/', participant.username, slug, qs)

    if participant.is_closed:
        if user.ADMIN:
            return participant
        raise Response(410)

    if participant.claimed_time is None and resolve_unclaimed:
        to = participant.resolve_unclaimed()
        if to:
            # This is a stub account (someone on another platform who hasn't
            # actually registered with Gratipay yet)
            redirect(to)
        else:
            # This is an archived account (result of take_over)
            if user.ADMIN:
                return participant
            raise Response(404)

    if restrict:
        if participant != user.participant:
            if not user.ADMIN:
                raise Response(403, _("You are not authorized to access this page."))

    return participant


def get_team(state):
    """Given a Request, raise Response or return Team.
    """
    redirect = state['website'].redirect
    request = state['request']
    user = state['user']
    slug = request.line.uri.path['team']
    qs = request.line.uri.querystring

    from gratipay.models.team import Team  # avoid circular import
    team = Team.from_slug(slug)

    if team is None:
        # Try to redirect to a Participant.
        from gratipay.models.participant import Participant # avoid circular import
        participant = Participant.from_username(slug)
        if participant is not None:
            qs = '?' + request.qs.raw if request.qs.raw else ''
            redirect('/~' + request.path.raw[1:] + qs)
        raise Response(404)

    canonicalize(redirect, request.line.uri.path.raw, '/', team.slug, slug, qs)

    if team.is_closed and not user.ADMIN:
        raise Response(410)

    return team


def encode_for_querystring(s):
    """Given a unicode, return a unicode that's safe for transport across a querystring.
    """
    if not isinstance(s, unicode):
        raise TypeError('unicode required')
    return urlsafe_b64encode(s.encode('utf8')).replace(b'=', b'~').decode('ascii')


def decode_from_querystring(s, **kw):
    """Given a unicode computed by encode_for_querystring, return the inverse.

    We raise Response(400) if the input value can't be decoded (i.e., it's not
    ASCII, not padded properly, or not decodable as UTF-8 once Base64-decoded).

    """
    if not isinstance(s, unicode):
        raise TypeError('unicode required')
    try:
        return urlsafe_b64decode(s.encode('ascii').replace(b'~', b'=')).decode('utf8')
    except:
        if 'default' in kw:
            # Enable callers to handle errors without using try/except.
            return kw['default']
        raise Response(400, "invalid input")


def update_cta(website):
    nusers = website.db.one("""
        SELECT nusers FROM paydays
        ORDER BY ts_end DESC LIMIT 1
    """, default=0)
    nreceiving_from = website.db.one("""
        SELECT nreceiving_from
          FROM teams
         WHERE slug = 'Gratipay'
    """, default=0)
    website.support_current = cur = int(round(nreceiving_from / nusers * 100)) if nusers else 0
    if cur < 10:    goal = 20
    elif cur < 15:  goal = 30
    elif cur < 25:  goal = 40
    elif cur < 35:  goal = 50
    elif cur < 45:  goal = 60
    elif cur < 55:  goal = 70
    elif cur < 65:  goal = 80
    elif cur > 70:  goal = None
    website.support_goal = goal


def _execute(this, sql, params=[]):
    print(sql.strip(), params)
    super(SimpleCursorBase, this).execute(sql, params)

def log_cursor(f):
    "Prints sql and params to stdout. Works globaly so watch for threaded use."
    def wrapper(*a, **kw):
        try:
            SimpleCursorBase.execute = _execute
            ret = f(*a, **kw)
        finally:
            del SimpleCursorBase.execute
        return ret
    return wrapper


def format_money(money):
    format = '%.2f' if money < 1000 else '%.0f'
    return format % money


def excerpt_intro(text, length=175, append=u'…'):
    if not text:
        return ''
    if len(text) > length:
        return text[:length] + append
    return text


def is_card_expiring(expiration_year, expiration_month):
    now = datetime.utcnow()
    expiring_date = datetime(expiration_year, expiration_month, 1)
    delta = expiring_date - now
    return delta < EXPIRING_DELTA


def set_cookie(cookies, key, value, expires=None, httponly=True, path=b'/'):
    cookies[key] = value
    cookie = cookies[key]
    if expires:
        if isinstance(expires, timedelta):
            expires += utcnow()
        if isinstance(expires, datetime):
            expires = to_rfc822(expires).encode('ascii')
        cookie[b'expires'] = expires
    if httponly:
        cookie[b'httponly'] = True
    if path:
        cookie[b'path'] = path
    if gratipay.use_secure_cookies:
        cookie[b'secure'] = True


def erase_cookie(cookies, key, **kw):
    set_cookie(cookies, key, '', BEGINNING_OF_EPOCH, **kw)


def filter_profile_nav(user, participant, pages):
    out = []
    for foo, bar, show_them, show_others in pages:
        if (user.participant == participant and show_them) \
        or (user.participant != participant and show_others) \
        or user.ADMIN:
            out.append((foo, bar, show_them, show_others))
    return out


def to_javascript(obj):
    """For when you want to inject an object into a <script> tag.
    """
    return json.dumps(obj).replace('</', '<\\/')


class LazyResponse(Response):

    def __init__(self, code, lazy_body, **kw):
        Response.__init__(self, code, '', **kw)
        self.lazy_body = lazy_body

    def render_body(self, state):
        f = self.lazy_body
        self.body = f(*resolve_dependencies(f, state).as_args)

gratipay/utils/__init__.py
diff --git a/gratipay/utils/__init__.py b/gratipay/utils/__init__.py
index e5d6f7dfb7..d61f9d852a 100644
--- a/gratipay/utils/__init__.py
+++ b/gratipay/utils/__init__.py
@@ -69,7 +69,7 @@ def get_participant(state, restrict=True, resolve_unclaimed=True):
 
     if restrict:
         if user.ANON:
-            raise Response(403, _("You need to log in to access this page."))
+            raise Response(401, _("You need to log in to access this page."))
 
     from gratipay.models.participant import Participant  # avoid circular import
     participant = Participant.from_username(slug)
diff --git a/tests/py/test_email.py b/tests/py/test_email.py
index 02f3e352fc..e8da9bc9cf 100644
--- a/tests/py/test_email.py
+++ b/tests/py/test_email.py
@@ -74,9 +74,9 @@ def test_adding_second_email_sends_verification_notice(self):
         expected = "We are connecting alice2@example.com to the alice account on Gratipay"
         assert expected in last_email['body_text']
 
-    def test_post_anon_returns_403(self):
+    def test_post_anon_returns_401(self):
         response = self.hit_email_spt('add-email', 'anon@gratipay.com', user=None, should_fail=True)
-        assert response.code == 403
+        assert response.code == 401
 
     def test_post_with_no_at_symbol_is_400(self):
         response = self.hit_email_spt('add-email', 'gratipay.com', should_fail=True)
diff --git a/tests/py/test_identity_pages.py b/tests/py/test_identity_pages.py
index 32d935b51c..5acb720776 100644
--- a/tests/py/test_identity_pages.py
+++ b/tests/py/test_identity_pages.py
@@ -33,11 +33,11 @@ def verify(self, username, *codes):
 
     # il - identities listing
 
-    def test_il_is_403_for_anon(self):
-        assert self.client.GxT('/~bob/identities/').code == 403
+    def test_il_is_401_for_anon(self):
+        assert self.client.GxT('/~bob/identities/').code == 401
 
-    def test_il_is_403_for_non_admin(self):
-        assert self.client.GxT('/~bob/identities/').code == 403
+    def test_il_is_401_for_non_admin(self):
+        assert self.client.GxT('/~bob/identities/').code == 401
 
     def test_il_is_200_for_self(self):
         assert self.client.GET('/~bob/identities/', auth_as='bob').code == 200
@@ -51,11 +51,11 @@ def test_il_is_200_for_admin(self):
     def test_ip_disallows_methods(self):
         assert self.client.hxt('HEAD', '/~bob/identities/TT').code == 405
 
-    def test_ip_is_403_for_anon(self):
-        assert self.client.GxT('/~bob/identities/TT').code == 403
+    def test_ip_is_401_for_anon(self):
+        assert self.client.GxT('/~bob/identities/TT').code == 401
 
-    def test_ip_is_403_for_non_admin(self):
-        assert self.client.GxT('/~bob/identities/TT').code == 403
+    def test_ip_is_401_for_non_admin(self):
+        assert self.client.GxT('/~bob/identities/TT').code == 401
 
     def test_ip_is_200_for_self(self):
         assert self.client.GET('/~bob/identities/TT', auth_as='bob').code == 200
diff --git a/tests/py/test_statement_json.py b/tests/py/test_statement_json.py
index de56c9b929..80edfd5093 100644
--- a/tests/py/test_statement_json.py
+++ b/tests/py/test_statement_json.py
@@ -23,13 +23,13 @@ def test_participant_can_change_their_statement(self):
         actual = json.loads(response.body)['html']
         assert actual == '<p>Lorem ipsum</p>\n'
 
-    def test_anonymous_gets_403(self):
+    def test_anonymous_gets_401(self):
         response = self.change_statement( 'en'
                                         , 'Some statement'
                                         , auth_as=None
                                         , expecting_error=True
                                          )
-        assert response.code == 403, response.code
+        assert response.code == 401, response.code
 
     def test_participant_cannot_change_their_statement_if_too_big(self):
         too_big_statement = 'A' * ((256 * 1024) + 1)
diff --git a/tests/py/test_username_json.py b/tests/py/test_username_json.py
index 4e5c7e0349..92c214bdf9 100644
--- a/tests/py/test_username_json.py
+++ b/tests/py/test_username_json.py
@@ -21,9 +21,9 @@ def test_participant_can_change_their_username(self):
         assert code == 200
         assert body['username'] == "bob"
 
-    def test_anonymous_gets_403(self):
+    def test_anonymous_gets_401(self):
         code, body = self.change_username("bob", auth_as=None)
-        assert code == 403
+        assert code == 401
 
     def test_empty(self):
         code, body = self.change_username('      ')

403 clicking "fix credit card" in email when not logged in
My credit card expired and I got the email reminding me to fix payment info. I clicked the "fix credit card" button in the email and was taken to a 403 Forbidden page. Would expect to be taken to login form when I'm not already logged in. Thanks!

