__author__ = 'Dmitriy Korsakov'

import os
import sys
import json
import shutil
import inspect

import yaml
import click
import requests

import commands
import settings
import spec

PROGNAME = "scalr-ctl"
DEFAULT_PROFILE = "default"
CMD_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'commands'))
CONFIG_FOLDER = os.path.expanduser(os.environ.get("SCALRCLI_HOME", "~/.scalr"))
CONFIG_PATH = os.path.join(CONFIG_FOLDER, "%s.yaml" % os.environ.get("SCALRCLI_PROFILE", DEFAULT_PROFILE))

SWAGGER_USER_NOUPDATE_TRIGGER = ".noupdate.user"
SWAGGER_USER_FILE = "user.yaml"
SWAGGER_USER_PATH = os.path.join(CONFIG_FOLDER, SWAGGER_USER_FILE)
SWAGGER_USER_JSONSPEC_FILE = SWAGGER_USER_FILE.split(".")[0] + ".json"
SWAGGER_USER_JSONSPEC_PATH = os.path.join(CONFIG_FOLDER, SWAGGER_USER_JSONSPEC_FILE)

SWAGGER_ACCOUNT_NOUPDATE_TRIGGER = ".noupdate.account"
SWAGGER_ACCOUNT_FILE = "account.yaml"
SWAGGER_ACCOUNT_PATH = os.path.join(CONFIG_FOLDER, SWAGGER_ACCOUNT_FILE)
SWAGGER_ACCOUNT_JSONSPEC_FILE = SWAGGER_ACCOUNT_FILE.split(".")[0] + ".json"
SWAGGER_ACCOUNT_JSONSPEC_PATH = os.path.join(CONFIG_FOLDER, SWAGGER_ACCOUNT_JSONSPEC_FILE)

AUTOCOMPLETE_FNAME = "path.bash.inc"
AUTOCOMPLETE_CONTENT = "_%s_COMPLETE=source %s" % (PROGNAME.upper().replace("-", "_"), PROGNAME)
AUTOCOMPLETE_PATH = os.path.join(os.path.expanduser(CONFIG_FOLDER), AUTOCOMPLETE_FNAME)


def setup_bash_complete():
    if "nt" == os.name: # Click currently only supports completion for Bash.
        return

    bashrc_path = os.path.expanduser("~/.bashrc")
    bashprofile_path = os.path.expanduser("~/.bash_profile")
    startup_path = bashprofile_path if os.path.exists(bashprofile_path) else bashrc_path
    startup_path = click.prompt("Enter path to an rc file to update, or leave blank to use", default=startup_path, err=True)
    if not os.path.exists(startup_path):
        click.echo("%s not found." % startup_path)
        return
    startupfile_content = open(startup_path, "r").read()

    if AUTOCOMPLETE_PATH not in startupfile_content:
        confirmed = click.confirm("Modify profile to update your $PATH and enable bash completion?", default=True, err=True)

        if confirmed:
            with open(AUTOCOMPLETE_PATH, "w") as fp:
                fp.write(AUTOCOMPLETE_CONTENT)

            backup_path = startup_path + ".backup"
            click.echo("Backing up [%s] to [%s]." % (startup_path, backup_path))
            shutil.copy(startup_path, backup_path)

            #source_line = "source '%s'" % AUTOCOMPLETE_PATH #XXX: for some reason this seized to work
            source_line = 'eval "$(%s)"' % AUTOCOMPLETE_CONTENT

            comment = "# The next line enables bash completion for %s." % PROGNAME
            newline = "" if startupfile_content.endswith("\n") else "\n"
            add = "%s%s\n%s" % (newline, comment, source_line)

            # Handling pip install --user and PATH
            local_binpath = os.path.join(os.path.expanduser("~/.local/bin/"), PROGNAME)
            if os.path.exists(local_binpath):
                add += "\nasias %s=%s" % (PROGNAME, local_binpath)

            with open(startup_path, "a") as afp:
                afp.write(add)

            click.echo("Start a new shell for the changes to take effect.")


