#!/usr/bin/python
# fedora-hosted - a commandline frontend for the Fedora Hosted Projects Trac
#
# Copyright (C) 2008 Red Hat Inc.
# Author: Jesse Keating <jkeating@redhat.com>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.

# TODO: this file should probably go away and be far more generic
# And we should load specific things like url structure from a config
# store of some sort.

import getpass
import optparse
import offtrac
import sys
import time
import os
import os.path
import subprocess
import textwrap
import six
from six.moves import input

# Define some constants
BASEURL = 'fedorahosted.org/'
WRAP_LEN = 60
LINE_LEN = 75

# List currently available commands
cmdlist = ('list', 'list-milestones', 'ticket-info', 'milestone-info',
           'new-ticket', 'new-milestone', 'show', 'update-ticket', 'close')


def getOtherConfiguration(opts):
    """
    Try to search for configuration variables in other locations,
    than just on the command line.

    """
    searched_keys = ('user', 'password', 'project', )
    other_conf = {
        'user': opts.user,
        'password': opts.password,
        'project': opts.project
    }

    # Look for configuration in the configuration file
    config_file = os.path.expanduser("~/.fedora/tracrc")
    if os.path.exists(config_file):
        from six.moves.configparser import SafeConfigParser
        conf_rc = SafeConfigParser()
        conf_rc.read(config_file)
        if conf_rc.has_section('trac'):
            for var in searched_keys:
                if (not other_conf[var]) and conf_rc.has_option('trac', var):
                    other_conf[var] = conf_rc.get('trac', var)

    # Look for configuration in the local git configuration
    proc = subprocess.Popen('git rev-parse --git-dir',
        stdout=file("/dev/null", 'w'), shell=True)
    ret = os.WEXITSTATUS(os.waitpid(proc.pid, 0)[1])
    if ret == 0:
        raw_value = subprocess.Popen(['git', 'config',
            '--get-regexp', r'trac\..*'],
            stdout=subprocess.PIPE).communicate()[0].strip()
        values = dict([line.strip().split()[:2]
            for line in raw_value.split('\n') if line])
        for var in searched_keys:
            key = "trac.%s" % var
            if (not other_conf[var]) and key in values:
                other_conf[var] = values[key]
    return other_conf


def wrap_text(text, width=WRAP_LEN):
    """
    Wrap text without joining all lines together first. See this
    note in the textwrap documentation:
        If replace_whitespace is false, newlines may appear in
        the middle of a line and cause strange output. For this
        reason, text should be split into paragraphs (using
        str.splitlines() or similar) which are wrapped
        separately.
    """
    out = []
    raw_lines = text.splitlines()
    for rawl in raw_lines:
        out += textwrap.wrap(rawl, width)
    return out


def edit_description():
    """
    Open editor on temporary file and return the result.
    """
    import tempfile
    if 'EDITOR' in os.environ:
        editprog = os.environ['EDITOR']
    else:
        editprog = "/bin/vi"

    descfile = tempfile.NamedTemporaryFile()
    editproc = subprocess.Popen(editprog + " " + descfile.name, shell=True)
    os.waitpid(editproc.pid, 0)[1]
    outstr = descfile.read().strip()
    descfile.close()
    return outstr


def list_tickets(opts):
    """
    List all tickets with some details.
    """
    # setup the query string
    query = "status%s" % opts.status
    if opts.owner:
        query += "&owner=%s" % opts.owner
    if opts.component:
        query += "&component=%s" % opts.component
    if opts.type:
        query += "&type=%s" % opts.type

    results = trac.query_tickets(query)
    tickets_info = get_ticket_info(results, comments=False)
    for ticket in tickets_info:
        meta = ticket[3]
        wrapped_summary = wrap_text(meta['summary'])
        print("%4s (%5s): %s" % ("#" + str(ticket[0]), meta['status'],
            wrapped_summary[0]))
        for line in wrapped_summary[1:]:
            print("%13s %s" % ('', line))


def collect_comments(ticketNos):
    idx_keys = ('time', 'who', 'what', 'oldValue', 'newValue', 'permanent',)
    trac.setup_multicall()
    for ticket in ticketNos:
        trac.server.ticket.changeLog(ticket)
    ret = trac.do_multicall()[0]
    changes = []

    for log_item in ret:
        change_item = {}
        for key in idx_keys:
            change_item[key] = log_item[idx_keys.index(key)]
        changes.append(change_item)
    return changes


