import json
import os
import xml.etree.ElementTree as ET

import yaml

from figgypy.decrypt import gpg_decrypt, kms_decrypt, ssm_decrypt
from figgypy.exceptions import FiggypyError


class Config:
    """Configuration object

    Args:
        config_file (optional[str]): filename
            see config_file property for more details
        aws_config (optional[dict]): aws credentials
            see aws_config property for more details
            dict of arguments passed into boto3 session
            example:
                aws_creds = {'aws_access_key_id': aws_access_key_id,
                             'aws_secret_access_key': aws_secret_access_key,
                             'region_name': 'us-east-1'}
        gpg_config (optional[dict]): gpg configuration
            see gpg_config property for more details
            dict of arguments for gpg including:
                homedir, binary, and keyring
            example:
                gpg_config = {'homedir': '~/.gnupg/',
                              'binary': 'gpg',
                              'keyring': 'pubring.kbx'}
        decrypt_gpg (optional[bool]): decrypt gpg secrets
            see decrypt_gpg property for more details
            defaults to True
        decrypt_kms (optional[bool]): decrypt kms secrets
            see decrypt_kms property for more details
            defaults to True
        decrypt_ssm (optional[bool]): decrypt/retrieve parameters
            from ssm parameter store.
            see decrypt_ssm property for more details
            defaults to True

    Returns:
        object: configuration object with 'values' dictionary

    Object can be created with a filename only, relative path, or absolute path.
    If only name or relative path is provided, look in this order:

    1. current directory
    2. `~/.config/<file_name>`
    3. `/etc/<file_name>`

    It is a good idea to include your __package__ in the file name.
    For example, `cfg = Config(os.path.join(__package__, 'config.yaml'))`.
    This way it will look for your_package/config.yaml,
    ~/.config/your_package/config.yaml, and /etc/your_package/config.yaml.
    """

    _dirs = [
        os.curdir,
        os.path.join(os.path.expanduser("~"), ".config"),
        "/etc/",
    ]

    def __init__(
        self,
        config_file=None,
        aws_config=None,
        gpg_config=None,
        decrypt_gpg=True,
        decrypt_kms=True,
        decrypt_ssm=True,
    ):
        self.values = {}
        self._aws_config = aws_config
        self._gpg_config = gpg_config
        self._decrypt_gpg = decrypt_gpg
        self._decrypt_kms = decrypt_kms
        self._decrypt_ssm = decrypt_ssm
        self._config_file = None
        if config_file is not None:
            self.config_file = config_file

    @staticmethod
    def _find_file(f):
        """Find a config file if possible."""
        if os.path.isabs(f):
            return f
        for d in Config._dirs:
            _f = os.path.join(d, f)
            if os.path.isfile(_f):
                return _f
        raise FiggypyError(f"could not find configuration file {f} in dirs {Config._dirs}")

    @staticmethod
    def _parse_xml_to_dict(element):
        """Recursively convert an XML element to a dictionary."""
        result = {}
        for child in element:
            child_data = Config._parse_xml_to_dict(child)
            if child_data:
                result[child.tag] = child_data
            else:
                result[child.tag] = child.text
        return result

    def _load_file(self, f):
        """Get values from config file"""
        try:
            with open(f, encoding="utf-8") as _fo:
                content = _fo.read()
        except IOError:
            raise FiggypyError("could not open configuration file")

        ext = os.path.splitext(f)[1].lower()
        try:
            if ext in (".yaml", ".yml"):
                data = yaml.full_load(content)
            elif ext == ".json":
                data = json.loads(content)
            elif ext == ".xml":
                root = ET.fromstring(content)
                data = {root.tag: Config._parse_xml_to_dict(root)}
            else:
                data = yaml.full_load(content)
        except Exception as err:
            raise FiggypyError(f"could not parse configuration file: {err}") from err
        self.values.update(data)

    def _post_load_process(self):
        if self.decrypt_gpg:
            gpg_decrypt(self.values, self.gpg_config)
        if self.decrypt_kms:
            kms_decrypt(self.values, self.aws_config)
        if self.decrypt_ssm:
            ssm_decrypt(self.values, self.aws_config)
        for k, v in self.values.items():
            setattr(self, k, v)

    @property
    def aws_config(self):
        if self._aws_config is None:
            self._aws_config = {}
        return self._aws_config

    @aws_config.setter
    def aws_config(self, value):
        if not isinstance(value, dict):
            raise ValueError("aws_config must be a dict")
        self._aws_config = value
        if self.values:
            self._post_load_process()

    @property
    def config_file(self):
        """Configuration file.

        File can be located with a filename only, relative path, or absolute path.
        If only name or relative path is provided, look in this order:

        1. current directory
        2. `~/.config/<file_name>`
        3. `/etc/<file_name>`

        It is a good idea to include your __package__ in the file name.
        For example, `cfg = Config(os.path.join(__package__, 'config.yaml'))`.
        This way it will look for your_package/config.yaml,
        ~/.config/your_package/config.yaml, and /etc/your_package/config.yaml.
        """
        return self._config_file

    @config_file.setter
    def config_file(self, config_file):
        self._load_file(self._find_file(config_file))
        self._config_file = config_file
        self._post_load_process()

    @property
    def decrypt_gpg(self):
        return self._decrypt_gpg is not False

    @decrypt_gpg.setter
    def decrypt_gpg(self, value):
        self._decrypt_gpg = value is not False
        if self.values:
            self._post_load_process()

    @property
    def decrypt_kms(self):
        return self._decrypt_kms is not False

    @decrypt_kms.setter
    def decrypt_kms(self, value):
        self._decrypt_kms = value is not False
        if self.values:
            self._post_load_process()

    @property
    def decrypt_ssm(self):
        return self._decrypt_ssm is not False

    @decrypt_ssm.setter
    def decrypt_ssm(self, value):
        self._decrypt_ssm = value is not False
        if self.values:
            self._post_load_process()

    def get_value(self, *args, **kwargs):
        """Get from values dictionary by exposing self.values.get method.

        dict.get() method on Config.values
        """
        return self.values.get(*args, **kwargs)

    @property
    def gpg_config(self):
        if self._gpg_config is None:
            self._gpg_config = {}
        return self._gpg_config

    @gpg_config.setter
    def gpg_config(self, value):
        if not isinstance(value, dict):
            raise ValueError("gpg_config must be a dict")
        self._gpg_config = value
        if self.values:
            self._post_load_process()

    def set_value(self, key, value):
        """Set value in values dict."""
        self.values[key] = value

    def setup(
        self,
        config_file=None,
        aws_config=None,
        gpg_config=None,
        decrypt_gpg=True,
        decrypt_kms=True,
        decrypt_ssm=True,
    ):
        """Make setup easier by providing a constructor method.

        File can be located with a filename only, relative path, or absolute path.
        If only name or relative path is provided, look in this order:

        1. current directory
        2. `~/.config/<file_name>`
        3. `/etc/<file_name>`

        It is a good idea to include your __package__ in the file name.
        For example, `cfg = Config(os.path.join(__package__, 'config.yaml'))`.
        This way it will look for your_package/config.yaml,
        ~/.config/your_package/config.yaml, and /etc/your_package/config.yaml.
        """
        if aws_config is not None:
            self.aws_config = aws_config
        if gpg_config is not None:
            self.gpg_config = gpg_config
        if decrypt_kms is not None:
            self.decrypt_kms = decrypt_kms
        if decrypt_gpg is not None:
            self.decrypt_gpg = decrypt_gpg
        if decrypt_ssm is not None:
            self.decrypt_ssm = decrypt_ssm
        if config_file is not None:
            self.config_file = config_file
        return self
