#!/usr/bin/env python
from http.server import BaseHTTPRequestHandler, HTTPServer
import os
import sys
import ssl
import secrets
import urllib
import subprocess
import datetime 

# server
bindHost = 'localhost'
serverPort = 11911

env = {
    'version': '0.1',
    # server options
    'bind': 'localhost',
    'port': 9801,
    # db options
    'source': '.',
    'include': '*',
    'separator': '',
    # security options
    'auth': False,
    'tls': False,
    'cert': './cert/awkward.crt',
    'key': './cert/awkward.key',
    'xtokenFile': './awkward.xtoken',
}

stat = {
    'startTime': datetime.datetime.now(),
    'hits':    0,
    'payload': 0,
    'upload':  0,
}

def option(key):
    if key in env:
        return env[key]
    else:
        return ''

def isEnabled(key):
    if key in env and env[key] == True:
        return True
    else:
        return False

def isDisabled(key):
    if key in env and env[key] == True:
        return False
    else:
        return True

def configure():
    key = ''
    n = len(sys.argv)
    for i in range(1,n):
        opt = sys.argv[i]

        if opt == '-h' or opt == '--help':
            help()
            exit(0)

        elif opt == '-n' or opt == '--version':
            version()
            exit(0)

        elif opt == '-v' or opt == '--verbose':
            env['verbose'] = True
        elif opt == '-e' or opt == '--trace':
            env['trace'] = True
        elif opt == '-s' or opt == '--source':
            key = 'source'
        elif opt == '-i' or opt == '--include':
            key = 'include'
        elif opt == '-f' or opt == '--separator':
            key = 'separator'
        elif opt == '-b' or opt == '--bind':
            key = 'bind'
        elif opt == '-p' or opt == '--port':
            key = 'port'
        elif opt == '-t' or opt == '--tls':
            env['tls'] = True
        elif opt == '-c' or opt == '--cert':
            key = 'cert'
        elif opt == '-k' or opt == '--key':
            key = 'key'
        elif opt == '-x' or opt == '--auth':
            env['auth'] = True
        else:
            if opt.startswith('-'):
                raise ValueError('Unknown option [' + opt + ']!')
            elif key != '':
                env[key] = opt
                key = ''
            else:
                raise ValueError('Illegal argument [' + opt + ']!')
    # check if there is a key expecting a value
    if key != '':
        raise ValueError('Value for [' + opt + '] is expected')

    if isEnabled('verbose'):
        print('===== configuration =====')
        for k in env:
            print(k + ':\t' + str(env[k]))
        print('=========================')

    return

def readToken():
    path = option('xtokenFile')
    if os.path.isfile(path):
        if isEnabled('verbose'):
            print('reading access token cached in [' + path + ']')
        with open(path, 'r') as f:
            data = f.read()
            f.close()
        return data.strip()
    else:
        return ''

def setupToken():
    if isDisabled('auth'):
        return

    fresh = False

    xtoken = readToken()
    if xtoken == '':
        # generate a new access token
        if isEnabled('verbose'):
            print('generating new access token')
        xtoken = secrets.token_urlsafe(16)
        fresh = True

    env['xtoken'] = xtoken
    print('Access Token: [' + xtoken + ']')

    if fresh:
        # cache the generated access token
        path = option('xtokenFile')
        with open(path, 'w') as f:
            if isEnabled('verbose'):
                print('caching access token to [' + path + ']')
            f.write(xtoken)
            f.close()


def authenticate(headers, xtoken):
    if isDisabled('auth'):
        return True

    if xtoken == '':
        if 'X-Token' in headers:
            xtoken = headers['X-Token']

    if xtoken == '':
        if isEnabled('verbose'):
            print('Access Denied! No "X-Token" in query or headers!')
        return False

    if (xtoken == env['xtoken']):
        return True
    else:
        if isEnabled('verbose'):
            print('Access Denied! [' + xtoken + '] != [' + env['xtoken'] + ']')
        return False


