#!/usr/bin/env python
__author__ = "schlitzer"
import argparse
import json
import logging
import sys
import textwrap
from collections import OrderedDict

from python_freeipa import Client


def main():
    parser = argparse.ArgumentParser(
        description="Recreate API Commands from JSON api spec"
    )

    source_group = parser.add_mutually_exclusive_group(required=True)

    source_group.add_argument(
        "--source-json",
        dest="source_json",
        action="store",
        default=None,
        help="Full path to json document, describing the API",
    )

    parser.add_argument(
        "--source-json-version",
        dest="source_json_version",
        action="store",
        default=None,
        help="api_version this schema file uses",
    )

    source_group.add_argument(
        "--source-url",
        dest="source_url",
        action="store",
        default=None,
        help="fqdn of ipa server, to fetch the API from",
    )

    parser.add_argument(
        "--source-url-user",
        dest="source_url_user",
        action="store",
        default=None,
        help="username for remote server, omit if TGT present",
    )

    parser.add_argument(
        "--source-url-pass",
        dest="source_url_pass",
        action="store",
        default=None,
        help="password for remote server, omit if TGT present",
    )

    parsed_args = parser.parse_args()

    creator = MetaAPICreator(
        src_json=parsed_args.source_json,
        src_url=parsed_args.source_url,
        src_url_pass=parsed_args.source_url_pass,
        src_url_user=parsed_args.source_url_user,
        version=parsed_args.source_json_version,
    )
    creator.run()


