import json
import os
import os.path
import uuid
from typing import Any, Dict, List, Optional
from yarl import URL

from .settings import _LOOKUP, Setting, _VALIDATORS
from ..logger import getLogger
from backup.file import JsonFileSaver

logger = getLogger(__name__)

ALWAYS_KEEP = {
    Setting.DAYS_BETWEEN_BACKUPS,
    Setting.MAX_BACKUPS_IN_HA,
    Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE,
}

KEEP_DEFAULT = {
    Setting.SEND_ERROR_REPORTS,
    Setting.IGNORE_UPGRADE_BACKUPS
}

# these are the options that should trigger a restart of the server
SERVER_OPTIONS = {
    Setting.USE_SSL,
    Setting.REQUIRE_LOGIN,
    Setting.CERTFILE,
    Setting.KEYFILE,
    Setting.EXPOSE_EXTRA_SERVER
}

NON_UI_SETTING = {
    Setting.SUPERVISOR_URL,
    Setting.TOKEN_SERVER_HOSTS,
    Setting.DRIVE_AUTHORIZE_URL,
    Setting.DRIVE_DEVICE_CODE_URL,
    Setting.DEFAULT_DRIVE_CLIENT_ID,
    Setting.NEW_BACKUP_TIMEOUT_SECONDS,
    Setting.LOG_LEVEL,
    Setting.CONSOLE_LOG_LEVEL,
    Setting.DEFAULT_SYNC_INTERVAL_VARIATION,
    Setting.CACHE_WARMUP_MAX_SECONDS,
    Setting.CACHE_WARMUP_ERROR_TIMEOUT_SECONDS,
    Setting.WATCH_BACKUP_DIRECTORY,
    Setting.TRACE_REQUESTS,
    Setting.MAX_BACKOFF_SECONDS
}

UPGRADE_OPTIONS = {
    Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: Setting.MAX_BACKUPS_IN_HA,
    Setting.DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE: Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE,
    Setting.DEPRECATED_DAYS_BETWEEN_BACKUPS: Setting.DAYS_BETWEEN_BACKUPS,
    Setting.DEPRECTAED_IGNORE_OTHER_BACKUPS: Setting.IGNORE_OTHER_BACKUPS,
    Setting.DEPRECTAED_IGNORE_UPGRADE_BACKUPS: Setting.IGNORE_UPGRADE_BACKUPS,
    Setting.DEPRECTAED_DELETE_BEFORE_NEW_BACKUP: Setting.DELETE_BEFORE_NEW_BACKUP,
    Setting.DEPRECTAED_BACKUP_NAME: Setting.BACKUP_NAME,
    Setting.DEPRECTAED_BACKUP_TIME_OF_DAY: Setting.BACKUP_TIME_OF_DAY,
    Setting.DEPRECTAED_SPECIFY_BACKUP_FOLDER: Setting.SPECIFY_BACKUP_FOLDER,
    Setting.DEPRECTAED_NOTIFY_FOR_STALE_BACKUPS: Setting.NOTIFY_FOR_STALE_BACKUPS,
    Setting.DEPRECTAED_ENABLE_BACKUP_STALE_SENSOR: Setting.ENABLE_BACKUP_STALE_SENSOR,
    Setting.DEPRECTAED_ENABLE_BACKUP_STATE_SENSOR: Setting.ENABLE_BACKUP_STATE_SENSOR,
    Setting.DEPRECATED_BACKUP_PASSWORD: Setting.BACKUP_PASSWORD
}

EMPTY_IS_DEFAULT = {
    Setting.ACCENT_COLOR,
    Setting.BACKGROUND_COLOR,
}


class GenConfig():
    def __init__(self, days=0, weeks=0, months=0, years=0, day_of_week='mon', day_of_month=1, day_of_year=1, aggressive=False):
        self.days = days
        self.weeks = weeks
        self.months = months
        self.years = years
        self.day_of_week = day_of_week
        self.day_of_month = day_of_month
        self.day_of_year = day_of_year
        self.aggressive = aggressive
        self._config_was_upgraded = False

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, GenConfig):
            return self.__dict__ == other.__dict__
        return NotImplemented

    def __hash__(self):
        """Overrides the default implementation"""
        return hash(tuple(sorted(self.__dict__.items())))


