# -*- coding: utf-8 -*-

from flask import Flask, render_template, request, url_for, redirect
from valency.Frame import Frame, Slot, frames_from_db_entry
from valency.reduce_functions import reduce_functions

"""
from valency import k_utils
from valency.ssj_struct import *
from valency.val_struct import *
from valency.reduce_functions import *
"""

import logging
import sys
import json
import yaml
from flask_cors import CORS
import hashlib
import uuid
import datetime
import string
import random
import smtplib
from email.mime.text import MIMEText
from copy import deepcopy as DC
from pathlib import Path
from pymongo import MongoClient
from flask_pymongo import PyMongo
import pymongo
import argparse


log = logging.getLogger(__name__)
app = Flask(__name__)

app.config.from_object("db_config")
mongo = PyMongo(app)

# app.config["CORPORA"] = ["ssj", "kres", "gigafida"]
app.config["CORPORA"] = ["gigafida"]
app.config["BANNED_HEADWORDS"] = ["biti"]
app.config["QUERY_LIMIT"] = 1000


# when running vuejs via webpack
# CORS(app)
# CORS(app, resources={r"/api/*": {
#     "origins": "*",
# }})
CORS(app)


# DEV -------------------------------.

@app.route("/api/dev")
def api_dev():
    print("DEV")
    cur = mongo.db.kres.find({"headwords": "nagovarjati"})
    frames = []
    for ent in cur:
        frames += frames_from_db_entry(ent)
    return json.dumps([x.to_json() for x in frames])

# DEV -------------------------------^

# INDEX SELECTION -------------------.

@app.route("/api/words/<corpus>")
def api_words(corpus):
    return json.dumps({
        "sorted_words": app.config["app_index"][corpus]["words"], # todo - make corpus as arg
    })

@app.route("/api/functors/<corpus>")
def api_functors(corpus):
    return json.dumps(app.config["app_index"][corpus]["functors"])

# INDEX SELECTION -------------------^


# AUTH ------------------------------.

@app.route("/api/register", methods=["POST"])
def api_register():
    b = request.get_data()
    data = json.loads(b.decode())
    username = data["username"]
    password = data["password"]
    email = data["email"]
    if (
        username == "" or
        password == "" or
        email == ""
    ):
        return "ERR"
    email_hash = hashlib.sha256(email.encode("utf-8")).hexdigest()
    existing = list(mongo.db.users.find({
        "$or": [{"username": username}, {"email": email_hash}]
    }))
    if len(existing) > 0:
        return "ERR: Username or email already exists."
    entry = {
        "username": username,
        "hpass": hashlib.sha256(
            password.encode("utf-8")).hexdigest(),
        "email": email_hash
    }
    mongo.db.users.insert(entry)
    return "OK"


@app.route("/api/login", methods=["POST"])
def api_login():
    b = request.get_data()
    data = json.loads(b.decode())
    username = data["username"]
    password = data["password"]
    hpass = hashlib.sha256(password.encode("utf-8")).hexdigest()

    db_user = list(mongo.db.users.find({
        "username": username,
        "hpass": hpass
    }))
    if len(db_user) == 0:
        return json.dumps({"token": None})

    # update or create token
    token = uuid.uuid4().hex
    token_entry = {
        "username": username,
        "date": datetime.datetime.utcnow(),
        "token": token
    }
    mongo.db.usertokens.update(
        {"username": token_entry["username"]},
        token_entry,
        upsert=True
    )
    return json.dumps({"token": token})


def send_new_pass_mail(recipient, new_pass):
    # dtime = str(datetime.datetime.now())
    SENDER = "valencaglagolov@gmail.com"
    msg = MIMEText(
        "Pošiljamo vam novo geslo za "
        "vstop v aplikacijo Vezljivostni vzorci slovenskih glagolov.\n"
        "Geslo: {}.".format(new_pass)
    )
    msg["Subject"] = "Pozabljeno geslo"
    msg["From"] = SENDER
    msg["To"] = recipient

    try:
        server = smtplib.SMTP("smtp.gmail.com", 587)
        server.ehlo()
        server.starttls()
        server.login(
            SENDER,
            "rapid limb soapy fermi"
        )
        server.sendmail(SENDER, [recipient], msg.as_string())
        server.close()
        log.info("Sent new password.")
    except Error as e:
        log.error("Sending new password failed")
        log.error(e)