def configure(profile=None):
    """
    Configure command-line client.
    Creates new profile in configuration directory
    and downloads spec file.
    :param profile: Profile name
    """
    confpath = os.path.join(CONFIG_FOLDER, "%s.yaml" % profile) if profile else CONFIG_PATH
    data = {}

    if os.path.exists(confpath):
        old_data = yaml.load(open(confpath, "r"))
        data.update(old_data)

    click.echo("Configuring %s:" % confpath)

    for obj in dir(settings):
        if not obj.startswith("__"):
            default_value = getattr(settings, obj)
            if isinstance(default_value, bool):
                data[obj] = click.confirm(obj, default=getattr(settings, obj))
            elif not default_value or type(default_value) in (int, str):
                data[obj] = str(click.prompt(obj, default=getattr(settings, obj))).strip()

    if not os.path.exists(CONFIG_FOLDER):
        os.makedirs(CONFIG_FOLDER)

    raw = yaml.dump(data, default_flow_style=False, default_style='')
    with open(confpath, 'w') as fp:
        fp.write(raw)

    click.echo()
    click.echo("New config saved:")
    click.echo()
    click.echo(open(confpath, "r").read())

    for setting, value in data.items():
        setattr(settings, setting, value)

    update()
    setup_bash_complete()


def update():
    """
    Downloads yaml spec and converts it to JSON
    Both files are stored in configuration directory.
    """
    text = None
    user_trigger_file = os.path.join(CONFIG_FOLDER, SWAGGER_USER_NOUPDATE_TRIGGER)

    user_url = spec.get_spec_url(api_level="user")
    user_dst = os.path.join(CONFIG_FOLDER, SWAGGER_USER_FILE)

    if user_url and not os.path.exists(user_trigger_file):
        click.echo("Trying to get new UserAPI Spec from %s" % user_url)
        r = requests.get(user_url)

        old = None

        if os.path.exists(user_dst):
            with open(user_dst, "r") as fp:
                old = fp.read()

        text = r.text

        if text == old:
            click.echo("UserAPI Spec is already up-to-date.")
        elif text:
            with open(user_dst, "w") as fp:
                fp.write(text)
            click.echo("UserAPI UserSpec successfully updated.")

    if text or os.path.exists(user_dst):
        struct = yaml.load(text or open(user_dst).read())
        json.dump(struct, open(SWAGGER_USER_JSONSPEC_PATH, "w"))


    # Fetch AccountAPI spec and convert to JSON
    text = None
    account_trigger_file = os.path.join(CONFIG_FOLDER, SWAGGER_ACCOUNT_NOUPDATE_TRIGGER)
    account_url = spec.get_spec_url(api_level="account")
    account_dst = os.path.join(CONFIG_FOLDER, SWAGGER_ACCOUNT_FILE)

    if account_url and not os.path.exists(account_trigger_file):
        click.echo("Trying to get new AccountAPI Spec from %s" % account_url)
        r = requests.get(account_url)

        old = None

        if os.path.exists(account_dst):
            with open(account_dst, "r") as fp:
                old = fp.read()

        text = r.text

        if text == old:
            click.echo("AccountAPI Spec is already up-to-date.")
        elif text:
            with open(account_dst, "w") as fp:
                fp.write(text)
            click.echo("AccountAPI Spec successfully updated.")

    if text or os.path.exists(account_dst):
        struct = yaml.load(text or open(account_dst).read())
        json.dump(struct, open(SWAGGER_ACCOUNT_JSONSPEC_PATH, "w"))


class HelpBuilder(object):
    document = None

    def __init__(self, document):
        #XXX: move methods to spec module
        self.document = document

    def list_paths(self):
        return self.document["paths"].keys()

    def list_http_methods(self, path):
        l = self.document["paths"][path].keys()
        if "parameters" in l:
            l.remove("parameters")
        return l

    def get_method_description(self, path, method="get"):
        return self.document["paths"][path][method]['description']

    def get_body_type_params(self, path, method="get"):
        params = []
        m = self.document["paths"][path][method]
        if "parameters" in m:
            for parameter in m['parameters']:
                params.append(parameter)
        return params

    def get_path_type_params(self, path):
        params = []
        paths = self.document["paths"].keys()
        d = self.document["paths"][path]
        if "parameters" in d:
            for parameter in d['parameters']:
                params.append(parameter)
        return params

    def get_params(self, path, method="get"):
        result = self.get_path_type_params(path)
        if method.upper() in ("GET", "DELETE"):
            body_params = self.get_body_type_params(path, method)
            result += body_params
        return result

    def returns_iterable(self, path):
        responces = self.document["paths"][path]["get"]['responses']
        if '200' in responces:
            ok200 = responces['200']
            if 'schema' in ok200:
                schema = ok200['schema']
                if '$ref' in schema:
                    reference = schema['$ref']
                    object_key = reference.split("/")[-1]
                    object_descr = self.document["definitions"][object_key]
                    object_properties = object_descr["properties"]
                    data_structure = object_properties["data"]
                    if "type" in data_structure:
                        responce_type = data_structure["type"]
                        if "array" == responce_type:
                            return True
        return False