class Config():
    @classmethod
    def fromFile(cls, config_path):
        return Config(JsonFileSaver.read(config_path))

    @classmethod
    def withOverrides(cls, overrides):
        config = Config()
        for key in overrides.keys():
            config.override(key, overrides[key])
        return config

    @classmethod
    def withFileOverrides(cls, override_path):
        data = JsonFileSaver.read(override_path)
        overrides = {}
        for key in data.keys():
            overrides[_LOOKUP[key]] = data[key]
        return Config.withOverrides(overrides)

    @classmethod
    def fromEnvironment(cls):
        config = {}
        for key in os.environ:
            if key in _LOOKUP:
                config[_LOOKUP[key]] = _VALIDATORS[_LOOKUP[key]].validate(os.environ[key])
            elif str.lower(key) in _LOOKUP:
                config[_LOOKUP[str.lower(key)]] = _VALIDATORS[_LOOKUP[str.lower(key)]].validate(os.environ[key])
        return Config(config)

    def __init__(self, data=None):
        self.overrides = {}
        if data is None:
            self.config = {}
        else:
            self.config = data
        self._legacy_ignored_behavior = False
        self._subscriptions = []
        self._clientIdentifier = None
        self.retained = self._loadRetained()
        self._gen_config_cache = self.getGenerationalConfig()

        # Tracks when hosts have been seen to be offline to retry on different hosts.
        self._commFailure = {}

    def getConfigFor(self, options):
        new_config = Config()
        new_config.overrides = self.overrides.copy()
        new_config.update(options)
        return new_config

    def validateUpdate(self, additions):
        new_config = self.config.copy()
        new_config.update(additions)
        validated, upgraded = self.validate(new_config)
        return validated

    def validate(self, new_config) -> Dict[str, Any]:
        final_config = {}

        upgraded = False
        # validate each item
        for key in new_config:
            if type(key) == str:
                if key not in _LOOKUP:
                    # its not in the schema, just ignore it
                    continue
                setting = _LOOKUP[key]
            else:
                setting = key

            value = setting.validator().validate(new_config[key])
            if setting in UPGRADE_OPTIONS:
                upgraded = True
            if isinstance(value, str) and len(value) == 0 and setting in EMPTY_IS_DEFAULT:
                value = setting.default()
            if value is not None and (setting in KEEP_DEFAULT or value != setting.default()):
                if setting in UPGRADE_OPTIONS and (UPGRADE_OPTIONS[setting] not in new_config or new_config[UPGRADE_OPTIONS[setting]] == UPGRADE_OPTIONS[setting].default()):
                    upgraded = True
                    final_config[UPGRADE_OPTIONS[setting]] = value
                elif setting not in UPGRADE_OPTIONS:
                    final_config[setting] = value

        if upgraded:
            final_config[Setting.CALL_BACKUP_SNAPSHOT] = True

        # add in non-ui settings
        for setting in NON_UI_SETTING:
            if self.get(setting) != setting.default() and not (setting in new_config or setting.key in new_config) and setting not in self.overrides:
                final_config[setting] = self.get(setting)

        # add defaults
        for key in ALWAYS_KEEP:
            if key not in final_config:
                final_config[key] = key.default()

        if not final_config.get(Setting.USE_SSL, False):
            for key in [Setting.CERTFILE, Setting.KEYFILE]:
                if key in final_config:
                    del final_config[key]

        return final_config, upgraded

    def update(self, new_config):
        validated, upgraded = self.validate(new_config)
        self._config_was_upgraded = upgraded
        self.config = validated
        self._gen_config_cache = self.getGenerationalConfig()
        for sub in self._subscriptions:
            sub()

    def getServerOptions(self):
        ret = {}
        for setting in SERVER_OPTIONS:
            ret[setting] = self.get(setting)
        return ret

    def subscribe(self, func):
        self._subscriptions.append(func)

    def clientIdentifier(self) -> str:
        if self._clientIdentifier is None:
            try:
                if JsonFileSaver.exists(self.get(Setting.ID_FILE_PATH)):
                    self._clientIdentifier = JsonFileSaver.read(self.get(Setting.ID_FILE_PATH))['id']
                else:
                    self._clientIdentifier = str(uuid.uuid4())
                    JsonFileSaver.write(self.get(Setting.ID_FILE_PATH), {'id': self._clientIdentifier})
            except Exception:
                self._clientIdentifier = str(uuid.uuid4())
        return self._clientIdentifier

    def getGenerationalConfig(self) -> Optional[Dict[str, Any]]:
        days = self.get(Setting.GENERATIONAL_DAYS)
        weeks = self.get(Setting.GENERATIONAL_WEEKS)
        months = self.get(Setting.GENERATIONAL_MONTHS)
        years = self.get(Setting.GENERATIONAL_YEARS)
        if days + weeks + months + years == 0:
            return None
        base = GenConfig(
            days=days,
            weeks=weeks,
            months=months,
            years=years,
            day_of_week=self.get(Setting.GENERATIONAL_DAY_OF_WEEK),
            day_of_month=self.get(Setting.GENERATIONAL_DAY_OF_MONTH),
            day_of_year=self.get(Setting.GENERATIONAL_DAY_OF_YEAR),
            aggressive=self.get(Setting.GENERATIONAL_DELETE_EARLY)
        )
        if base.days <= 1:
            # must always be >= 1, otherwise we'll just create and delete backups constantly.
            base.days = 1
        return base

    def _loadRetained(self) -> List[str]:
        if JsonFileSaver.exists(self.get(Setting.RETAINED_FILE_PATH)):
            try:
                return JsonFileSaver.read(self.get(Setting.RETAINED_FILE_PATH))['retained']
            except json.decoder.JSONDecodeError:
                logger.error("Unable to parse retained backup settings")
                return []
        return []

    def isRetained(self, slug):
        return slug in self.retained

    def setRetained(self, slug, retain):
        if retain and slug not in self.retained:
            self.retained.append(slug)
            JsonFileSaver.write(self.get(Setting.RETAINED_FILE_PATH), {'retained': self.retained})
        elif not retain and slug in self.retained:
            self.retained.remove(slug)
            JsonFileSaver.write(self.get(Setting.RETAINED_FILE_PATH), {'retained': self.retained})

    def isExplicit(self, setting):
        return setting in self.config or setting.value in self.config

    def override(self, setting: Setting, value):
        self.overrides[setting] = value
        return self

    def get(self, setting: Setting) -> Any:
        if setting in self.overrides:
            return self.overrides[setting]
        if setting in self.config:
            return self.config[setting]
        if setting.key() in self.config:
            return self.config[setting.key()]
        else:
            if setting == Setting.IGNORE_UPGRADE_BACKUPS and self._legacy_ignored_behavior:
                # Use the old behavior, rather than the new one
                return False
            return setting.default()

    def getForUi(self, setting: Setting):
        return _VALIDATORS[setting].formatForUi(self.get(setting))

    def getTokenServers(self, path: str = "") -> List[URL]:
        return list(map(lambda s: URL(s).with_path(path), self.get(Setting.TOKEN_SERVER_HOSTS).split(",")))

    def mustSaveUpgradeChanges(self):
        return self._config_was_upgraded

    def getAllConfig(self) -> Dict[Setting, Any]:
        return self.config.copy()

    def persistedChanges(self):
        self._config_was_upgraded = False

    def useLegacyIgnoredBehavior(self, value: bool):
        """If the user upgrades from an old version and hasn't explicitely said they want to include upgrade backups, then this reverts them to the old behavior where they aren't ignored"""
        self._legacy_ignored_behavior = value
