#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# Maintainer: Pablo Saavedra
# Contact: saavedra.pablo@gmail.com

import os
import sys
import signal

try:
    reload(sys)
    sys.setdefaultencoding('utf-8')
except Exception:
    pass

try:
  try:
    import ConfigParser
  except ImportError:
    import confiparser.ConfigParser
except Exception:
    pass


import cmd
import threading
import time
import datetime

import simplejson as json
from httplib2 import Http
from optparse import OptionParser



################################################################################
conffile = ".redmine_cmd.cfg"
logfile = "/dev/stdout"
loglevel = 20

console = None

LAST_TIME=time.time()
CURRENT_TICKET = {}
CURRENT_USER={}
PREVIOUS_TICKET={}

sentinel=".end."
threshold=30*60
baseurl="https://tracker.host.com"
key="9999999999999999999999999999999999999999"
headers = {
  'User-Agent': 'redmine-cmd',
  'Content-Type': 'application/json'
}


################################################################################

class CancelException(Exception):
    pass

def exit():
    global thread
    thread.end = True
    thread.join()
    sys.exit(0)


def setup():
    global conffile
    cfg = ConfigParser.ConfigParser()
    cfg.read(conffile)
    try:
        for o in cfg.options("global"):
            try:
                value = cfg.get("global",o)
                if o == "threshold":
                    globals()[o] = int(value)
                else:
                    globals()[o] = value
                logger.debug("Setting %s to %s" % (o,value))
            except Exception as e:
                logger.error("Error parsing %s - %s: %s" % ("global",o,e))
    except Exception as e:
        logger.error("Raised exception: %s" % e)


def get_http_requester():
    try:
        h = Http(disable_ssl_certificate_validation=True, timeout=20)
    except Exception:
        h = Http(disable_ssl_certificate_validation=True)
    return h


def get_response(method, URL, body=None):
    global headers
    global logger
    logger.debug("%s: %s" % (method,URL))
    h = get_http_requester()
    resp, content = h.request(URL, method, body , headers=headers)
    logger.debug("resp: %s" % resp)
    logger.debug("content: %s" % content)
    return resp,content

def change_issue_status(issue_id,status,comment):
    global baseurl
    global key
    global headers
    global logger

    json_doc={
      "issue": {
        "status_id": status,
        "notes": "Comment: %s" % comment,
      }
    }
    params="?key=%s" % (key)
    URL="%s/issues/%s.json%s" % (baseurl,issue_id,params)
    json_doc=json.dumps(json_doc)
    resp,content = get_response("PUT", URL, json_doc)
    print ("Response: %s" % resp["status"])

def create_issue(p_id,t_id,u_id,subject,description):
    global baseurl
    global key
    global headers
    global CURRENT_TICKET
    global CURRENT_USER
    global logger

    json_doc={
      "issue": {
        "description": description,
        "subject": subject,
        "project_id": p_id,
        "tracker_id": t_id,
        # "assigned_to_id": CURRENT_USER["id"],
        "assigned_to_id": u_id,
      }
    }
    params="?key=%s" % (key)
    createIssueURL="%s/issues.json%s" % (baseurl,params)
    json_doc=json.dumps(json_doc)
    resp,content = get_response("POST", createIssueURL, json_doc)
    print ("Response: %s" % resp["status"])
    try:
        return json.loads(content)["issue"]
    except  Exception:
        return None


def create_time_entry(issue_id,activity_id,hours,comments):
    global baseurl
    global key
    global headers
    global logger
    json_doc={
      "time_entry": {
        "issue_id": issue_id,
        "hours": hours,
        "activity_id": activity_id,
        "comments":comments,
      }
    }
    params="?key=%s" % (key)
    URL="%s/time_entries.xml%s" % (baseurl,params)
    json_doc=json.dumps(json_doc)
    resp,content = get_response("POST", URL, json_doc)
    print ("Response: %s" % resp["status"])


