# Copyright 2023 Canonical Ltd.
# Licensed under the Apache V2, see LICENCE file for details.

import argparse
import builtins
import functools
import json
import keyword
import pprint
import re
import textwrap
import typing
from collections import defaultdict
from glob import glob
from pathlib import Path
from typing import Any, Mapping, Sequence, TypeVar

import typing_inspect

from . import codegen

_marker = object()

JUJU_VERSION = re.compile(r'[0-9]+\.[0-9-]+[\.\-][0-9a-z]+(\.[0-9]+)?')
# Workaround for https://bugs.launchpad.net/juju/+bug/1683906
NAUGHTY_CLASSES = ['ClientFacade', 'Client', 'ModelStatusInfo']


# Map basic types to Python's typing with a callable
SCHEMA_TO_PYTHON = {
    'string': str,
    'integer': int,
    'float': float,
    'number': float,
    'boolean': bool,
    'object': Any,
}


# Friendly warning message to stick at the top of generated files.
HEADER = """\
# DO NOT CHANGE THIS FILE! This file is auto-generated by facade.py.
# Changes will be overwritten/lost when the file is regenerated.

"""


# Classes and helper functions that we'll write to _client.py
LOOKUP_FACADE = '''
def lookup_facade(name, version):
    """
    Given a facade name and version, attempt to pull that facade out
    of the correct client<version>.py file.

    """
    for _version in range(int(version), 0, -1):
        try:
            facade = getattr(CLIENTS[str(_version)], name)
            return facade
        except (KeyError, AttributeError):
            continue
    else:
        raise ImportError("No supported version for facade: "
                          "{}".format(name))

'''

TYPE_FACTORY = '''
class TypeFactory:
    @classmethod
    def from_connection(cls, connection):
        """
        Given a connected Connection object, return an initialized and
        connected instance of an API Interface matching the name of
        this class.

        @param connection: initialized Connection object.

        """
        facade_name = cls.__name__
        if not facade_name.endswith('Facade'):
           raise TypeError('Unexpected class name: {}'.format(facade_name))
        facade_name = facade_name[:-len('Facade')]
        version = connection.facades.get(facade_name)
        if version is None:
            raise Exception('No facade {} in facades {}'.format(facade_name,
                                                                connection.facades))

        c = lookup_facade(cls.__name__, version)
        c = c()
        c.connect(connection)

        return c

    @classmethod
    def best_facade_version(cls, connection):
        """
        Returns the best facade version for a given facade. This will help with
        trying to provide different functionality for different facade versions.

        @param connection: initialized Connection object.
        """
        facade_name = cls.__name__
        if not facade_name.endswith('Facade'):
           raise TypeError('Unexpected class name: {}'.format(facade_name))
        facade_name = facade_name[:-len('Facade')]
        return connection.facades.get(facade_name)


'''

CLIENT_TABLE = '''
CLIENTS = {{
    {clients}
}}

'''


class KindRegistry(dict):

    def register(self, name, version, obj):
        self[name] = {version: {
            "object": obj,
        }}

    def lookup(self, name, version=None):
        """If version is omitted, max version is used"""
        versions = self.get(name)
        if not versions:
            return None
        if version:
            return versions[version]
        return versions[max(versions)]

    def getObj(self, name, version=None):
        result = self.lookup(name, version)
        if result:
            obj = result["object"]
            return obj
        return None


class TypeRegistry(dict):

    def __init__(self, schema):
        self.schema = schema

    def get(self, name):
        # Two way mapping
        refname = self.schema.referenceName(name)
        if refname not in self:
            result = TypeVar(refname)
            self[refname] = result
            self[result] = refname

        return self[refname]

    def getRefType(self, ref):
        return self.get(ref)

    def objType(self, obj):
        kind = obj.get('type')
        if not kind:
            raise ValueError("%s has no type" % obj)
        result = SCHEMA_TO_PYTHON.get(kind)
        if not result:
            raise ValueError("%s has type %s" % (obj, kind))
        return result

    def refType(self, obj):
        return self.getRefType(obj["$ref"])


CLASSES = {}
factories = codegen.Capture()


def booler(v):
    if isinstance(v, str):
        if v == "false":
            return False
    return bool(v)


basic_types = [str, bool, int, float]


type_mapping = {
    'str': '(bytes, str)',
    'Sequence': '(bytes, str, list)',
    'Union': 'dict',
    'Mapping': 'dict',
}


def name_to_py(name):
    result = name.replace("-", "_")
    result = result.lower()
    if keyword.iskeyword(result) or result in dir(builtins):
        result += "_"
    return result


