__author__ = 'Dmitriy Korsakov'

import os
import re
import json
import datetime
import traceback
from collections import defaultdict

from scalrctl import click
from scalrctl import settings
from scalrctl import request
from scalrctl import spec
from scalrctl.view import build_table, build_tree

import yaml


enabled = False


class SubCommandMeta(type):

    def __new__(mcs, name, parents, dct):
        route = dct.get("route")
        c = super(SubCommandMeta, mcs).__new__(mcs, name, parents, dct)
        if name!="SubCommand" and route:
            SubCommand._siblings[route].append(c)
        return c


class SubCommand(object):
    __metaclass__ = SubCommandMeta
    _siblings = defaultdict(list)
    name = None
    route = None
    method = None
    enabled = False
    _table_columns = None
    api_level = "user"

    mutable_body_parts = None # temporary, object definitions in YAML spec are not always correct
    object_reference = None # optional, e.g. '#/definitions/GlobalVariable'
    prompt_for = None #optional, Some values like GCE imageId cannot be passed through command line

    def __init__(self):
        self._table_columns = []

    @property
    def spc(self):
        return spec.Spec(spec.get_raw_spec(self.api_level), self.route, self.method)

    def get_siblings(self):
        return SubCommand._siblings.get(self.route)

    @property
    def _basepath_uri(self):
        return spec.get_raw_spec(self.api_level)["basePath"]

    @property
    def _request_template(self):
        return "%s%s" % (self._basepath_uri, self.route)

    def modify_options(self, options):
        """
        this is the place where command line options can be fixed
        after they are loaded from yaml spec
        """
        #print "In SubCommand modifier"
        for option in options:
            if self.prompt_for and option.name in self.prompt_for:
                option.prompt = option.name

            if option.name == "envId" and settings.envId:
                option.required = False

        return options


    def pre(self, *args, **kwargs):
        """
        before request is made
        """
        raw_columns = kwargs.pop("columns", False)
        if raw_columns:
            self._table_columns = raw_columns.split(",")

        settings.debug_mode = kwargs.pop("debug", False)
        settings.view = kwargs.pop("transformation", "tree")
        settings.colored_output = not kwargs.pop("nocolor", False)

        raw_filters = kwargs.pop("filters", None)

        if raw_filters:
            for pair in raw_filters.split(","):
                kv = pair.split("=")
                if 2==len(kv):
                    kwargs[kv[0]]=kv[1]


        stdin = kwargs.pop("stdin", False)
        from_file = kwargs.pop("from_file", False)  # [ST-88]

        if self.method.upper() in ("PATCH", "POST"):
            #prompting for body and then validating it
            for param in self._post_params():
                name = param["name"]

                if stdin:
                    raw = click.termui.prompt("Input %s in JSON format" % name)
                elif from_file:  # [ST-88]
                    path = os.path.expanduser(from_file)

                    if not os.path.exists(path):
                        raise click.ClickException("Cannot import Scalr object: file not found")

                    t = yaml.load(open(path, "r"))
                    assert t["data"], "Incorrect data structure"
                    raw = json.dumps(t["data"])  # XXX assert meta information first! # XXX excessive transformation

                else:
                    text = ''
                    if self.method.upper() == "PATCH":
                        try:
                            for cls in self.get_siblings():
                                if cls.method.upper() == "GET":
                                    rawtext = cls().run(*args, **kwargs)

                                    if settings.debug_mode:
                                        click.echo(rawtext)

                                    json_text = json.loads(rawtext)
                                    filtered = self._filter_json_object(json_text['data'], filter_createonly=True)
                                    text = json.dumps(filtered)
                        except (Exception, BaseException) as e:
                            if settings.debug_mode:
                                click.echo(traceback.format_exc())
                            else:
                                click.echo(e)
                    raw = click.edit(text)

                try:
                    user_object = json.loads(raw)
                except (Exception, BaseException) as e:
                    if settings.debug_mode:
                        raise
                    raise click.ClickException(str(e))

                valid_object = self._filter_json_object(user_object)
                valid_object_str = json.dumps(valid_object)
                kwargs[name] = valid_object_str
        return args, kwargs


    def post(self, response):
        """
        after request is made
        """
        return response


    def run(self, *args, **kwargs):
        """
        callback for click subcommand
        """
        export_path = kwargs.pop("export", None)  # [ST-88]

        args, kwargs = self.pre(*args, **kwargs)
        uri = self._request_template
        payload = {}
        data = None

        if settings.envId and '{envId}' in uri and ('envId' not in kwargs or not kwargs['envId']):
            kwargs['envId'] = settings.envId  # XXX

        if kwargs:
            uri = self._request_template.format(**kwargs)

            for key, value in kwargs.items():
                t = "{%s}" % key
                # filtering in-body and empty params
                if value and t not in self._request_template:
                    if self.method.upper() in ("GET", "DELETE"):
                        payload[key] = value
                    elif self.method.upper() in ("POST", "PATCH"):
                        data = value  #XXX

        raw_response = request.request(self.method, uri, payload, data)
        response = self.post(raw_response)

        if settings.view == "raw":
            click.echo(raw_response)

        if raw_response:

            try:
                response_json = json.loads(response)
            except ValueError, e:
                if settings.debug_mode:
                    raise
                raise click.ClickException(str(e))

            if "errors" in response_json and response_json["errors"]:
                raise click.ClickException(response_json["errors"][0]['message'])

            if export_path:  # [ST-88]
                d = {
                        "API_VERSION": settings.API_VERSION,
                        "envId": settings.envId,
                        "date": datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
                        "envId": settings.envId,
                        "API_HOST": settings.API_HOST,
                        "METHOD": self.method,
                        "URI": uri
                        }
                o = response_json.copy()
                o["meta"]["scalrctl"] = d
                #dump = json.dumps(o)
                dump = yaml.safe_dump(o, encoding='utf-8', allow_unicode=True, default_flow_style=False)
                try:
                    with open(os.path.expanduser(export_path), "w") as fp:
                        fp.write(dump)
                except IOError as err:
                    raise click.ClickException(str(err))

            data = response_json["data"]
            text = json.dumps(data)

            if settings.debug_mode:
                click.echo(response_json["meta"])

            if settings.view == "tree":
                click.echo(build_tree(text))



            elif settings.view == "table":
                columns = self._table_columns or self.spc.get_column_names()
                rows = []
                for block in data:
                    row = []
                    for name in columns:
                        for item in block:
                            if name.lower() == item.lower():
                                row.append(block[item])
                                break
                        else:
                            raise click.ClickException("Cannot apply filter. No such column: %s" % name)
                    if row:
                        rows.append(row)

                pagination = response_json.get("pagination", None)
                if pagination:
                    pagenum_last, current_pagenum = 1, 1
                    url_last = pagination.get('last', None)
                    if url_last:
                        number = re.search("pageNum=(\d*)", url_last)
                        pagenum_last = number.group(1) if number else 1

                    url_next = pagination.get('next', None)
                    if url_next:
                        num = re.search("pageNum=(\d*)", url_next)
                        pagenum_next = num.group(1) if num else 1
                        current_pagenum = int(pagenum_next) - 1



                click.echo(build_table(columns, rows, "Page: %s of %s" % (current_pagenum, pagenum_last))) #XXX

        return response


    def _list_mutable_body_parts(self):
        """
        finds object in yaml spec and determines it's mutable fields
        to filter user JSON
        XXX: Move to Spec (?) after TODO#3
        """
        mutable = []
        reference_path = None

        if not self.object_reference:
            for param in spec.get_raw_spec(self.api_level)["paths"][self.route][self.method]["parameters"]:
                name = param["name"] # image
                if "schema" in param:
                    if '$ref' in param["schema"]:
                        reference_path = param["schema"]['$ref']  # e.g. #/definitions/Image
                    elif "properties" in param["schema"]:  # ST-98, got object instead of $ref
                        #reference_path = param["schema"]
                        for property, descr in param["schema"]["properties"].items():
                            if 'readOnly' not in descr or not descr['readOnly']:
                                mutable.append(property)

                elif 'readOnly' not in param:
                    # e.g. POST /{envId}/farms/{farmId}/actions/terminate/
                    mutable.append(name)

        else:
            #XXX: Temporary code, see GlobalVariableDetailEnvelope or "role-global-variables update"
            reference_path = self.object_reference
        
        if reference_path:
            parts = reference_path.split("/")
            object =  spec.get_raw_spec(self.api_level)[parts[1]][parts[2]]
            object_properties = object["properties"]
            for property, descr in object_properties.items():
                if 'readOnly' not in descr or not descr['readOnly']:
                        mutable.append(property)
        return mutable

    def _list_createonly_properties(self):
        """
        Some properties (mostly IDs) cannot be marked as read-only because they are
        passed in PUT-methods (but still must be filtered in UPDATE-methods)
        """
        result = []
        properties = self.spc._result_descr['properties']
        data = properties['data']
        if 'items' in data:
            path = data['items']['$ref']
        else:
            path = data['$ref']
        obj = self.spc.lookup(path)
        if 'x-createOnly' in obj:
            result = obj['x-createOnly']
        return result

    def _filter_json_object(self, obj, filter_createonly=False):
        """
        removes immutable parts from JSON object before sending it in POST or PATCH
        """
        #XXX: make it recursive
        result = {}
        mutable_parts = self.mutable_body_parts or self._list_mutable_body_parts()
        for name, value in obj.items():
            if filter_createonly and name in self._list_createonly_properties():
                continue
            elif name in mutable_parts:
                result[name] = obj[name]
        return result

    def _post_params(self):
        """
        Determines list of body params
        e.g. 'image' JSON object for 'change-attributes' command
        """
        params = []
        m = spec.get_raw_spec(self.api_level)["paths"][self.route][self.method]
        if "parameters" in m:
            for parameter in m['parameters']:
                params.append(parameter)
        return params