def update_issue(issue_id, comment, note):
    global baseurl
    global key
    global headers
    global logger
    notes = ""
    if comment.strip() == "":
        notes = "Notes:\n%s" % (note)
    else:
        notes = "Comment: _%s_\n\nNotes:\n%s" % (comment,note)
    json_doc={
      "issue": {
        "notes": notes,
      }
    }
    params="?key=%s" % (key)
    URL="%s/issues/%s.json%s" % (baseurl,issue_id,params)
    json_doc=json.dumps(json_doc)
    resp,content = get_response("PUT", URL, json_doc)
    print ("Response: %s" % resp["status"])
    print (content)
    try:
        return json.loads(content)["issue"]
    except  Exception:
        return None


def get_input(option_name, multiline=False):
    global sentinel
    if multiline:
        print ("\033[35;01mEnter %s (type %s to end):\033[0;0m" \
                % (option_name,sentinel))
        res = '\n'.join(iter(raw_input, sentinel))
    else:
        res = raw_input('\033[35;01mEnter %s: \033[0;0m' % option_name)
    return res


def get_current_user():
    global baseurl
    global key
    global logger
    res = {}
    params="?key=%s" % \
        (key)
    getURL="%s/users/current.json%s" % (baseurl,params)
    resp, content = get_response("GET", getURL)
    try:
        res = json.loads(content)
    except Exception as e:
        logger.error("Exception: %s" % e)
    return res

def get_object(objects_name,id_,object_key_id, object_key_name):
    global baseurl
    global key
    global headers
    global CURRENT_TICKET

    res_object = {}
    output = ""
    print ('''\033[33;01mGetting %s:\033[31;01m>\033[0;0m''' % objects_name)
    params="?key=%s" % (key)
    getURL="%s/%s/%s.json%s" % (baseurl,objects_name,id_,params)
    resp, content = get_response("GET", getURL)
    try:
        res_object = json.loads(content)
        o = res_object[objects_name[:-1]]
        output =  o[object_key_name] + ": " + str(o[object_key_id])
    except Exception as e:
        logger.error("Exception: %s" % e)

    print (output)
    return res_object

def get_objects(objects_name,object_key_id, object_key_name):
    global baseurl
    global key
    global headers
    global CURRENT_TICKET
    global CURRENT_USER
    limit = 50
    offset = 0
    options=[]
    default = None
    print ('''\033[33;01mAvailable %s:\033[31;01m>\033[0;0m''' \
            % objects_name)
    output = ""
    while True:
        params="?key=%s&offset=%s&limit=%s" % (key,offset,limit)
        getURL="%s/%s.json%s" % (baseurl,objects_name,params)
        resp, content = get_response("GET", getURL)
        try:
            received = json.loads(content)
            try:
              for o in received[objects_name]:
                output += "; " + o[object_key_name] + ": " + str(o[object_key_id])
                options.append(int(o[object_key_id]))
            except Exception as e:
              logger.error("Exception: %s" % e)
            offset+=limit
            if len(received[objects_name])<limit:
                break # no more items to iter
        except Exception as e:
            logger.error("Exception: %s" % e)
            break
    try:
        current_ticket_related_key = objects_name[:-1]
        if current_ticket_related_key == "user":
            current_ticket_related_key = "assigned_to"
        if CURRENT_TICKET and not( CURRENT_TICKET == {}):
            output += "; Default - %s: %s" \
% (CURRENT_TICKET[current_ticket_related_key]["name"], CURRENT_TICKET[current_ticket_related_key]["id"])
            options.append(CURRENT_TICKET[current_ticket_related_key]["id"])
            default = CURRENT_TICKET[current_ticket_related_key]["id"]
        else:
            if objects_name[:-1] == "user":
                output += "; Default - %s: %s" \
% (CURRENT_USER["firstname"], CURRENT_USER["id"])
                options.append(CURRENT_USER["id"])
                default = CURRENT_USER["id"]

    except Exception as e:
        logger.error("Unexpected error: %s" % e)
    try:
      print (output[2:])
    except Exception:
      print (output)
    return options, default

def get_projects():
    return get_objects("projects","id","name")

def get_users():
    return get_objects("users","id","login")

def get_issues(assigned_to_id="me", status="open"):
    global baseurl
    global key
    global logger
    options=[]
    limit = 50
    offset = 0
    default = None
    print ('''\033[33;01mIssues %s assigned to %s:\033[31;01m>\033[0;0m''' \
            % (status,assigned_to_id))
    output = ""
    while True:
        params="?key=%s&offset=%s&limit=%s&status_id=%s&assigned_to_id=%s" % \
            (key,offset,limit,status,assigned_to_id)
        getURL="%s/issues.json%s" % (baseurl,params)
        resp, content = get_response("GET", getURL)
        try:
            received = json.loads(content)
            # print (received)
            try:
              for p in received["issues"]:
                output += str(p["id"]) + ": " + str(p["project"]["name"]) + " -> " + str(p["subject"]) + "\n"
                options.append(int(p["id"]))
            except Exception as e:
              logger.error("Exception: %s" % e)
              pass
            offset+=limit
            if len(received["issues"])<limit:
                break # no more items to iter
        except Exception as e:
            logger.error("Exception: %s" % e)
            break
    try:
        if CURRENT_TICKET and not( CURRENT_TICKET == {}):
            output += "Current - %s: %s %s" \
% (CURRENT_TICKET["id"], CURRENT_TICKET["subject"] ,CURRENT_TICKET["project"]["name"])
            options.append(CURRENT_TICKET["id"])
            default = CURRENT_TICKET["id"]
    except Exception as e:
        logger.error("Unexpected error: %s" % e)
    print (output)
    return options, default

def get_myissues():
    return get_issues()

def get_time_entries(user_id=None):
    global baseurl
    global key
    global logger
    options=[]
    limit = 100
    offset = 0
    default = None
    print ('''\033[33;01mTime entries logged:\033[31;01m>\033[0;0m''')
    output = ""
    params="?key=%s&offset=%s&limit=%s" % \
        (key,offset,limit)
    getURL="%s/time_entries.json%s" % (baseurl,params)
    resp, content = get_response("GET", getURL)
    try:
        received = json.loads(content)
        try:
          for p in received["time_entries"]:
            if not user_id or p["user"]["id"] == int(user_id):
                output += \
                  str(p["spent_on"]) \
                  + " - " + str(p["project"]["name"]) \
                  + " - " + str(p["activity"]["name"]) \
                  + " - " + str(p["user"]["name"]) \
                  + " - " + str(p["hours"]) \
                  + " - " + str(p["comments"]) + "\n"
                options.append(int(p["id"]))
        except Exception as e:
          logger.error("Exception: %s" % e)
    except Exception as e:
        logger.error("Exception: %s" % e)
    print (output)
    return options, default

def get_activities(quiet=False):
    global conffile
    output = ""
    options = []
    default = None
    if not quiet:
        print ('''\033[33;01mAvailable %s:\033[31;01m>\033[0;0m''' \
                % "activities")
    cfg = ConfigParser.ConfigParser()
    cfg.read(conffile)
    try:
        for o in cfg.options("activities"):
            try:
                value = cfg.get("activities",o)
                output += o + ": " + value + "\n"
                options.append(int(value))
                if o.lower() == "default":
                    default = int(value)
            except Exception as e:
                logger.error("Error parsing %s - %s: %s" % ("activities",o,e))
    except Exception as e:
        logger.error("Raised exception: %s" % e)
    if not quiet:
        print (output)
    return options, default

def get_issue_status(quiet=False):
    global conffile
    output = ""
    options = []
    default = None
    if not quiet:
        print ('''\033[33;01mAvailable %s:\033[31;01m>\033[0;0m''' \
                % "issue status")
    cfg = ConfigParser.ConfigParser()
    cfg.read(conffile)
    try:
        for o in cfg.options("issue_status"):
            try:
                value = cfg.get("issue_status",o)
                output += o + ": " + value + "\n"
                options.append(int(value))
                if o.lower() == "default":
                    default = int(value)
            except Exception as e:
                logger.error("Error parsing %s - %s: %s" % ("activities",o,e))
    except Exception as e:
        logger.error("Raised exception: %s" % e)
    if not quiet:
        print (output)
    return options, default


