import bottle
request  = bottle.request
response = bottle.response
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.message import EmailMessage
import os # for environ vars
import sys # to print ot stderr
import re # to match our template system
import pymongo # database
from dotenv import load_dotenv
import random, string # for tokens
import html # for sanitization
from bson.json_util import dumps
import datetime # For email date


##################################################### Bottle stuff ############################################

class StripPathMiddleware(object):
    '''
    Get that slash out of the request
    '''
    def __init__(self, a):
        self.a = a
    def __call__(self, e, h):
        e['PATH_INFO'] = e['PATH_INFO'].rstrip('/')
        return self.a(e, h)

class EnableCors(object):
    name = 'enable_cors'
    api = 2

    def apply(self, fn, context):
        def _enable_cors(*args, **kwargs):
            # set CORS headers
            response.headers['Access-Control-Allow-Origin'] = '*'
            response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
            response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'

            if bottle.request.method != 'OPTIONS':
                # actual request; reply with the actual response
                return fn(*args, **kwargs)

        return _enable_cors

app = application = bottle.Bottle(catchall=False)
#app.install(EnableCors())

##################################################### Configuration ############################################

# The exception that is thrown when an argument is missing
class MissingParameterException (Exception):
    pass

def get_env(var, default=None):
    """var is an env var name, default is the value to return if var does not exist. If no default and no value, an exception is raised."""
    if var in os.environ:
        return os.environ[var]
    elif default is not None:
        return default
    else:
        raise MissingParameterException("Environment variable {} is missing".format(var))


# Token generation
token_chars = string.ascii_lowercase+string.ascii_uppercase+string.digits
token_len   = 50

# form template regex
form_regex = '\{\{(\w+)(\|[\w\s,\.\?\-\'\"\!\[\]]+)?\}\}'

# Load file from .env file.
load_dotenv(os.path.dirname(__file__) + '.env')

# Get address and port from env
listen_address = get_env('LISTEN_ADDRESS', '0.0.0.0')
listen_port    = get_env('LISTEN_PORT', 8080)

# Get SMTP infos from env
smtp_server_address  = get_env('SMTP_SERVER_ADDRESS')
smtp_server_port     = get_env('SMTP_SERVER_PORT')
smtp_server_username = get_env('SMTP_SERVER_USERNAME')
smtp_server_password = get_env('SMTP_SERVER_PASSWORD')
smtp_server_sender   = get_env('SMTP_SERVER_SENDER')

# Get mongodb connection
mongodb_host = get_env('MONGODB_HOST')
mongodb_port   = get_env('MONGODB_PORT', '27017')
mongodb_dbname = get_env('MONGODB_DBNAME', 'contact_mailer')

# Security
admin_password = get_env('ADMIN_PASSWORD')

if 'SMTP_SSL' in os.environ and os.environ['SMTP_SSL'] == 'true':
    security = 'ssl'
elif 'SMTP_STARTTLS' in os.environ and os.onviron['SMTP_STARTTLS'] == 'true':
    security = 'starttls'
else:
    raise MissingParameterException('No security env var (SMTP_SSL or SMTP_STARTTLS) have been defined. (Expected true or false)')

# mongodb initialization
mongodb_client = pymongo.MongoClient("mongodb://{}:{}/".format(mongodb_host, mongodb_port), connect=False, serverSelectionTimeoutMS=10000, connectTimeoutMS=10000)
mongodb_database = mongodb_client[mongodb_dbname]
print(mongodb_database)


##################################################### main route: mail submission ############################################