def var_type_to_py(kind):
    return 'None'


def kind_to_py(kind):
    if kind is None or kind is typing.Any:
        return 'None', '', False

    name = ""
    if typing_inspect.is_generic_type(kind):
        origin = typing_inspect.get_origin(kind)
        name = origin.__name__
    else:
        name = kind.__name__

    if (kind in basic_types or type(kind) in basic_types):
        return name, type_mapping.get(name) or name, True
    if (name in type_mapping):
        return name, type_mapping[name], True

    suffix = name.lstrip("~")
    return suffix, "(dict, {})".format(suffix), True


def strcast(kind, keep_builtins=False):
    if (kind in basic_types or
            type(kind) in basic_types) and keep_builtins is False:
        return kind.__name__
    if str(kind).startswith('~'):
        return str(kind)[1:]
    if kind is typing.Any:
        return 'Any'
    try:
        if issubclass(kind, typing.GenericMeta):
            return str(kind)[1:]
    except AttributeError:
        pass
    return kind


class Args(list):

    def __init__(self, schema, defs):
        self.schema = schema
        self.defs = defs
        if defs:
            rtypes = schema.registry.getObj(schema.types[defs])
            if len(rtypes) == 1:
                if not self.do_explode(rtypes[0][1]):
                    for name, rtype in rtypes:
                        self.append((name, rtype))
            else:
                for name, rtype in rtypes:
                    self.append((name, rtype))

    def do_explode(self, kind):
        if kind is Any:
            return False
        if kind in basic_types or type(kind) is typing.TypeVar:
            return False
        if typing_inspect.is_generic_type(kind) and issubclass(typing_inspect.get_origin(kind), Sequence):
            return False
        if typing_inspect.is_generic_type(kind) and issubclass(typing_inspect.get_origin(kind), Mapping):
            return False
        self.clear()
        self.extend(Args(self.schema, kind))
        return True

    def PyToSchemaMapping(self):
        m = {}
        for n, rt in self:
            m[name_to_py(n)] = n
        return m

    def SchemaToPyMapping(self):
        m = {}
        for n, tr in self:
            m[n] = name_to_py(n)
        return m

    def _format(self, name, rtype, typed=True):
        if typed:
            return "{} : {}".format(
                name_to_py(name),
                strcast(rtype)
            )
        else:
            return name_to_py(name)

    def _get_arg_str(self, typed=False, joined=", "):
        if self:
            parts = []
            for item in self:
                parts.append(self._format(item[0], item[1], typed))
            if joined:
                return joined.join(parts)
            return parts
        return ''

    def as_kwargs(self):
        if self:
            parts = []
            for item in self:
                var_name = name_to_py(item[0])
                var_type = var_type_to_py(item[1])
                parts.append('{}={}'.format(var_name, var_type))
            return ', '.join(parts)
        return ''

    def as_validation(self):
        """
        as_validation returns a series of validation statements for every item
        in the the Args.
        """
        parts = []
        for item in self:
            var_name = name_to_py(item[0])
            var_type, var_sub_type, ok = kind_to_py(item[1])
            if ok:
                parts.append(buildValidation(var_name, var_type, var_sub_type))
        return '\n'.join(parts)

    def typed(self):
        return self._get_arg_str(True)

    def __str__(self):
        return self._get_arg_str(False)

    def get_doc(self):
        return self._get_arg_str(True, "\n")


def buildValidation(name, instance_type, instance_sub_type, ident=None):
    INDENT = ident or "    "
    source = """{ident}if {name} is not None and not isinstance({name}, {instance_sub_type}):
{ident}    raise Exception("Expected {name} to be a {instance_type}, received: {{}}".format(type({name})))
""".format(ident=INDENT,
           name=name,
           instance_type=instance_type,
           instance_sub_type=instance_sub_type)
    return source


