#! /usr/bin/python
# -*- coding: utf-8 -*-
import sys
import os
import shlex

import json

import argparse
from subprocess import Popen, PIPE
from clint.textui import colored
from clint import piped_in

from pystash.common import StashedItem, ShelveStorage, NotListException, output
from pystash.web import API, AlreadyLoggedIn
import getpass

import requests
import xerox

# todo: create groups or subcommands
# todo: index should be positive int
# todo: colored
# todo: multiple objects in delete
# todo: convert to list (append flag)
# todo: convert to single value
# todo: create home dir
# todo: keys
# todo: not found error
# todo: numbered list
# todo: show index
# todo: prepend
# todo: user sticky bit in rights
# todo: add login and token to database

DB = 'stash.db'
VERSION = 0.2
HELP = {'all': 'show all keys',
        'help': 'show this help',
        'delete KEY': 'remove a particular key',
        'sync': 'sync data with the Stash cloud',
        'set KEY VALUE': 'set KEY to VALUE',
        'get KEY': 'retrieve the KEY',
        'search QUERY': 'full-text search on a query (remote only!)',
        'login': 'login to the Stash cloud',
        'logout': 'logout of',
        'status': 'show the cloud sync options',
        'local [off|on]': 'turn local database on/off',
        'remote [off|on]': 'turn the cloud on/off'}


# todo; don't disable stash after first search
# todo: create storage adapter and find fastest storage
# todo: fix accept with enter instead yes/no

"""
def output(message, color='white', text_only=False):
    if text_only:
        return str(getattr(colored, color)(message))
    else:
        sys.stdout.write(str(getattr(colored, color)(message)))
"""


def catch_errors(fn):
    def wrapper(*args, **kwargs):
        try:
            return fn(*args, **kwargs)
        except (IndexError, KeyError):
            output("Item doesn't exist \n", 'red')
        except NotListException:
            output("Item isn't a list \n", 'red')
        except Exception, e:
            output('%s\n' % e.message, 'red')

    return wrapper