@app.post('/submit')
def submission ():
    # Getting token
    if 'token' in request.forms:
        token = request.forms.getunicode('token')
    else:
        response.status = 400
        return resp('error', 'Le jeton d’autentification est requis')

    # Getting mail address
    if 'mail' in request.forms:
        from_address = request.forms.getunicode('mail')
    else:
        #response.status = 400
        #return 'Le mail est requis'
        from_address = ''

    try:
        form = mongodb_database['forms'].find({'token': token})[0]
    except IndexError as e:
        response.status = 400
        return resp('error', 'Le formulaire demandé est introuvable, merci de vérifier que le token utilisé est le bon')
    except pymongo.errors.ServerSelectionTimeoutError as e:
        response.status = 500
        return resp('error', 'La base de donnée n’est pas accessible.')

    try:
        subject_fields = fill_fields(request, get_fields(form['subject']))
        content_fields = fill_fields(request, get_fields(form['content']))
        # Did the bot filled the honeypot field?
        if 'honeypotfield' in form and form['honeypotfield'] in request.forms and request.forms.get(form['honeypotfield']) != '':
            response.status = 400
            return resp('error', 'We identified you as a bot. If this is an error, try to contact us via another way.')

    except MissingParameterException as e:
        response.status = 404
        return resp('error', str(e))

    subject = re.sub(form_regex, r'{\1}', form['subject']).format(**subject_fields)
    content = re.sub(form_regex, r'{\1}', form['content']).format(**content_fields)

    try:
        if not send_mail(from_address, form['mail'], subject, content):
            response.status = 500
            return resp('error', 'Le mail n’a pas pu être envoyé.')
    except smtplib.SMTPDataError as e:
        response.status = 500
        error = 'Le mail a été refusé. Merci de réessayer plus tard.'
    except smtplib.SMTPRecipientsRefused as e:
        response.status = 500
        error = 'Impossible de trouver le destinataire du mail. Merci de réessayer plus tard'
    except Exception as e:
        raise


    # Redirection
    #bottle.redirect(success_redirect_default)
    origin = request.headers.get('origin')
    return resp('success', 'Mail envoyé !')

##################################################### Helpers ############################################

def resp (status, msg, data='{}'):
    return '{{"status": "{}", "msg": "{}", "data": {}}}'.format(status, msg, data)

def get_fields (string):
    """ Parse the string looking for template elements and create an array with template to fill and their default values. None if mandatory. """
    result = {}
    for match in re.findall(form_regex, string):
        result[match[0]] = None if match[1] == '' else match[1][1:]
    return result

def fill_fields(request, fields):
    """Look for fields in request and fill fields dict with values or let default ones. If the value is required, throw exception."""
    for field in fields:
        if field in request.forms:
            fields[field] = request.forms.getunicode(field)
            if fields[field] is None: # if unicode failed
                fields[field] = request.forms.get(field)
        elif fields[field] is None:
            raise MissingParameterException("Le champs {} est obligatoire".format(field))
    return fields

def send_mail (from_address, to, subject, content):
    """Actually connect to smtp server, build a message object and send it as a mail"""
    msg = EmailMessage()
    msg['From'] = smtp_server_sender
    msg.add_header('reply-to', from_address)
    msg['To'] = to
    msg['Subject'] = subject
    msg['Date'] = datetime.datetime.now()
    msg.set_content(MIMEText(content, 'plain', "utf-8"))
    #or
    #msg.set_content(content)

    # SMTP preambles
    if security == 'ssl':
        smtp = smtplib.SMTP_SSL(smtp_server_address, smtp_server_port)
    elif security == 'starttls':
        smtp = smtplib.SMTP(smtp_server_address, smtp_server_port)
    smtp.ehlo()
    if security == 'starttls':
        smtp.starttls()
        smtp.ehlo()

    # SMTP connection
    smtp.login(smtp_server_username, smtp_server_password)
    refused = smtp.sendmail(smtp_server_sender, to, msg.as_string())
    smtp.close()
    if refused:
        print('Message was not send to ' + str(refused))
        return False
    return True

def login(request):
    """
    Check if user is admin or simple user. Return a disct with _privilege key. dict is also a user if _privilege == 1
    Privileges : 0=admin 1=loggedIn 1000=guest
    """
    if 'admin_pass' in request.forms and request.forms['admin_pass'] == admin_password:
        return {'_privilege':0}
    if 'token' in request.forms:
        token = request.forms.getunicode('token')
        try:
            user = mongodb_database['users'].find({'token': token})[0]
            user['_privilege'] = 1
            return user
        except IndexError as e:
            pass
        except pymongo.errors.ServerSelectionTimeoutError as e:
            response.status = 500
            return 'La base de donnée n’est pas accessible'

    return {'_privilege': 1000} # anonymous


##################################################### Forms ############################################