def get_trackers():
    global conffile
    output = ""
    options = []
    default = None
    print ('''\033[33;01mAvailable %s:\033[31;01m>\033[0;0m''' % "trackers")
    cfg = ConfigParser.ConfigParser()
    cfg.read(conffile)
    try:
        for o in cfg.options("trackers"):
            try:
                value = cfg.get("trackers",o)
                output += o + ": " + value + "\n"
                options.append(int(value))
                if o.lower() == "default":
                    default = int(value)
            except Exception as e:
                logger.error("Error parsing %s - %s: %s" % ("trackers",o,e))
    except Exception as e:
        logger.error("Raised exception: %s" % e)
    print (output)
    return options, default

def select_option(option_name, get_options):
    option = None
    options,default = get_options()
    # print (options)
    options.append(-1)
    while option not in options:
        try:
          option = int(raw_input('\033[35;01mSelect %s (set -1 to cancel): \033[0;0m' % option_name))
        except Exception:
          option = default
    if option == -1:
        raise CancelException()
    print ("Selected %s: %s" % (option_name,option))
    return option


################################################################################
def change_task_status(comment):
    global CURRENT_TICKET
    global PREVIOUS_TICKET
    global LAST_TIME
    try:
      if CURRENT_TICKET == {}:
        print ("No ticket to be modified")
        pass
      else:
        status = select_option("issue", get_issue_status)
        if comment.strip() == "":
          comment = get_input("comment")
        if comment.strip() == "":
          comment = "Task status updated"
        now = time.time()
        _,activity_id = get_activities()
        issue_id = CURRENT_TICKET["id"]
        hours = (now - LAST_TIME) / 3600.0
        create_time_entry(issue_id,activity_id,hours,comment)
        change_issue_status(issue_id, status, comment)
        LAST_TIME = now
        PREVIOUS_TICKET = CURRENT_TICKET
        CURRENT_TICKET = {}
    except CancelException as e:
        print ("Action cancelled")
    except Exception as e:
        logger.error("Unexpected exception: %s" % e)

def create_task():
    global CURRENT_TICKET
    global CURRENT_USER
    global PREVIOUS_TICKET
    global LAST_TIME
    try:
      p_id = select_option("project", get_projects)
      # t_id = select_option("tracker", get_trackers)
      try:
        u_id = select_option("user", get_users)
      except Exception:
        u_id = int(CURRENT_USER["id"])
      subject = get_input("subject")
      description = get_input("description",multiline=True)
      new_ticket = create_issue(p_id,"9",u_id,subject,description)
      if new_ticket:
        now = time.time()
        LAST_TIME = now
        PREVIOUS_TICKET = CURRENT_TICKET
        CURRENT_TICKET = new_ticket
    except CancelException as e:
        print ("Action cancelled")
    except Exception as e:
        logger.error("Unexpected exception: %s" % e)

def end_task(comment):
    global CURRENT_TICKET
    global PREVIOUS_TICKET
    global LAST_TIME
    try:
      if CURRENT_TICKET == {}:
        pass
      else:
        if comment.strip() == "":
          comment = get_input("comment")
        now = time.time()
        # print ("Ending task - comment: %s"  % comment)
        print ('''\033[35;01mEnding task %s - comment %s\033[0;0m''' \
          % (CURRENT_TICKET["id"], comment))
        _,activity_id = get_activities(quiet=True)
        issue_id = CURRENT_TICKET["id"]
        hours = (now - LAST_TIME) / 3600.0
        create_time_entry(issue_id,activity_id,hours,comment)
        LAST_TIME = now
        PREVIOUS_TICKET = CURRENT_TICKET
        CURRENT_TICKET = {}
    except CancelException as e:
        print ("Action cancelled")
    except Exception as e:
        logger.error("Unexpected exception: %s" % e)

