#
#   Copyright © 2021,2022 IsardVDI S.L.
#   Copyright © 2022 Evilham <contact@evilham.com>
#
#   This file is part of DD
#
#   DD is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Affero General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or (at your
#   option) any later version.
#
#   DD is distributed in the hope that it will be useful, but WITHOUT ANY
#   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#   FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
#   details.
#
#   You should have received a copy of the GNU Affero General Public License
#   along with DD. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: AGPL-3.0-or-later
import concurrent.futures
import json
import logging as log
from operator import itemgetter
import os
import re
import sys

# import Queue
import threading
import time
import traceback
from uuid import uuid4

from flask import Response, jsonify, redirect, request, url_for
from flask_login import current_user, login_required

from typing import TYPE_CHECKING, cast, Any, Callable, Dict, List, Optional, Tuple
if TYPE_CHECKING:
    from admin.flaskapp import AdminFlaskApp

from admin.views import render_template
from ..lib.helpers import system_group
from .decorators import login_or_token, OptionalJsonResponse

# TODO: this is quirky and non-trivial to manage
threads : Dict[str, threading.Thread] = {}
# q = Queue.Queue()

from keycloak.exceptions import KeycloakGetError

from ..lib.dashboard import Dashboard
from ..lib.exceptions import UserExists, UserNotFound


from ..lib.legal import get_legal, gen_legal_if_not_exists, new_legal

def run_in_thread(
        op : Callable[..., Any],
        args : Tuple = tuple(),
        err_msg : str = "Something went wrong",
        err_code : int = 500,
        busy_err_msg : str ="Precondition failed: already operating users"
    ) -> OptionalJsonResponse:
    if threads.get("external", None) is not None:
        if threads["external"].is_alive():
            return (
                json.dumps(
                    {"msg": busy_err_msg}
                ),
                412,
                {"Content-Type": "application/json"},
            )
        else:
            del threads["external"]
    try:
        threads["external"] = threading.Thread(
            target=op, args=args
        )
        # TODO: this probably returns immediately and client gets no real feedback
        threads["external"].start()
        return json.dumps({}), 200, {"Content-Type": "application/json"}
    except:
        log.error(traceback.format_exc())
        return (
            json.dumps({"msg": err_msg}),
            err_code,
            {"Content-Type": "application/json"},
        )