@app.post('/form')
def create_form ():
    # Getting subject template
    if 'subject' in request.forms:
        subject = request.forms.getunicode('subject')
    elif mail_default_subject != '':
        subject = mail_default_subject
    else:
        response.status = 400
        return resp('error', 'Le champs « sujet » est requis')

    # Getting mail content
    if 'content' in request.forms:
        content = request.forms.getunicode('content')
    else:
        response.status = 400
        return resp('error', 'Le champs « contenu » est requis')

    if 'honeypotfield' in request.forms:
        honeypotfield = request.forms.getunicode('honeypotfield')
    else:
        honeypotfield = None

    # Getting from address
    if 'mail' in request.forms:
        mail = request.forms.getunicode('mail')
    else:
        response.status = 400
        return resp('error', 'Le champs « adresse » est requis')

    user = login(request)
    if user['_privilege'] > 1:
        response.status = 400
        return resp('error', 'Privilèges insufisants')

    # TODO limit the insertion rate
    token = ''.join(random.sample(token_chars, token_len))
    try:
        inserted = mongodb_database['forms'].insert_one({
            'mail': mail,
            'content': content,
            'subject': subject,
            'user_id': user['_id'],
            'token': token,
            'honeypotfield': honeypotfield,
        })
    except pymongo.errors.ServerSelectionTimeoutError as e:
        response.status = 500
        return resp('error', 'La base de donnée n’est pas accessible')

    return resp('success', 'Créé : ' + token)

@app.post('/form/list')
def list_forms ():
    try:
        user = login(request)
        if user['_privilege'] == 0:
            filt = {}
        elif user['_privilege'] == 1:
            filt = {'user_id': user['_id']}
        else:
            response.status = 400
            return resp('error', 'Privilèges insufisants')
        data = mongodb_database['forms'].find(filt)
        return resp('success','', dumps(list(data)))
    except pymongo.errors.ServerSelectionTimeoutError as e:
        response.status = 500
        return resp('error','La base de donnée n’est pas accessible')



@app.delete('/form/<token>')
def delete_form(token):
    # If admin or form owner
    user = login(request)
    if user['_privilege'] > 1:
        response.status = 400
        return resp('error', 'Privilèges insufisants')

    # Actually delete
    try:
        form = mongodb_database['forms'].find({'token':token })[0]
    except IndexError as e:
        response.status = 400
        return resp('error', 'Le token n’est pas valide')
    except pymongo.errors.ServerSelectionTimeoutError as e:
        response.status = 500
        return resp('error', 'La base de donnée n’est pas accessible')

    if user['_privilege'] == 0 or (form['user_id'] == user['_id']):
        try:
           mongodb_database['forms'].delete_one({
               'token': token,
           })
        except pymongo.errors.ServerSelectionTimeoutError as e:
            response.status = 500
            return resp('error', 'La base de donnée n’est pas accessible')
        return resp('success', 'Supprimé ' + token)
    response.status = 400
    return resp('error', 'Privilèges insufisants')


##################################################### Users ############################################

@app.post('/user/list')
def list_users ():
    user = login(request)
    if user['_privilege'] > 0:
        response.status = 400
        return resp('error', 'Privilèges insufisants')
    try:
        data = mongodb_database['users'].find()
        return resp('success', '', dumps(list(data)))
    except pymongo.errors.ServerSelectionTimeoutError as e:
        response.status = 500
        return resp('error', 'La base de donnée n’est pas accessible')


@app.route('/user/<username>', method=['OPTIONS', 'PUT'])
def create_user (username):
    user = login(request)
    if user['_privilege'] > 0:
        response.status = 400
        return resp('error', 'Privilèges insufisants')
    try:
        mongodb_database['users'].find({'username': username})[0]
        return resp('error', 'L’utilisateur existe déjà')
    except IndexError as e:
        try:
            inserted = mongodb_database['users'].insert_one({
                'username': username,
                'token': ''.join(random.sample(token_chars, token_len))
            })
            return resp('success', 'Créé : ' + username)
        except pymongo.errors.ServerSelectionTimeoutError as e:
            response.status = 500
            return resp('error', 'La base de donnée n’est pas accessible')
    except pymongo.errors.ServerSelectionTimeoutError as e:
        response.status = 500
        return resp('error','La base de donnée n’est pas accessible')


@app.delete('/user/<username>')
def delete_user (username):
    user = login(request)
    if user['_privilege'] > 0:
        response.status = 400
        return resp('error', 'Privilèges insufisants')
    try:
        mongodb_database['users'].find({'username': username})[0]
        mongodb_database['users'].delete_one({
            'username': username,
        })
        return resp('success', 'Supprimé ' + username)
    except IndexError as e:
        response.status = 400
        return resp('error', 'L’utilisateur n’existe pas')
    except pymongo.errors.ServerSelectionTimeoutError as e:
        response.status = 500
        return resp('error', 'La base de donnée n’est pas accessible')



##################################################### app startup ############################################
if __name__ == '__main__':
    bottle.run(app=StripPathMiddleware(app), host=listen_address, port=listen_port, debug=True)
else:
    prod_app = StripPathMiddleware(app)