import contextlib
import logging
import os
import re
from ceph_volume import terminal, conf
from ceph_volume import exceptions
from sys import version_info as sys_version_info
import configparser
from typing import Optional

logger = logging.getLogger(__name__)


class _TrimIndentFile(object):
    """
    This is used to take a file-like object and removes any
    leading tabs from each line when it's read. This is important
    because some ceph configuration files include tabs which break
    ConfigParser.
    """
    def __init__(self, fp):
        self.fp = fp

    def readline(self):
        line = self.fp.readline()
        return line.lstrip(' \t')

    def __iter__(self):
        return iter(self.readline, '')


def load_ceph_conf_path(cluster_name='ceph'):
    abspath = '/etc/ceph/%s.conf' % cluster_name
    conf.path = os.getenv('CEPH_CONF', abspath)
    conf.cluster = cluster_name


def load(abspath: Optional[str] = None):
    if abspath is None:
        abspath = conf.path

    if not os.path.exists(abspath):
        raise exceptions.ConfigurationError(abspath=abspath)

    parser = Conf()

    try:
        ceph_file = open(abspath)
        trimmed_conf = _TrimIndentFile(ceph_file)
        with contextlib.closing(ceph_file):
            parser.read_conf(trimmed_conf)
            conf.ceph = parser
            return parser
    except configparser.ParsingError as error:
        logger.exception('Unable to parse INI-style file: %s' % abspath)
        terminal.error(str(error))
        raise RuntimeError('Unable to read configuration file: %s' % abspath)


class Conf(configparser.ConfigParser):
    """
    Subclasses from ConfigParser to give a few helpers for Ceph
    configuration.
    """

    def read_path(self, path):
        self.path = path
        return self.read(path)

    def is_valid(self):
        try:
            self.get('global', 'fsid')
        except (configparser.NoSectionError, configparser.NoOptionError):
            raise exceptions.ConfigurationKeyError('global', 'fsid')

    def optionxform(self, optionstr):
        optionstr = optionstr.replace('_', ' ')
        optionstr = '_'.join(optionstr.split())
        return optionstr

    def get_safe(self, section, key, default=None, check_valid=True):
        """
        Attempt to get a configuration value from a certain section
        in a ``cfg`` object but returning None if not found. Avoids the need
        to be doing try/except {ConfigParser Exceptions} every time.
        """
        if check_valid:
            self.is_valid()
        try:
            return self.get(section, key)
        except (configparser.NoSectionError, configparser.NoOptionError):
            return default

    def get_list(self, section, key, default=None, split=','):
        """
        Assumes that the value for a given key is going to be a list separated
        by commas. It gets rid of trailing comments.  If just one item is
        present it returns a list with a single item, if no key is found an
        empty list is returned.

        Optionally split on other characters besides ',' and return a fallback
        value if no items are found.
        """
        self.is_valid()
        value = self.get_safe(section, key, [])
        if value == []:
            if default is not None:
                return default
            return value

        # strip comments
        value = re.split(r'\s+#', value)[0]

        # split on commas
        value = value.split(split)

        # strip spaces
        return [x.strip() for x in value]

    # XXX Almost all of it lifted from the original ConfigParser._read method,
    # except for the parsing of '#' in lines. This is only a problem in Python 2.7, and can be removed
    # once tooling is Python3 only with `Conf(inline_comment_prefixes=('#',';'))`
    def _read(self, fp, fpname):
        """Parse a sectioned setup file.

        The sections in setup file contains a title line at the top,
        indicated by a name in square brackets (`[]'), plus key/value
        options lines, indicated by `name: value' format lines.
        Continuations are represented by an embedded newline then
        leading whitespace.  Blank lines, lines beginning with a '#',
        and just about everything else are ignored.
        """
        cursect = None                        # None, or a dictionary
        optname = None
        lineno = 0
        e = None                              # None, or an exception
        while True:
            line = fp.readline()
            if not line:
                break
            lineno = lineno + 1
            # comment or blank line?
            if line.strip() == '' or line[0] in '#;':
                continue
            if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
                # no leading whitespace
                continue
            # continuation line?
            if line[0].isspace() and cursect is not None and optname:
                value = line.strip()
                if value:
                    cursect[optname].append(value)
            # a section header or option header?
            else:
                # is it a section header?
                mo = self.SECTCRE.match(line)
                if mo:
                    sectname = mo.group('header')
                    if sectname in self._sections:
                        cursect = self._sections[sectname]
                    elif sectname == 'DEFAULT':
                        cursect = self._defaults
                    else:
                        cursect = self._dict()
                        cursect['__name__'] = sectname
                        self._sections[sectname] = cursect
                    # So sections can't start with a continuation line
                    optname = None
                # no section header in the file?
                elif cursect is None:
                    raise configparser.MissingSectionHeaderError(fpname, lineno, line)
                # an option line?
                else:
                    mo = self._optcre.match(line)
                    if mo:
                        optname, vi, optval = mo.group('option', 'vi', 'value')
                        optname = self.optionxform(optname.rstrip())
                        # This check is fine because the OPTCRE cannot
                        # match if it would set optval to None
                        if optval is not None:
                            # XXX Added support for '#' inline comments
                            if vi in ('=', ':') and (';' in optval or '#' in optval):
                                # strip comments
                                optval = re.split(r'\s+(;|#)', optval)[0]
                                # if what is left is comment as a value, fallback to an empty string
                                # that is: `foo = ;` would mean `foo` is '', which brings parity with
                                # what ceph-conf tool does
                                if optval in [';','#']:
                                    optval = ''
                            optval = optval.strip()
                            # allow empty values
                            if optval == '""':
                                optval = ''
                            cursect[optname] = [optval]
                        else:
                            # valueless option handling
                            cursect[optname] = optval
                    else:
                        # a non-fatal parsing error occurred.  set up the
                        # exception but keep going. the exception will be
                        # raised at the end of the file and will contain a
                        # list of all bogus lines
                        if not e:
                            e = configparser.ParsingError(fpname)
                        e.append(lineno, repr(line))
        # if any parsing errors occurred, raise an exception
        if e:
            raise e

        # join the multi-line values collected while reading
        all_sections = [self._defaults]
        all_sections.extend(self._sections.values())
        for options in all_sections:
            for name, val in options.items():
                if isinstance(val, list):
                    options[name] = '\n'.join(val)

    def read_conf(self, conffile):
        if sys_version_info.major >= 3:
            self.read_file(conffile)
        elif sys_version_info.major < 3:
            self.readfp(conffile)
        else:
            raise RuntimeError('Not expecting python version > 3 yet.')