class AwkwardServer(BaseHTTPRequestHandler):

    #protocol_version = 'HTTP/1.1'
    protocol_version = 'HTTP/1.0'

    def deny(self):
        self.send_response(401)
        self.end_headers()
        self.wfile.write(bytes("ACCESS DENIED!", "utf-8"))

    def auth(self, token):
        auth = authenticate(self.headers, token)
        if not auth:
            self.deny()
            return False
        else:
            return True

    def send(self, data, res):
        length = len(data)
        if isEnabled('trace'):
            print(data.decode('utf-8', errors='ignore'))
            print('Length: ' + str(length))
            print('')

        self.send_response(res)
        self.send_header("Content-type", "text/plain")
        self.send_header("Content-length", length)
        self.send_header("Connection", "close")
        self.end_headers()
        self.wfile.write(data)
        self.wfile.flush()
        stat['payload'] += length

    def send_text(self, message, res):
        self.send(message.encode('utf-8'), res)

    def OK(self, data):
        self.send(data, 200)

    def notFound(self, message):
        self.send_text(message, 404)

    def doPing(self):
        self.send_text('OK', 200)

    def doStat(self):
        now = datetime.datetime.now()
        uptime = str(now - stat['startTime']).split('.')[0]

        payload = str(round(stat['payload']/1024)) + 'k'
        upload = str(round(stat['upload']/1024)) + 'k'

        free_mem = str(os.popen('free -m | grep Mem | awk \'{ print $4 "M/" $2 "M" }\'').readline())
        cpu_usage = str(os.popen("top -b -n1 | grep Cpu | cut -d ':' -f2").readline())
        
        st = ('awkward v' + env['version']      + '\n'
                + 'hits: ' + str(stat['hits'])  + '\n'
                + 'uptime: '   + uptime         + '\n'
                + 'payload: '  + payload        + '\n'
                + 'upload: '   + upload         + '\n'
                + 'free mem: ' + free_mem
                + 'cpu_usage: ' + cpu_usage
        )
        self.send_text(st, 200)

    def doVersion(self):
        self.send_text('awkward v' + env['version'], 200)

    def select(self, url):
        qs = urllib.parse.parse_qs(url.query)

        query = ''
        if 'q' in qs:
            if len(qs['q']) > 0:
                query = qs['q'][0]

        token = ''
        if 'x' in qs:
            if len(qs['x']) > 0:
                token = qs['x'][0]

        if not self.auth(token):
            return

        if isEnabled('trace'):
            print('===========================================')
            print(query)
            print('-------------------------------------------')

        # form a query
        q = ("find " + option('source') + " -type f -name '" +
                    option('include') + "' -exec cat {} +")

        if len(query) > 0:
            if isEnabled('verbose'):
                print('AWK: [' + query + ']')
            q += " | awk "
            fs = option('separator')
            if fs != '':
                q += " -F\\" + fs
            q += " '" + query + "'"

        result = subprocess.run([q], shell=True, stdout=subprocess.PIPE)
        self.OK(result.stdout)


    def do_GET(self):
        stat['hits'] += 1
        url = urllib.parse.urlparse(self.path)

        if url.path == '/ping':
            self.doPing()
        elif url.path == '/stat':
            self.doStat()
        elif url.path == '/version':
            self.doVersion()
        elif url.path == '' or url.path == '/':
            self.select(url)

        else:
            self.notFound('Unable to find [' + url.path + ']')



    def do_POST(self):
        stat['hits'] += 1
        if not self.auth(''):
            return

        content_length = int(self.headers['Content-Length'])
        data = self.rfile.read(content_length)
        stat['upload'] += content_length

        query = data.decode('utf-8')
        if isEnabled('trace'):
            print('===========================================')
            print(query)
            print('-------------------------------------------')
            

        # form a query
        q = ("find " + option('source') + " -type f -name '" +
                    option('include') + "' -exec cat {} +")

        if len(query) > 0:
            if isEnabled('verbose'):
                print('AWK: [' + query + ']')
            q += " | awk "
            fs = option('separator')
            if fs != '':
                q += " -F\\" + fs
            q += " '" + query + "'"

        result = subprocess.run([q], shell=True, stdout=subprocess.PIPE)
        self.OK(result.stdout)

def version():
    print('awkward version ' + env['version'])

def help():
    print('awkward v' + env['version'] + ' - zero maintenance on-demand awk-powered database engine') 
    print('''usage: awkward [-s | --source] [-i | --include] [-f | --separator]
                [-b | --bind ] [-p | --port]
                [-t | --tls] [-c | --cert] [-k | --key] [-a | --auth]
                [-v | --verbose] [--version] [-h | --help]

=== data options ===
    -s | --source <path>     source data path (default: '.')
    -i | --include <pattern> file names to include, e.g '*.log' or '*.csv' (default: '')
    -f | --separator <expr>  default awk data separator, e.g. ',' or ':' (default: '')

=== server options ===
    -b | --bind <host>   address to bind the socket (localhost)
    -p | --port <number> a server listen port (9108)

=== security options ===
    -t | --tls         enable tls
    -c | --cert <path> path to a tls certificate file (./cert/awkward.crt)
    -k | --key <path>  path to a tls private key file (./cert/awkward.key)
    -a | --auth - enable simple token authentication

=== help and debug options ===
    -v | --verbose - enable verbose output
    -e | --trace   - show request and response data
    -h | --help    - show help data
    -n | --version - show the version
''')


if __name__ == '__main__':
    #print('awkward v' + env['version'])
    configure()
    setupToken()

    # setup http server
    protocol = 'http'
    httpd = HTTPServer((option('bind'), option('port')), AwkwardServer)

    if isEnabled('tls'):
        protocol = 'https'
        if isEnabled('verbose'):
            print('== using https/tls ==')
            print('private key: ' + env['key'])
            print('certificate: ' + env['cert'])
        httpd.socket = ssl.wrap_socket(httpd.socket,
                        server_side = True,
                        certfile = env['cert'],
                        keyfile = env['key'],
                        ssl_version = ssl.PROTOCOL_TLS)
    else:
        if isEnabled('verbose'):
            print('== using http ==')


    print("awkward start at %s://%s:%s" % (protocol, env['bind'], env['port']))

    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass

    httpd.server_close()
    print("...awkward server stopped!")