def buildTypes(schema, capture):
    INDENT = "    "
    for kind in sorted((k for k in schema.types if not isinstance(k, str)),
                       key=lambda x: str(x)):
        name = schema.types[kind]
        if name in capture and name not in NAUGHTY_CLASSES:
            continue
        args = Args(schema, kind)
        # Write Factory class for _client.py
        make_factory(name)
        # Write actual class
        source = ["""
class {}(Type):
    _toSchema = {}
    _toPy = {}
    def __init__(self{}{}, **unknown_fields):
        '''
{}
        '''""".format(
            name,
            # pprint these to get stable ordering across regens
            pprint.pformat(args.PyToSchemaMapping(), width=999),
            pprint.pformat(args.SchemaToPyMapping(), width=999),
            ", " if args else "",
            args.as_kwargs(),
            textwrap.indent(args.get_doc(), INDENT * 2))]

        if not args:
            source.append("{}self.unknown_fields = unknown_fields".format(INDENT * 2))
        else:
            # do the validation first, before setting the variables
            for arg in args:
                arg_name = name_to_py(arg[0])
                arg_type = arg[1]
                arg_type_name = strcast(arg_type)
                if arg_type in basic_types or arg_type is typing.Any:
                    source.append("{}{}_ = {}".format(INDENT * 2,
                                                      arg_name,
                                                      arg_name))
                elif type(arg_type) is typing.TypeVar:
                    source.append("{}{}_ = {}.from_json({}) "
                                  "if {} else None".format(INDENT * 2,
                                                           arg_name,
                                                           arg_type_name,
                                                           arg_name,
                                                           arg_name))
                elif typing_inspect.is_generic_type(arg_type) and issubclass(typing_inspect.get_origin(arg_type), Sequence):
                    parameters = typing_inspect.get_parameters(arg_type)
                    value_type = (
                        parameters[0]
                        if len(parameters)
                        else None
                    )
                    if type(value_type) is typing.TypeVar:
                        source.append(
                            "{}{}_ = [{}.from_json(o) "
                            "for o in {} or []]".format(INDENT * 2,
                                                        arg_name,
                                                        strcast(value_type),
                                                        arg_name))
                    else:
                        source.append("{}{}_ = {}".format(INDENT * 2,
                                                          arg_name,
                                                          arg_name))
                elif typing_inspect.is_generic_type(arg_type) and issubclass(typing_inspect.get_origin(arg_type), Mapping):
                    parameters = typing_inspect.get_parameters(arg_type)
                    value_type = (
                        parameters[0]
                        if len(parameters)
                        else None
                    )
                    if type(value_type) is typing.TypeVar:
                        source.append(
                            "{}{}_ = {{k: {}.from_json(v) "
                            "for k, v in ({} or dict()).items()}}".format(
                                INDENT * 2,
                                arg_name,
                                strcast(value_type),
                                arg_name))
                    else:
                        source.append("{}{}_ = {}".format(INDENT * 2,
                                                          arg_name,
                                                          arg_name))
                else:
                    source.append("{}{}_ = {}".format(INDENT * 2,
                                                      arg_name,
                                                      arg_name))
            if len(args) > 0:
                source.append('\n{}# Validate arguments against known Juju API types.'.format(INDENT * 2))
            for arg in args:
                arg_name = "{}_".format(name_to_py(arg[0]))
                arg_type, arg_sub_type, ok = kind_to_py(arg[1])
                if ok:
                    source.append('{}'.format(buildValidation(arg_name,
                                                              arg_type,
                                                              arg_sub_type,
                                                              ident=INDENT * 2)))

            for arg in args:
                arg_name = name_to_py(arg[0])
                source.append('{}self.{} = {}_'.format(INDENT * 2, arg_name, arg_name))
            # Ensure that we take the kwargs (unknown_fields) and put it on the
            # Results/Params so we can inspect it.
            source.append("{}self.unknown_fields = unknown_fields".format(INDENT * 2))

        source = "\n".join(source)
        capture.clear(name)
        capture[name].write(source)
        capture[name].write("\n\n")
        if name is None:
            print(source)
        co = compile(source, __name__, "exec")
        ns = _getns(schema)
        exec(co, ns)
        cls = ns[name]
        CLASSES[name] = cls


def retspec(schema, defs):
    # return specs
    # only return 1, so if there is more than one type
    # we need to include a union
    # In truth there is only 1 return
    # Error or the expected Type
    if not defs:
        return None
    if defs in basic_types:
        return strcast(defs, False)
    return strcast(defs, False)


def ReturnMapping(cls):
    # Annotate the method with a return Type
    # so the value can be cast
    def decorator(f):
        @functools.wraps(f)
        async def wrapper(*args, **kwargs):
            nonlocal cls
            reply = await f(*args, **kwargs)
            if cls is None:
                return reply
            if 'error' in reply:
                cls = CLASSES['Error']
            if typing_inspect.is_generic_type(cls) and issubclass(typing_inspect.get_origin(cls), Sequence):
                parameters = typing_inspect.get_parameters(cls)
                result = []
                item_cls = parameters[0]
                for item in reply:
                    result.append(item_cls.from_json(item))
                    """
                    if 'error' in item:
                        cls = CLASSES['Error']
                    else:
                        cls = item_cls
                    result.append(cls.from_json(item))
                    """
            else:
                result = cls.from_json(reply['response'])

            return result
        return wrapper
    return decorator


