import os
import json
import shutil
import tomlkit

from slugify import slugify
from importlib import resources
from dataclasses import dataclass, asdict, field


@dataclass
class DBConf:
    """A data class for DB config dependency-injection."""

    # Required fields
    database: str
    dbdriver: str

    # Optional driver-dependent fields
    hostname: str = ''
    username: str = ''
    password: str = field(default='', repr=False)
    port: int = 0

    # URL or DSN?
    url: str = ''
    dsn: str = ''


class Conf:
    """A class to encapsulate .env settings for dep-injection."""

    # Directories
    user_home = ''
    app_root = ''
    conf_root = ''
    etc_root = ''
    opt_root = ''
    data_root = ''

    # Local config: 'wurker' section
    name = ''
    slug = ''
    app_id = ''
    env = ''
    log = ''
    loglevel = ''
    is_queen = True
    debug = True

    # 'ui' section
    domain = ''
    workers = 3

    # Database config objects
    local_db = None
    remote_db = None

    # Other attribs
    seed_root = ''
    seed_glob = "[0-9][0-9][0-9]_seed.json"

    url_format = "{dbdriver}:///{database}".format
    dsn_format = "{dbdriver}://{username}:{password}@{hostname}:{port}/{database}".format


    def __init__(self):
        """Init slots to empty; delay filesystem/config setup until initialize()."""

        self.user_home = os.getenv("HOME", os.path.expanduser("~"))
        self.app_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        self._initialized = False

    def initialize(self):
        """Initialize config directories, load config, and API auth."""

        if self._initialized:
            return

        self._init_etc()
        self._init_config()
        self._init_api_auth()
        self._initialized = True


    def _init_etc(self):
        """Init etc/ and dependent config dirs."""

        # Hardcode system and user defaults
        user_config = os.path.join(self.user_home, ".config", "wurker")

        # Allow override through an environment variable.
        etc_override = os.getenv("WURKER_CONFIG_DIR", '')

        # Determine the config choice: ('user', 'custom')
        if not etc_override:
            config_choice = 'user'
        else:
            if etc_override.lower() == 'user' or (
                    etc_override and os.path.abspath(etc_override) == os.path.abspath(user_config)
            ):
                config_choice = 'user'
            else:
                config_choice = 'custom'

        # Now set the config root paths
        if config_choice == 'user' or os.path.isdir(user_config):
            self.conf_root = user_config
            self.etc_root = os.path.join(self.conf_root, "etc")
            self.opt_root = os.path.join(self.conf_root, "opt")
            self.data_root = os.path.join(self.conf_root, "data")

        elif config_choice == 'custom' or (etc_override and os.path.isdir(etc_override)):
            self.conf_root = os.path.abspath(os.path.expanduser(etc_override))
            self.etc_root = os.path.join(self.conf_root, "etc")
            self.opt_root = os.path.join(self.conf_root, "opt")
            self.data_root = os.path.join(self.conf_root, "data")

        # Create the directories if needed
        for root in [self.etc_root, self.opt_root, self.data_root]:
            if not os.path.isdir(root):
                try:
                    os.makedirs(root, exist_ok=True)
                except Exception as ex:
                    raise Exception(f"Cannot create config directory {root}: {ex}")

        # Copy etc/ files into app config dir
        try:
            default_etc = resources.files("wurker").joinpath("etc")
            for item in default_etc.iterdir():
                dest_path = os.path.join(self.etc_root, item.name)
                if os.path.exists(dest_path):
                    continue

                if item.is_dir():
                    shutil.copytree(str(item), dest_path, dirs_exist_ok=True)
                else:
                    shutil.copy2(str(item), dest_path)

        except Exception as ex:
            raise Exception(f"Failed to copy additional default resources from the package etc/ directory: {ex}")

        config_path = os.path.join(self.etc_root, "wurker.toml")
        self.config_file = config_path
        self.seed_root = os.path.join(self.etc_root, "seeds")


    def _init_config(self):
        """Init config, reading from wurker.toml."""

        with open(self.config_file, "rb") as f:
            config = tomlkit.load(f)

        # Loaded OK, so let's make some DBConf objects 1st
        local_db = config["local_db"]
        remote_db = config["remote_db"]

        if not remote_db["dbdriver"]:
            remote_db = local_db.copy()

        self._init_db_conf('local_db', local_db)
        self._init_db_conf('remote_db', remote_db)

        for key, value in config["wurker"].items():
            setattr(self, key, value)

        for key, value in config["ui"].items():
            setattr(self, key, value)

        # Make a slug, if not set in .env
        if self.name and not self.slug:
            self.slug = slugify(self.name)

        # Checks out, so set it
        self.config = config


    def _init_db_conf(self, db_attr:str, db_cfg:dict):
        """Initialize DBConf objects for given DB."""

        if db_cfg["dbdriver"] == "sqlite":
            if not db_cfg["database"]:
                db_cfg["database"] = getattr(self.local_db, "database", "wurker.db")

            db_cfg["database"] = os.path.join(self.data_root, db_cfg["database"])
            url = self.url_format(dbdriver='sqlite', database=db_cfg["database"])
            db_conf = DBConf(dbdriver='sqlite', database=db_cfg["database"], url=url)

        elif db_cfg["dbdriver"] in ["mysql", "postgresql"]:
            dbdriver = (
                "postgresql+psycopg2"
                if db_cfg["dbdriver"] == "postgresql" else
                "mysql+mysqldb"
            )

            kwargs = {
                "hostname": db_cfg["hostname"],
                "username": db_cfg["username"],
                "password": db_cfg["password"],
                "database": db_cfg["database"],
                "dbdriver": dbdriver,
                "port": int(db_cfg["port"]) if db_cfg["port"] else None,
            }
            kwargs["dsn"] = self.dsn_format(**kwargs)

            db_conf = DBConf(**kwargs)

        else:
            raise ValueError(f"Invalid dbdriver '{dbdriver}'. Must be 'sqlite', 'mysql', or 'postgresql'")

        setattr(self, db_attr, db_conf)


    def _init_api_auth(self):
        """We need local .gitignored plaintext drone API auth, if applicable."""

        self.api_auth = None

        api_auth_json = os.path.join(self.data_root, "api_auth.json")
        if not os.path.exists(api_auth_json):
            return

        with open(api_auth_json, 'r') as f:
            users = json.load(f)
        if not users:
            return

        user = users.pop()
        if "username" not in user or "password" not in user:
            return

        self.api_auth = (user["username"], user["password"])


    @property
    def RemoteDB(self) -> DBConf:
        return self.remote_db


    @property
    def LocalDB(self) -> DBConf:
        return self.local_db


    @property
    def ApiAuth(self) -> tuple:
        return self.api_auth


    def get(self, table:str, key:str):
        """Read from wurker.toml and return the value."""

        with open(self.config_file, 'rb') as f:
            config = tomlkit.load(f)

        if table not in config or not config[table]:
            raise ValueError(f"Invalid table '{table}' for config file: '{self.config_file}'")

        if key not in config[table] or not config[table][key]:
            raise ValueError(f"Invalid key '{key}' for table '{table}' in config file: '{self.config_file}'")

        return config[table][key]


    def asdict(self, attr:str=''):
        if not attr:
            return {k:getattr(self, k) for k in self.__slots__}

        db_conf = None
        if attr == "RemoteDB":
            db_conf = self.RemoteDB
        elif attr == "LocalDB":
            db_conf = self.LocalDB

        if not db_conf:
            return {}

        return asdict(db_conf)


_conf = None


def get_conf() -> Conf:
    """Return a lazily-initialized Conf instance."""

    global _conf
    if _conf is None:
        _conf = Conf()
        _conf.initialize()

    return _conf


class _LazyConf:
    """Proxy for lazy access to Conf without import-time side effects."""

    def __getattr__(self, name):
        return getattr(get_conf(), name)

    def __setattr__(self, name, value):
        return setattr(get_conf(), name, value)

    def __repr__(self):
        return repr(get_conf())


# Attach lazy proxy to the wurker.settings module
conf = _LazyConf()
