import os
import sys
import jinja2
import re
import json
import traceback
from dpath import util as dpath_util
from jinja2.exceptions import UndefinedError, FilterArgumentError, TemplateSyntaxError
from jsonschema import validate, ValidationError, draft4_format_checker
from json.decoder import JSONDecodeError
from e2j2.exceptions import E2j2Exception
from e2j2.constants import CONFIG_SCHEMAS, TAGS, NESTED_TAGS, MARKER_SETS
from e2j2.tags import (
    base64_tag,
    consul_tag,
    file_tag,
    json_tag,
    jsonfile_tag,
    list_tag,
    vault_tag,
    dns_tag,
    escape_tag,
)
from e2j2.display import write, get_colors

try:
    from jinja2_ansible_filters import AnsibleCoreFiltersExtension

    j2_extensions = [AnsibleCoreFiltersExtension]
except ImportError:
    j2_extensions = []


def recursive_iter(obj, keys=()):
    if isinstance(obj, dict):
        for k, v in obj.items():
            yield from recursive_iter(v, keys + (k,))
    elif any(isinstance(obj, t) for t in (list, tuple)):
        for idx, item in enumerate(obj):
            yield from recursive_iter(item, keys + (idx,))
    else:
        yield keys, obj


def find(searchlist, j2file_ext, recurse=False):
    if recurse:
        return [
            os.path.realpath(os.path.join(dirpath, j2file))
            for searchlist_item in searchlist
            for dirpath, dirnames, files in os.walk(searchlist_item, followlinks=True)
            for j2file in files
            if j2file.endswith(j2file_ext)
        ]
    else:
        return [
            os.path.realpath(os.path.join(searchlist_item, j2file))
            for searchlist_item in searchlist
            for j2file in os.listdir(searchlist_item)
            if j2file.endswith(j2file_ext)
        ]


def get_vars(config, whitelist, blacklist):
    env_list = [entry for entry in whitelist if entry not in blacklist]
    env_vars = os.environ
    return resolv_vars(config, env_list, env_vars)


def resolv_vars(config, var_list, env_vars):
    colors = get_colors()
    varcontext = {}
    for var in var_list:
        var_value = env_vars[var]
        defined_tag = "".join(
            [tag for tag in TAGS if ":" in var_value and var_value.startswith(tag)]
        )
        try:
            if not defined_tag:
                varcontext[var] = var_value
            else:
                tag_config, tag_value = parse_tag(config, defined_tag, var_value)
                varcontext[var] = tag_value

                if (
                    "flatten" in tag_config
                    and tag_config["flatten"]
                    and isinstance(tag_value, dict)
                ):
                    for key, value in tag_value.items():
                        varcontext[key] = value

        except E2j2Exception as e:
            write(
                f"{colors.yellow}** WARNING: parsing {var} failed with error: {str(e)} **{colors.reset}\n"
            )
    return varcontext


def parse_tag(config, tag, value):
    tag_config = {}
    value = re.sub(r"^{}".format(tag), "", value).strip()
    if tag in CONFIG_SCHEMAS:
        envvars = os.environ
        config_var = tag.upper()[:-1] + "_CONFIG"
        token_var = tag.upper()[:-1] + "_TOKEN"
        # FIXME be more specific on raising error (config or data)
        try:
            tag_config = json.loads(envvars.get(config_var, "{}"))
            pattern = re.compile(r"config=(.+)")
            match = pattern.match(value)

            markers = detect_markers(config, value)
            value_with_config = (
                match.group(1).split(markers["config_end"] + ":")
                if pattern.match(value)
                else None
            )

            if value_with_config and len(value_with_config) > 1:
                config_str, value = value_with_config
                config_str = config_str.lstrip(markers["config_start"])
                tag_config.update(json.loads("{%s}" % config_str))
            elif value_with_config:
                raise E2j2Exception(
                    "invalid config markers used, please place the config between the markers '%s' and '%s'"
                    % (markers["config_start"], markers["config_end"])
                )

            if token_var in envvars:
                tag_config["token"] = (
                    tag_config["token"] if "token" in tag_config else envvars[token_var]
                )

            if "token" in tag_config and tag_config["token"].startswith("file:"):
                token_value = re.sub(r"^file:", "", tag_config["token"])
                tag_config["token"] = file_tag.parse(token_value).strip()

        except JSONDecodeError:
            raise E2j2Exception("decoding JSON failed")

        try:
            validate(
                instance=tag_config,
                schema=CONFIG_SCHEMAS[tag],
                format_checker=draft4_format_checker,
            )
        except ValidationError:
            if config["stacktrace"]:
                write(traceback.format_exc())

            raise E2j2Exception("config validation failed")

    if tag == "json:":
        tag_value = json_tag.parse(value)
    elif tag == "jsonfile:":
        tag_value = jsonfile_tag.parse(value)
    elif tag == "base64:":
        tag_value = base64_tag.parse(value)
    elif tag == "consul:":
        tag_value = consul_tag.parse(tag_config, value)
    elif tag == "list:":
        tag_value = list_tag.parse(value)
    elif tag == "file:":
        tag_value = file_tag.parse(value)
    elif tag == "vault:":
        tag_value = vault_tag.parse(tag_config, value)
    elif tag == "dns:":
        tag_value = dns_tag.parse(tag_config, value)
    elif tag == "escape:":
        tag_value = escape_tag.parse(value)
    else:
        return None, "** ERROR: tag: %s not implemented **" % tag

    if config["nested_tags"] and tag in NESTED_TAGS:
        try:
            for keys, item in recursive_iter(tag_value):
                if isinstance(item, str):
                    dpath_util.set(
                        tag_value,
                        list(keys),
                        resolv_vars(config, ["item"], {"item": item})["item"],
                    )
        except Exception:
            raise E2j2Exception("failed to resolve nested tag")

    return tag_config, tag_value


def render(config, j2file, j2vars):
    path, filename = os.path.split(j2file)
    j2 = jinja2.Environment(
        loader=jinja2.FileSystemLoader([path or "./", "/"]),
        undefined=jinja2.StrictUndefined,
        keep_trailing_newline=True,
        extensions=j2_extensions,
    )

    try:
        with open(j2file, "r") as file:
            content = file.read()

        markers = detect_markers(config, content)
        j2.block_start_string = markers["block_start"]
        j2.block_end_string = markers["block_end"]
        j2.variable_start_string = markers["variable_start"]
        j2.variable_end_string = markers["variable_end"]
        j2.comment_start_string = markers["comment_start"]
        j2.comment_end_string = markers["comment_end"]
        first_pass = j2.from_string(content).render(j2vars)

        if config["twopass"]:
            # second pass
            markers = detect_markers(config, first_pass)
            j2.block_start_string = markers["block_start"]
            j2.block_end_string = markers["block_end"]
            j2.variable_start_string = markers["variable_start"]
            j2.variable_end_string = markers["variable_end"]
            j2.comment_start_string = markers["comment_start"]
            j2.comment_end_string = markers["comment_end"]
            return j2.from_string(first_pass).render(j2vars)
        else: