import importlib
import logging
import os
import pkgutil
import sys
from functools import lru_cache
from pathlib import Path
from typing import Literal, Optional

from pydantic import DirectoryPath, field_validator, EmailStr, Field, SecretStr, ValidationInfo
from pydantic_settings import BaseSettings, SettingsConfigDict


# For some reason, this statement initializes logging for the rest of the application.
#  Without it, we don't get log statements for IndexManager.
logging.info("Loading config.py")


def get_transformer_modules(transformer_pkg: str = 'lexy.transformers'):
    """ Return a list of transformer modules. """
    lexy_transformer_modules = []
    transformer_pkg_m = importlib.import_module(transformer_pkg)
    for m in pkgutil.iter_modules(transformer_pkg_m.__path__,
                                  prefix=transformer_pkg + '.'):
        if not m.ispkg:
            lexy_transformer_modules.append(m.name)
    return lexy_transformer_modules


def expand_transformer_imports(transformer_imports: set[str]) -> set[str]:
    """ Expand transformer imports to include all submodules. """
    expanded_imports = set()
    for imp in transformer_imports:
        if '.' in imp and imp.rsplit('.', 1)[1] == '*':
            expanded_imports.update(get_transformer_modules(imp.rsplit('.', 1)[0]))
        else:
            expanded_imports.add(imp)
    return expanded_imports


