#
#   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 logging as log
import os
import os.path
import secrets
import traceback
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple

import yaml
from cerberus import Validator
from flask import Flask, Response, jsonify, send_from_directory

from admin.lib.api_exceptions import Error
from admin.lib.keys import ThirdPartyIntegrationKeys
from admin.views import render_template
from admin.views.decorators import OptionalJsonResponse
from admin.views.ApiViews import setup_api_views
from admin.views.AppViews import setup_app_views
from admin.views.LoginViews import setup_login_views
from admin.views.WebViews import setup_web_views
from admin.views.WpViews import setup_wp_views
from admin.auth.authentication import setup_auth

if TYPE_CHECKING:
    from admin.lib.admin import Admin
    from admin.lib.postup import Postup


class AdminValidator(Validator):  # type: ignore  # cerberus type hints MIA
    # TODO: What's the point of this class?
    None
    # def _normalize_default_setter_genid(self, document):
    #     return _parse_string(document["name"])

    # def _normalize_default_setter_genidlower(self, document):
    #     return _parse_string(document["name"]).lower()

    # def _normalize_default_setter_gengroupid(self, document):
    #     return _parse_string(
    #         document["parent_category"] + "-" + document["uid"]
    #     ).lower()


class AdminFlaskApp(Flask):
    """
    Subclass Flask app to ease customisation and type checking.

    In order for an instance of this class to be useful,
    the setup method should be called after instantiating.
    """

    admin: "Admin"
    api_3p : Dict[str, ThirdPartyIntegrationKeys]
    custom_dir: str
    data_dir: str
    domain : str
    node_modules_dir : str
    ready: bool = False
    validators: Dict

    def __init__(self, *args: Any, **kwargs: Any):
        super().__init__(*args, **kwargs)
        self.api_3p = {}
        self.domain = os.environ["DOMAIN"]
        self.url_map.strict_slashes = False
        self._load_config()
        # Minor setup tasks
        self.validators = self._load_validators()
        self._setup_routes()
        self._setup_api_3p()
        setup_api_views(self)
        setup_app_views(self)
        setup_login_views(self)
        setup_web_views(self)
        setup_wp_views(self)
        setup_auth(self)

    @property
    def legal_path(self) -> str:
        return os.path.join(self.root_path, "static/templates/pages/legal/")

    @property
    def avatars_path(self) -> str:
        return os.path.join(self.custom_dir, "avatars/")

    @property
    def secrets_dir(self) -> str:
        return os.path.join(self.data_dir, "secrets")

    def setup(self) -> None:
        """
        Perform setup tasks that might do network
        """
        from admin.lib.postup import Postup
        Postup(self)
        # This must happen after Postup since it, e.g. fetches moodle secrets
        from admin.lib.admin import Admin
        self.admin = Admin(self)
        # We now must setup the third-party callbacks
        from admin.lib.callbacks import ThirdPartyCallbacks
        if "correu" in self.api_3p:
            tp = self.api_3p["correu"]
            self.admin.third_party_cbs.append(ThirdPartyCallbacks(tp))

    def json_route(self, rule: str, **options: Any) -> Callable[..., OptionalJsonResponse]:
        return self.route(rule, **options)  # type: ignore  # mypy issue #7187

    def _load_validators(self, purge_unknown: bool = True) -> Dict[str, Validator]:
        validators = {}
        schema_path = os.path.join(self.root_path, "schemas")
        for schema_filename in os.listdir(schema_path):
            try:
                with open(os.path.join(schema_path, schema_filename)) as file:
                    schema_yml = file.read()
                    schema = yaml.load(schema_yml, Loader=yaml.FullLoader)
                    validators[schema_filename.split(".")[0]] = AdminValidator(
                        schema, purge_unknown=purge_unknown
                    )
            except IsADirectoryError:
                None
        return validators

    def _load_config(self) -> None:
        try:
            self.data_dir = os.environ.get("DATA_FOLDER", ".")
            self.custom_dir = os.environ.get("CUSTOM_FOLDER", ".")
            self.node_modules_dir = os.environ.get("NODE_MODULES_FOLDER", "node_modules")
            # Handle secrets like Flask's session key
            secret_key_file = os.path.join(self.secrets_dir, "secret_key")
            if not os.path.exists(self.secrets_dir):
                os.mkdir(self.secrets_dir, mode=0o700)
            if not os.path.exists(secret_key_file):
                # Generate as needed
                # https://flask.palletsprojects.com/en/2.1.x/config/#SECRET_KEY
                with os.fdopen(
                    os.open(secret_key_file, os.O_WRONLY | os.O_CREAT, 0o600), "w"
                ) as f:
                    f.write(secrets.token_hex())
            self.secret_key = open(secret_key_file, "r").read()

            # Move on with settings from the environment
            self.config.update({
                "DOMAIN": self.domain,
                "KEYCLOAK_POSTGRES_USER": os.environ.get("KEYCLOAK_DB_USER"),
                "KEYCLOAK_POSTGRES_PASSWORD": os.environ.get("KEYCLOAK_DB_PASSWORD"),
                "MOODLE_POSTGRES_USER": os.environ.get("MOODLE_POSTGRES_USER"),
                "MOODLE_POSTGRES_PASSWORD": os.environ.get("MOODLE_POSTGRES_PASSWORD"),
                "NEXTCLOUD_POSTGRES_USER": os.environ.get("NEXTCLOUD_POSTGRES_USER"),
                "NEXTCLOUD_POSTGRES_PASSWORD": os.environ.get("NEXTCLOUD_POSTGRES_PASSWORD"),
                "VERIFY": os.environ.get("VERIFY") == "true",
                "API_SECRET": os.environ.get("API_SECRET"),
            })
        except Exception as e:
            log.error(traceback.format_exc())
            raise

    def _setup_api_3p(self) -> None:
        # Register third parties if / as requested
        email_domain = os.environ.get("MANAGED_EMAIL_DOMAIN", "")
        integrations : List[Tuple[str, str]] = []
        if email_domain:
            integrations.append(("correu", f"correu.{self.domain}"))

        api_3p_secrets_dir = os.path.join(self.secrets_dir, "api_3p")
        if not os.path.exists(api_3p_secrets_dir):
            os.mkdir(api_3p_secrets_dir, mode=0o700)
        for integration, int_domain in integrations:
            ks = os.path.join(api_3p_secrets_dir, integration)
            if not os.path.exists(ks):
                os.mkdir(ks, mode=0o700)
            self.api_3p[integration] = ThirdPartyIntegrationKeys(key_store=ks, our_name="DD", their_name=integration, their_service_domain=int_domain)

        if "correu" in self.api_3p:
            from admin.views.MailViews import setup_mail_views
            setup_mail_views(self)
            # Temporary work-around while we are not receiving the 'kid'
            # This effectively uses 'correu' as the default
            # TODO: remove when dd-email-panel has changed this
            self.api_3p[""] = self.api_3p["correu"]

    def _setup_routes(self) -> None:
        """
        Setup routes to Serve static files
        """

        @self.route("/build/<path:path>")
        def send_build(path: str) -> Response:
            return send_from_directory(
                os.path.join(self.node_modules_dir, "gentelella/build"), path
            )

        @self.route("/vendors/<path:path>")
        def send_vendors(path: str) -> Response:
            return send_from_directory(
                os.path.join(self.node_modules_dir, "gentelella/vendors"), path
            )

        @self.route("/node_modules/<path:path>")
        def send_nodes(path: str) -> Response:
            return send_from_directory(
                self.node_modules_dir, path
            )

        @self.route("/templates/<path:path>")
        def send_templates(path: str) -> Response:
            return send_from_directory(os.path.join(self.root_path, "templates"), path)

        # @self.route('/templates/<path:path>')
        # def send_templates(path):
        #     return send_from_directory(os.path.join(self.root_path, 'static/templates'), path)

        @self.route("/static/<path:path>")
        def send_static_js(path: str) -> Response:
            return send_from_directory(os.path.join(self.root_path, "static"), path)

        @self.route("/avatars/<path:path>")
        def send_avatars_img(path: str) -> Response:
            return send_from_directory(
                os.path.join(self.root_path, "../avatars/master-avatars"), path
            )

        @self.route("/custom/<path:path>")
        def send_custom(path: str) -> Response:
            return send_from_directory(self.custom_dir, path)

        # @self.errorhandler(404)
        # def not_found_error(error):
        #     return render_template('page_404.html'), 404

        # @self.errorhandler(500)
        # def internal_error(error):
        #     return render_template('page_500.html'), 500

        @self.errorhandler(Error)
        def handle_user_error(ex : Error) -> Response:
            response : Response = jsonify(ex.error)
            response.status_code = ex.status_code
            response.headers.extend(ex.content_type)  # type: ignore  # werkzeug type hint MIA
            return response