def show_tickets(tickets):
    for ticket in tickets:
        meta = ticket[3]
        wrapped_summary = wrap_text(meta['summary'])
        wrapped_desc = wrap_text(meta['description'])
        if not wrapped_desc:  # yes, I have managed to have empty description
            wrapped_desc = [""]
        if 'milestone' not in meta:
            meta['milestone'] = "<none>"

        print("%4s (%5s): %s" % ('#' + str(ticket[0]), meta['status'],
            wrapped_summary[0]))
        for line in wrapped_summary[1:]:
            print("%14s %s" % ('', line))
        print("%s" % "-" * LINE_LEN)
        print("component: %s, priority: %s, milestone: %s" % \
            (meta['component'], meta['priority'], meta['milestone']))
        print("%s" % "=" * LINE_LEN)
        print("Description: %s" % wrapped_desc[0])
        for line in wrapped_desc[1:]:
            print("%12s %s" % ('', line))

        for item in meta['changelog']:
            if item['what'] == 'comment' and item['newValue']:
                wrapped_comment = wrap_text(six.u(item['newValue']),
                    WRAP_LEN + 9)
                print("%s\n%2s: %s" % ('-' * LINE_LEN, item['oldValue'],
                    wrapped_comment[0]))
                for line in wrapped_comment[1:]:
                    print("%3s %s" % ('', line))
        if len(tickets) > 1:
            print("\n")


def get_ticket_info(args, comments=True):
    out = []
    # Setup the trac object for multicall
    trac.setup_multicall()
    for number in args:
        trac.get_ticket(number)
    # Do the multicall and print out the results
    for result in trac.do_multicall():
        if comments:
            result[3]['changelog'] = collect_comments([result[0]])
        out.append(result)
    return out


# Define some functions
def setup_action_parser(action):
    """Setup parsers for the various action types"""

    usage = "usage: %%prog %s [options]" % action
    p = optparse.OptionParser(usage=usage)

    if action == "list":
        p.add_option("--owner", "-o")
        p.add_option("--status", "-s", default="!=closed",
                     help="Query string for status, default is '!=closed'")
        p.add_option("--component", "-c")
        p.add_option("--type", "-t")

#    elif action == "list-milestones":
#        p.add_option("--name", "-n",
#                     help="Show information about a particular milestone")
#        p.add_option("--all", "-a", action="store_true",
#                     help="Show all milestones, otherwise only show active.")

    elif action == "ticket-info":
        p.set_usage("usage: %%prog %s [ticket numbers]" % action)

    elif action == "show":
        p.set_usage("usage: %%prog %s [ticket numbers]" % action)

    elif action == "milestone_info":
        p.set_usage("usage: %%prog %s [milestones]" % action)

    elif action == "new-ticket":
        p.add_option("--summary", "-s", help="REQUIRED!")
        p.add_option("--description", "-d", default=None)
        p.add_option("--descedit", "-D", action="store_true", dest="descedit")
        p.add_option("--type", "-t", default=None)
        p.add_option("--priority", "-p", default=None)
        p.add_option("--milestone", "-m", default=None)
        p.add_option("--component", "-C", default=None)
        p.add_option("--version", "-v", default=None)
        p.add_option("--keyword", "-k", action="append",
                     help="Keyword to add, can be used multiple times.")
        p.add_option("--assignee", "-a", default=None)
        p.add_option("--cc", action="append",
                     help="Carbon Copy address, can be used multiple times.")
        # This one is a little backwards.  The rpc call is actually notify,
        # and defaults to false, but we want to default to true.
        p.add_option("--stealth", action="store_false", default=True,
                     help="Suppress initial notification of this ticket.")

    elif action == "update-ticket":
        p.add_option("--ticket", "-n", help="Ticket number.  REQUIRED!")
        p.add_option("--comment", "-c", default='',
            help="Value - make program to open $EDITOR for editing it.")
        p.add_option("--summary", "-s", default=None)
        p.add_option("--description", "-d", default=None)
        p.add_option("--type", "-t", default=None)
        p.add_option("--priority", "-p", default=None)
        p.add_option("--milestone", "-m", default=None)
        p.add_option("--component", "-C", default=None)
        p.add_option("--version", "-v", default=None)
        p.add_option("--keyword", "-k", action="append",
                     help="Keyword to add, can be used multiple times.")
        p.add_option("--assignee", "-a", default=None)
        p.add_option("--cc", action="append",
                     help="Carbon Copy address, can be used multiple times.")
        p.add_option("--status", "-S", default=None)
        p.add_option("--resolution", "-r", default=None)
        # This one is a little backwards.  The rpc call is actually notify,
        # and defaults to false, but we want to default to true.
        p.add_option("--stealth", action="store_false", default=True,
                     help="Suppress notification of this update.")

    elif action == "close":
        p.add_option("--comment", "-c", default='', help="Comment; - to open $EDITOR.")
        p.add_option("--resolution", "-r", default=None)
        p.add_option("--ticket", "-n", default=None, help="Ticket number")
        p.add_option("--stealth", action="store_false", default=True,
                     help="Suppress notification of this update.")

    elif action == "new-milestone":
        p.add_option("--name", "-n", help="REQUIRED!")
        p.add_option("--description", "-d", default=None)
        p.add_option("--due", "-D", default=None,
                     help="Due date in MM-DD-YY format.")

    return p