class AppSettings(BaseSettings):

    # FastAPI settings
    TITLE: str = "Lexy Server"
    VERSION: str = "1.0.0"
    SUMMARY: str = "REST API for Lexy Server"
    LICENSE_INFO: dict = {
        "name": "Apache 2.0",
        "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
    }
    OPENAPI_PREFIX: str = ""
    DOCS_URL: str = "/docs"
    REDOC_URL: str = "/redoc"
    OPENAPI_URL: str = "/openapi.json"
    API_PREFIX: str = "/api"
    SERVERS: list[dict] = [
        {"url": "http://localhost:9900"}
    ]

    # Security settings
    # Uncomment the line below if you want to generate a new secret key every time the server restarts. Then, to use a
    #  fixed key, you would simply set the value of SECRET_KEY in your .env file. If you uncomment the next line,
    #  add `import secrets` to the top of this file.
    # SECRET_KEY: SecretStr = Field(default_factory=secrets.token_urlsafe, env="SECRET_KEY")
    SECRET_KEY: SecretStr = Field(default="changethis", validation_alias="SECRET_KEY")
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8  # 8 days

    # Database settings
    POSTGRES_USER: str = Field(default="postgres", validation_alias="POSTGRES_USER")
    POSTGRES_PASSWORD: SecretStr = Field(default="postgres", validation_alias="POSTGRES_PASSWORD")
    POSTGRES_HOST: str = Field(default="localhost", validation_alias="POSTGRES_HOST")
    POSTGRES_DB: str = Field(default="lexy", validation_alias="POSTGRES_DB")
    DB_ECHO_LOG: bool = False

    # User settings
    FIRST_SUPERUSER_EMAIL: EmailStr = Field("lexy@lexy.ai", validation_alias="FIRST_SUPERUSER_EMAIL")
    FIRST_SUPERUSER_PASSWORD: SecretStr = Field("lexy", validation_alias="FIRST_SUPERUSER_PASSWORD")

    # AWS settings
    AWS_ACCESS_KEY_ID: Optional[str] = Field(default=None, validation_alias="AWS_ACCESS_KEY_ID")
    AWS_SECRET_ACCESS_KEY: Optional[SecretStr] = Field(default=None, validation_alias="AWS_SECRET_ACCESS_KEY")
    AWS_REGION: Optional[str] = Field(default=None, validation_alias="AWS_REGION")

    # Google Cloud settings
    # Path to a file containing JSON credentials for a service account. Using Optional[str] because setting to
    #  Optional[FilePath] triggers a validation error if GOOGLE_APPLICATION_CREDENTIALS is an empty string.
    #  If set to devnull, or to an invalid filepath, GOOGLE_APPLICATION_CREDENTIALS will be set to None.
    GOOGLE_APPLICATION_CREDENTIALS: Optional[str] = (
        Field(default=None, validation_alias="GOOGLE_APPLICATION_CREDENTIALS")
    )

    @field_validator('GOOGLE_APPLICATION_CREDENTIALS')
    def check_google_application_credentials(cls, value):
        if not value:
            # Occurs when value is set to an empty string
            return None
        if value == os.path.devnull:
            return None
        if value and not Path(value).is_file():
            logging.warning(f"GOOGLE_APPLICATION_CREDENTIALS file '{value}' does not exist. "
                            f"Setting value to `None`.")
            return None
        if value and Path(value).is_file() and os.path.getsize(value) == 0:
            # For example, when set to 'lexy/docker/null-credentials.json'
            logging.warning(f"GOOGLE_APPLICATION_CREDENTIALS file '{value}' is empty. "
                            f"Setting value to `None`.")
            return None
        return value

    # Storage settings
    DEFAULT_STORAGE_SERVICE: Optional[Literal['s3', 'gcs']] = (
        Field(default="s3", validation_alias="DEFAULT_STORAGE_SERVICE")
    )
    DEFAULT_STORAGE_BUCKET: Optional[str] = Field(default=None, validation_alias="DEFAULT_STORAGE_BUCKET")
    DEFAULT_STORAGE_PREFIX: Optional[str] = Field(default=None, validation_alias="DEFAULT_STORAGE_PREFIX")

    # Default config for Collection objects and images
    COLLECTION_DEFAULT_CONFIG: dict = {
        'store_files': True,
        'generate_thumbnails': True
    }

    @field_validator('COLLECTION_DEFAULT_CONFIG')
    def set_collection_default_config(cls, v: dict, info: ValidationInfo) -> dict:
        if v.get('store_files') is True:
            if not v.get('storage_service'):
                v['storage_service'] = info.data.get('DEFAULT_STORAGE_SERVICE')
            if not v.get('storage_bucket'):
                v['storage_bucket'] = info.data.get('DEFAULT_STORAGE_BUCKET')
            if not v.get('storage_prefix'):
                v['storage_prefix'] = info.data.get('DEFAULT_STORAGE_PREFIX')
        return v

    IMAGE_THUMBNAIL_SIZES: set[tuple] = {
        # (100, 100),
        (200, 200),
    }

    # Transformer settings
    LEXY_SERVER_TRANSFORMER_IMPORTS: set[str] = {
        # 'lexy.transformers.*'
        'lexy.transformers.counter',
        'lexy.transformers.embeddings',
        'lexy.transformers.multimodal',
        'lexy.transformers.openai',
    }
    LEXY_WORKER_TRANSFORMER_IMPORTS: set[str] = {
        # 'lexy.transformers.*'
        'lexy.transformers.counter',
        'lexy.transformers.embeddings',
        'lexy.transformers.multimodal',
        'lexy.transformers.openai',
        'pipelines.*'
    }

    # Pipeline directory (relative to the project root)
    PIPELINE_DIR: DirectoryPath = Field(default="pipelines", validation_alias="PIPELINE_DIR")

    @property
    def sync_database_url(self) -> str:
        return (f"postgresql://"
                f"{self.POSTGRES_USER}"
                f":{self.POSTGRES_PASSWORD.get_secret_value()}"
                f"@{self.POSTGRES_HOST}"
                f"/{self.POSTGRES_DB}")

    @property
    def async_database_url(self) -> str:
        return (f"postgresql+asyncpg://"
                f"{self.POSTGRES_USER}"
                f":{self.POSTGRES_PASSWORD.get_secret_value()}"
                f"@{self.POSTGRES_HOST}"
                f"/{self.POSTGRES_DB}")

    @property
    def app_transformer_imports(self):
        return expand_transformer_imports(self.LEXY_SERVER_TRANSFORMER_IMPORTS)

    @property
    def worker_transformer_imports(self):
        # TODO: Move to a separate property for pipeline imports
        # if self.PIPELINE_DIR:
        #     # Add pipeline imports to the worker transformer imports (i.e., adds 'pipelines.*' to the imports)
        #     pipeline_imports = f'{self.PIPELINE_DIR.name}.*'
        #     expanded_set = self.LEXY_WORKER_TRANSFORMER_IMPORTS.union({pipeline_imports})
        #     return expand_transformer_imports(expanded_set)
        return expand_transformer_imports(self.LEXY_WORKER_TRANSFORMER_IMPORTS)

    @field_validator('PIPELINE_DIR', mode='before')
    @classmethod
    def check_pipeline_dir(cls, value):
        if value:
            pipeline_dir = Path(value).resolve()
            if not pipeline_dir.is_dir():
                err_msg = (f"Pipeline directory '{pipeline_dir}' does not exist. "
                           f"Run `lexy init` to initialize your project and create the directory.")
                raise ValueError(err_msg)
            # log a warning if pipeline_dir is not in the Python path (Celery can't import pipelines)
            if str(pipeline_dir) not in sys.path and str(pipeline_dir) != os.getcwd():
                logging.warning(f"Pipeline directory '{pipeline_dir}' is not in the Python path. "
                                f"Your pipelines may not be imported correctly.")
        return value

    model_config = SettingsConfigDict(case_sensitive=True, env_file='.env', env_file_encoding='utf-8', extra="allow")