def set_task(issue_id):
    global CURRENT_TICKET
    global PREVIOUS_TICKET
    global LAST_TIME
    try:
        if issue_id.strip() == "":
          print ("No issue number entered")
          issue_id = select_option("issue", get_myissues)
        res = get_object("issues",issue_id,"id", "subject")
        if res != {}:
          PREVIOUS_TICKET = CURRENT_TICKET
          end_task("Change of activity")
          CURRENT_TICKET = res["issue"]
          LAST_TIME = time.time()
    except CancelException as e:
        print ("Action cancelled")
    except Exception as e:
        logged.error("Unexpected exception: %s" % e)


def previous_task():
    global CURRENT_TICKET
    global PREVIOUS_TICKET
    global LAST_TIME
    try:
      if not PREVIOUS_TICKET=={} and not CURRENT_TICKET=={}:
          tmp_previous = PREVIOUS_TICKET
          tmp_current = CURRENT_TICKET
          end_task("Change of activity")
          CURRENT_TICKET = tmp_previous
          PREVIOUS_TICKET = tmp_current
          LAST_TIME = time.time()
          print ("Selected task is %s now" % CURRENT_TICKET["id"])
      else:
        print ("No previous ticket")
    except Exception as e:
        logged.error("Unexpected exception: %s" % e)


def update_task(comment):
    global CURRENT_TICKET
    global LAST_TIME
    try:
      if CURRENT_TICKET == {}:
        print ("No ticket to be updated")
      else:
        now = time.time()
        if comment.strip() == "":
            comment = get_input("comment")
        note = get_input("note",multiline=True)
        activity_id = select_option("activity", get_activities)
        issue_id = CURRENT_TICKET["id"]
        update_issue(issue_id, comment, note)
        hours = (now - LAST_TIME) / 3600.0
        create_time_entry(issue_id,activity_id,hours,comment)
        LAST_TIME = now
    except CancelException as e:
        print ("Action cancelled")
    except Exception as e:
        logger.error("Unexpected exception: %s" % e)


################################################################################
class TimeThread(threading.Thread):
      global end_task
      global LAST_TIME
      global CURRENT_TICKET
      global threshold
      global logger
 
      def __init__(self, console):
          threading.Thread.__init__(self)
          self.end = False
          self.console = console

      def run(self):
          while not self.end:
              logger.debug("Thread running ... ")
              time.sleep(6)
              now = time.time()
              if now > LAST_TIME + threshold and CURRENT_TICKET:
                  end_task("Auto end")
          end_task("Auto end. Application closed")