def makeFunc(cls, name, description, params, result, _async=True):
    INDENT = "    "
    args = Args(cls.schema, params)
    assignments = []
    toschema = args.PyToSchemaMapping()
    for arg in args._get_arg_str(False, False):
        assignments.append("{}_params[\'{}\'] = {}".format(INDENT,
                                                           toschema[arg],
                                                           arg))
    assignments = "\n".join(assignments)
    res = retspec(cls.schema, result)
    source = """

@ReturnMapping({rettype})
{_async}def {name}(self{argsep}{args}):
    '''
{docstring}
    Returns -> {res}
    '''
{validation}
    # map input types to rpc msg
    _params = dict()
    msg = dict(type='{cls.name}',
               request='{name}',
               version={cls.version},
               params=_params)
{assignments}
    reply = {_await}self.rpc(msg)
    return reply

"""

    if description != "":
        description = "{}\n\n".format(description)
    doc_string = "{}{}".format(description, args.get_doc())
    fsource = source.format(_async="async " if _async else "",
                            name=name,
                            argsep=", " if args else "",
                            args=args.as_kwargs(),
                            res=res,
                            validation=args.as_validation(),
                            rettype=result.__name__ if result else None,
                            docstring=textwrap.indent(doc_string, INDENT),
                            cls=cls,
                            assignments=assignments,
                            _await="await " if _async else "")
    ns = _getns(cls.schema)
    exec(fsource, ns)
    func = ns[name]
    return func, fsource


def makeRPCFunc(cls):
    source = """

async def rpc(self, msg):
    '''
    Patch rpc method to add Id.
    '''
    if not hasattr(self, 'Id'):
        raise RuntimeError('Missing "Id" field')
    msg['Id'] = id

    from .facade import TypeEncoder
    reply = await self.connection.rpc(msg, encoder=TypeEncoder)
    return reply

"""
    ns = _getns(cls.schema)
    exec(source, ns)
    func = ns["rpc"]
    return func, source


def buildMethods(cls, capture):
    properties = cls.schema['properties']
    for methodname in sorted(properties):
        method, source = _buildMethod(cls, methodname)
        setattr(cls, methodname, method)
        capture["{}Facade".format(cls.__name__)].write(source, depth=1)


def _buildMethod(cls, name):
    params = None
    result = None
    method = cls.schema['properties'][name]
    description = ""
    if 'description' in method:
        description = method['description']
    if 'properties' in method:
        prop = method['properties']
        spec = prop.get('Params')
        if spec:
            params = cls.schema.types.get(spec['$ref'])
        spec = prop.get('Result')
        if spec:
            if '$ref' in spec:
                result = cls.schema.types.get(spec['$ref'])
            else:
                result = SCHEMA_TO_PYTHON[spec['type']]
    return makeFunc(cls, name, description, params, result)


def buildWatcherRPCMethods(cls, capture):
    properties = cls.schema['properties']
    if "Next" in properties and "Stop" in properties:
        method, source = makeRPCFunc(cls)
        setattr(cls, "rpc", method)
        capture["{}Facade".format(cls.__name__)].write(source, depth=1)


def buildFacade(schema):
    cls = type(schema.name, (Type,), dict(name=schema.name,
                                          version=schema.version,
                                          schema=schema))
    source = """
class {name}Facade(Type):
    name = '{name}'
    version = {version}
    schema = {schema}
    """.format(name=schema.name,
               version=schema.version,
               schema=textwrap.indent(pprint.pformat(schema), "    "))
    return cls, source


class TypeEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Type):
            return obj.serialize()
        return json.JSONEncoder.default(self, obj)