class DevelopmentAppSettings(AppSettings):
    pass


class ProductionAppSettings(AppSettings):

    @field_validator('SECRET_KEY')
    @classmethod
    def check_secret_key(cls, value):
        if value.get_secret_value() == "changethis":
            logging.error("Using default value of SECRET_KEY in production settings! "
                          "Change this value to a secure secret key.")
            raise Exception("Using default value of SECRET_KEY in production settings!")
        return value


class TestAppSettings(AppSettings):

    # Database settings
    POSTGRES_DB: str = Field(default="lexy_tests", validation_alias="POSTGRES_TEST_DB")
    DB_ECHO_LOG: bool = False

    # User settings
    # Without `env=` argument, this will revert to the environment value of FIRST_SUPERUSER_EMAIL
    FIRST_SUPERUSER_EMAIL: EmailStr = Field("test@lexy.ai", validation_alias="TEST_SUPERUSER_EMAIL")
    FIRST_SUPERUSER_PASSWORD: SecretStr = Field("test", validation_alias="TEST_SUPERUSER_PASSWORD")

    # Storage settings
    # By default, use the same storage service and bucket for testing, with a different prefix
    DEFAULT_STORAGE_PREFIX: Optional[str] = Field(default="lexy_tests", validation_alias="DEFAULT_STORAGE_PREFIX")
    # Bucket used in testing S3 storage, regardless of the default storage service
    S3_TEST_BUCKET: Optional[str] = Field(default=None, validation_alias="S3_TEST_BUCKET")
    # Bucket used in testing GCS storage, regardless of the default storage service
    GCS_TEST_BUCKET: Optional[str] = Field(default=None, validation_alias="GCS_TEST_BUCKET")

    # Worker settings
    # Remove 'pipelines.*' from worker_transformer_imports - this avoids ModuleNotFoundError when running tests on
    #  lexy-server, which does not have any of the extra pipeline requirements installed.
    LEXY_WORKER_TRANSFORMER_IMPORTS: set[str] = {
        # 'lexy.transformers.*'
        'lexy.transformers.counter',
        'lexy.transformers.embeddings',
        'lexy.transformers.multimodal',
        'lexy.transformers.openai',
    }


@lru_cache()
def get_settings():
    config_cls_dict = {
        "development": DevelopmentAppSettings,
        "testing": TestAppSettings,
    }
    config_name = os.environ.get("LEXY_CONFIG", "development")
    config_cls = config_cls_dict[config_name]
    return config_cls()


settings = get_settings()
