##
## Copyright (c) 2006 Jason Dillon
##
## Licensed under the Apache License, Version 2.0 (the "License");
## you may not use this file except in compliance with the License.
## You may obtain a copy of the License at
##
##     http://www.apache.org/licenses/LICENSE-2.0
##
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.
##

##
## $Id: __init__.py 77 2009-08-25 12:06:19Z mindwanderer $
##

import sys, os, getopt, smtplib, time
from datetime import datetime
from email.MIMEText import MIMEText
import email.Utils
import re

from p4spam import config, log, subscription
from p4spam.perforce import P4
from p4spam.htmlmessage import HtmlMessageBuilder
from p4spam.parser import DescriptionParser
from p4spam.version import VERSION

##
## P4Spam
##

DEFAULT_CONFIG_FILENAME = os.path.join(os.path.dirname(sys.argv[0]), "p4spam.conf")
    
class P4Spam:
    def __init__(this):
        this.log = log.getLogger(this)
        this.p4 = P4()
        this.smtp = None # Lazy config in sendmail()
        
    def usage(this):
        print "usage: p4spam [options]"
        print ""
        print "[options]"
        print "    --help,-h                Show this help message"
        print "    --version,-V             Show the version of P4Spam"
        print "    --debug                  Enable debug output"
        print "    --quiet                  Try to be quiet"
        print "    --config,-c <filename>   Specify the configuration file"
        print "                             Default: %s" % DEFAULT_CONFIG_FILENAME
        print ""
    
    def configure(this, args):
        # this.log.debug("Configuring...")
        
        sopts = "hc:V"
        lopts = [ "help", "config=", "debug", "quiet", "version" ]
        
        try:
            opts, args = getopt.getopt(args, sopts, lopts)
        except getopt.GetoptError:
            this.usage()
            sys.exit(2)
        
        filename = DEFAULT_CONFIG_FILENAME
        
        for o, a in opts:
            if o in ("-h", "--help"):
                this.usage()
                sys.exit()
            
            if o in ("-V", "--version"):
                print "P4Spam %s" % VERSION
                sys.exit()
            
            if o in ("--debug"):
                log.setThresholdDebug()
            
            if o in ("--quiet"):
                log.setThresholdError()
            
            if o in ("-c", "--config"):
                filename = a
        
        this.log.debug("Post option parsing...")
        
        # Read the configuration file
        config.read(filename)
    
    def getStartingChange(this):
        if config.FORCE_STARTING_CHANGE != None:
            this.log.warning("Forced starting change: %s" % config.FORCE_STARTING_CHANGE)
            return config.FORCE_STARTING_CHANGE
        
        lastchange = this.p4.counter(config.LAST_CHANGELIST_COUNTER)
        this.log.debug("Last change: %s" % (lastchange.value))
        
        # starting change is the next one
        return lastchange.value + 1
    
    def saveLastChange(this, change):
        if not config.SAVE_LAST_CHANGE_ENABLED:
            this.log.warning("Last change save to counter has been disabled")
            return
        
        this.p4.counter(config.LAST_CHANGELIST_COUNTER, change)
        this.log.debug("Set last change: %s" % change)
    
    def sendmail(this, fromAddr, recipients, messageText):
        this.log.info("Sending mail to: %s" % (recipients))
        this.log.debug(">>>\n%s" % messageText)
        
        # Don't send mail when testing
        if not config.SPAM_ENABLED:
            this.log.info("Skipping send mail; spamming is disabled")
            return
        
        i = 0
        maxtries = 3 # TODO: Expose this as configuration
        
        while True:
            # Lazy init sendmail
            if this.smtp == None:
                mailhost = config.SMTP_HOST
                
                this.log.info("Connecting to mail server: %s" % mailhost)
                smtp = smtplib.SMTP()
                smtp.connect(mailhost)
                this.smtp = smtp
                this.log.info("Connected")
            
            try:
                this.smtp.sendmail(fromAddr, recipients, messageText)
                this.log.debug("Sent")
                break
                
            except smtplib.SMTPServerDisconnected, e:
                this.log.warning("Disconnected; trying again: %s" % e)
                this.smtp = None
                
                i = i + 1
                if i >= maxtries:
                    raise "Failed to connect to SMTP server after '%s' tries" % i
    
    def processReview(this, review):
        change = review.changenumber
        this.log.info("Processing review for change: %s" % change)
        
        info = ChangeInfo(this, change)
        
        # The list of email addresses
        recipients = info.getRecipients()
        
        if len(recipients) == 0:
            this.log.info("No one is interested in this change; skipping")
            return
        
        # Don't spam to users for testing
        if not config.SPAM_USERS:
            recipients = []
        
        this.log.debug("Recipients: %s" % (recipients))
        addrs = recipients[:]
        
        # Add admin BCC
        if config.ADMIN_BCC_ADDR != None:
            # TODO: Support list types
            addrs.append(config.ADMIN_BCC_ADDR)
        
        this.log.debug("Addrs: %s" % (addrs))
        
        # If we don't have anyone to spam yet, then skip it
        if len(addrs) == 0:
            this.log.warning("No receipients for change; skipping")
            return
        
        # Figure out who this came from
        if config.FROM_ADDR != None:
            fromAddr = config.FROM_ADDR
        else:
            fromAddr = config.FROM_ADDR_FORMAT % {'fullname': review.fullname, 'email': review.email, 'user': review.user}
        this.log.debug("From addr: %s" % fromAddr)
        
        # Email the message to all recipients, one at a time so we can send different people different messages
        for recipient in addrs:

            # the first 'info'/change object is garbage collected by the time this loop runs the second
            # time, so let's instantiate a second, local change object and use that. Otherwise only the
            # first E-mail message will have the diffs in it, and the rest will be cut off at that point
            # in the E-mail body
            info2 = ChangeInfo(this, change)
            
            # Build the message body
            builder = HtmlMessageBuilder # TODO: Make configrable
            msg = builder(info2).getMessage(recipient)

            msg['From'] = fromAddr

            # use the recipient's username to get their E-mail address
            # unless this is a subscription, in which case the recipient
            # is already an E-mail address. detect this by searching for
            # an @ in the recipient's username
            have_at = re.match(".*\@", recipient) 
            if have_at: 
                revieweremail = recipient
            else:
	        revieweremail = extractEmail(this, recipient)
	    
            this.sendmail(fromAddr, revieweremail, msg.as_string())

    def processOneBatch(this):
        this.log.info("Processing one batch...")
        
        # Where do we start working?
        startchange = this.getStartingChange()
        this.log.info("Starting with change: %s" % startchange)
        
        # Track the last change processed
        lastchange = None
        
        # Query all pending reviews since the last change we processed
        pending = this.p4.review('-c', startchange)
        
        # Iter count & max batch
        i = 0
        maxbatch = config.MAX_BATCH
        
        # Process reviews for changes...
        for review in pending:
            this.processReview(review)
            
            # If we get this far we have full processed/emailed the change details
            lastchange = review.changenumber
            i = i + 1
            
            # Limit changes per batch here
            if i >= maxbatch:
                this.log.info("Reached maximum batch threashold: %s; aborting further processing" % maxbatch)
                break
        
        # Save the change for the next round
        if lastchange != None:
            this.saveLastChange(lastchange)
        
        this.log.info("Finished one batch")
    
    def cleanup(this):
        this.log.info("Cleaning up...")
        
        if this.smtp != None:
            this.log.debug("Closing smtp connection...")
            this.smtp.close()
            this.smtp = None
        
        this.log.info("Done")
    
    def main(this, args):
        this.configure(args)
        
        ##
        ## TODO: Enable error email
        ##
        
        this.log.info("Starting main loop...")
        
        repeat = config.REPEAT
        sleeptime = config.SLEEPTIME
        
        if repeat:
            this.log.info("Processing batches; delay: %s seconds" % sleeptime)
            
            while True:
                this.processOneBatch()
                this.log.info("Sleeping for %s seconds" % sleeptime)
                time.sleep(sleeptime)
        else:
            this.processOneBatch()
        
        this.cleanup()