# get command line options
usage = "usage: %prog [global options] COMMAND [options]"
usage += "\nCommands: %s" % ', '.join(cmdlist)
parser = optparse.OptionParser(usage=usage)
parser.disable_interspersed_args()

parser.add_option("--user", "-u")
parser.add_option("--password", "-p")
parser.add_option("--project", "-P")

# Parse our global options
(opts, args) = parser.parse_args()

# See if we got a command
if len(args) and args[0] in cmdlist:
    action = args.pop(0)
else:
    parser.print_help()
    sys.exit(1)

# Parse the command
action_parser = setup_action_parser(action)
(actopt, actargs) = action_parser.parse_args(args)

# Check other locations of configuration for required variables
oopts = getOtherConfiguration(opts)

if not oopts['user']:
    oopts['user'] = input('Username: ')

if not oopts['password']:
    oopts['password'] = getpass.getpass('Password for %s: ' % oopts['user'])

if not oopts['project']:
    oopts['project'] = input('Project space: ')


# Create the TracServ object
uri = 'https://%s:%s@%s/%s/login/xmlrpc' % (oopts['user'],
                                            oopts['password'],
                                            BASEURL,
                                            oopts['project'])
trac = offtrac.TracServer(uri)

# Try to do something
if action == "list":
    list_tickets(actopt)
elif action == "list-milestones":
    results = trac.list_milestones()
    print(results)

elif action == "ticket-info":
    if not actargs:  # FIXME, this isn't working
        action_parser.print_help()
        sys.exit(1)
    print("\n".join([six.u(ticket) for ticket in get_ticket_info(actargs)]))

elif action == "show":
    if not actargs:  # FIXME, this isn't working
        action_parser.print_help()
        sys.exit(1)
    show_tickets(get_ticket_info(actargs))

elif action == "milestone-info":
    if not actargs:  # FIXME, this isn't working
        action_parser.print_help()
        sys.exit(1)
    trac.setup_multicall()
    for milestone in actargs:
        trac.get_milestone(milestone)
    for result in trac.do_multicall():
        print(result)

elif action == "new-ticket":
    # Check to make sure we got all we need
    if actopt.summary and (actopt.description or actopt.descedit):
        pass
    else:
        action_parser.print_help()
        sys.exit(1)
    # Wrap up our keywords and cc into one string, if any
    keywords = None
    ccs = None
    if actopt.keyword:
        keywords = ' '.join(actopt.keyword)
    if actopt.cc:
        ccs = ' '.join(actopt.cc)

    if actopt.descedit:
        actopt.description = edit_description()

    result = trac.create_ticket(actopt.summary, actopt.description,
                              actopt.type, actopt.priority, actopt.milestone,
                              actopt.component, actopt.version, keywords,
                              actopt.assignee, ccs, actopt.stealth)
    print(result)

elif action == "update-ticket":
    # Check to make sure we got all we need
    if actopt.ticket:
        pass
    else:
        action_parser.print_help()
        sys.exit(1)

    if actopt.comment == "-":
        actopt.comment = edit_description()

    # Wrap up our keywords and cc into one string, if any
    keywords = None
    ccs = None
    if actopt.keyword:
        keywords = ' '.join(actopt.keyword)
    if actopt.cc:
        ccs = ' '.join(actopt.cc)

    result = trac.update_ticket(actopt.ticket, actopt.comment, actopt.summary,
                                actopt.type, actopt.description,
                                actopt.priority, actopt.milestone,
                                actopt.component, actopt.version, keywords,
                                ccs, actopt.status, actopt.resolution,
                                actopt.assignee, actopt.stealth)

    if result[0].isdigit():
        print("Something's wrong; result:\n%s" % result)
        sys.exit(2)

elif action == "close":
    if (len(actargs) > 0) and actargs[-1].isdigit():
        actopt.ticket = int(actargs[-1])

    if (not actopt.ticket):
        action_parser.print_help()
        sys.exit(1)
    else:
        actopt.status = "closed"

    if not actopt.resolution:
        if (len(actargs) > 1):
            actopt.resolution = actargs[-2]
        else:
            actopt.resolution = "fixed"

    if actopt.comment == "-":
        actopt.comment = edit_description()

    result = trac.update_ticket(actopt.ticket, actopt.comment,
                                status=actopt.status, resolution=actopt.resolution,
                                notify=actopt.stealth)

    if result[3]['status'] == 'closed':
        print("OK")
    else:
        print("Something is wrong; result:\n%s" % result)
        sys.exit(2)

elif action == "new-milestone":
    if not actopt.name:
        action_parser.print_help()
        sys.exit(1)
    # Convert due date to seconds if needed
    due = None
    if actopt.due:
        due = int(time.mktime(time.strptime(actopt.due, "%m-%d-%y")))

    result = trac.create_milestone(actopt.name, actopt.description, due)
    print(result)  # The result here is "0" if successful, printing isn't fun
