from typing import List, Union, Tuple
from pathlib import Path

from slugify import slugify

import glob

import os

import json

from ehelply_bootstrapper.bootstrap import Bootstrap
from ehelply_bootstrapper.drivers.mysql import MySQLCredentials
from ehelply_bootstrapper.utils.state import State
from ehelply_bootstrapper.utils.environment import Environment
from ehelply_bootstrapper.utils.service import ServiceMeta, ServiceStatus
from ehelply_bootstrapper.bootstrap import LOADABLE_MYSQL

from ehelply_logger.Logger import VERBOSITY_NORMAL

from ehelply_microservice_library.integrations.m2m import M2M
from ehelply_microservice_library.integrations.fact import Fact, FactBase, get_fact_endpoint, get_fact_stage
from ehelply_microservice_library.integrations.logging import Log
from ehelply_microservice_library.integrations.meta import Meta
from ehelply_microservice_library.integrations.note import Note
from ehelply_microservice_library.integrations.monitor import Monitor
from ehelply_microservice_library.integrations.user import User
from ehelply_microservice_library.integrations.access import Access, NodeCreate
from ehelply_microservice_library.integrations.security import Security
from ehelply_microservice_library.integrations.appointments import Appointments
from ehelply_microservice_library.integrations.places import Places
from ehelply_microservice_library.integrations.reviews import Reviews

from ehelply_microservice_library.utils.service_updater import ServiceUpdater

from ehelply_microservice_library.utils.secret_names import secret_name_database, secret_name_facts_vault, \
    secret_name_security_vault

from ehelply_microservice_library.aws.aws_asm import ASM

GATEKEEPER_KEY_ENVIRONMENT_NAME: str = "EHELPLY_GATEKEEPER_KEY"