class MetaAPICreator:
    def __init__(self, src_json, src_url, src_url_pass, src_url_user, version):
        self._src_json = src_json
        self._src_url = src_url
        self._src_url_pass = src_url_pass
        self._src_url_user = src_url_user
        self._json_spec = None
        self.log = logging.getLogger()
        fmt = logging.Formatter("%(levelname)s  %(message)s")
        handler = logging.StreamHandler(sys.stdout)
        handler.setFormatter(fmt)
        self.log.addHandler(handler)
        self.log.setLevel(logging.DEBUG)
        self._meta_api = []
        self._client = None
        self._version = version

    @property
    def client(self):
        if self._client:
            return self._client
        self._client = Client(self._src_url, verify_ssl=False)
        if self._src_url_pass:
            self._client.login(self._src_url_user, self._src_url_pass)
        else:
            self._client.login_kerberos()
        return self._client

    @property
    def meta_api(self):
        return self._meta_api

    @property
    def src_json(self):
        return self._src_json

    @property
    def src_url(self):
        return self._src_url

    @property
    def json_spec(self):
        if self._json_spec:
            return self._json_spec
        if self.src_json:
            self._json_spec = self.sort_spec(self._json_spec_from_file())
        else:
            self._json_spec = self.sort_spec(self._json_spec_from_url())
        return self._json_spec

    @property
    def version(self):
        if self._version:
            return self._version
        for key, value in self.client._request("env", [], {})["result"].items():
            self.log.info(key)
            if key == "api_version":
                self._version = value
        if not self._version:
            self.log.fatal("could not fetch version from upstream server")
            sys.exit(1)
        return self._version

    def sort_spec(self, od):
        res = OrderedDict()
        for k, v in sorted(od.items()):
            if isinstance(v, dict):
                res[k] = self.sort_spec(v)
            else:
                res[k] = v
        return res

    def append(self, line, base_indent=None):
        if not line:
            base_indent = ""
        elif not base_indent:
            base_indent = ""
        else:
            base_indent = "    " * base_indent
        self.meta_api.append("{0}{1}".format(base_indent, line.rstrip()))

    @staticmethod
    def _name_mapping(name, _type):
        return "{0}_{1}".format(_type, name)

    def _json_spec_from_file(self):
        try:
            with open(self.src_json) as json_file:
                return json.load(json_file)
        except OSError as err:
            self.log.fatal(err)
            sys.exit(1)

    def _json_spec_from_url(self):
        self.log.info("fetching data from {0}".format(self._src_url))
        self.client._version = self._version
        result = self.client._request("json_metadata", [], {"command": "all"})
        return {"result": result}

    def _class_header(self):
        self.append("from python_freeipa.client import Client")
        self.append("")
        self.append("")
        self.append("class ClientMeta(Client):")
        self.append("    version = '{0}'".format(self.version))
        self.append("")
        self.append(
            "    def __init__(self, host=None, verify_ssl=True, dns_discovery=True):"
        )
        self.append(
            "        super(ClientMeta, self).__init__(host=host, verify_ssl=verify_ssl, version=self.version, dns_discovery=dns_discovery)"
        )

    def _func_add(self, command, spec):
        self.append("")
        self.append("def {0}(".format(command), 1)
        self.append("    self,", 1)
        self.log.info("adding function {0}".format(command))

        args_body, args_options = self.func_add_args(spec["takes_args"])
        opts_body, opts_options = self.func_add_options(spec["takes_options"])

        head_later = list()

        for i in args_options:
            if i["default_value"]:
                head_later.append(i["head"])
            else:
                for line in i["head"]:
                    self.append(line, 2)

        for i in opts_options:
            if i["default_value"]:
                head_later.append(i["head"])
            else:
                for line in i["head"]:
                    self.append(line, 2)

        for i in head_later:
            for line in i:
                self.append(line, 2)

        self.append("**kwargs", 2)
        self.append("):", 1)
        try:
            self.append(
                textwrap.indent(
                    '"""\n' + textwrap.dedent(spec["doc"].lstrip("\n")), "    " * 2
                )
            )
        except AttributeError:
            self.append('"""\n', 2)
        self.meta_api.append("\n")
        for i in args_options:
            for line in i["doc"]:
                self.append(line, 2)
        for i in opts_options:
            for line in i["doc"]:
                self.append(line, 2)
        self.append('"""', 2)

        self.append("method = '{0}'".format(command), 2)
        for i in args_body:
            self.append(i, 2)
        for i in args_options:
            for line in i["body"]:
                self.append(line, 2)
        for i in opts_body:
            self.append(i, 2)
        for i in opts_options:
            for line in i["body"]:
                self.append(line, 2)

        self.append("")
        self.append("_params.update(kwargs)", 2)
        self.append("")
        self.append("return self._request(method, _args, _params)", 2)

    def func_add_args(self, specs):
        options = list()
        body = list()

        body.append("")
        body.append("_args = list()")

        for spec in specs:
            options.append(self.func_add_arg(spec))
        return body, options

    def func_add_arg(self, spec):
        result = dict()
        result["body"] = list()
        result["doc"] = list()
        result["head"] = list()
        result["default_value"] = False
        if not isinstance(spec, dict):
            self.log.warning("found arg spec, that is not a dictionary, adding *args")
            return result
        self.func_add_arg_head(spec, result)
        return result

    def func_add_arg_head(self, spec, result):
        arg_name = spec["name"]
        mapped_arg_name = self._name_mapping(arg_name, "a")
        if "default" in spec and (spec["required"] or spec["autofill"]):
            result["default_value"] = True
            if isinstance(spec["default"], str):
                result["head"].append(
                    "{0}='{1}',".format(mapped_arg_name, spec["default"])
                )
            elif isinstance(spec["default"], list):
                result["head"].append("{0}=None,".format(mapped_arg_name))
            else:
                result["head"].append(
                    "{0}={1},".format(mapped_arg_name, spec["default"])
                )
        elif spec["required"]:
            result["head"].append("{0},".format(mapped_arg_name))
        else:
            result["default_value"] = True
            result["head"].append("{0}=None,".format(mapped_arg_name))
        self.func_add_arg_doc(arg_name, spec, result, "a")
        self.func_add_arg_body(arg_name, spec, result)

    def func_add_arg_doc(self, arg_name, spec, result, _prefix):
        mapped_arg_name = self._name_mapping(arg_name, _prefix)
        lines = textwrap.wrap(
            " ".join(
                ":param {0}: {1}".format(
                    mapped_arg_name, spec["doc"].replace("\\n", r"\\n")
                ).splitlines()
            )
        )
        result["doc"].append(lines[0])
        for line in lines[1:]:
            result["doc"].append("    " + line)
        _class = spec["class"]
        if _class == "Flag":
            _class = "bool"
        elif _class == "Dict":
            _class = "dict"
        elif _class == "Int":
            _class = "int, min value {0}, max value {1}".format(
                spec["minvalue"], spec["maxvalue"]
            )
        elif _class == "IntEnum":
            _class = "int, valid values {0}".format(spec["cli_metavar"])
        elif _class == "Str":
            _class = "str"
        elif _class == "StrEnum":
            if spec["multivalue"]:
                _class = "list of str, valid values {0}".format(spec["cli_metavar"])
            else:
                _class = "str, valid values {0}".format(spec["cli_metavar"])
        result["doc"].append(":type  {0}: {1}".format(mapped_arg_name, _class))

    def func_add_arg_body(self, arg_name, spec, result):
        mapped_arg_name = self._name_mapping(arg_name, "a")
        result["body"].append("_args.append({0})".format(mapped_arg_name))

    def func_add_options(self, specs):
        options = list()
        body = list()

        body.append("")
        body.append("_params = dict()")

        for spec in specs:
            if spec["name"] == "version":
                continue
            options.append(self.func_add_option(spec))
        return body, options

    def func_add_option(self, spec):
        result = dict()
        result["body"] = list()
        result["doc"] = list()
        result["head"] = list()
        result["default_value"] = False
        if not isinstance(spec, dict):
            self.log.warning(
                "found option spec, that is not a dictionary, adding **kwargs?"
            )
            return result
        self.func_add_option_head(spec, result)
        return result

    def func_add_option_head(self, spec, result):
        arg_name = spec["name"]
        mapped_arg_name = self._name_mapping(arg_name, "o")
        if arg_name == "all":
            result["default_value"] = True
            result["head"].append("{0}=True,".format(mapped_arg_name))
        elif "default" in spec and (spec["required"] or spec["autofill"]):
            result["default_value"] = True
            if isinstance(spec["default"], str):
                result["head"].append(
                    "{0}='{1}',".format(mapped_arg_name, spec["default"])
                )
            elif isinstance(spec["default"], list):
                result["head"].append("{0}=None,".format(mapped_arg_name))
            else:
                result["head"].append(
                    "{0}={1},".format(mapped_arg_name, spec["default"])
                )
        elif spec["required"]:
            result["head"].append("{0},".format(mapped_arg_name))
        else:
            result["default_value"] = True
            result["head"].append("{0}=None,".format(mapped_arg_name))
        self.func_add_arg_doc(arg_name, spec, result, "o")
        self.func_add_option_body(arg_name, spec, result)

    def func_add_option_body(self, arg_name, spec, result):
        mapped_arg_name = self._name_mapping(arg_name, "o")
        if spec["required"]:
            result["body"].append(
                "_params['{0}'] = {1}".format(arg_name, mapped_arg_name)
            )
        else:
            result["body"].append("if {0} is not None:".format(mapped_arg_name))
            result["body"].append(
                "    _params['{0}'] = {1}".format(arg_name, mapped_arg_name)
            )

    def _render(self):
        meta_api = "\n".join(self.meta_api)
        return meta_api

    def run(self):
        self._class_header()
        for command, spec in self.json_spec["result"]["commands"].items():
            self._func_add(command, spec)
        self.append("")
        with open("meta_api.py", "w") as meta_api:
            meta_api.write(self._render())


if __name__ == "__main__":
    main()
