from modules.utils import (
    adjust_file_response_header,
    adjust_response_header,
    load_config,
    validate_request,
    get_log_data,
    save_json_log,
    get_list_of_process
)
from modules.oicf_execute import prepare_for_oicf
from bottle import (
    BaseRequest,
    Bottle,
    HTTPResponse,
    jinja2_view,
    jinja2_template,
    request,
    response,
    redirect,
    static_file
)
from time import strftime
from multiprocessing import Process, Queue
from logging.handlers import RotatingFileHandler
from datetime import datetime, date, timedelta

from traceback import format_exc
import json
import functools
import os
import sys
import uuid
import hashlib

sys.path.insert(0, './modules/libs.zip')


__author__ = "Aleksandar Dragacevac"
__version__ = "3.0.7"
BaseRequest.MEMFILE_MAX = 1024 * 1024 * 50

app = Bottle()
app.config["CONF"] = load_config()
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024
app.config['SECRET_KEY'] = "!jdfhskdhdf389828403928942jdiasndfnsdixbbnm#"
view = functools.partial(jinja2_view, template_lookup=['templates'])
template = functools.partial(jinja2_template, template_lookup=['templates'])
CACHE = {}
STOP_FILENAME = ".stop"


def save_to_log(remote_ip, message):
    file_name = os.path.join(
        app.config["CONF"]["logging_api"], datetime.now().strftime(
            "%Y-%m-%d.log")
    )
    now = datetime.now()
    with open(file_name, 'a') as f_out:
        f_out.write(
            f"{now.strftime('%Y-%m-%d')}\t{now.strftime('%H:%M:%S')}\t{remote_ip}\t{message}\n")


def hash_password(password):
    if not password:
        return ""
    key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), b"", 100000)
    return key.hex()


def check_logged(request):
    logged = request.get_cookie("logged", secret=app.config["SECRET_KEY"])
    time_logged = request.get_cookie(
        "logged_time", secret=app.config["SECRET_KEY"])
    logged_minutes = 30

    if time_logged:
        time_logged = datetime.strptime(time_logged, '%Y-%m-%d %H:%M:%S.%f')
        logged_minutes = (datetime.now() - time_logged).seconds / 60

    if logged == "yes" and logged_minutes < 30:
        return "yes"
    return "no"


def check_maintenance():
    if os.path.exists(STOP_FILENAME):
        running = True
    else:
        running = False
    return running


@app.route("/")
@view("index.html")
def main():
    logged = check_logged(request)

    dates = []
    for idx in range(15):
        dates.append((date.today() - timedelta(idx)).strftime("%Y-%m-%d"))
    dates = dates[::-1]

    requests = []
    errors = []

    for item in dates:
        log_data = get_log_data(item, app)
        errors.append(len([x for x in log_data if x[-1] < 0]))
        requests.append(len(log_data))
    return {
        "dates": dates,
        "errors": errors,
        "requests": requests,
        "logged": logged,
        "version": __version__,
        "maintenance": check_maintenance()
    }


@app.get("/login")
@view("LogIn.html")
def login():
    logged = check_logged(request)
    return {
        "error": False, "logged": logged, "version": __version__, "maintenance": check_maintenance()
    }


@app.post("/login")
@view("LogIn.html")
def do_login():
    logged = check_logged(request)
    username = request.forms.get('inputUser')
    password = request.forms.get('inputPassword')

    # "95b1fed915d451660e35c0b8b24f2189e6c264adf524da871705fdcd780cf676"
    if username == "info" and hash_password(password) == "a7d2a601cae12199d97bd142c792feddd92aa360e6a1a27808a706a9bfdb2c77":
        response.set_cookie("logged", "yes", secret=app.config["SECRET_KEY"])
        response.set_cookie("logged_time", str(
            datetime.now()), secret=app.config["SECRET_KEY"])

        redirect("/")

    return {
        "error": True, "logged": logged, "version": __version__, "maintenance": check_maintenance()
    }


@app.route("/logout")
def logout():
    response.set_cookie("logged", "no", secret=app.config["SECRET_KEY"])
    response.set_cookie("logged_time", "", secret=app.config["SECRET_KEY"])
    redirect('/')


@app.route("/resttest")
@view("resttest.html")
def rest_test():
    logged = check_logged(request)
    return {"logged": logged, "version": __version__, "maintenance": check_maintenance()}


@app.route("/restlogsweb")
@view("restlogweb.html")
def rest_logs_web():
    logged = check_logged(request)
    if logged != "yes":
        return template('401.html')

    file_name = f'{app.config["CONF"]["logging_web"]}\\{app.config["port"]}access.log'
    log_data = ""
    if os.path.exists(file_name):
        with open(file_name, 'r') as f_in:
            log_data = f_in.readlines()
            log_data = "<br/>".join(log_data[::-1])
    return {"log": log_data, "logged": logged, "version": __version__, "maintenance": check_maintenance()}