class ServiceBootstrap(Bootstrap):
    """
    This class helps to set up the entire service
    """

    def __init__(self):
        """
        Prepares the bootstrapper class.

        Ignore this method unless you need to init any values for the microservice itself.
        """
        service_environment_path: str = str(self.get_service_environment_path())
        Environment(path=service_environment_path)

        service_gatekeeper_key: str = self.get_service_gatekeeper_key()

        service_loadables: List[str] = self.get_service_loadables()
        service_configs: Union[List[str], None] = self.get_service_configs()
        service_verbosity: int = self.get_service_verbosity()
        service_config_path: str = str(self.get_service_config_path())

        service_name: str = self.get_service_name()
        service_key: str = slugify(service_name)
        service_version: str = self.get_service_version()
        service_summary: str = self.get_service_summary()
        service_authors: List[str] = self.get_service_authors()
        service_author_emails: List[str] = self.get_service_author_emails()

        service_meta: ServiceMeta = ServiceMeta(
            name=service_name,
            key=service_key,
            version=service_version,
            summary=service_summary,
            authors=service_authors,
            author_emails=service_author_emails
        )

        self.service_status: ServiceStatus = ServiceStatus()

        super().__init__(
            service_meta=service_meta,
            service_gatekeeper_key=service_gatekeeper_key,
            service_environment_path=service_environment_path,
            service_loadables=service_loadables,
            service_config_path=service_config_path,
            service_configs=service_configs,
            service_verbosity=service_verbosity
        )

    def get_service_name(self) -> str:
        """
        Returns the service name
        :return:
        """
        raise Exception(
            "get_service_name must be overridden in your service file. Please see `service.py`")

    def get_service_version(self) -> str:
        """
        Returns the service version
        :return:
        """
        raise Exception(
            "get_service_version must be overridden in your service file. Please see `service.py`")

    def get_service_loadables(self) -> List[str]:
        """
        This defines which drivers should be loaded when the service runs
        Each driver has an associated constant
        Add the applicable constant to the list for the drivers you wish to use
        :return:
        """
        raise Exception(
            "get_service_loadables must be overridden in your service file. Please see `service.py`")

    def get_service_summary(self) -> str:
        """
        A summary of the service
        :return:
        """
        raise Exception(
            "get_service_summary must be overridden in your service file. Please see `service.py`")

    def get_service_authors(self) -> List[str]:
        """
        Service authors
        :return:
        """
        raise Exception(
            "get_service_authors must be overridden in your service file. Please see `service.py`")

    def get_service_author_emails(self) -> List[str]:
        """
        Email addresses of the service authors
        :return:
        """
        raise Exception(
            "get_service_author_emails must be overridden in your service file. Please see `service.py`")

    def get_service_gatekeeper_key(self) -> str:
        """
        Gets the gatekeeper key from the environment variables
        :return:
        """
        return json.loads(os.environ[GATEKEEPER_KEY_ENVIRONMENT_NAME])['secret_key']

    def get_https_certificate_paths(self) -> Tuple[Union[None, str], Union[None, str]]:
        """
        Gets the certificates to run the dev server as https

        If, we return None, None the server will be launched as http
        :return:
        """
        # certificates_path: Path = Path(Path(__file__).resolve().parents[2]).joinpath('certificates')
        #
        # return str(certificates_path.joinpath('private.key')), str(certificates_path.joinpath('certificate.crt'))
        return None, None

    def get_service_configs(self) -> Union[List[str], None]:
        """
        This is used to define the names (including .yaml) of other configuraiton files you would like to have the application
            load for you.
        If you are satisfied with the app.yaml and bootstrap.yaml configuration files, leave this value at None
        :return:
        """
        configs: List[str] = []

        service_configs_path: Path = self.get_service_config_path()

        for config in glob.glob(str(service_configs_path.joinpath('service')) + "/*.yaml"):
            config: Path = Path(config)
            configs.append(str(config.relative_to(service_configs_path)))

        return configs

    def get_service_verbosity(self) -> int:
        """
        This is used to define the level of debug we want to preform. Typically this is just used within the logger to output
          different amounts of information. The typical range is between 0 (No debug) to 3 (Full Debug)
        :return:
        """
        return VERBOSITY_NORMAL

    def get_service_environment_path(self) -> str:
        """
        This is an override for the environment path
        :return:
        """
        return None

    def get_service_config_path(self) -> Path:
        """
        This is an override for the configuration path
        :return:
        """
        return None

    def get_service_package_path(self) -> Path:
        """
        This is an override for the package path
        :return:
        """
        return None

    def get_fact_subscriptions(self) -> List[str]:
        """
        List of facts to subscribe to
        :return:
        """
        return []

    def get_fact_providers(self) -> List[FactBase]:
        """
        List of facts to push at a regular basis
        :return:
        """
        return []

    def get_access_nodes(self) -> List[NodeCreate]:
        """
        List of permission nodes to push when the application loads
        You cannot use permissions for this microservice which are not defined here
        :return:
        """
        return []

    def fastapi_routers(self):
        """
        Register fastapi routers
        :return:
        """

        # !!!!! Imports for routers must go here to remove an order of operation bug that occurs when endpoints need to
        #          talk to a DB
        from ehelply_microservice_library.routers import router_service_monitor
        self.fastapi_driver.instance.include_router(
            router_service_monitor.router,
            prefix="/ehelply_monitor",
            tags=["ehelply", "service"],
            responses={404: {"description": "Not found"}},
        )

        from ehelply_microservice_library.routers import router_service_meta as service_meta_router
        self.fastapi_driver.instance.include_router(
            service_meta_router.router,
            prefix="/ehelply_meta",
            tags=["ehelply", "service"],
            responses={404: {"description": "Not found"}},
        )

        from ehelply_microservice_library.routers import router_service as service_router
        self.fastapi_driver.instance.include_router(
            service_router.router,
            prefix="/ehelply_service",
            tags=["ehelply", "service"],
            responses={404: {"description": "Not found"}},
        )

        from ehelply_microservice_library.routers import router_service_security as service_security_router
        self.fastapi_driver.instance.include_router(
            service_security_router.router,
            prefix="/ehelply_security",
            tags=["ehelply", "service"],
            responses={404: {"description": "Not found"}},
        )

    def get_secret_subscriptions(self) -> List[str]:
        """
        Hook into adding additional secret names to fetch during microservice boot

        :return:
        """
        return []

    def fetch_secrets(self):
        """
        Fetches secrets from AWS ASM

        :return:
        """
        subscribe_to_secrets: list = [
            secret_name_facts_vault(),
            secret_name_security_vault(),
        ]

        if LOADABLE_MYSQL in self.get_service_loadables():
            subscribe_to_secrets.append(secret_name_database())

        subscribe_to_secrets += self.get_secret_subscriptions()

        for secret_name in subscribe_to_secrets:
            State.secrets.add(ASM.get_secret(secret_name=secret_name), category=secret_name)

    def register_integrations(self):
        """
        Register service integrations
        A service integration is a wrapper around ehelply microservices to make them easier to talk to
        :return:
        """

        # The fact service integration works on a subscription model. You provide the integration with a list of facts
        #   you care about and it will grab the facts from the fact service for you and ensure it stays updated for
        #   if/when the value of the fact changes.
        subscribe_to_facts: list = [
                                       "cognito-ehelply",

                                       "db-rds-ehelply",

                                       "mq-logging",
                                       "mq-monitoring",

                                       "ehelply-access",
                                       "ehelply-security",
                                       "ehelply-notes",
                                       "ehelply-meta",
                                       "ehelply-monitors",
                                       "ehelply-users",
                                       "ehelply-access",
                                       "ehelply-templates",
                                       # "ehelply-places", TODO: Uncomment once added into facts seeding
                                       # "ehelply-reviews", TODO: Uncomment once added into facts seeding
                                       # "ehelply-appointments", TODO: Uncomment once added into facts seeding

                                   ] + self.get_fact_subscriptions()

        # Register the fact service integration
        self.register_integration(Fact(State.logger, subscribe_to_facts))

        # Register the M2M service integration
        self.register_integration(M2M())

        # Register the service integrations
        self.register_integration(
            Access(self.service_meta.name, self.service_meta.summary, self.get_access_nodes()))
        self.register_integration(Security())
        self.register_integration(Log(self.service_meta.key))
        self.register_integration(Note())
        self.register_integration(Meta())
        self.register_integration(Monitor())
        self.register_integration(User())
        self.register_integration(Appointments())
        self.register_integration(Places())
        self.register_integration(Reviews())

    def get_mysql_credentials(self) -> MySQLCredentials:
        """
        Returns MySQL credentials
        :return:
        """
        # rds_fact: dict = get_fact_stage("db-rds-ehelply")
        db_secret = State.secrets.get(category=secret_name_database())[0]
        return MySQLCredentials(
            host=db_secret.host,
            database=db_secret.dbname,
            port=db_secret.port,
            username=db_secret.username,
            password=db_secret.password,
        )

    def facts(self):
        """
        Push all the fact providers to the fact service
        :return:
        """
        if len(self.get_fact_providers()) > 0:
            State.logger.info("Pushing facts...")
        for fact in self.get_fact_providers():
            fact_integration: Fact = State.integrations.get("fact")
            fact_integration.push_fact(fact)

    def migrate(self):
        """
        Preforms MySQL Migrations
        :return:
        """
        if LOADABLE_MYSQL in self.service_loadables and State.config.sql.migrate:
            State.logger.info("Running Migrations..")
            import alembic.config
            alembicArgs = [
                '--raiseerr',
                'upgrade', 'head',
            ]
            alembic.config.main(argv=alembicArgs)

    def monitor(self):
        """
        Creates link to monitor service via a new thread
        :return:
        """
        monitor_integration: Monitor = State.integrations.get("monitor")
        monitor_integration.register_service()

        State.logger.info("Booting async monitor... Expect to see out of order Batcher info in the logs.")
        from ehelply_microservice_library.routers.router_service_monitor import MonitorHeartbeatThread
        monitor_thread = MonitorHeartbeatThread(
            logger=State.logger,
            heartbeat_interval_minutes=get_fact_stage("mq-monitoring")["interval"],
            queue_url=get_fact_endpoint("mq-monitoring"),
            environment=Environment.stage(),
            service_meta=self.service_meta,
            service_status=self.service_status,
        )
        monitor_thread.start()

    def check_for_updates(self):
        """
        Checks for service template updates
        :return:
        """
        access_key: str = State.config.updater.access_key
        secret_key: str = State.config.updater.secret_key

        service_updater: ServiceUpdater = ServiceUpdater(access_key=access_key, secret_key=secret_key, root_path=self.get_service_package_path())
        # update_available: bool = service_updater.is_update_available()

        try:
            update_available: bool = service_updater.is_update_available()
            if update_available:
                State.logger.info("New updates are available for the service template")
                State.logger.info("Service template update preview: " + str(service_updater.preview().dict()))
            else:
                State.logger.info("No updates available")
        except:
            State.logger.warning("Unable to check for service template updates")

    def fetch_keys(self):
        """
        Fetches keys from the security microservice
        :return:
        """
        State.logger.info("Booting security key fetcher... Expect to see out of order Batcher/Timer info in the logs.")
        from ehelply_microservice_library.routers.router_service_security import SecurityKeysThread
        security_keys_thread = SecurityKeysThread(
            logger=State.logger,
            delay=1200  # 20 min
        )
        security_keys_thread.start()

    def before_integrations(self):
        """
        EVENT HOOK

        Runs prior to integrations being loaded
        :return:
        """
        State.logger.info("Fetching secrets...")

        self.fetch_secrets()

    def before_drivers(self):
        """
        EVENT HOOK

        Runs prior to drivers being loaded
        :return:
        """
        State.logger.info("Pushing service specific facts...")

        self.facts()

        State.logger.info("Fetching encryption keys...")

        self.fetch_keys()

    def post_load(self):
        """
        Entry point of the application that is executed after the bootstrapping process completes

        If the microservice operates completely based on FastAPI endpoints, you can leave this method as is.
        If you require any custom logic or additional threads to be spun up, do it in this method.
        :return:
        """

        State.logger.info("Integrating into cluster...")

        self.monitor()

        State.logger.info("Checking for service template updates...")

        self.check_for_updates()

        State.logger.info("Running application code...")