##
## ChangeInfo
##

class ChangeInfo:
    def __init__(this, p4spam, change):
        assert p4spam != None
        assert change != None
        
        this.log = log.getLogger(this)
        this.p4spam = p4spam
        this.p4 = p4spam.p4
        this.change = change
        
        # Parse the description
        stream = this.p4.rawstream('describe', '-du', this.change)
        parser = DescriptionParser(stream)
        this.desc = parser.parse()
    
    def getChange(this):
        return this.desc.change
    
    def getAuthor(this):
        return this.desc.author
    
    def getComment(this):
        return "".join(this.desc.comments).strip()
    
    def getComments(this):
        return this.desc.comments
    
    def getJobsFixed(this):
        return this.desc.jobs
    
    def getClient(this):
        return this.desc.client
    
    def getDateTime(this):
        return "%s %s" % (this.desc.date, this.desc.time)
    
    def getAffectedFiles(this):
        return this.desc.files
    
    def getDifferences(this):
        return this.desc.diffs
    
    def getRecipients(this):
        recipients = []
        
        if config.USER_REVIEWS_ENABLED:
            # First check the reviewers
            reviewers = this.p4.reviews('-c', this.change)
            for reviewer in reviewers:
                recipients.append(reviewer.user)
        else:
            this.log.warning("User reviews disabled")
        
        if config.SUBSCRIPTIONS_ENABLED:
            # Next check for subscriptions
            subscription.applySubscriptions(recipients, this.desc.files)
        else:
            this.log.warning("Subscriptions disabled")
        
        return recipients

def extractEmail(this, user):
    revieweremail = []

    # extract the reviewer's E-mail from their username
    revieweruserdef = this.p4.raw('user', '-o', user)

    # suck out the E-mail address
    for line in revieweruserdef:
        matchObj = re.match( r'(Email:\s*)(.*)', line, re.M)

        if matchObj:
            revieweremail = str(matchObj.group(2))

    return revieweremail
    
def requirePythonVersion(_major, _minor, _micro=0):
    try:
        (major, minor, micro, releaselevel, serial) = sys.version_info
        if major >= _major and minor >= _minor and micro >= _micro:
            return
    except:
        pass
    
    raise "This program requires Python %s.%s.%s; detected: %s" % (major, minor, micro, sys.version)

def main(args):
    # Need at least Python 2.3
    requirePythonVersion(2,3)
    
    spammer = P4Spam()
    spammer.main(args)