class Stash(object):
    COMMANDS = ('all', 'help', 'delete', 'sync', 'set', 'get', 'login', 'logout', 'status', 'local', 'remote', 'search', 'tags')
    SYNONYMS = {'remove': 'delete', 'rm': 'delete', 'del': 'delete', 'search' : 'find'}

    command = None
    key = None
    value = None
    index = None
    is_list = False
    numbered = False
    overwrite = False

    def __init__(self, args):
        self.args = args
        self.load_config()
        self.db = ShelveStorage(db_file=self.DBFILE)
        self.os = self.detect_os()
        self.ltags = []
        if self.REMOTE:
            self.api = API()

    def check_installation(self):
        netrc_path = os.path.join(os.path.expanduser('~'), '.netrc')
        if not os.path.exists(netrc_path):
            open(netrc_path, 'w').close()
            os.chmod(600, netrc_path)

        home = os.path.join(os.path.expanduser('~'), '.stash')
        if not os.path.exists(home):
            os.mkdir(home)

    def load_config(self):

        # defaults
        self.REMOTE = 0
        self.LOCAL = 1
        self.DBFILE = os.path.join(os.path.expanduser('~'), '.stash', 'stash.db')

        home = os.path.join(os.path.expanduser('~'), '.stash')
        config = home + 'conf'
        if os.path.exists(config):
            text_config = "\n".join(open(config, "r").readlines())
            text_config = text_config.strip()
            config = {}
            if (text_config):
                config = eval(text_config)
                for key in config:
                    setattr(self, key, config[key])

    def save_config(self):
        conf = {'LOCAL': self.LOCAL, 'REMOTE': self.REMOTE}
        home = os.path.join(os.path.expanduser('~'), '.stash')
        config = home + 'conf'
        if os.path.exists(home):
            config_file = open(config, "w")
            config_file.write(json.dumps(conf))

    @staticmethod
    def detect_os():
        if sys.platform.startswith('win'):
            return 'windows'
        elif sys.platform.startswith('darwin'):
            return 'mac'
        elif sys.platform.startswith('linux'):
            return 'linux'
        return

    def validate_args(self):
        # todo: finish
        """Validate args values and pairs"""
        pass

    def detect_command(self):
        # todo: code review
        command_mapping = {
            'get': self.get_item_or_index,
            'set': self.add_or_update,
            'delete': self.delete_item_or_index,
            'sync': self.sync,
            'login': self.login,
            'logout': self.logout,
            'all': self.all,
            'help': self.help,
            'remote': self.remote,
            'local': self.local,
            'status': self.status,
            'search': self.search,
            'tags': self.tags
        }

        piped_data = piped_in()

        tags = []
        if self.args.tags is not None:
            tags = self.args.tags.split(',')
            self.ltags = tags

        if self.args.name is not None:

            command = None
            if self.args.name.lower() in self.COMMANDS:
                command = self.args.name.lower()
            elif self.args.name.lower() in self.SYNONYMS:
                command = self.SYNONYMS[self.args.name.lower()]
            else:
                self.key = self.args.name

            if len(self.args.value) > 0:
                value = self.args.value
            elif piped_data:
                value = piped_data.strip('\n')
            else:
                value = None

            if command in ['remote', 'local']:
                if value is not None:
                    self.value = ''.join(value)

            else:

                if value is not None:

                    if command is not None:
                        # we need to parse value
                        if len(value) > 1:
                            self.key = str(value[0])
                            self.value = value[1:] if self.is_list else ' '.join(value[1:])
                        else:
                            self.key = ''.join(value)

                    else:
                        # detect value
                        self.value = value if isinstance(value, str) else ' '.join(value)
                        command = 'set'
                else:
                    command = 'get' if command is None else command
        else:
            command = 'help'
        try:
            if piped_data:
                self.value = piped_data

            command_mapping[command]()
        except Exception as inst:
            output(inst, color="red")

        output("\n")

    def remote(self):

        if (self.value == '1' or self.value == 'on'):
            self.REMOTE = 1
            self.LOCAL = 0
            output("REMOTE set to " + str(self.REMOTE))
        elif (self.value == '0' or self.value == 'off'):
            self.REMOTE = 0
            self.LOCAL = 1
            output("REMOTE set to " + str(self.REMOTE))
        else:
            self.REMOTE = 1
            self.LOCAL = 0
            output("REMOTE set to " + str(self.REMOTE))

            #output("Wrong value", color="red")
        self.save_config()

    def local(self):

        if (self.value == '1' or self.value == 'on'):
            self.LOCAL = 1
            self.REMOTE = 0
        elif (self.value == '0' or self.value == 'off'):
            self.LOCAL = 0
            self.REMOTE = 1
        else:
            self.LOCAL = 1
            self.REMOTE = 0
        self.save_config()
        output("LOCAL set to " + str(self.LOCAL))

    def status(self):
        """
        output("local: " + str(self.LOCAL))
        output("\n")
        output("remote: " + str(self.REMOTE))
        output("\n")
        """
        if self.LOCAL > 0:
            output("local")
        else:
            output("remote")

    #@catch_errors
    def open_stash(self):
        self.validate_args()
        self.process_args()
        self.detect_command()

    def process_args(self):
        #self.is_list = self.args.list
        #self.overwrite = self.args.overwrite
        #self.index = self.args.index
        pass

    def all(self):
        db = {}
        if self.LOCAL:
            output('(local)\n')
            db = self.db.get_all()
        if self.REMOTE:
            output('(remote)\n')
            db = self.api.all()
        result = ''
        for k, v in db.iteritems():
            result += '%s: ' % k
            if self.numbered:
                v.numbered = True
            result += str(v)
            result += '\n'
        output(result) if len(result) > 0 else output('Your stash is empty\n')


    def tags(self):

        if self.key:

            db = {}
            if self.LOCAL:
                output('(local)\n')
                db = self.db.tags(self.key)
            if self.REMOTE:
                output('(remote)\n')
                db = self.api.tags(self.key)
                if 'result' in db:
                    db = db['result']

            result = ''
            if 1:
                for k in db:
                    result += '%s' % k
                    result += '\n'
            output(result) if len(result) > 0 else output('No items found.\n')
        else:
            db = {}
            if self.LOCAL:
                output('(local)\n')
                db = self.db.alltags()
            if self.REMOTE:
                output('(remote)\n')
                db = self.api.gettags()
            result = ''
            for tag in db:
                result += '%s' % tag
                result += '\n'
            output(result) if len(result) > 0 else output('No tags found.\n')

    # the API doesn't have to be there when the use logs in
    def login(self):
        output('Login: ')
        sys.stdin = open('/dev/tty')
        login = raw_input().lower()
        password = getpass.getpass(output('Enter password: ', 'white', text_only=True))
        api = API()
        try:
            if api.login(login, password):
                self.REMOTE = 1
                self.LOCAL = 0
                self.save_config()
                output('You have been logged in!\n', 'green')
            else:
                output('Something went wrong...\n', 'red')
        except AlreadyLoggedIn:
            output('You are already logged in\n', 'white')

    def logout(self):
        api = API()
        if api.logout():
            self.REMOTE = 0
            self.save_config()
            output('Logged out of Stash\n', 'red')
        else:
            output('Something went wrong...\n', 'red')

    def sync(self):
        api = API()
        local_data = self.db.get_database_data()
        synced_data = api.sync(local_data)
        if 'result' not in synced_data:
            output('Something went wrong...\n', 'red')
        else:
            self.db.set_database_data(synced_data['result'])
            output('Synced.')

    def get_item_or_index(self):

        item = None
        if self.LOCAL:
            try:
                item = self.db.get(self.key, self.index)
                if self.numbered:
                    item.numbered = True
            except KeyError:
                output('Nothing here. (local)', 'yellow')
            else:
                #DEBUG
                if item.tags:
                    output("Tags: %s\n" % " ".join(item.tags))
                output(item)
                xerox.copy(str(item))

        elif self.REMOTE:
            key = self.key
            data = self.api.get(key)
            if 'result' in data:
                if data['result'] is None:
                    output('Nothing here. (remote)', 'yellow')
                else:
                    output(data['result'])
                    xerox.copy(data['result'])
            else:
                output('Nothing here. (remote)', 'yellow')

    def search(self):

        item = None

        if self.LOCAL:
            output('Search is only available for remote mode. (remote)', 'yellow')

        elif self.REMOTE:

            key = self.key
            data = self.api.search(key)
            if 'result' in data:
                if data['result'] is None:
                    output('Nothing here. (remote)', 'yellow')
                else:
                    for result in data['result']:
                        output("%s:\n%s\n\n" % (result['key'], result['value']))
            else:
                output('Nothing here. (remote)', 'yellow')

    def __copy_to_clipboard(self, value):
        val = Popen(('echo', value.strip('\n')), stdout=PIPE)
        Popen(self.__generate_copy_command(), stdin=val.stdout)

    def delete_item_or_index(self):

        if self.LOCAL:
            if self.db.exist(self.key, self.index):
                if self.query_yes_no("Are you sure?", default="no") == "yes":
                    self.db.delete(self.key, self.index)
                    output('Item has been deleted (local)\n')
                else:
                    # todo: create message mapping
                    output('Aborted (local)\n', 'yellow')
            else:
                output('Item doesn\'t exist (local)\n', 'red')

        if self.REMOTE:
            # todo: on delete check if key exists
            key = self.key
            response = self.api.delete(key)
            output('%s has been deleted (remote)' % key)

    def add_or_update(self):

        if self.LOCAL:
            if self.db.exist(self.key, None):
                val = self.db.get(self.key)
                if not self.db.exist(self.key, self.index) and val.is_list and \
                                self.index != len(val.get_value()):
                    raise IndexError
                else:

                    if self.query_yes_no("Are you sure that you want update?") == 'yes':
                        item = self.db.update(self.key, self.get_value(), self.ltags, self.index, True)
                        output('Item has been updated (local)\n')
                        if True:
                            output(str(item))
                    else:
                        output('Aborted (local)\n', 'red')
            else:
                item = self.db.add(self.key, self.get_value(), self.ltags)
                output('Item has been created (local)\n', 'green')

        if self.REMOTE:

            key = self.key
            value = self.get_value()
            tags = self.ltags

            data = self.api.set(key, value, tags)
            if data.has_key('error'):
                return colored.yellow(data['error'])

            if ('result' in data) and (data['result'] == 'exists'):
                # todo: colorize
                sys.stdin = open('/dev/tty')
                res = raw_input('Key already exist. (O)verwrite/(A)ppend/E(X)it: ')
                #while res.lower() != 'y' and res.lower() != 'n':
                #TODO
                if res.lower() == 'y':
                    self.api.set(key, value, tags, True, False)
                    output('Key overwritten (remote)', 'green')
                elif res.lower() == 'o':
                    self.api.set(key, value, tags, True, False)
                    output('Key overwritten (remote)', 'green')
                elif res.lower() == 'a':
                    #TODO: what's up with output here???
                    output('Appended data (remote)', 'green')
                    self.api.set(key, value, tags, False, True)
                else:
                    output('Aborted (remote)', 'red')

            elif ('result' in data) and (data['result'] == 'ok'):
                output('Item has been created (remote)', 'green')
            else:
                output('Something went wrong somewhere... (remote)', 'red')



    @staticmethod
    def help():
        print "Usage: stash COMMAND"
        for key in HELP:
            if len(key) > 6:
                print("\t{0} \t\t\t# {1}".format(key, HELP[key]))
            else:
                print("\t{0} \t\t\t\t# {1}".format(key, HELP[key]))

        output('\n')

    def get_value(self):
        """
        Format value
        """
        if self.value is None:
            raise Exception("Value isn't set")
        if self.is_list:
            value = self.value if isinstance(self.value, list) else [self.value]
        else:
            value = ' '.join(self.value) if isinstance(self.value, list) else self.value
        return value

    def __generate_copy_command(self):
        """
        Return copy command for current os
        """
        if self.os == 'mac':
            return 'pbcopy'
        return shlex.split('xclip -selection clipboard')

    def __generate_open_command(self):
        """
        Return open command for current os
        """
        if self.os == 'mac':
            return 'open'
        return 'xdg-open'

    def run(self):
        value = self.db.get(self.key, self.index)
        # todo: open list or text with text editor
        output('Opening...\n')
        Popen([self.__generate_open_command(), '%s' % str(value)], stdout=PIPE, stderr=PIPE)

    @staticmethod
    def query_yes_no(question, default="yes"):

        """
        From http://code.activestate.com/recipes/577058/

        Ask a yes/no question via raw_input() and return their answer.

        "question" is a string that is presented to the user.
        "default" is the presumed answer if the user just hits <Enter>.
            It must be "yes" (the default), "no" or None (meaning
            an answer is required of the user).

        The "answer" return value is one of "yes" or "no".
        """
        valid = {"yes": "yes", "y": "yes", "ye": "yes", "no": "no", "n": "no"}

        if default is None:
            prompt = " [y/n] "
        elif default == "yes":
            prompt = " [Y/n] "
        elif default == "no":
            prompt = " [y/N] "
        else:
            raise ValueError("invalid default answer: '%s'" % default)

        while 1:
            output(question + prompt, 'yellow')
            sys.stdin = open('/dev/tty')
            choice = raw_input().lower()
            if default is not None and choice == '':
                return default
            elif choice in valid.keys():
                return valid[choice]
            else:
                output("Please respond with 'yes' or 'no' (or 'y' or 'n').\n", 'yellow')


if __name__ == '__main__':

    usage = "stash COMMAND\n"
    for key in HELP:
        if len(key) > 6:
            usage += "\t{0} \t\t\t# {1}\n".format(key, HELP[key])
        else:
            usage += "\t{0} \t\t\t\t# {1}\n".format(key, HELP[key])

    parser = argparse.ArgumentParser(usage=usage)

    #subparsers = parser.add_subparsers(help='status')
    #status = subparsers.add_parser('status', help='show local/remote save options')


    parser.add_argument('name', nargs='?')
    parser.add_argument('value', nargs='*')
    parser.add_argument('-t', '--tag', dest='tags', help="Add tag")
    # """
    # parser.add_argument('-c', '--copy', action='store_true',
    #                     help="Copy item, but don\'t output it to terminal. Opposite to -p")
    #
    # parser.add_argument('-p', '--println', action='store_true', help='Print item after action')
    # parser.add_argument('-o', '--overwrite', action='store_true', help="Overwrite value if exists")
    # parser.add_argument('-r', '--run', action='store_true', help="Run with associated program")
    # """
    arg = parser.parse_args()
    Stash(arg).open_stash()

