# -*- coding: utf-8 -*-
import glob
import os
import json

from .lexer import lex
from .analyzer import analyze, enter_block_ctx
from .errors import NgxParserDirectiveError

# map of external / third-party directives to a parse function
EXTERNAL_PARSERS = {}


# TODO: raise special errors for invalid "if" args
def _prepare_if_args(stmt):
    """Removes parentheses from an "if" directive's arguments"""
    args = stmt['args']
    if args and args[0].startswith('(') and args[-1].endswith(')'):
        args[0] = args[0][1:].lstrip()
        args[-1] = args[-1][:-1].rstrip()
        start = int(not args[0])
        end = len(args) - int(not args[-1])
        args[:] = args[start:end]


def parse(filename, onerror=None, catch_errors=True, ignore=(), single=False,
        comments=False, strict=False, combine=False, check_ctx=True,
        check_args=True):
    """
    Parses an nginx config file and returns a nested dict payload

    :param filename: string contianing the name of the config file to parse
    :param onerror: function that determines what's saved in "callback"
    :param catch_errors: bool; if False, parse stops after first error
    :param ignore: list or tuple of directives to exclude from the payload
    :param combine: bool; if True, use includes to create a single config obj
    :param single: bool; if True, including from other files doesn't happen
    :param comments: bool; if True, including comments to json payload
    :param strict: bool; if True, unrecognized directives raise errors
    :param check_ctx: bool; if True, runs context analysis on directives
    :param check_args: bool; if True, runs arg count analysis on directives
    :returns: a payload that describes the parsed nginx config
    """
    config_dir = os.path.dirname(filename)

    payload = {
        'status': 'ok',
        'errors': [],
        'config': [],
    }

    # start with the main nginx config file/context
    includes = [(filename, ())]  # stores (filename, config context) tuples
    included = {filename: 0} # stores {filename: array index} map

    def _handle_error(parsing, e):
        """Adds representaions of an error to the payload"""
        file = parsing['file']
        error = str(e)
        line = getattr(e, 'lineno', None)

        parsing_error = {'error': error, 'line': line}
        payload_error = {'file': file, 'error': error, 'line': line}
        if onerror is not None:
            payload_error['callback'] = onerror(e)

        parsing['status'] = 'failed'
        parsing['errors'].append(parsing_error)

        payload['status'] = 'failed'
        payload['errors'].append(payload_error)

    def _parse(parsing, tokens, ctx=(), consume=False):
        """Recursively parses nginx config contexts"""
        fname = parsing['file']
        parsed = []

        # parse recursively by pulling from a flat stream of tokens
        for token, lineno, quoted in tokens:
            comments_in_args = []

            # we are parsing a block, so break if it's closing
            if token == '}' and not quoted:
                break

            # if we are consuming, then just continue until end of context
            if consume:
                # if we find a block inside this context, consume it too
                if token == '{' and not quoted:
                    _parse(parsing, tokens, consume=True)
                continue

            # the first token should always(?) be an nginx directive
            directive = token

            if combine:
                stmt = {
                    'file': fname,
                    'directive': directive,
                    'line': lineno,
                    'args': []
                }
            else:
                stmt = {
                    'directive': directive,
                    'line': lineno,
                    'args': []
                }

            # if token is comment
            if directive.startswith('#') and not quoted:
                if comments:
                    stmt['directive'] = '#'
                    stmt['comment'] = token[1:]
                    parsed.append(stmt)
                continue

            # TODO: add external parser checking and handling

            # parse arguments by reading tokens
            args = stmt['args']
            token, __, quoted = next(tokens)  # disregard line numbers of args
            while token not in ('{', ';', '}') or quoted:
                if token.startswith('#') and not quoted:
                    comments_in_args.append(token[1:])
                else:
                    stmt['args'].append(token)

                token, __, quoted = next(tokens)

            # consume the directive if it is ignored and move on
            if stmt['directive'] in ignore:
                # if this directive was a block consume it too
                if token == '{' and not quoted:
                    _parse(parsing, tokens, consume=True)
                continue

            # prepare arguments
            if stmt['directive'] == 'if':
                _prepare_if_args(stmt)

            try:
                # raise errors if this statement is invalid
                analyze(
                    fname=fname, stmt=stmt, term=token, ctx=ctx, strict=strict,
                    check_ctx=check_ctx, check_args=check_args
                )
            except NgxParserDirectiveError as e:
                if catch_errors:
                    _handle_error(parsing, e)

                    # if it was a block but shouldn't have been then consume
                    if e.strerror.endswith(' is not terminated by ";"'):
                        if token != '}' and not quoted:
                            _parse(parsing, tokens, consume=True)
                        else:
                            break

                    # keep on parsin'
                    continue
                else:
                    raise e

            # add "includes" to the payload if this is an include statement
            if not single and stmt['directive'] == 'include':
                pattern = args[0]
                if not os.path.isabs(args[0]):
                    pattern = os.path.join(config_dir, args[0])

                stmt['includes'] = []

                # get names of all included files
                if glob.has_magic(pattern):
                    fnames = glob.glob(pattern)
                    fnames.sort()
                else:
                    try:
                        # if the file pattern was explicit, nginx will check
                        # that the included file can be opened and read
                        open(str(pattern)).close()
                        fnames = [pattern]
                    except Exception as e:
                        fnames = []
                        e.lineno = stmt['line']
                        if catch_errors:
                            _handle_error(parsing, e)
                        else:
                            raise e

                for fname in fnames:
                    # the included set keeps files from being parsed twice
                    # TODO: handle files included from multiple contexts
                    if fname not in included:
                        included[fname] = len(includes)
                        includes.append((fname, ctx))
                    index = included[fname]
                    stmt['includes'].append(index)

            # if this statement terminated with '{' then it is a block
            if token == '{' and not quoted:
                inner = enter_block_ctx(stmt, ctx)  # get context for block
                stmt['block'] = _parse(parsing, tokens, ctx=inner)

            parsed.append(stmt)

            # add all comments found inside args after stmt is added
            for comment in comments_in_args:
                comment_stmt = {
                    'directive': '#',
                    'line': stmt['line'],
                    'args': [],
                    'comment': comment
                }
                parsed.append(comment_stmt)

        return parsed

    # the includes list grows as "include" directives are found in _parse
    for fname, ctx in includes:
        tokens = lex(fname)
        parsing = {
            'file': fname,
            'status': 'ok',
            'errors': [],
            'parsed': []
        }
        try:
            parsing['parsed'] = _parse(parsing, tokens, ctx=ctx)
        except Exception as e:
            _handle_error(parsing, e)

        payload['config'].append(parsing)

    if combine:
        return _combine_parsed_configs(payload)
    else:
        return payload


