# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations
import os
import inspect
from gaarf.api_clients import BaseClient, GoogleAdsApiClient

if os.getenv("ADS_HOUSEKEEPER_DEPLOYMENT_TYPE") == "Google Cloud":
  try:
    from google.cloud import firestore, pubsub_v1
  except ImportError as e:
    raise ImportError(
        "Please install googleads-housekeeper with Google Cloud support - `pip install googleads-housekeeper[gcp]`"
    ) from e

from googleads_housekeeper.adapters import orm, notifications, publisher
from googleads_housekeeper.services import handlers, messagebus, unit_of_work


class Bootstrapper:

  def __init__(self,
               type: str = "Local",
               topic_prefix: str | None = None) -> None:
    self.notification_service = notifications.ConsoleNotifications()
    if type == "Local":
      self.bootstrapper = LocalBootstrapper(topic_prefix=topic_prefix)
    if type == "Google Cloud":
      self.bootstrapper = GoogleCloudBootstrapper(topic_prefix=topic_prefix)
    if type == "Dev":
      self.bootstrapper = DevBootstrapper(topic_prefix=topic_prefix)

  def bootstrap_app(self,
                    ads_api_client=None,
                    start_orm=False,
                    uow=None,
                    notification_service=None,
                    publish_service=None):
    return self.bootstrapper._bootstrap(
        ads_api_client=ads_api_client,
        start_orm=start_orm,
        uow=uow,
        notification_service=notification_service,
        publish_service=publish_service)

  def _bootstrap(self, ads_api_client, start_orm, uow, notification_service,
                 publish_service):
    return bootstrap(
        ads_api_client=ads_api_client,
        start_orm=start_orm or self.start_orm,
        uow=uow or self.uow,
        notification_service=notification_service or self.notification_service,
        publish_service=publish_service or self.publish_service)


class GoogleCloudBootstrapper(Bootstrapper):

  def __init__(self,
               project_id: str = os.getenv("GOOGLE_CLOUD_PROJECT"),
               start_orm: bool = False,
               topic_prefix: str | None = None) -> None:
    from google.cloud import firestore, pubsub_v1
    self.start_orm = False
    self.uow = unit_of_work.FirestoreUnitOfWork(
        firestore.Client(project=project_id))
    self.notification_service = notifications.GoogleCloudAppEngineEmailNotifications(
        project_id)
    self.publish_service = publisher.GoogleCloudPubSubPublisher(
        client=pubsub_v1.PublisherClient(),
        project_id=project_id,
        topic_prefix=topic_prefix)


class LocalBootstrapper(Bootstrapper):

  def __init__(self, topic_prefix: str | None = None) -> None:
    self.start_orm = True
    self.uow = unit_of_work.SqlAlchemyUnitOfWork(get_db_uri())
    self.publish_service = publisher.LogPublisher(topic_prefix)
    self.notification_service = notifications.ConsoleNotifications()


class DevBootstrapper(Bootstrapper):

  def __init__(self, topic_prefix: str | None = None) -> None:
    self.start_orm = True
    self.uow = unit_of_work.SqlAlchemyUnitOfWork(
        f"sqlite:///{os.getcwd()}/ads_housekeeper_tasks.db")
    self.publish_service = publisher.LogPublisher(topic_prefix)
    self.notification_service = notifications.ConsoleNotifications()


def get_db_uri(host: str = os.environ.get("DB_HOST", "localhost"),
               port: int = int(os.environ.get("DB_PORT", 54321)),
               user: str = os.environ.get("DB_USER", "ads_housekeeper"),
               password: str = os.environ.get("DB_PASSWORD", "ads_housekeeper"),
               db_name: str = os.environ.get("DB_NAME", "ads_housekeeper"),
               db_engine: str = os.environ.get("DB_ENGINE",
                                               "postgresql")) -> str:
  if dev_db := os.environ.get("DEV_DB_HOST"):
    return f"sqlite:///{dev_db}"
  return f"{db_engine}://{user}:{password}@{host}:{port}/{db_name}"


def get_default_google_ads_api_client():
  return GoogleAdsApiClient(
      path_to_config=get_path_to_ads_config(), version=get_ads_api_version())


def get_ads_api_version() -> str:
  return os.environ.get("GOOGLE_ADS_API_VERSION", "v14")


def get_path_to_ads_config() -> str:
  return os.environ.get("GOOGLE_ADS_PATH_TO_CONFIG", "~/google-ads.yaml")


def get_notification_service() -> notifications.BaseNotifications:
  notification_service_factory = notifications.NotificationFactory()
  notification_type = os.environ.get("ADS_HOUSEKEEPER_NOTIFICATION_TYPE",
                                     "console")
  notification_service = notification_service_factory.create_nofication_service(
      notification_type=notification_type)
  return notification_service


def bootstrap(
    ads_api_client: BaseClient | None = None,
    start_orm: bool = True,
    uow: unit_of_work.AbstractUnitOfWork = unit_of_work.SqlAlchemyUnitOfWork(
        get_db_uri()),
    notification_service: notifications
    .BaseNotifications = get_notification_service(),
    publish_service: publisher.BasePublisher = publisher.LogPublisher()
) -> messagebus.MessageBus:

  if start_orm:
    orm.start_mappers(engine=uow.engine)

  dependencies = {
      "uow": uow,
      "ads_api_client": ads_api_client or get_default_google_ads_api_client(),
      "notification_service": notification_service,
      "publisher": publish_service
  }
  injected_event_handlers = {
      event_type: [
          inject_dependencies(handler, dependencies) for handler in handlers
      ] for event_type, handlers in handlers.EVENT_HANDLERS.items()
  }
  injected_command_handlers = {
      command_type: inject_dependencies(handler, dependencies)
      for command_type, handler in handlers.COMMAND_HANDLERS.items()
  }
  return messagebus.MessageBus(
      uow=uow,
      event_handlers=injected_event_handlers,
      command_handlers=injected_command_handlers,
      dependencies=dependencies)


def inject_dependencies(handler, dependencies):
  params = inspect.signature(handler).parameters
  deps = {
      name: dependency
      for name, dependency in dependencies.items()
      if name in params
  }
  return lambda message: handler(message, **deps)