@app.route("/api/new_pass", methods=["POST"])
def api_new_pass():
    b = request.get_data()
    data = json.loads(b.decode())
    username = data["username"]
    email = data["email"]
    hemail = hashlib.sha256(email.encode("utf-8")).hexdigest()
    db_res = list(mongo.db.users.find({
        "username": username,
        "email": hemail
    }))
    # check if user is valid
    if len(db_res) == 0:
        return json.dumps({"confirmation": False})
    # create a new password
    new_pass = "".join([random.choice(
        string.ascii_letters + string.digits) for i in range(10)])
    # update locally
    hpass = hashlib.sha256(new_pass.encode("utf-8")).hexdigest()
    mongo.db.users.update(
        {
            "username": username,
            "email": hemail
        },
        {"$set": {
            "hpass": hpass
        }}
    )
    # send via mail
    send_new_pass_mail(email, new_pass)
    return json.dumps({"confirmation": True})


def token_to_username(token):
    key = {
        "token": token
    }
    res = list(mongo.db.usertokens.find(key))
    if len(res) != 1:
        return None
    username = res[0]["username"]
    # update deletion interval
    mongo.db.usertokens.update(
        key, {"$set": {"date": datetime.datetime.utcnow()}})
    return username


@app.route("/api/token", methods=["POST"])
def api_token():
    # check if token is valid
    b = request.get_data()
    data = json.loads(b.decode())
    token = data.get("token")
    # user = data.get("user")
    user = token_to_username(token)
    confirm = (user is not None)
    return json.dumps({
        "confirmation": confirm,
        "username": user
    })

# AUTH ------------------------------^


# FRAMES ----------------------------.

# input: hw, reduct_function
@app.route("/api/frames")
def api_get_frames():
    hw = request.args.get("hw")
    if hw is None:
        return json.dumps({"error": "Required argument: hw (headword)."})

    rf_name = request.args.get("rf", "reduce_0")
    RF = reduce_functions[rf_name]["f"]

    corpus = request.args.get("cor")
    if corpus not in app.config["CORPORA"]:
        return json.dumps({"error": "cor={kres,ssj}"})

    log.info("Test1")
    cur = mongo.db[corpus].find({"headwords": hw})
    log.info("Test2")
    frames = []
    for ent in cur[:app.config["QUERY_LIMIT"]]:
        frames += frames_from_db_entry(ent)  # pre-process this step for prod TODO
    cur.close()
    log.info("Test3")
    # filter by relevant hw
    frames = [x for x in frames if x.hw == hw]

    ret_frames = RF(frames, mongo.db.sensemap)
    log.info("Test3")
    json_ret = {"frames": []}
    for frame in ret_frames:
        json_ret["frames"].append(frame.to_json())
    log.info("Test4")
    return json.dumps(json_ret)
    # return prepare_frames(ret_frames)

def _aggregate_by_hw(ret_frames):

    def _tid_to_lemma(tid, sentence):
        # slow and hackish
        for pair in sentence:
            if pair[0] == tid:
                return pair[1]["lemma"]
        return None

    # append sentences
    for frame in ret_frames:
        # unique_sids = {".".join(x.split(".")[:-1]): x for x in frame.tids}
        frame.aggr_sent = {}  # map of headword: [sentence indexes]
        # sid, tid==hw
        for i, tid in enumerate(frame.tids):
            # hwl = vallex.get_token(tid)["lemma"]
            hwl = _tid_to_lemma(tid, frame.sentences[i])
            if hwl not in frame.aggr_sent:
                frame.aggr_sent[hwl] = []
            frame.aggr_sent[hwl].append(i)
    return ret_frames

# input: functor, reduce_function
@app.route("/api/functor-frames")
def api_get_functor_frames():
    functor = request.args.get("functor")
    if functor is None:
        return json.dumps({"error": "Missing argument: functor."})

    rf_name = request.args.get("rf", "reduce_0")
    RF = reduce_functions[rf_name]["f"]

    corpus = request.args.get("cor")
    if corpus not in app.config["CORPORA"]:
        return json.dumps({"error": "cor={kres,ssj}"})

    cur = mongo.db[corpus].find({"functors": functor})
    frames = []
    for ent in cur[:app.config["QUERY_LIMIT"]]:
        frames += frames_from_db_entry(ent)  # pre-process this step for prod TODO
    cur.close()

    # filter by relevant functor
    frames = [x for x in frames if functor in x.get_functors()]

    # raw_frames = vallex.functors_index[functor]  # TODO
    ret_frames = RF(frames, mongo.db.sensemap)
    ret_frames = _aggregate_by_hw(ret_frames)

    json_ret = {"frames": []}
    for frame in ret_frames:
        json_ret["frames"].append(DC(frame.to_json()))
    return json.dumps(json_ret)

