#!/usr/bin/env python3

import os
import signal
import time
import json
import random
import argparse
import subprocess
import dateparser

import urllib
import mimetypes
import asyncio
from aiohttp import web, WSCloseCode

import webshoes as ws
import propertybag as pb
import sparen
Log = sparen.log

import authwert

#-------------------------------------------------------------------
# Authentication

async def authVerify(ctx, q):

    _p = ctx.opts._p

    si = None
    try:
        if _p.cookieid in ctx.req.cookies:
            si = pb.Bag(authwert.getSesionInfo(ctx.req.cookies[_p.cookieid], _p))
    except Exception as e:
        Log(e)

    if not si:
        return web.Response(text='Access Denied', status=401)

    if _p.verbose:
        Log(f'Logged in: {si} : {ctx.req.path}')

    return web.Response(text='ok', status=200)



def authResp(code, p=None, page=None, loc=None):
    if not page:
        page = 'web/login.html'
    h = {}
    if loc and p:
        location = loc + '?' + urllib.parse.urlencode(p)
        Log(f'Location: {location}')
        h['Location'] = location
        h['Content-Location'] = location

    return web.FileResponse(authwert.libPath(page), status=code, headers=h)


async def authLogin(ctx, q):

    _p = ctx.opts._p

    if ctx.req.body_exists:
        m = await ctx.req.post()
        for k in set(m.keys()):
            q[k] = m[k]

    # Get session info
    si = None
    try:
        if _p.cookieid in ctx.req.cookies:
            si = pb.Bag(authwert.getSesionInfo(ctx.req.cookies[_p.cookieid], _p))
    except Exception as e:
        Log(e)

    # Log out user if requested
    if q.logout:
        if si:
            Log(f'Logging out : {si.username} : {si.sid}')
            if si.sid and si.sid in _p.sessions:
                del _p.sessions[si.sid]
            r = authResp(200, {'rd':q.rd})
            r.set_cookie(_p.cookieid, '', domain=_p.domain, max_age=0, secure=True, samesite='Strict')
            return r

    # Is user logged in?
    if si:
        return authResp(200, {}, 'web/logout.html')

    # Default redirect
    if not q.rd:
        if _p.rootpath:
            q.rd = f'{_p.rootpath}/login'
        else:
            q.rd = '/'

    if _p.verbose:
        Log(f'Redirect: {q.rd}')

    # Is user trying to login?
    if q.username and q.password:

        verified = False

        if _p.userinf and q.username in _p.userinf:
            if q.password == _p.userinf[q.username]['password']:
                verified = True
        elif _p.userinf and '*' in _p.userinf:
            if q.password == _p.userinf['*']['password']:
                verified = True

        # if not verified:
        if not verified and _p.dbauth:
            if _p.dbauth.verify(_p, q.username, q.password):
                verified = True

        if not verified:
            Log(f'Login failed : {q.username}')
            r = authResp(403, {'rd':q.rd, 'error':'Invalid username or password'}, None, f'{_p.rootpath}/login')
            r.set_cookie("login_error", "Invalid username or password", domain=_p.domain, max_age=3, secure=True, samesite='Strict')
            return r

        # Session id
        sid = ''.join(random.choice('1324567890ABCDEFGHIJKLMNOPQRSTUVWXYZ') for i in range(32))

        # Cookie expiration
        exp = round(time.time() + _p.exptime)

        try:

            # JWT
            if _p.prvpem:
                claims = {
                        "username"  : q.username,
                        "exp"       : exp,
                        "sid"       : sid
                    }
                cookie = authwert.createSessionToken(claims, _p)

            # Session id tokens
            else:
                cookie = sid
                _p.sessions[sid] = {'username': q.username, 'exp': exp, 'sid': sid}

            Log(f'Logging in : {_p.domain} : {q.username}')

            r = web.HTTPSeeOther(q.rd)
            r.set_cookie(_p.cookieid, cookie, domain=_p.domain, max_age=_p.exptime, secure=True, samesite='Strict')
            return r

        except Exception as e:
            Log(e)

    return authResp(200, {'rd':q.rd})


#-------------------------------------------------------------------
# Debug
async def debugSite(ctx, q):

    p = ctx.req.path.split('/')
    while len(p) and not p[0]:
        p = p[1:]
    fname = '/'.join(p[1:])

    p = os.path.join(ctx.opts._p.debug, fname)
    if not os.path.isfile(p):
        p = os.path.join(p, 'index.html')
        if os.path.isfile(p):
            return web.HTTPSeeOther(os.path.join(ctx.req.path, 'index.html'))
        return web.Response(text='Not Found', status=404)

    mime = mimetypes.guess_type(p)[0]
    if not mime:
        mime = 'text/plain'

    if ctx.opts._p.verbose:
        Log(f'[{mime}] {p}')

    return web.FileResponse(p, headers={"Content-Type":mime})


#-------------------------------------------------------------------
# Main