@app.route("/restlogsapi")
@view("restlogapi.html")
def rest_logs_api():
    logged = check_logged(request)
    if logged != "yes":
        return template('401.html')

    today = datetime.now().strftime("%Y-%m-%d")
    if request.query.get('date', None):
        today = request.query.get('date')

    file_name = f"{today}.log"
    full_path = os.path.join(app.config["CONF"]["logging_api"], file_name)

    log_data = []
    if os.path.exists(full_path):
        with open(full_path, 'r') as f_in:
            log_data = [x.split("\t") for x in f_in.readlines()[::-1]]

    return {
        "log": log_data,
        "filename": file_name,
        "date": today,
        "logged": logged,
        "version": __version__,
        "maintenance": check_maintenance()
    }


@app.route("/restapilog")
@view("restapilog.html")
def rest_logs_api():
    logged = check_logged(request)
    if logged != "yes":
        return template('401.html')

    today = datetime.now().strftime("%Y-%m-%d")
    if request.query.get('date', None):
        today = request.query.get('date')

    if request.query.get("action", None) == "download":
        file_name = f"log_{today.replace('-', '')}.log"
        full_path = os.path.join(app.config["CONF"]["api_log_path"], file_name)
        if os.path.exists(full_path):
            return static_file(file_name, root=app.config["CONF"]["api_log_path"], download=True)

        file_name = f"log_{today.replace('-', '')}-v0.log"
        full_path = os.path.join(app.config["CONF"]["api_log_path"], file_name)
        if os.path.exists(full_path):
            return static_file(file_name, root=app.config["CONF"]["api_log_path"], download=True)

    only_errors = request.query.get('only_errors', False)
    log_data = get_log_data(today, app)
    if only_errors:
        log_data = [x for x in log_data if x[-1] < 0]
    return {
        "log": log_data,
        "date": today,
        "only_errors": only_errors,
        "logged": logged,
        "version": __version__,
        "maintenance": check_maintenance()
    }


@app.route("/restsettings")
@view("restsettings.html")
def rest_settings():
    logged = check_logged(request)
    if logged != "yes":
        return template('401.html')

    file_name = f'{app.config["CONF"]["infoapi_path"]}\\api_server.ini'
    log_data = ""
    if os.path.exists(file_name):
        with open(file_name, 'r', encoding="utf-8-sig") as f_in:
            log_data = f_in.readlines()
            log_data = "<br/>".join(log_data)
    return {
        "log": log_data, "logged": logged, "version": __version__, "maintenance": check_maintenance()
    }