# FRAMES ----------------------------^


# SENSES ----------------------------.
# ssj_id is legacy notation, read
# it as general sentence_id

@app.route("/api/senses/get")
def api_senses_get():
    # returns senses and mapping for hw
    hw = request.args.get("hw")
    senses = list(mongo.db.senses.find({
        "hw": hw
    }))
    sense_map_query = list(mongo.db.sensemap.find({
        "hw": hw
    }))
    # aggregation by max date possible on DB side
    # but no simple way of returning full entries
    # aggregate hw and ssj_id by max date
    sense_map_aggr = {}
    for sm in sense_map_query:
        key = sm["hw"] + sm["ssj_id"]
        if key in sense_map_aggr:
            sense_map_aggr[key] = max(
                [sm, sense_map_aggr[key]], key=lambda x: x["date"])
        else:
            sense_map_aggr[key] = sm
    sense_map_list = [x[1] for x in sense_map_aggr.items()]
    sense_map = {}
    for el in sense_map_list:
        sense_map[el["ssj_id"]] = el
    for k, e in sense_map.items():
        del(e["_id"])
        del(e["date"])
    for e in senses:
        del(e["_id"])
        if "date" in e:
            del(e["date"])

    # sort senses: user defined first, sskj second
    # sskj senses sorted by sskj sense_id
    user_senses = [s for s in senses if s["author"] != "SSKJ"]
    sskj_senses = [s for s in senses if s["author"] == "SSKJ"]

    def sorting_helper(sense):
        arr = sense["sense_id"].split("-")
        return "{:03d}-{:03d}-{:03d}".format(
            int(arr[1]), int(arr[2]), int(arr[3]))

    sskj_senses = sorted(sskj_senses, key=sorting_helper)
    senses = user_senses + sskj_senses

    return json.dumps({
        "senses": senses,
        "sense_map": sense_map,
    })


@app.route("/api/senses/update", methods=["POST"])
def api_senses_update():
    b = request.get_data()
    data = json.loads(b.decode())
    token = data.get("token")
    hw = data.get("hw")
    sense_map = data.get("sense_map")
    new_senses = data.get("new_senses")

    username = token_to_username(token)
    if username is None:
        log.debug("Not a user.")
        return "Not a user."

    # store new senses,
    # create new sense_ids
    id_map = {}
    for ns in new_senses:
        tmp_dt = datetime.datetime.utcnow()
        new_sense_id = "{}-{}".format(
            username,
            hashlib.sha256("{}{}{}".format(
                username,
                ns["desc"],
                str(tmp_dt)
            ).encode("utf-8")).hexdigest()[:10]
        )
        frontend_sense_id = ns["sense_id"]
        ns["sense_id"] = new_sense_id
        ns["date"] = tmp_dt
        id_map[frontend_sense_id] = new_sense_id

        print(ns)

        # insert into db
        mongo.db.senses.insert(ns)

    # replace tmp_id with mongo's _id
    for ssj_id, el in sense_map.items():
        sense_id = el["sense_id"]
        if sense_id in id_map.keys():
            sense_id = id_map[sense_id]
        data = {
            "user": username,
            "hw": hw,
            "ssj_id": ssj_id,
            "sense_id": sense_id,
            "date": datetime.datetime.utcnow()
        }
        # vallex.db["v2_sense_map"].update(key, data, upsert=True)
        mongo.db.sensemap.insert(data)
    return "OK"

# SENSES ----------------------------^


# APP PREFLIGHT ---------------------.
def _is_banned(hw):
    banned = True
    if hw in app.config["BANNED_HEADWORDS"]:
        banned = True
    elif hw in sskj_wordlist["wordlist"]:
        banned = False
    elif (hw + " se") in sskj_wordlist["wordlist"]:
        banned = False
    return banned