async def run(_p):

    # Web server
    _p.wsa = ws.WebShoesApp(_p.addr, _p.port, {'_p': _p, 'verbose': _p.verbose})

    # Test server
    if _p.debug:
        _p.wsa.register('debug', 'cmd', 'q', 'evt', 'r', {
                '*'    : debugSite
            })

    # Authentication
    _p.wsa.register('auth', 'cmd', 'q', 'evt', 'r', {
            'verify'    : authVerify,
            'login'     : authLogin
        })

    _p.wsa.start()

    # Idle
    while _p.run:
        await asyncio.sleep(3)


def main(_p):

    ap = argparse.ArgumentParser()
    ap.add_argument('--domain', '-d', default='', type=str, help='Domain name')
    ap.add_argument('--rootpath', '-r', default='', type=str, help='Root Path')
    ap.add_argument('--buildver', '-b', default='', type=str, help='Build Version')
    ap.add_argument('--addr', '-a', default='127.0.0.1', type=str, help='Server address')
    ap.add_argument('--port', '-p', default=18401, type=int, help='Server port')
    ap.add_argument('--logdir', '-l', default='', type=str, help='Default log directory')
    ap.add_argument('--logfile', '-L', default='', type=str, help='Default log directory')
    ap.add_argument('--verbose', '-V', action='store_true', help='Verbose mode')
    ap.add_argument('--debug', '-D', default='', type=str, help='Debug site')
    ap.add_argument('--cookieid', '-k', default='', type=str, help='cookieid')
    ap.add_argument('--userinf', '-u', default='', type=str, help='User information')
    ap.add_argument('--scheme', '-s', default='https', type=str, help='Network Scheme')
    ap.add_argument('--authfile', default='', type=str, help='Python authorize file')
    ap.add_argument('--authparams', default='', type=str, help='Parameters to pass to auth file')
    ap.add_argument('--prvkey', default='', type=str, help='File containing private key to sign JWT tokens')
    ap.add_argument('--exptime', default=0, type=int, help='Login expire time in seconds')
    ap.add_argument('--expstr', default="after 6 days", type=str, help='Login expire time as string')
    ap.add_argument('--userlist', default=False, type=bool, help='Set to always maintain an internal list of active users')
    ap.add_argument('--algorithm', default='RS256', type=str, help='Private key algorithm')

    _p.merge(vars(ap.parse_args()))

    if _p.logdir:
        if not os.path.isdir(_p.logdir):
            os.mkdir(_p.logdir)
        if not _p.logfile:
            _p.logfile = os.path.join(_p.logdir, 'server.log')
    if _p.logfile:
        sparen.log.setLogFile(_p.logfile)

    if not _p.domain:
        raise Exception('domain name (--domain) must be provided')

    if not _p.cookieid:
        raise Exception('Cookie id/name (--cookieid) must be provided')

    if not _p.rootpath:
        if _p.port:
            _p.rootpath = f'{_p.scheme}://{_p.domain}:{_p.port}/auth'
        else:
            _p.rootpath = f'{_p.scheme}://{_p.domain}/auth'
        # _p.rootdomain = '.'.join(_p.domain.split('.')[-2:])

    deft =  6 * 24 * 60 * 60
    maxt = 90 * 24 * 60 * 60
    if 10 > _p.exptime or maxt < _p.exptime:
        if _p.expstr:
            dt = dateparser.parse(_p.expstr)
            _p.exptime = int(dt.timestamp() - time.time() + 1)
        if 10 > _p.exptime or maxt < _p.exptime:
            _p.exptime = deft

    # Unique instance id
    _p.iid = ''.join(random.choice('1324567890ABCDEFGHIJKLMNOPQRSTUVWXYZ') for i in range(32))

    # Custom auth
    if _p.authfile:
        if '!' == _p.authfile[0]:
            _p.authfile = authwert.libPath(_p.authfile[1:])
        if os.path.isfile(_p.authfile):
            import importlib.util
            spec = importlib.util.spec_from_file_location("authfile",_p.authfile)
            _p.dbauth = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(_p.dbauth)
            _p.dbauth.init(_p)

    Log("Parameters: ", _p)

    _p.sessions = {}

    # Read the private key
    if _p.prvkey:
        if not authwert.readPrivateKey(_p.prvkey, _p):
            raise Exception(f'Failed to read private key from : {_p.prvkey}')

    # Get user info
    _p.userinf = _p.userinf.strip()
    if _p.userinf:
        if '{' == _p.userinf[0]:
            _p.userinf = json.loads(_p.userinf)
        elif os.path.isfile(_p.userinf):
            with open(_p.userinf) as f:
                _p.userinf = pb.Bag(json.loads(f.read()))
    if not _p.userinf:
        _p.userinf = {}

    _p.run = True
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    loop.run_until_complete(run(_p))


if __name__ == '__main__':
    try:
        _p = pb.Bag({'threads': {}})
        main(_p)

    except KeyboardInterrupt:
        Log(" ~ keyboard ~ ")
    except Exception as e:
        Log(" ~ exception ~ ", e)
    finally:
        if _p.dbauth:
            _p.dbauth.close(_p)
        if _p.wsa:
            _p.wsa.stop()
            del _p.wsa
        Log("Bye...")