@app.route("/wssettings")
@view("wssettings.html")
def rest_settings():
    logged = check_logged(request)
    if logged != "yes":
        return template('401.html')
    running = not check_maintenance()

    if request.query.get("action", None) == "start":
        running = True
        if os.path.exists(STOP_FILENAME):
            os.unlink(STOP_FILENAME)
            
    if request.query.get("action", None) == "stop":
        minutes = request.query.get("minutes", None)
        if minutes:
            running = False
            with open(STOP_FILENAME, "w", encoding="utf8") as f_out:
                f_out.write(datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')+"\n")
                f_out.write(minutes)

    return {
        "logged": logged, "version": __version__, "running": running, "maintenance": check_maintenance()
    }


@app.route('/infoapi.aspx', method=['GET', 'POST'])
@app.route('/papi', method=['GET', 'POST'])
@app.route('/api', method=['GET', 'POST'])
def rest_service():
    """
        endpoint for communicating with OICF
    """
    if os.path.exists(STOP_FILENAME):
        with open(STOP_FILENAME, "r", encoding="UTF8") as f_in:
            params = [x.strip() for x in f_in.readlines() if x]
            stopped = params[0]
            minutes = params[1]

        time_stopped = datetime.strptime(stopped, '%Y-%m-%d %H:%M:%S.%f')
        time_stopped = (datetime.now() - time_stopped).seconds / 60
        if time_stopped >= int(minutes):
            os.unlink(STOP_FILENAME)
        else:
            turn_on = datetime.strptime(stopped, '%Y-%m-%d %H:%M:%S.%f') + timedelta(minutes=int(minutes))            
            time_turn_on = datetime.strftime(turn_on, '%Y-%m-%d %H:%M:%S.%f')
            return adjust_response_header(
                HTTPResponse(
                    {
                        "error_status": "maintenance_mode",
                        "error_message": f"Servis je isključen zbog održavanja. Biće uključen najkasnije do: {time_turn_on}",
                    },
                    status=200
                )
            )

    f_uniq = str(uuid.uuid4())
    data = {
        "json": request.forms.get("json", None) if request.forms.get("json", None) else "",
        "PdaError": request.forms.error if request.forms.error else "",
        "DebugLevel": request.forms.DebugLevel if request.forms.DebugLevel else 0,
        "DebugSection": request.forms.DebugSection if request.forms.DebugSection else "",
        "APIVersion": request.forms.APIVersion if request.forms.APIVersion else "",
        "deserialize": request.forms.deserialize if request.forms.deserialize else "",
        "OicfType": "S",
        "NewApi": app.config["CONF"]["new_api_execute"],
        "InfoAPIPath": app.config["CONF"]["infoapi_path"],
        "remote_ip": request.environ["REMOTE_ADDR"]
    }

    # validate request data
    valid = validate_request(data, app.config["CONF"], CACHE)
    if valid.get("error", None):
        save_to_log(data["remote_ip"], valid["error"])
        if app.config["CONF"]["restricted_mode"] == "D":
            return adjust_response_header(
                HTTPResponse(
                    {"error": valid["error"]},
                    status=400
                )
            )

    # get envirion params from request
    data["DebugParams"] = ""
    for key, value in request.environ.items():
        if not key.startswith("w") and not key.startswith("bottle"):
            data["DebugParams"] += "{}={}\r\n".format(key, value)

    # save json request
    if app.config["CONF"].get("log_json", "N") == "D":
        try:
            f_name_out = f"{f_uniq}.request.json"
            save_json_log(data["json"], f_name_out, app.config["CONF"])
        except BaseException as ex:
            f_name_out = f"{f_uniq}.error.txt"
            save_json_log(format_exc(), f_name_out, app.config["CONF"])
        f_name_out = f"{f_uniq}.body.txt"
        save_json_log(str(request.body.read().decode('UTF-8')), f_name_out, app.config["CONF"])
        f_name_out = f"{f_uniq}.header.txt"
        save_json_log(data["DebugParams"], f_name_out, app.config["CONF"])

    # return if body is empty
    if app.config["CONF"].get("reject_empty_request", "N") == "D" and not data["json"]:
        return adjust_response_header(
            HTTPResponse(
                {
                    "error": "Empty body of request.",
                    "error_message": "Poziv je prazan.",
                    "error_state": "empty_body",
                },
                status=400
            )
        )

    # convert json to base64
    convert_json = app.config["CONF"].get("json_to_base64", "N")

    # start new process that will execute OICF
    q = Queue()
    p = Process(target=prepare_for_oicf, name="OICF Worker", args=(q, data, convert_json, ))
    p.start()
    result = q.get()
    p.join(app.config["CONF"]["timeout"])

    # check for errors from OICF
    if result.get("error", None):
        save_to_log(request.environ["REMOTE_ADDR"], result["error_message"])

    # save json response
    if app.config["CONF"].get("log_json", "N") == "D":
        f_name_out = f"{f_uniq}.response.json"
        save_json_log(result["response"], f_name_out, app.config["CONF"])

    # Fix InfoSYS bad formated JSON
    if 'createobject("' in result["response"]:
        result["response"] = result["response"].replace(
            '("', '(\\"').replace('")', '\\")')

    json_result = json.loads(result["response"])
    binary_format = json_result.get("result_error_state", None)
    if binary_format and json_result["result_error_state"] == "http_response":
        return adjust_file_response_header(json_result["result_error_data"])
    return adjust_response_header(
        HTTPResponse(
            result["response"],
            status=result["status"]
        )
    )


@app.route('/getfile/<filename:path>')
def rest_file(filename):
    """
        serving files for download
    """
    if app.config["CONF"].get(f"infosys_static_{app.config['port']}", None):
        filename = os.path.join(
            app.config["CONF"][f"infosys_static_{app.config['port']}"], filename)
    else:
        filename = os.path.join(app.config["CONF"]["infosys_static"], filename)

    file_exists = True if os.path.exists(filename) else False
    file_directory, file_name = os.path.split(filename)

    if file_exists:
        return static_file(file_name, root=file_directory, download=filename)
    else:
        save_to_log(request.environ["REMOTE_ADDR"],
                    f"file name or path is invalid ('{filename}')")
        return adjust_response_header(
            HTTPResponse(
                json.dumps(
                    {
                        "status": "error",
                        "message": "invalid filename or path"
                    }
                ),
                status=404
            )
        )


@app.route('/static/<filename:path>')
def send_static(filename):
    return static_file(filename, root='./static')


@app.route('/web/<filename:path>')
def send_web(filename):
    return static_file(filename, root='./html')


class NormalizePathMiddleware(object):
    """
        middleware for normalizing url paths
        InfoAPI.aspx/ - > infoapi.aspx
    """

    def __init__(self, app):
        self.app = app

    def __call__(self, e, h):
        e['PATH_INFO'] = e['PATH_INFO'].rstrip('/').lower()
        return self.app(e, h)


myapp = NormalizePathMiddleware(app)