class Console(cmd.Cmd):
    """Tracker command processor."""

    def do_createtask(self, line):
        """createtask
        Create a new ticket"""
        create_task()

    def do_viewcurrenttask(self, line):
        """viewcurrenttask
        View information about the current task"""
        global CURRENT_TICKET
        print (json.dumps(CURRENT_TICKET, sort_keys=True,
            indent=4, separators=(',', ': ')))

    def do_viewtasks(self, args_line):
        """viewtasks
        View tasks [user - default:me] [status - default: open]"""
        args = args_line.split()
        try:
            user = args[0]
        except:
            user = "me"
        try:
            status = args[1]
        except:
            status = "open"
        get_issues(user,status)

    def do_viewtimes(self, user):
        """viewtimes
        View logged times. Only over the 100 last entries"""
        if user.strip() == "":
            get_time_entries()
        else:
            get_time_entries(user)

    def do_viewusers(self, line):
        """viewusers
        View users"""
        try:
            get_users()
        except Exception:
            print ("No privileges")

    def do_settask(self, issue_id):
        """settask
        Select a task as working on it """
        set_task(issue_id)

    def do_status(self, line):
        """viewstatus
        View enviroment"""
        global LAST_TIME
        global CURRENT_USER
        global CURRENT_TICKET
        global PREVIOUS_TICKET
        global threshold
        print ("Baseurl: " + str(baseurl))
        print ("Key: ..." + str(key)[-5:])
        print ("Threshold: %s seconds" % threshold)
        print ("User info: " + json.dumps(CURRENT_USER, sort_keys=True,
            indent=4, separators=(',', ': ')))
        now = time.time()
        if CURRENT_TICKET != {}:
          print ("Current ticket Id: %s" % CURRENT_TICKET["id"])
          print ("Current ticket Subject: %s" % CURRENT_TICKET["subject"])
        if PREVIOUS_TICKET != {}:
          print ("Previous ticket Id: %s" % PREVIOUS_TICKET["id"])
          print ("Previous ticket Subject: %s" % PREVIOUS_TICKET["subject"])
        print ("Last update: %s" %\
datetime.datetime.fromtimestamp(LAST_TIME).isoformat(' '))
        print ("Auto end: %s seconds" % ((LAST_TIME + threshold) - now))
    
    def do_updatetask(self, comment):
        """updatetask [comment]
        Update task"""
        update_task(comment)
    
    def do_endtask(self, comment):
        """endtask [comment]
        End task"""
        end_task(comment)

    def do_previoustask(self, line):
        """previoustask
        Select the previous task as working on it """
        previous_task()

    def do_changetaskstatus(self, comment):
        """changetaskstatus [comment]
        Change status of the task"""
        change_task_status(comment)
    
    def do_exit(self, line):
        """exit
        End the program"""
        print ("Ending!. Sending kill to threads...")
        exit()
     
    def do_EOF(self, line):
        """EOF
        End the program"""
        exit()

    def do_create(self, line):
        """createtask
        Create a new ticket"""
        create_task()

    def do_set(self, issue_id):
        """settask
        Select a task as working on it """
        set_task(issue_id)

    def do_prev(self, line):
        """previoustask
        Select the previous task as working on it """
        previous_task()


    def do_tasks(self, args_line):
        """viewtasks
        View tasks [user - default:me] [status - default: open|closed]"""
        args = args_line.split()
        try:
            user = args[0]
        except:
            user = "me"
        try:
            status = args[1]
        except:
            status = "open"
        get_issues(user,status)

    def do_up(self, comment):
        """updatetask [comment]
        Update task"""
        update_task(comment)

    def do_end(self, comment):
        """endtask [comment]
        End task"""
        end_task(comment)

    def do_change(self, comment):
        """changetaskstatus [comment]
        Change status of the task"""
        change_task_status(comment)



# command line options parser ##################################################
parser = OptionParser()
parser.add_option("-c", "--conffile", dest="conffile", default=conffile,
                  help="Conffile (default: %s)" % conffile)
parser.add_option("-l", "--logfile",
        dest="logfile", help="Log file (default: %s)" % logfile,
        default=logfile)
parser.add_option("--loglevel",
        dest="loglevel", help="Log level (default: %s)" % loglevel,
        default=loglevel)
(options, args) = parser.parse_args()

logfile = options.logfile
loglevel = options.loglevel
conffile = options.conffile


# logging ######################################################################
import logging
hdlr = logging.FileHandler(logfile)
hdlr.setFormatter(logging.Formatter('%(levelname)s %(asctime)s %(message)s'))
logger = logging.getLogger('tracker_cmd')
logger.addHandler(hdlr)
logger.setLevel(int(loglevel))


# setting up ###################################################################
logger.debug("Default encoding: %s" % sys.getdefaultencoding())
setup()

def signal_int_handler(signal, frame):
    print ("Ctrl-c received! Sending kill to threads...")
    exit()
signal.signal(signal.SIGINT, signal_int_handler)

if __name__ == '__main__':
    try:
      CURRENT_USER = get_current_user()["user"]
    except Exception:
      pass
    console = Console()
    console.prompt = "\033[33;01mTracker\033[31;01m>\033[0;0m "
    thread = TimeThread(console)
    thread.start()
    console.cmdloop()
sys.exit(0)