def list_module_filenames():
    files = os.listdir(CMD_FOLDER)
    """
    account_files = os.listdir(os.path.join(CMD_FOLDER, "account"))
    print account_files
    files += account_files
    """
    l = [fname for fname in files if fname.endswith('.py') and not fname.startswith("_")]
    return l



class MyCLI(click.Group):

    _modules = None

    def __init__(self, name=None, commands=None, **attrs):
        click.Group.__init__(self, name, commands, **attrs)
        self._modules = {}
        self._init()
        self.metaspec = spec.MetaSpec.lookup()


    def get_help_builder(self, api_level="user"):
        spec_dict = self.metaspec._get_spec_dict(api_level=api_level)
        return HelpBuilder(spec_dict)


    def _list_module_objects(self):
        objects = [module for module in self._modules.values() if module.enabled]
        #print "MODULE OBJECTS: %s" % objects
        return objects


    def _list_subcommands(self, command_name):
        objects = []
        for name, obj in inspect.getmembers(self._modules[command_name]):
            if inspect.isclass(obj) and hasattr(obj, 'enabled') and getattr(obj, 'enabled'):
                subcommand = obj()
                if isinstance(subcommand, commands.SubCommand):
                    objects.append(subcommand)
        #print "SUBCOMMANDS: %s", objects
        return objects


    def _list_options(self, route, method, subcommand_name, api_level="user"):  #XXX: subcommand_name
        help_builder = self.get_help_builder(api_level=api_level)
        params = help_builder.get_params(route, method)
        options = []

        debug = click.Option(('--debug/--no-debug', 'debug'), default=False, help="Print debug messages")
        options.append(debug)

        for param in params:
            option = click.Option(("--%s" % param['name'], param['name']), required=param['required'], help=param["description"])
            options.append(option)

        if method.upper() == 'GET':
            raw = click.Option(('--raw', 'transformation'), is_flag=True, flag_value='raw', default=False, help="Print raw response")
            tree = click.Option(('--tree', 'transformation'), is_flag=True, flag_value='tree', default=True, help="Print response as a colored tree")
            nocolor = click.Option(('--nocolor', 'nocolor'), is_flag=True, default=False, help="Use colors")
            options += [raw, tree, nocolor]
            if subcommand_name != "retrieve": # [ST-54]
                table = click.Option(('--table', 'transformation'), is_flag=True, flag_value='table', default=False, help="Print response as a colored table")
                options.append(table)

            if help_builder.returns_iterable(route):
                maxrez = click.Option(("--maxresults", "maxResults"), type=int, required=False, help="Maximum number of records. Example: --maxresults=2")
                options.append(maxrez)

                pagenum = click.Option(("--pagenumber", "pageNum"), type=int, required=False, help="Current page number. Example: --pagenumber=3")
                options.append(pagenum)

                filthelp = "Apply filters. Example: type=ebs,size=8. "
                spc = spec.Spec(spec.get_raw_spec(api_level=api_level), route, method)
                if spc.filters:
                    filters = sorted(spc.filters)
                    filthelp += "Available filters: %s." % ", ".join(filters)
                    filters = click.Option(("--filters", "filters"), required=False, help=filthelp)
                    options.append(filters)

                columnshelp = "Filter columns in table view [--table required]. Example: NAME,SIZE,SCOPE. "
                available_columns = spc.get_column_names()
                columnshelp +=  "Available columns: %s." % ", ".join(available_columns)
                columns = click.Option(("--columns", "columns"), required=False, help=columnshelp)
                options.append(columns)

        if method.upper() in ('PATCH','POST'):
            stdin_help = "Ask for input instead of opening default text editor"
            stdin = click.Option(("--stdin", "stdin"), is_flag=True, default=False, help=stdin_help)
            options.append(stdin)

        return options


    def _init(self):
        for name in list_module_filenames():
            try:
                if sys.version_info[0] == 2:
                    name = name.encode('ascii', 'replace')
                mod = __import__('scalrctl.commands.' + name[:-3], None, None, ['enabled'])
                if hasattr(mod, "name"):
                    self._modules[mod.name] = mod
            except ImportError:
                raise  # pass
        #print self._modules


    def list_commands(self, ctx):
        rv = [module.name for module in self._list_module_objects()]
        rv += ["configure", "update", "account"]
        rv.sort()
        return rv


    def get_command(self, ctx, name):

        if name == "account":
            account_help = "All AccountAPI commands"  # TODO: HelpStr
            account_group = click.Group("account", callback=lambda: None, help=account_help)

            #print self.metaspec._list_subcmd_aliases(command_name="os", api_level="account")
            #print self.metaspec._list_cmd_aliases(api_level="account")

            for command_name in self.metaspec._list_cmd_aliases(api_level="account"):
                command_descr = self.metaspec._get_cmd_descr(command_name=command_name, api_level="account")
                grp = click.Group(command_name, callback=lambda: None, help=command_descr)

                for subcommand_name in self.metaspec._list_subcmd_aliases(command_name=command_name, api_level="account"):

                    route = self.metaspec.get_route(command_name, subcommand_name, api_level="account")
                    method = self.metaspec.get_http_method(command_name, subcommand_name, api_level="account")
                    subcommand = commands.SubCommand()
                    subcommand.name = subcommand_name
                    subcommand.route = route
                    subcommand.method = method
                    subcommand.api_level = "account"
                    options = self._list_options(route, method, subcommand_name, api_level="account")
                    options = subcommand.modify_options(options)
                    acc_spec = spec.Spec(spec.get_raw_spec(api_level="account"), route, method)
                    subcommand_descr = acc_spec.description
                    cmd = click.Command(subcommand_name, params=options, callback=subcommand.run, help=subcommand_descr)
                    grp.add_command(cmd)

                account_group.add_command(grp)

            """
            for subcommand in self._list_subcommands(name):
                if subcommand.route in self.hb.list_paths() \
                        and subcommand.method in self.hb.list_http_methods(subcommand.route):
                    options = self._list_options(subcommand)

                    options = subcommand.modify_options(options)

                    spc = spec.Spec(spec.get_raw_spec(), subcommand.route, subcommand.method)
                    cmd = click.Command(subcommand.name, params=options, callback=subcommand.run, help=spc.description)
                    group.add_command(cmd)
            """


            return account_group

        elif name == "configure":
            configure_help = "Set configuration options in interactive mode"
            profile_argument = click.Argument(("profile",), required=False) # [ST-30]
            configure_cmd = click.Command("configure", callback=configure, help=configure_help, params=[profile_argument,])
            return configure_cmd

        elif name == "update":
            update_help = "Fetch new API specification if available."
            update_cmd = click.Command("update", callback=update, help=update_help)
            return update_cmd

        elif name not in self._modules:
            raise click.ClickException("No such command: %s" % name)

        group = click.Group(name, callback=self._modules[name].callback, help=self._modules[name].__doc__)

        hb = self.get_help_builder(api_level="user")
        subcommands = self._list_subcommands(name)  #TODO: BROKEN CODE! see scalr-ctl os list
        routes = hb.list_paths()
        #print routes
        for subcommand in subcommands:
            #print subcommand.route
            if subcommand.route in routes \
                    and subcommand.method in hb.list_http_methods(subcommand.route):
                options = self._list_options(route=subcommand.route, method=subcommand.method, subcommand_name=subcommand.name, api_level="user")

                options = subcommand.modify_options(options)

                spc = spec.Spec(spec.get_raw_spec(api_level="user"), subcommand.route, subcommand.method)
                cmd = click.Command(subcommand.name, params=options, callback=subcommand.run, help=spc.description)
                group.add_command(cmd)

        return group