class Type:
    def connect(self, connection):
        self.connection = connection

    def __repr__(self):
        return "{}({})".format(self.__class__, self.__dict__)

    def __eq__(self, other):
        if not isinstance(other, Type):
            return NotImplemented

        return self.__dict__ == other.__dict__

    async def rpc(self, msg):
        result = await self.connection.rpc(msg, encoder=TypeEncoder)
        return result

    @classmethod
    def from_json(cls, data):
        if isinstance(data, cls):
            return data
        if isinstance(data, str):
            try:
                data = json.loads(data)
            except json.JSONDecodeError:
                raise
        if isinstance(data, dict):
            d = {}
            for k, v in (data or {}).items():
                d[cls._toPy.get(k, k)] = v
            try:
                return cls(**d)
            except TypeError:
                raise
        if isinstance(data, list):
            # check: https://juju.is/docs/sdk/assumes
            # assumes are in the form of a list
            d = {}
            for entry in data:
                if '>' in entry or '>=' in entry:
                    # something like juju >= 2.9.31
                    i = entry.index('>')
                    key = entry[:i].strip()
                    value = entry[i:].strip()
                    d[key] = value
                else:
                    # this is a simple entry
                    d[entry] = ''
            return cls(**d)
        return None

    def serialize(self):
        d = {}
        for attr, tgt in self._toSchema.items():
            d[tgt] = getattr(self, attr)
        return d

    def to_json(self):
        return json.dumps(self.serialize(), cls=TypeEncoder, sort_keys=True)

    # treat subscript gets as JSON representation
    def __getitem__(self, key):
        attr = self._toPy[key]
        return getattr(self, attr)

    # treat subscript sets as JSON representation
    def __setitem__(self, key, value):
        attr = self._toPy[key]
        setattr(self, attr, value)

    # legacy: generated definitions used to not correctly
    # create typed objects and would use dict instead (from JSON)
    # so we emulate some dict methods.
    def get(self, key, default=None):
        try:
            attr = self._toPy[key]
        except KeyError:
            return default
        return getattr(self, attr, default)


class Schema(dict):
    def __init__(self, schema):
        self.name = schema['Name']
        self.version = schema['Version']
        self.update(schema['Schema'])

        self.registry = KindRegistry()
        self.types = TypeRegistry(self)

    def referenceName(self, ref):
        if ref.startswith("#/definitions/"):
            ref = ref.rsplit("/", 1)[-1]
        return ref

    def buildDefinitions(self):
        # here we are building the types out
        # anything in definitions is a type
        # but these may contain references themselves
        # so we dfs to the bottom and build upwards
        # when a types is already in the registry
        defs = self.get('definitions')
        if not defs:
            return
        definitions = {}
        for d, data in defs.items():
            if d in self.registry and d not in NAUGHTY_CLASSES:
                continue
            if data.get("type") != "object":
                continue
            definitions[d] = data
        for d, definition in definitions.items():
            node = self.buildObject(definition, d)
            self.registry.register(d, self.version, node)
            self.types.getRefType(d)

    def buildObject(self, node, name=None):
        # we don't need to build types recursively here
        # they are all in definitions already
        # we only want to include the type reference
        # which we can derive from the name
        struct = []
        add = struct.append
        props = node.get("properties")
        pprops = node.get("patternProperties")
        if props:
            # Sort these so the __init__ arg list for each Type remains
            # consistently ordered across regens of client.py
            for p in sorted(props):
                prop = props[p]
                if "$ref" in prop:
                    add((p, self.types.refType(prop)))
                else:
                    kind = prop['type']
                    if kind == "array":
                        add((p, self.buildArray(prop)))
                    elif kind == "object":
                        struct.extend(self.buildObject(prop, p))
                    else:
                        add((p, self.types.objType(prop)))
        if pprops:
            if ".*" not in pprops:
                raise ValueError(
                    "Cannot handle actual pattern in patternProperties %s" %
                    pprops)
            pprop = pprops[".*"]
            if "$ref" in pprop:
                add((name, Mapping[str, self.types.refType(pprop)]))
                return struct
            ppkind = pprop["type"]
            if ppkind == "array":
                add((name, Mapping[str, self.buildArray(pprop)]))
            else:
                add((name, Mapping[str, SCHEMA_TO_PYTHON[ppkind]]))

        if not struct and node.get('additionalProperties', False):
            add((name, SCHEMA_TO_PYTHON.get('object')))

        return struct

    def buildArray(self, obj):
        # return a sequence from an array in the schema
        if "$ref" in obj:
            return Sequence[self.types.refType(obj)]
        else:
            kind = obj.get("type")
            if kind and kind == "array":
                items = obj['items']
                return self.buildArray(items)
            else:
                return Sequence[self.types.objType(obj)]


def _getns(schema):
    ns = {'Type': Type,
          'typing': typing,
          'ReturnMapping': ReturnMapping
          }
    # Copy our types into the globals of the method
    for facade in schema.registry:
        ns[facade] = schema.registry.getObj(facade)
    return ns


def make_factory(name):
    if name in factories:
        del factories[name]
    factories[name].write("class {}(TypeFactory):\n    pass\n\n".format(name))


