#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Markus Korn <thekorn@gmx.de>
#
# ##################################################################
#
# 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 3
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# See file /usr/share/common-licenses/GPL for more details.
#
# ##################################################################

import sys
import re
import os
import urllib
import urllib2
import libxml2
import cookielib
try:
    import sqlite3 as sqlite
except ImportError:
    try:
        from pysqlite2 import dbapi2 as sqlite
    except ImportError:
        sqlite = None

from StringIO import StringIO
from ConfigParser import SafeConfigParser
from optparse import OptionParser, make_option

# deactivate error messages from the validation [libxml2.htmlParseDoc]
def noerr(ctx, str):
    pass

libxml2.registerErrorHandler(noerr, None)

PAGE = "https://wiki.ubuntu.com/UbuntuBugDay/1" # this page must not exist!
RE_BUGDAYS = re.compile("/UbuntuBugDay/([0-9]+)(/KDE)?")

CONFIGFILE = os.path.expanduser("~/.hugday_config")
DEFAULT_CONFIG = ("[config]\n"
                  "user =\n"
                  "moin_id =\n"
                  "current =\n")
                  
RE_BUGS = "%s(%%s)%s" %(re.escape("https://bugs.launchpad.net/bugs/"), re.escape("|"))


class CmdOptions(OptionParser):
    
    USAGE = (
        "\t%prog init --user <USERNAME> [--wiki-id <MOINID>] [--cookie <PATH>]\n"
        "\t%prog current [--remember]\n"
        "\t%prog close <Bugs> [--day <ID>] [--kde] [--user <TRIAGER>]\n"
        "\t%prog list [--day <ID>] [--kde] [--filter <open|done>]\n"
        "\t%prog list-days\n"
    )
    
    OPTIONS = (
        make_option("--remember", action="store_true", dest="remember",
                    help="store result in user's config file"),
        make_option("--day", action="store", type="string", dest="day",
                    help="change target to the hugday with this ID"),
        make_option("--kde", action="store_true", dest="kde",
                    help="work on /KDE hugday"),
        make_option("--filter", action="store", choices=["open", "done"],
                    dest="filter", help="filter list of bugs"),
        make_option("--user", action="store", type="string", dest="user",
                    help="username to show in the 'triager' column"),
        make_option("--wiki-id", action="store", type="string", dest="wiki_id",
                    help="the users MOIN-ID"),
        make_option("--cookie", action="store", type="string", dest="cookie",
                    help="path to users mozilla-like cookie file")
    )
            
    TOOLS = {
        "init": (
                 ("user", ),
                 ("wiki_id", "cookie"),
                ),
        "current": (
                    tuple(),
                    ("remember", ),
                   ),
        "close": (
                    tuple(),
                    ("day", "kde", "user"),
                 ),
        "list": (
                    tuple(),
                    ("day", "kde", "filter"),
                ),
        "list-days": (tuple(), tuple())
    }
    
    def __init__(self):
        OptionParser.__init__(self, option_list=self.OPTIONS)
        self.set_usage(self.USAGE)
        
    def parse_args(self, args=None, values=None):
        options, args = OptionParser.parse_args(self, args, values)
        if not args:
            self.error("Please define a sub-tool you would like to use")
        tool = args.pop(0)
        if tool not in CmdOptions.TOOLS:
            self.error("Unknown tool '%s'" %tool)
        options.bugs = list()
        if args:
            if tool == "close":
                options.bugs = args
            else:
                self.error("Only one sub-tool allowed")
        if tool == "close" and not options.bugs:
            self.error("at elast one bug needed")
        given_options = set(i for i, k in self.defaults.iteritems() if not getattr(options, i) == k)
        needed_options = set(CmdOptions.TOOLS[tool][0]) - given_options
        if needed_options:
            self.error("Please define the following options: %s" %", ".join(needed_options))
        optional_options = given_options - set(sum(CmdOptions.TOOLS[tool], ()))
        if optional_options:
            self.error("The following options are not allowed for this tool: %s" %", ".join(optional_options))
        if "cookie" in given_options and not "user" in given_options:
            # currently no effect, but needed in the future
            self.error("--cookie always needs an --user argument")
        if options.day is not None:
            try:
                options.day = int(options.day)
            except ValueError:
                if not options.day.lower() == "current":
                    self.error("option --day: value has to be 'CURRENT' or an integer")
        return tool, options
        
        
class Ignore404ErrorHandler(urllib2.HTTPDefaultErrorHandler):
    """ Error handler which does not raise a HTTPError if a page is not
    found, but returns the Error page instead.
    """
    def http_error_404(self, req, fp, code, msg, hdrs):
        return fp
        
        