def prepare_app_index(appindex_json):
    log.info("[*] preparing app_index")
    # create app_index (used in frontend, left side word index)
    tmp_app_index = {c: {} for c in app.config["CORPORA"]}
    for corpus in app.config["CORPORA"]:
        res_hws = {}
        res_fns = {}

        # print('CORPUS...!!...')
        # print(corpus)
        # a = mongo.db[corpus]
        # print('TEST_OK')
        # print(a)
        # print(mongo.db)
        # a = mongo.db.list_collection_names()
        # print('TEST_OK2')
        nentries = mongo.db[corpus].count()
        idx = 0
        for e in mongo.db[corpus].find({}):
            if "headwords" not in e:
                continue
            for hw in e["headwords"]:
                if hw in res_hws:
                    res_hws[hw] += 1
                else:
                    res_hws[hw] = 1
            if "functors" not in e:
                continue
            for fn in e["functors"]:
                if fn in res_fns:
                    res_fns[fn] += 1
                else:
                    res_fns[fn] = 1
            idx += 1
            if idx % 10000 == 0:
                log.debug("indexing {}: {}/{}".format(
                    corpus, idx, nentries))

        alphabetical = {}
        for k, e in res_hws.items():
            fst = k[0].lower()
            if fst in alphabetical:
                alphabetical[fst].append((k, e))
            else:
                alphabetical[fst] = [(k, e)]

        for letter, words in alphabetical.items():
            filtered_words = [x for x in words if not _is_banned(x[0])]
            # filtered_words = [x for x in words]
            alphabetical[letter] = sorted(filtered_words, key=lambda x: x[0])

        tmp_app_index[corpus]["words"] = alphabetical


        functors = [(k, e) for (k, e) in res_fns.items()]
        functors = sorted(functors, key=lambda x: x[0])
        tmp_app_index[corpus]["functors"] = functors

    with Path(appindex_json).open("w") as fp:
        json.dump(tmp_app_index, fp)

# APP PREFLIGHT ---------------------^


def init_wsgi(app):
    print("Initiating wsgi")
    config = None
    with Path("/project/prod_conf.yaml").open("r") as fp:
        config = list(yaml.safe_load_all(fp))[0]

    app.debug = False
    logfile = config["logfile"]
    logging.basicConfig(filename=logfile, level=logging.INFO)

    # app index from db
    with Path(config["appindex"]).open("r") as fp:
        # a dirty hack but ok
        app.config["app_index"] = json.load(fp)

    # log.info("[*] Starting app.py with config:\n%s".format(config))
    log.info("[*] Starting app.py with config:\n{}".format(config))


# if we don't pass arguments, assume production environment (gunicorn)
if "gunicorn" in sys.argv[0]:
    init_wsgi(app)


if __name__ == "__main__":
    print("Starting app.py main()")
    aparser = argparse.ArgumentParser(description="Arguments for app.py")
    aparser.add_argument("--config-file", type=str, help="check ./conf_files/")
    aparser.add_argument('--prepare-db', action="store_true", default=False)
    aparser.add_argument("--dbuser", type=str)
    aparser.add_argument("--dbpass", type=str)
    aparser.add_argument("--dbaddr", type=str)
    aparser.add_argument("--sskj-wordlist", type=str)
    aparser.add_argument("--appindex-json", type=str)
    args = aparser.parse_args()

    with Path(args.config_file).open("r") as fp:
        config = list(yaml.safe_load_all(fp))[0]

    app.debug = bool(config["debug"])
    logfile = config["logfile"]
    if app.debug:
        logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
    else:
        logging.basicConfig(filename=logfile, level=logging.INFO)

    """
    # db login
    client = MongoClient(
        "mongodb://{}".format(args.dbaddr),
        username=args.dbuser,
        password=args.dbpass,
        authSource="mongo.db",
        authMechanism='SCRAM-SHA-1'
    )
    valdb = client.mongo.db
    """

    if args.prepare_db:
        with Path(args.sskj_wordlist).open("r") as fp:
            sskj_wordlist = json.load(fp)
        prepare_app_index(args.appindex_json)
        sys.exit()

    # app index from db
    with Path(args.appindex_json).open("r") as fp:
        app.config["app_index"] = json.load(fp)
        # a = app.config["app_index"]
        # b = app.config["app_index"]["kres"]
        # c = app.config["app_index"]["kres"]["words"]
        # print('HERE')

    # log.info("[*] Starting app.py with config:\n%s".format(config))
    log.info("[*] Starting app.py with config:\n{}".format(config))

    app.run(host=str(config["host"]), port=int(config["port"]))