def write_facades(captures, options):
    """
    Write the Facades to the appropriate _client<version>.py

    """
    for version in sorted(captures.keys()):
        filename = "{}/_client{}.py".format(options.output_dir, version)
        with open(filename, "w") as f:
            f.write(HEADER)
            f.write("from juju.client.facade import Type, ReturnMapping\n")
            f.write("from juju.client._definitions import *\n\n")
            for key in sorted(
                    [k for k in captures[version].keys() if "Facade" in k]):
                print(captures[version][key], file=f)

    # Return the last (most recent) version for use in other routines.
    return version


def write_definitions(captures, options):
    """
    Write auxillary (non versioned) classes to
    _definitions.py The auxillary classes currently get
    written redudantly into each capture object, so we can look in
    one of them -- we just use the last one from the loop above.

    """
    with open("{}/_definitions.py".format(options.output_dir), "w") as f:
        f.write(HEADER)
        f.write("from juju.client.facade import Type, ReturnMapping\n\n")
        for key in sorted(
                [k for k in captures.keys() if "Facade" not in k]):
            print(captures[key], file=f)


def write_client(captures, options):
    """
    Write the TypeFactory classes to _client.py, along with some
    imports and tables so that we can look up versioned Facades.

    """
    with open("{}/_client.py".format(options.output_dir), "w") as f:
        f.write(HEADER)
        f.write("from juju.client._definitions import *\n\n")
        clients = ", ".join("_client{}".format(v) for v in captures)

        # from juju.client import _client2, _client1, _client3 ...
        f.write("\nfrom juju.client import " + clients + "\n\n")
        # CLIENTS = { ....
        f.write(CLIENT_TABLE.format(clients=",\n    ".join(
            ['"{}": _client{}'.format(v, v) for v in captures])))

        f.write(LOOKUP_FACADE)
        f.write(TYPE_FACTORY)
        for key in sorted([k for k in factories.keys() if "Facade" in k]):
            print(factories[key], file=f)


def generate_definitions(schemas):
    # Build all of the auxillary (unversioned) classes
    # TODO: get rid of some of the excess trips through loops in the
    # called functions.
    definitions = codegen.Capture()

    for juju_version in sorted(schemas.keys()):
        for schema in schemas[juju_version]:
            schema.buildDefinitions()

    # ensure we write the latest ones first, so that earlier revisions
    # get dropped.
    for juju_version in sorted(schemas.keys(), reverse=True):
        for schema in schemas[juju_version]:
            buildTypes(schema, definitions)

    return definitions


def generate_facades(schemas):
    captures = defaultdict(codegen.Capture)

    # Build the Facade classes
    for juju_version in sorted(schemas.keys()):
        for schema in schemas[juju_version]:
            cls, source = buildFacade(schema)
            cls_name = "{}Facade".format(schema.name)

            captures[schema.version].clear(cls_name)
            # Make the factory class for _client.py
            make_factory(cls_name)
            # Make the actual class
            captures[schema.version][cls_name].write(source)
            # Build the methods for each Facade class.
            buildMethods(cls, captures[schema.version])
            # Build the override RPC method if the Facade is a watcher.
            buildWatcherRPCMethods(cls, captures[schema.version])
            # Mark this Facade class as being done for this version --
            # helps mitigate some excessive looping.
            CLASSES[schema.name] = cls

    return captures


def load_schemas(options):
    schemas = {}

    for p in sorted(glob(options.schema)):
        if 'latest' in p:
            juju_version = 'latest'
        else:
            try:
                juju_version = re.search(JUJU_VERSION, p).group()
            except AttributeError:
                print("Cannot extract a juju version from {}".format(p))
                print("Schemas must include a juju version in the filename")
                raise SystemExit(1)

        new_schemas = json.loads(Path(p).read_text("utf-8"))
        schemas[juju_version] = [Schema(s) for s in new_schemas]
    return schemas


def setup():
    parser = argparse.ArgumentParser()
    parser.add_argument("-s", "--schema", default="juju/client/schemas*")
    parser.add_argument("-o", "--output_dir", default="juju/client")
    options = parser.parse_args()
    return options


def main():
    options = setup()

    schemas = load_schemas(options)

    # Generate some text blobs
    definitions = generate_definitions(schemas)
    captures = generate_facades(schemas)

    # ... and write them out
    write_definitions(definitions, options)
    write_facades(captures, options)
    write_client(captures, options)


if __name__ == '__main__':
    main()