def account():
    # debug code
    pass

def apply_settings(data):
    for key, value in data.items():
        if hasattr(settings, key):
            setattr(settings, key, value)


if not os.path.exists(CONFIG_FOLDER):
    os.makedirs(CONFIG_FOLDER)

if os.path.exists(CONFIG_PATH):
    apply_settings(yaml.load(open(CONFIG_PATH, "r")))

if not os.path.exists(SWAGGER_USER_PATH) or not os.path.exists(SWAGGER_USER_JSONSPEC_PATH):
    update() # [ST-53]


@click.command(cls=MyCLI)
@click.version_option()
@click.pass_context
@click.option('--key_id', help="API key ID")
@click.option('--secret_key', help="API secret key")
def cli(ctx, key_id, secret_key, *args, **kvargs):
    """Scalr-ctl is a command-line interface to your Scalr account"""
    if key_id:
        settings.API_KEY_ID = str(key_id)
    if secret_key:
        settings.API_SECRET_KEY = str(secret_key)
    elif settings.API_KEY_ID and settings.API_KEY_ID.strip() and not settings.API_SECRET_KEY: # [ST-21]
        if ctx.invoked_subcommand not in ("configure", "update"):
            raw = click.prompt(text="API SECRET KEY", hide_input=True)
            settings.API_SECRET_KEY = str(raw)


if __name__ == '__main__':
    cli()
