import importlib.resources as pkg_resources
import json
import logging
import os
from typing import Any, Dict, Optional, Union

from sqlalchemy import func
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session

from ... import defaults
from ..database.models import Setting, SettingType
from ..models.settings import (
    AppSetting,
    BaseSetting,
    LLMSetting,
    ReportSetting,
    SearchSetting,
)

# Setup logging
logger = logging.getLogger(__name__)


def check_env_setting(key: str) -> str | None:
    """
    Checks environment variables for a particular setting.

    Args:
        key: The database key for the setting.

    Returns:
        The setting from the environment variables, or None if the variable
        is not set.

    """
    env_variable_name = f"LDR_{'_'.join(key.split('.')).upper()}"
    env_value = os.getenv(env_variable_name)
    if env_value is not None:
        logger.debug(f"Overriding {key} setting from environment variable.")
    return env_value


class SettingsManager:
    """
    Manager for handling application settings with database storage and file fallback.
    Provides methods to get and set settings, with the ability to override settings in memory.
    """

    def __init__(self, db_session: Session):
        """
        Initialize the settings manager

        Args:
            db_session: SQLAlchemy session for database operations
        """
        self.db_session = db_session
        self.db_first = True  # Always prioritize DB settings

        # Load default settings.
        default_settings = pkg_resources.read_text(defaults, "default_settings.json")
        self.default_settings = json.loads(default_settings)

    def get_setting(self, key: str, default: Any = None, check_env: bool = True) -> Any:
        """
        Get a setting value

        Args:
            key: Setting key
            default: Default value if setting is not found
            check_env: If true, it will check the environment variable for
                this setting before reading from the DB.

        Returns:
            Setting value or default if not found
        """
        if check_env:
            env_value = check_env_setting(key)
            if env_value is not None:
                return env_value

        # If using database first approach and session available, check database
        if self.db_first and self.db_session:
            try:
                settings = (
                    self.db_session.query(Setting)
                    # This will find exact matches and any subkeys.
                    .filter(Setting.key.startswith(key)).all()
                )
                if len(settings) == 1:
                    # This is a bottom-level key.
                    value = settings[0].value
                    return value
                elif len(settings) > 1:
                    # This is a higher-level key.
                    settings_map = {
                        s.key.removeprefix(f"{key}."): s.value for s in settings
                    }
                    # We deliberately don't update the cache here to avoid
                    # conflicts between low-level keys and their parent keys.
                    return settings_map
            except SQLAlchemyError as e:
                logger.error(f"Error retrieving setting {key} from database: {e}")

        # Return default if not found
        return default

    def set_setting(self, key: str, value: Any, commit: bool = True) -> bool:
        """
        Set a setting value

        Args:
            key: Setting key
            value: Setting value
            commit: Whether to commit the change

        Returns:
            True if successful, False otherwise
        """
        # Always update database if available
        if self.db_session:
            try:
                setting = (
                    self.db_session.query(Setting).filter(Setting.key == key).first()
                )
                if setting:
                    setting.value = value
                    setting.updated_at = (
                        func.now()
                    )  # Explicitly set the current timestamp
                else:
                    # Determine setting type from key
                    setting_type = SettingType.APP
                    if key.startswith("llm."):
                        setting_type = SettingType.LLM
                    elif key.startswith("search."):
                        setting_type = SettingType.SEARCH
                    elif key.startswith("report."):
                        setting_type = SettingType.REPORT

                    # Create a new setting
                    new_setting = Setting(
                        key=key,
                        value=value,
                        type=setting_type,
                        name=key.split(".")[-1].replace("_", " ").title(),
                        description=f"Setting for {key}",
                    )
                    self.db_session.add(new_setting)

                if commit:
                    self.db_session.commit()

                return True
            except SQLAlchemyError as e:
                logger.error(f"Error setting value for {key}: {e}")
                self.db_session.rollback()
                return False

        # No database session, only update cache
        return True

    def get_all_settings(self) -> Dict[str, Any]:
        """
        Get all settings

        Returns:
            Dictionary of all settings
        """
        result = {}

        # Add database settings if available
        if self.db_session:
            try:
                for setting in self.db_session.query(Setting).all():
                    result[setting.key] = dict(
                        value=setting.value,
                        type=setting.type.name,
                        name=setting.name,
                        description=setting.description,
                        category=setting.category,
                        ui_element=setting.ui_element,
                        options=setting.options,
                        min_value=setting.min_value,
                        max_value=setting.max_value,
                        step=setting.step,
                        visible=setting.visible,
                        editable=setting.editable,
                    )

                    # Override from the environment variables if needed.
                    env_value = check_env_setting(setting.key)
                    if env_value is not None:
                        result[setting.key]["value"] = env_value
                        # Mark it as non-editable, because changes to the DB
                        # value have no effect as long as the environment
                        # variable is set.
                        result[setting.key]["editable"] = False
            except SQLAlchemyError as e:
                logger.error(f"Error retrieving all settings from database: {e}")

        return result

    def create_or_update_setting(
        self, setting: Union[BaseSetting, Dict[str, Any]], commit: bool = True
    ) -> Optional[Setting]:
        """
        Create or update a setting

        Args:
            setting: Setting object or dictionary
            commit: Whether to commit the change

        Returns:
            The created or updated Setting model, or None if failed
        """
        if not self.db_session:
            logger.warning(
                "No database session available, cannot create/update setting"
            )
            return None

        # Convert dict to BaseSetting if needed
        if isinstance(setting, dict):
            # Determine type from key if not specified
            if "type" not in setting and "key" in setting:
                key = setting["key"]
                if key.startswith("llm."):
                    setting_obj = LLMSetting(**setting)
                elif key.startswith("search."):
                    setting_obj = SearchSetting(**setting)
                elif key.startswith("report."):
                    setting_obj = ReportSetting(**setting)
                else:
                    setting_obj = AppSetting(**setting)
            else:
                # Use generic BaseSetting
                setting_obj = BaseSetting(**setting)
        else:
            setting_obj = setting

        try:
            # Check if setting exists
            db_setting = (
                self.db_session.query(Setting)
                .filter(Setting.key == setting_obj.key)
                .first()
            )

            if db_setting:
                # Update existing setting
                db_setting.value = setting_obj.value
                db_setting.name = setting_obj.name
                db_setting.description = setting_obj.description
                db_setting.category = setting_obj.category
                db_setting.ui_element = setting_obj.ui_element
                db_setting.options = setting_obj.options
                db_setting.min_value = setting_obj.min_value
                db_setting.max_value = setting_obj.max_value
                db_setting.step = setting_obj.step
                db_setting.visible = setting_obj.visible
                db_setting.editable = setting_obj.editable
                db_setting.updated_at = (
                    func.now()
                )  # Explicitly set the current timestamp
            else:
                # Create new setting
                db_setting = Setting(
                    key=setting_obj.key,
                    value=setting_obj.value,
                    type=SettingType[setting_obj.type.upper()],
                    name=setting_obj.name,
                    description=setting_obj.description,
                    category=setting_obj.category,
                    ui_element=setting_obj.ui_element,
                    options=setting_obj.options,
                    min_value=setting_obj.min_value,
                    max_value=setting_obj.max_value,
                    step=setting_obj.step,
                    visible=setting_obj.visible,
                    editable=setting_obj.editable,
                )
                self.db_session.add(db_setting)

            if commit:
                self.db_session.commit()

            return db_setting

        except SQLAlchemyError as e:
            logger.error(f"Error creating/updating setting {setting_obj.key}: {e}")
            self.db_session.rollback()
            return None

    def delete_setting(self, key: str, commit: bool = True) -> bool:
        """
        Delete a setting

        Args:
            key: Setting key
            commit: Whether to commit the change

        Returns:
            True if successful, False otherwise
        """
        if not self.db_session:
            logger.warning("No database session available, cannot delete setting")
            return False

        try:
            # Remove from database
            result = self.db_session.query(Setting).filter(Setting.key == key).delete()

            if commit:
                self.db_session.commit()

            return result > 0
        except SQLAlchemyError as e:
            logger.error(f"Error deleting setting {key}: {e}")
            self.db_session.rollback()
            return False

    def load_from_defaults_file(self, commit: bool = True, **kwargs: Any) -> None:
        """
        Import settings from the defaults settings file.

        Args:
            commit: Whether to commit changes to database
            **kwargs: Will be passed to `import_settings`.

        """
        self.import_settings(self.default_settings, commit=commit, **kwargs)

    def db_version_matches_defaults(self) -> bool:
        """
        Returns:
            True if the version saved in the DB matches that in the default
            settings file.

        """
        db_version = self.get_setting("app.version")
        default_version = self.default_settings["app.version"]["value"]
        logger.debug(
            f"App version saved in DB is {db_version}, have default "
            f"settings from version {default_version}."
        )

        return db_version == default_version

    @classmethod
    def get_instance(cls, db_session: Optional[Session] = None) -> "SettingsManager":
        """
        Get a singleton instance of the settings manager

        Args:
            db_session: Optional database session

        Returns:
            SettingsManager instance
        """
        if not hasattr(cls, "_instance"):
            cls._instance = cls(db_session)
        elif db_session and not cls._instance.db_session:
            # Update existing instance with a session
            cls._instance.db_session = db_session

        return cls._instance

    def import_settings(
        self,
        settings_data: Dict[str, Any],
        commit: bool = True,
        overwrite: bool = True,
        delete_extra: bool = False,
    ) -> None:
        """
        Import settings directly from the export format. This can be used to
        re-import settings that have been exported with `get_all_settings()`.

        Args:
            settings_data: The raw settings data to import.
            commit: Whether to commit the DB after loading the settings.
            overwrite: If true, it will overwrite the value of settings that
                are already in the database.
            delete_extra: If true, it will delete any settings that are in
                the database but don't have a corresponding entry in
                `settings_data`.

        """
        for key, setting_values in settings_data.items():
            if not overwrite:
                existing_value = self.get_setting(key)
                if existing_value is not None:
                    # Preserve the value from this setting.
                    setting_values["value"] = existing_value

            # Delete any existing setting so we can completely overwrite it.
            self.delete_setting(key, commit=False)

            setting = Setting(key=key, **setting_values)
            self.db_session.add(setting)

        if delete_extra:
            all_settings = self.get_all_settings()
            for key in all_settings:
                if key not in settings_data:
                    logger.debug(f"Deleting extraneous setting: {key}")
                    self.delete_setting(key, commit=False)

        if commit:
            self.db_session.commit()

    def _create_setting(self, key, value, setting_type):
        """Create a setting with appropriate metadata"""

        # Determine appropriate category
        category = None
        ui_element = "text"

        # Determine category based on key pattern
        if key.startswith("app."):
            category = "app_interface"
        elif key.startswith("llm."):
            if any(
                param in key
                for param in ["temperature", "max_tokens", "n_batch", "n_gpu_layers"]
            ):
                category = "llm_parameters"
            else:
                category = "llm_general"
        elif key.startswith("search."):
            if any(
                param in key
                for param in ["iterations", "questions", "results", "region"]
            ):
                category = "search_parameters"
            else:
                category = "search_general"
        elif key.startswith("report."):
            category = "report_parameters"

        # Determine UI element type based on value
        if isinstance(value, bool):
            ui_element = "checkbox"
        elif isinstance(value, (int, float)) and not isinstance(value, bool):
            ui_element = "number"
        elif isinstance(value, (dict, list)):
            ui_element = "textarea"

        # Build setting object
        setting_dict = {
            "key": key,
            "value": value,
            "type": setting_type.value.lower(),
            "name": key.split(".")[-1].replace("_", " ").title(),
            "description": f"Setting for {key}",
            "category": category,
            "ui_element": ui_element,
        }

        # Create the setting in the database
        self.create_or_update_setting(setting_dict, commit=False)
