from abc import ABC, abstractmethod
from owrx.config.core import CoreConfig
from datetime import datetime, timezone
import json
import hashlib
import os
import stat

import logging

logger = logging.getLogger(__name__)


class PasswordException(Exception):
    pass


class Password(ABC):
    @staticmethod
    def from_dict(d: dict):
        if "encoding" not in d:
            raise PasswordException("password encoding not set")
        if d["encoding"] == "string":
            return CleartextPassword(d)
        elif d["encoding"] == "hash":
            return HashedPassword(d)
        raise PasswordException("invalid passord encoding: {0}".format(d["type"]))

    @abstractmethod
    def is_valid(self, inp: str) -> bool:
        pass

    @abstractmethod
    def toJson(self) -> dict:
        pass


class CleartextPassword(Password):
    def __init__(self, pwinfo):
        if isinstance(pwinfo, str):
            self._value = pwinfo
        elif isinstance(pwinfo, dict):
            self._value = pwinfo["value"]
        else:
            raise ValueError("invalid argument to ClearTextPassword()")

    def is_valid(self, inp: str) -> bool:
        return self._value == inp

    def toJson(self) -> dict:
        return {
            "encoding": "string",
            "value": self._value
        }


class HashedPassword(Password):
    def __init__(self, pwinfo, algorithm="sha256"):
        self.iterations = 100000
        if isinstance(pwinfo, str):
            self._createFromString(pwinfo, algorithm)
        else:
            self._loadFromDict(pwinfo)

    def _createFromString(self, pw: str, algorithm: str):
        self._algorithm = algorithm
        self._salt = os.urandom(32)
        dk = hashlib.pbkdf2_hmac(self._algorithm, pw.encode(), self._salt, self.iterations)
        self._hash = dk.hex()
        pass

    def _loadFromDict(self, d: dict):
        self._hash = d["value"]
        self._algorithm = d["algorithm"]
        self._salt = bytes.fromhex(d["salt"])
        pass

    def is_valid(self, inp: str) -> bool:
        dk = hashlib.pbkdf2_hmac(self._algorithm, inp.encode(), self._salt, self.iterations)
        return dk.hex() == self._hash

    def toJson(self) -> dict:
        return {
            "encoding": "hash",
            "value": self._hash,
            "algorithm": self._algorithm,
            "salt": self._salt.hex(),
        }


DefaultPasswordClass = HashedPassword


class User(object):
    def __init__(self, name: str, enabled: bool, password: Password, must_change_password: bool = False):
        self.name = name
        self.enabled = enabled
        self.password = password
        self.must_change_password = must_change_password

    def toJson(self):
        return {
            "user": self.name,
            "enabled": self.enabled,
            "must_change_password": self.must_change_password,
            "password": self.password.toJson()
        }

    @staticmethod
    def fromJson(d):
        if "user" in d and "password" in d and "enabled" in d:
            mcp = d["must_change_password"] if "must_change_password" in d else False
            return User(d["user"], d["enabled"], Password.from_dict(d["password"]), mcp)

    def setPassword(self, password: Password, must_change_password: bool = None):
        self.password = password
        if must_change_password is not None:
            self.must_change_password = must_change_password

    def is_enabled(self):
        return self.enabled

    def enable(self):
        self.enabled = True

    def disable(self):
        self.enabled = False


class UserList(object):
    sharedInstance = None

    @staticmethod
    def getSharedInstance():
        if UserList.sharedInstance is None:
            UserList.sharedInstance = UserList()
        return UserList.sharedInstance

    def __init__(self):
        self.file_modified = None
        self.users = {}

    def refresh(self):
        if self.file_modified is None or self._getUsersFileModifiedTimestamp() > self.file_modified:
            logger.debug("reloading users from disk due to file modification")
            self.users = self._loadUsers()

    def _getUsersFile(self):
        config = CoreConfig()
        return "{data_directory}/users.json".format(data_directory=config.get_data_directory())

    def _getUsersFileModifiedTimestamp(self):
        timestamp = 0
        try:
            timestamp = os.path.getmtime(self._getUsersFile())
        except FileNotFoundError:
            pass
        return datetime.fromtimestamp(timestamp, timezone.utc)

    def _loadUsers(self):
        usersFile = self._getUsersFile()
        # to avoid concurrency issues and problems when parsing errors occur:
        # get early, store late
        modified = self._getUsersFileModifiedTimestamp()
        try:
            with open(usersFile, "r") as f:
                users_json = json.load(f)

            users = {u.name: u for u in [User.fromJson(d) for d in users_json]}
            self.file_modified = modified
            return users
        except FileNotFoundError:
            self.file_modified = modified
            return {}
        except json.JSONDecodeError:
            logger.exception("error while parsing users file %s", usersFile)
            return {}
        except Exception:
            logger.exception("error while processing users from %s", usersFile)
            return {}

    def _userToJson(self, u):
        return u.toJson()

    def store(self):
        usersFile = self._getUsersFile()
        users = [u.toJson() for u in self.values()]
        try:
            # don't write directly to file to avoid corruption on exceptions
            jsonContent = json.dumps(users, indent=4)
            with open(usersFile, "w") as f:
                f.write(jsonContent)
            # file should be readable by us only
            os.chmod(usersFile, stat.S_IWUSR + stat.S_IRUSR)
        except Exception:
            logger.exception("error while writing users file %s", usersFile)
        self.refresh()

    def _getUsername(self, user):
        if isinstance(user, User):
            return user.name
        elif isinstance(user, str):
            return user
        else:
            raise ValueError("invalid user type")

    def addUser(self, user: User):
        self[user.name] = user

    def deleteUser(self, user):
        del self[self._getUsername(user)]

    def __delitem__(self, key):
        self.refresh()
        if key not in self.users:
            raise KeyError("User {user} doesn't exist".format(user=key))
        del self.users[key]
        self.store()

    def __getitem__(self, item):
        self.refresh()
        return self.users[item]

    def __contains__(self, item):
        self.refresh()
        return item in self.users

    def __setitem__(self, key, value):
        self.refresh()
        if key in self.users:
            raise KeyError("User {user} already exists".format(user=key))
        self.users[key] = value
        self.store()

    def values(self):
        self.refresh()
        return self.users.values()