def parse_all_hugdays():
    """ returns a sorted list of all hugdays, latest first.
    It is impossible to use the page search of the wiki for this because
    too much search requests are not allowed in a short time for each user.
    """
    result = list()
    opener = urllib2.build_opener(Ignore404ErrorHandler)
    page = opener.open(PAGE)
    data = page.read()
    for i in RE_BUGDAYS.findall(data):
        if i[0] == "1":
            continue
        result.append("".join(i))
    return sorted(result, reverse=True)
    
def get_current(kde=False):
    """ return url to current hugday """
    days = parse_all_hugdays()
    assert days
    current = days.pop(0)
    if "/" in current:
        day, sub = current.split("/", 1)
        if day in days:
            current = day
    return "https://wiki.ubuntu.com/UbuntuBugDay/%s" %current
    
def update_config(config=None, user=None, moin_id=None, current=None):
    if config is None:
        config = SafeConfigParser()
        config.readfp(StringIO(DEFAULT_CONFIG))
        config.read([CONFIGFILE])
    if user is not None:
        config.set("config", "user", user)
    if moin_id is not None:
        config.set("config", "moin_id", moin_id)
    if current is not None:
        config.set("config", "current", current)
    f = open(CONFIGFILE, "w")
    try:
        os.chmod(CONFIGFILE, 0600)
        config.write(f)
    finally:
        f.close()
    return config
    
class Sections(object):
    
    @staticmethod
    def line_from_id(id):
        try:
            _, ln = id.split("-")
            return int(ln)
        except:
            return None
            
    @classmethod
    def from_ctx(cls, ctx):
        sections = dict()
        for sec in ctx.xpathEval("//h2"):
            ln = sec.xpathEval("following-sibling::span[@class='anchor' and @id][1]")
            assert ln
            ln = ln.pop()
            line = cls.line_from_id(ln.prop("id"))
            sections[line] = sec.content
        return cls(sections)
            
    def __init__(self, sections):
        self.sections = sections
        
    def get_section(self, id):
        ln = Sections.line_from_id(id)
        if ln is None:
            return None
        diff = [(i, ln - i) for i in self.sections.iterkeys() if ln - i > 0]
        if not diff:
            return None
        min_diff = min(diff, key=lambda x: x[1])
        return self.sections[min_diff[0]]
        
    
def parse_bugs(url, filter=None):
    """ parses a hugday wiki-page for bugs """
    try:
        data = urllib2.urlopen(url)
    except urllib2.HTTPError, e:
        if e.code == 404:
            raise ValueError("'%s' is not a valid url for a hugday page" %url)
        else:
            raise
    ctx = libxml2.htmlParseDoc(data.read(), "UTF-8")
    sections = Sections.from_ctx(ctx)
    bugs = ctx.xpathEval("""//a[contains(@href, "https://bugs.launchpad.net/bugs/")]/../../..""")
    for b in bugs:
        done = "lightgreen" in b.prop("style")
        if (filter == "open" and done) or (filter == "done" and not done):
            continue
        section = sections.get_section(b.xpathEval("td[1]/span")[0].prop("id"))
        nr = int(b.xpathEval("td[1]")[0].content)
        title = b.xpathEval("td[2]")[0].content.strip()
        triager = b.xpathEval("td[3]")[0].content.strip() or None
        yield (nr, title, done, triager, section)
        
def get_credentials(user, wiki_id=None, cookie=None):
    if wiki_id:
        config = update_config(user=user, moin_id=wiki_id)
    elif cookie:
        # parse cookie
        # this includes a workaround for new cookie format
        result = None
        try:
            cj = cookielib.MozillaCookieJar()
            cj.load(os.path.expanduser(cookie))
            result = [i.value for i in cj if i.domain == "wiki.ubuntu.com" \
                and i.name in ("MOIN_ID", "MOIN_SESSION")]
        except cookielib.LoadError:
            if not sqlite:
                raise TypeError(("Try to read cookiefile, cannot handle "
                                 "format of '%s'" %cookie))
            try:
                con = sqlite.connect(cookie)
                con.row_factory = sqlite.Row

                cur = con.cursor()
                cur.execute(("select value from moz_cookies where "
                             "(name=? or name=?) and host=?"),
                             ("MOIN_SESSION", "MOIN_ID", "wiki.ubuntu.com",))
                #TODO: add check for expired cookie
                result = [i["value"] for i in cur]
            except sqlite.Error, e:
                raise TypeError(("Error while trying to read cookie in "
                                 "sql format, cannot handle "
                                 "format of '%s'" %cookie))
        if not result:
            raise ValueError("No cookie with name 'MOIN_ID' found in '%s'" %cookie)
        config = update_config(user=user, moin_id=result.pop())
    else:
        # interactive mode or openid, TBD
        raise NotImplementedError
        