def setup_app_views(app : "AdminFlaskApp") -> None:
    dashboard = Dashboard(app)
    @app.json_route("/sysadmin/api/resync")
    @app.json_route("/api/resync")
    @login_required
    def resync() -> OptionalJsonResponse:
        return (
            json.dumps(app.admin.resync_data()),
            200,
            {"Content-Type": "application/json"},
        )


    @app.json_route("/api/users", methods=["GET", "PUT"])
    @app.json_route("/api/users/<provider>", methods=["POST", "PUT", "GET", "DELETE"])
    @login_or_token
    def users(provider : bool=False) -> OptionalJsonResponse:
        if request.method == "DELETE":
            if current_user.role != "admin":
                return json.dumps({}), 301, {"Content-Type": "application/json"}
            if provider == "keycloak":
                return (
                    json.dumps(app.admin.delete_keycloak_users()),
                    200,
                    {"Content-Type": "application/json"},
                )
            if provider == "nextcloud":
                return (
                    json.dumps(app.admin.delete_nextcloud_users()),
                    200,
                    {"Content-Type": "application/json"},
                )
            if provider == "moodle":
                return (
                    json.dumps(app.admin.delete_moodle_users(app)),
                    200,
                    {"Content-Type": "application/json"},
                )
        if request.method == "POST":
            if current_user.role != "admin":
                return json.dumps({}), 301, {"Content-Type": "application/json"}
            if provider == "moodle":
                return (
                    json.dumps(app.admin.sync_to_moodle()),
                    200,
                    {"Content-Type": "application/json"},
                )
            if provider == "nextcloud":
                return (
                    json.dumps(app.admin.sync_to_nextcloud()),
                    200,
                    {"Content-Type": "application/json"},
                )
        if request.method == "PUT" and not provider:
            if current_user.role != "admin":
                return json.dumps({}), 301, {"Content-Type": "application/json"}

            return run_in_thread(app.admin.update_users_from_keycloak, err_msg="Add user error.")

        users = app.admin.get_mix_users()
        if current_user.role != "admin":
            for user in users:
                user["keycloak_groups"] = [
                    g for g in user["keycloak_groups"] if not system_group(g)
                ]
        return json.dumps(users), 200, {"Content-Type": "application/json"}


    @app.json_route("/api/users_bulk/<action>", methods=["PUT"])
    @login_required
    def users_bulk(action : str) -> OptionalJsonResponse:
        data = request.get_json(force=True)
        if request.method == "PUT":
            if action == "enable":
                return run_in_thread(app.admin.enable_users, args=(data,), err_msg="Enable users error.")
            if action == "disable":
                return run_in_thread(app.admin.disable_users, args=(data,), err_msg="Disabling users error.")
            if action == "delete":
                return run_in_thread(app.admin.delete_users, args=(data,), err_msg="Deleting users error.")

        return json.dumps({}), 405, {"Content-Type": "application/json"}


    # Update pwd
    @app.json_route("/api/user_password", methods=["GET"])
    @app.json_route("/api/user_password/<userid>", methods=["PUT"])
    @login_required
    def user_password(userid : Optional[str]=None) -> OptionalJsonResponse:
        if request.method == "GET":
            return (
                json.dumps(app.admin.get_dice_pwd()),
                200,
                {"Content-Type": "application/json"},
            )
        if request.method == "PUT":
            data = request.get_json(force=True)
            password = data["password"]
            temporary = data.get("temporary", True)
            uid = cast(str, userid)
            try:
                res = app.admin.user_update_password(uid, password, temporary)
                return json.dumps({}), 200, {"Content-Type": "application/json"}
            except KeycloakGetError as e:
                log.error(e.error_message.decode("utf-8"))
                return (
                    json.dumps({"msg": "Update password error."}),
                    500,
                    {"Content-Type": "application/json"},
                )

        return json.dumps({}), 405, {"Content-Type": "application/json"}


    # User
    @app.json_route("/api/user", methods=["POST"])
    @app.json_route("/api/user/<userid>", methods=["PUT", "GET", "DELETE"])
    @login_required
    def user(userid : Optional[str]=None) -> OptionalJsonResponse:
        # This is where changes happen from the UI
        uid : str = userid if userid else ''
        if request.method == "DELETE":
            app.admin.delete_user(uid)
            return json.dumps({}), 200, {"Content-Type": "application/json"}
        if request.method == "POST":
            data = request.get_json(force=True)
            if app.admin.get_user_username(data["username"]):
                return (
                    json.dumps({"msg": "Add user error: already exists."}),
                    409,
                    {"Content-Type": "application/json"},
                )
            data["enabled"] = data.get("enabled", False) in [True, "on"]
            data["quota"] = data["quota"] if data["quota"] != "false" else False
            data["groups"] = data["groups"] if data.get("groups", False) else []
            return run_in_thread(app.admin.add_user, args=(data,), err_msg="Add user error")

        if request.method == "PUT":
            data = request.get_json(force=True)
            data["enabled"] = True if data.get("enabled", False) else False
            data["groups"] = data["groups"] if data.get("groups", False) else []
            data["roles"] = [data.pop("role-keycloak")]
            try:
                app.admin.user_update(data)
                return json.dumps({}), 200, {"Content-Type": "application/json"}
            except UserNotFound:
                return (
                    json.dumps({"msg": "User not found."}),
                    404,
                    {"Content-Type": "application/json"},
                )
        if request.method == "DELETE":
            pass
        if request.method == "GET":
            user = app.admin.get_user(uid)
            if not user:
                return (
                    json.dumps({"msg": "User not found."}),
                    404,
                    {"Content-Type": "application/json"},
                )
            return json.dumps(user), 200, {"Content-Type": "application/json"}
        return None

    @app.json_route("/api/roles")
    @login_required
    def roles() -> OptionalJsonResponse:
        sorted_roles = sorted(app.admin.get_roles(), key=itemgetter("name"))
        if current_user.role != "admin":
            sorted_roles = [sr for sr in sorted_roles if sr["name"] != "admin"]
        return json.dumps(sorted_roles), 200, {"Content-Type": "application/json"}


    @app.json_route("/api/group", methods=["POST", "DELETE"])
    @app.json_route("/api/group/<group_id>", methods=["PUT", "GET", "DELETE"])
    @login_required
    def group(group_id : Optional[str]=None) -> OptionalJsonResponse:
        if request.method == "POST":
            data = request.get_json(force=True)
            log.error(data)
            data["parent"] = data["parent"] if data["parent"] != "" else None
            return (
                json.dumps(app.admin.add_group(data)),
                200,
                {"Content-Type": "application/json"},
            )
        if request.method == "DELETE":
            try:
                data = request.get_json(force=True)
            except:
                data = False

            if data:
                res = app.admin.delete_group_by_path(data["path"])
            else:
                if not group_id:
                    return (
                        json.dumps({"error": "bad_request","msg":"Bad request"}),
                        400,
                        {"Content-Type": "application/json"},
                    )
                res = app.admin.delete_group_by_id(group_id)
            return json.dumps(res), 200, {"Content-Type": "application/json"}
        return None

    @app.json_route("/api/groups")
    @app.json_route("/api/groups/<provider>", methods=["POST", "PUT", "GET", "DELETE"])
    @login_required
    def groups(provider : Optional[str] = None) -> OptionalJsonResponse:
        if request.method == "GET":
            sorted_groups = sorted(app.admin.get_mix_groups(), key=lambda k: str(k["name"]))
            if current_user.role != "admin":
                ## internal groups should be avoided as are assigned with the role
                sorted_groups = [sg for sg in sorted_groups if not system_group(sg["name"])]
            else:
                sorted_groups = [sg for sg in sorted_groups]
            return json.dumps(sorted_groups), 200, {"Content-Type": "application/json"}
        if request.method == "DELETE":
            if provider == "keycloak":
                return (
                    json.dumps(app.admin.delete_keycloak_groups()),
                    200,
                    {"Content-Type": "application/json"},
                )
        return None

    ### SYSADM USERS ONLY


    @app.json_route("/api/external", methods=["POST", "PUT", "GET", "DELETE"])
    @login_required
    def external() -> OptionalJsonResponse:
        if request.method == "POST":
            data = request.get_json(force=True)
            if data["format"] == "json-ga":
                return run_in_thread(app.admin.upload_json_ga, args=(data,))
            if data["format"] == "csv-ug":
                valid = check_upload_errors(data)
                if valid["pass"]:
                    return run_in_thread(app.admin.upload_csv_ug, args=(data,))
                else:
                    return json.dumps(valid), 422, {"Content-Type": "application/json"}
        if request.method == "PUT":
            data = request.get_json(force=True)
            return run_in_thread(app.admin.sync_external, args=(data,))
        if request.method == "DELETE":
            print("RESET")
            app.admin.reset_external()
            return json.dumps({}), 200, {"Content-Type": "application/json"}
        return json.dumps({}), 500, {"Content-Type": "application/json"}


    @app.json_route("/api/external/users")
    @login_required
    def external_users_list() -> OptionalJsonResponse:
        while threads["external"] is not None and threads["external"].is_alive():
            time.sleep(0.5)
        return (
            json.dumps(app.admin.get_external_users()),
            200,
            {"Content-Type": "application/json"},
        )


    @app.json_route("/api/external/groups")
    @login_required
    def external_groups_list() -> OptionalJsonResponse:
        while threads["external"] is not None and threads["external"].is_alive():
            time.sleep(0.5)
        return (
            json.dumps(app.admin.get_external_groups()),
            200,
            {"Content-Type": "application/json"},
        )


    @app.json_route("/api/external/roles", methods=["PUT"])
    @login_required
    def external_roles() -> OptionalJsonResponse:
        if request.method == "PUT":
            return (
                json.dumps(app.admin.external_roleassign(request.get_json(force=True))),
                200,
                {"Content-Type": "application/json"},
            )
        return None


    def check_upload_errors(data : Dict[Any, Any]) -> Dict[Any, Any]:
        email_regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
        for u in data["data"]:
            try:
                user_groups = [g.strip() for g in u["groups"].split(",")]
            except:
                resp = {
                    "pass": False,
                    "msg": "User " + u["username"] + " has invalid groups: " + u["groups"],
                }
                log.error(resp)
                return resp

            if not re.fullmatch(email_regex, u["email"]):
                resp = {
                    "pass": False,
                    "msg": "User " + u["username"] + " has invalid email: " + u["email"],
                }
                log.error(resp)
                return resp

            if u["role"] not in ["admin", "manager", "teacher", "student"]:
                if u["role"] == "":
                    resp = {
                        "pass": False,
                        "msg": "User " + u["username"] + " has no role assigned!",
                    }
                    log.error(resp)
                    return resp
                resp = {
                    "pass": False,
                    "msg": "User " + u["username"] + " has invalid role: " + u["role"],
                }
                log.error(resp)
                return resp
        return {"pass": True, "msg": ""}


    @app.json_route("/api/dashboard/<item>", methods=["PUT"])
    @login_required
    def dashboard_put(item : str) -> OptionalJsonResponse:
        if item == "colours":
            try:
                data = request.get_json(force=True)
                dashboard.update_colours(data)
            except:
                log.error(traceback.format_exc())
            return json.dumps({"colours": data}), 200, {"Content-Type": "application/json"}
        if item == "menu":
            try:
                data = request.get_json(force=True)
                dashboard.update_menu(data)
            except:
                log.error(traceback.format_exc())
            return json.dumps(data), 200, {"Content-Type": "application/json"}
        if item == "logo":
            dashboard.update_logo(request.files["croppedImage"])
            return json.dumps({}), 200, {"Content-Type": "application/json"}
        if item == "background":
            dashboard.update_background(request.files["croppedImage"])
            return json.dumps({}), 200, {"Content-Type": "application/json"}
        return (
            json.dumps(
                {
                    "error": "update_error",
                    "msg": "Error updating item " + item + "\n" + traceback.format_exc(),
                }
            ),
            500,
            {"Content-Type": "application/json"},
        )


    @app.json_route("/api/legal/<item>", methods=["GET"])
    # @login_required
    def legal_get(item : str) -> OptionalJsonResponse:
        if request.method == "GET":
            if item == "legal":
                lang = request.args.get("lang")
                if not lang or lang not in ["ca","es","en","fr"]:
                    lang="ca"
                gen_legal_if_not_exists(app, lang)
                return (
                    json.dumps({"html": get_legal(app, lang)}),
                    200,
                    {"Content-Type": "application/json"},
                )
            # if item == "privacy":
            #     return json.dumps({ "html": "<b>Privacy policy</b><br>This works!"}), 200, {'Content-Type': 'application/json'}
        return None


    @app.json_route("/api/legal/<item>", methods=["POST"])
    @login_required
    def legal_put(item : str) -> OptionalJsonResponse:
        if request.method == "POST":
            if item == "legal":
                data = None
                try:
                    data = data = request.get_json(force=True)
                    html = data["html"]
                    lang = data["lang"]
                    if not lang or lang not in ["ca","es","en","fr"]:
                        lang="ca"
                    new_legal(app, lang, html)
                except:
                    log.error(traceback.format_exc())
                return json.dumps(data), 200, {"Content-Type": "application/json"}
        return None
            # if item == "privacy":
            #     data = None
            #     try:
            #         data = request.json
            #         html = data["html"]
            #         lang = data["lang"]
            #     except:
            #         log.error(traceback.format_exc())
            #     return json.dumps(data), 200, {'Content-Type': 'application/json'}