def _combine_parsed_configs(old_payload):
    """
    Combines config files into one by using include directives.

    :param old_payload: payload that's normally returned by parse()
    :return: the new combined payload
    """
    old_configs = old_payload['config']

    def _perform_includes(block):
        for stmt in block:
            if 'block' in stmt:
                stmt['block'] = list(_perform_includes(stmt['block']))
            if 'includes' in stmt:
                for index in stmt['includes']:
                    config = old_configs[index]['parsed']
                    for stmt in _perform_includes(config):
                        yield stmt
            else:
                yield stmt  # do not yield include stmt itself

    combined_config = {
        'file': old_configs[0]['file'],
        'status': 'ok',
        'errors': [],
        'parsed': []
    }

    for config in old_configs:
        combined_config['errors'] += config.get('errors', [])
        if config.get('status', 'ok') == 'failed':
            combined_config['status'] = 'failed'

    first_config = old_configs[0]['parsed']
    combined_config['parsed'] += _perform_includes(first_config)

    combined_payload = {
        'status': old_payload.get('status', 'ok'),
        'errors': old_payload.get('errors', []),
        'config': [combined_config]
    }
    return combined_payload


def register_external_parser(parser, directives):
    """
    :param parser: parser function
    :param directives: list of directive strings
    :return:
    """
    for directive in directives:
        EXTERNAL_PARSERS[directive] = parser

def parse_to_json(path):
    dict = parse(path)
    return json.dumps(dict)