def build_request(url, moin_id, proxy=None, data=None):
    request = urllib2.Request(url, data)
    #~ # moinmoin < 1.6
    #~ request.add_header("Cookie", "MOIN_ID=\"%s\"" % moin_id)
    # moinmoin >= 1.6
    request.add_header("Cookie", "MOIN_SESSION=\"%s\"" % moin_id)
    if proxy:
        request.set_proxy(proxy, "http")
    return request
    
        
def close_bugs(bugs, url, user=None):
    LINE_RE = re.compile(RE_BUGS %"|".join(map(str, bugs)))
    comment = urllib.quote("edited bugs %s with 'hugday close'" \
        %", ".join(map(lambda x: "#%s" %x, bugs)))
    #~ url = "https://wiki.ubuntu.com/MarkusKorn/HUGDAYTEST" #for Testing only
    #~ print url
    config = update_config()
    moin_id = config.get("config", "moin_id")
    if not moin_id:
        raise ValueError("No wiki_id entry found in '%s', please run 'hugday init'" %CONFIGFILE)
    edit_url = "%s?action=edit&editor=text" %url
    proxy = os.environ.get("http_proxy")
    request = build_request(edit_url, moin_id, proxy)
    page = urllib2.urlopen(request)
    content = page.read()
    ctx = libxml2.htmlParseDoc(content, "UTF-8")
    try:
        form = ctx.xpathEval("""//form[@id="editor"]""")
        assert form
        data = form[0].xpathEval("//textarea")
        assert data
    except Exception, e:
        msg = ctx.xpathEval("""//div[@id="message"]/p[contains(., "not allowed")]""")
        if msg:
            raise RuntimeError(("You are not allowed to change the content "
                "of '%s'. Plase run 'hugday init' and try again." %url))
        print >> sys.stderr, content
        raise RuntimeError("Error while parsing '%s'" %url)
    data = StringIO(data[0].content)
    new_data = ""
    if user is None:
        user = config.get("config", "user")
    closed_bugs = set()
    for line in data:
        if line.startswith("||"):
            ctx = LINE_RE.search(line)
            if ctx:
                nr = ctx.groups()[0]
                closed_bugs.add(nr)
                cols = line.split("||")
                triager = cols[3].strip()
                if "lightgreen" in cols[1] or triager:
                    raise ValueError(("Bug '%s' has already been marked "
                        "as triaged by '%s'" %(nr, triager or "unknown")))
                marker, url_part = cols[1].split("[[", 1)
                cols[1] = """<rowstyle="background-color: lightgreen;"> [[%s""" %url_part
                cols[3] = " %s " %user
                line = "||".join(cols)
        new_data += line
    bugs = set(bugs) - closed_bugs
    if bugs:
        raise ValueError("One or more bugs can't be found: %s" %", ".join(map(str, bugs)))
    data = {
        "button_save": "1",
        "savetext": urllib.quote(new_data),
        "comment": comment,
    }
    for i in form[0].xpathEval("""//input[@type="hidden"]"""):
        data[i.prop("name")] =  i.prop("value")
    data = "&".join("=".join(i) for i in data.items())
    request = build_request(edit_url, moin_id, proxy, data)
    page = urllib2.urlopen(request)
            
    
def get_url(day, kde=False):
    if isinstance(day, str) and day.lower() == "current":
        url = get_current()
    elif day is None:
        config = update_config()
        url = config.get("config", "current")
        if not url:
            raise RuntimeError(("No value for 'current' found in '%s'\n"
                                "Please run 'hugday current --remember' or "
                                "give a date" %CONFIGFILE))
    else:
        url = "https://wiki.ubuntu.com/UbuntuBugDay/%s" %day
    if kde and not url.endswith("KDE"):
        url = "%s/KDE" %url
    return url

def main():
    cmdoptions = CmdOptions()
    tool, options = cmdoptions.parse_args()
    if tool == "list-days":
        print "list of all hugdays, latest first:"
        for day in parse_all_hugdays():
            print "\t* https://wiki.ubuntu.com/UbuntuBugDay/%s" %day
    elif tool == "current":
        current = get_current()
        print current
        if options.remember:
            update_config(current=current)
    elif tool == "list":
        url = get_url(options.day, options.kde)
        i = None
        for i, bug in enumerate(parse_bugs(url, filter=options.filter)):
            print ", ".join(map(str, bug[:-1]))
        if i is not None:
            print "="*50
            print "Total:", i + 1
    elif tool == "init":
        get_credentials(options.user, options.wiki_id, options.cookie)
    elif tool == "close":
        url = get_url(options.day, options.kde)
        close_bugs(options.bugs, url, options.user)
    else:
        raise RuntimeError("unknown tool '%s'" %tool)
        
        

if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception, e:
        print "%s: %s" %(e.__class__.__name__, e)
        sys.exit(1)
    
