#!/usr/bin/pythonw

from appscript import app, k, CommandError
from aemreceive.sfba import *
from Foundation import NSLog, NSUserDefaults, NSDate
from subprocess import call
from traceback import print_exc
from cStringIO import StringIO
from datetime import datetime
import RSS
import email, smtplib
from email.MIMEMessage import MIMEMessage
from email.MIMEText import MIMEText
from email.Utils import formataddr, formatdate
import os, re, sys
import objc # XXX should switch to public DADiskMount/Eject functions
objc.loadBundle('DiskManagement', globals(),
                '/System/Library/PrivateFrameworks/DiskManagement.framework')

EVENTS_DIR = os.path.expanduser('~/Sites/RetroStatus')
EVENTS_MAX = 100

FROM_NAME, FROM_ADDR = 'Retrospect backup server', 'retrospect' + '@rileys.us'
TO_ADDR = ['%s@rileys.us' % user for user in ['julia', 'gary']]
SMTP_SERVER = 'calamity.bos.sabi.net'

DEBUG = True

# XXX implement Web page with current status

### notifications

GROWL_APP_NAME = 'RetroStatus'
NOTIFICATION_BACKUP = 'Backup'
NOTIFICATION_MEDIA = 'Media change'
NOTIFICATIONS_ALL = [NOTIFICATION_BACKUP, NOTIFICATION_MEDIA]
EVENTS_FILE = os.path.join(EVENTS_DIR, 'index.rdf')
EVENTS_FILE_TEMP = EVENTS_FILE + '~'

growl = app('GrowlHelperApp')
events = RSS.TrackingChannel()

try: os.mkdir(EVENTS_DIR)
except: pass
events.setMD((RSS.ns.rss10, 'channel'),
             {(RSS.ns.rss10, 'title'): 'Retrospect status',
              (RSS.ns.rss10, 'link'): os.path.basename(EVENTS_FILE)})

def growlNotify(name, title, description, **kw):
    # XXX restore coalescing support from LocationDo
    params = dict(with_name=name,
                  title=title,
                  description=unicode(description),
                  application_name=GROWL_APP_NAME,
                  icon_of_application='Retrospect')
    params.update(kw)
    if DEBUG:
        NSLog('%s: %s' % (title, description))
    try:
        growl.notify(**params)
    except CommandError:
        pass

def addEvent(title, description, preformatted=False):
    description = description.replace('&', '&amp;').replace('<', '&lt;')
    if preformatted:
        description = '<pre>%s</pre>' % description
    else:
        description = description.replace('\n\n', '<p>').replace('\n', '<br>')
    # XXX provide valid URL for event details
    events.addItem({(RSS.ns.dc, 'date'): datetime.today().isoformat(),
                    (RSS.ns.rss10, 'title'): title,
    #               (RSS.ns.rss10, 'link'): datetime.today().isoformat(),
                    (RSS.ns.rss10, 'description'): description})
    events.truncateToLength(EVENTS_MAX)
    file(EVENTS_FILE_TEMP, 'w').write(str(events))
    os.rename(EVENTS_FILE_TEMP, EVENTS_FILE)

def sendMail(subject, text):
    message = MIMEText(text.encode('utf8'), _charset='utf8')
    message.set_unixfrom(FROM_ADDR)
    message['From'] = formataddr((FROM_NAME, FROM_ADDR))
    message['To'] = ', '.join(TO_ADDR)
    message['Date'] = formatdate(localtime=True)
    message['Subject'] = subject
    smtp = smtplib.SMTP(SMTP_SERVER)
    smtp.sendmail(message['From'], TO_ADDR, message.as_string())

### disk management

DEFAULT_LAST_USED_NAME = 'Last used backup volume name'
DEFAULT_LAST_USED_DATE = 'Date of last backup volume use'

defaults = NSUserDefaults.standardUserDefaults()
diskManager = DMManager.sharedManager()
diskManager.generateConnection()

def diskWithName(name):
    disks = diskManager.disks()
    for disk in disks:
        if disk.volumeName() == name:
            return disk

def lastUsedDisk():
    diskName = defaults.get(DEFAULT_LAST_USED_NAME)
    if diskName:
        return diskWithName(diskName)

def diskIsMounted(disk):
    mountPoint = disk.mountPoint()
    return (mountPoint and os.path.exists(mountPoint))

def ejectDisk(disk):
    if not diskIsMounted(disk):
        return True
    if not diskManager.ejectDisk_synchronous_(disk, True):
        return False
    if diskIsMounted(disk):
        growlNotify(NOTIFICATION_MEDIA, 'Eject failed',
                    'Using diskutil to eject %s' % disk)
        return call('diskutil', 'eject', disk.diskIdentifier()) is 0
    return True

def mountDisk(disk):
    if diskIsMounted(disk):
        return True
    if not diskManager.mountDisk_includeChildren_synchronous_(disk, False, True):
        return False
    if not diskIsMounted(disk):
        growlNotify(NOTIFICATION_MEDIA, 'Mount failed',
                    'Using diskutil to mount %s' % disk)
        return call('diskutil', 'mount', disk.diskIdentifier()) is 0
    return True

### Retrospect event handlers

def retrospectstart(autolaunchboolean):
    try:
        app(id='com.apple.Backup').quit(timeout=5)
    except CommandError:
        addEvent('Apple Backup failed to quit', 'Please check on it.')

def scriptstart(scriptname, startdate): pass
# yes, this really should be "remotetname"
def volumestart(volumename, clientname='', remotetname=''): pass
def volumeend(volumename, kbcopied, filecount, durationinseconds, backupdate,
              enddate, startdate, destinationname, clientname, scriptname,
              backuptypestring, fileerrorcount, volumeerrorcode,
              volumeerrormessage, zonename='', remotename='',
              subvolumediskname=''):
    growlNotify(NOTIFICATION_BACKUP, scriptname,
                '%s\n%s' % (volumename, volumeerrormessage))
    addEvent('%s: %s' % (volumename, volumeerrormessage),
             '%d file(s) copied; %d had errors.' % (filecount, fileerrorcount))

def mediarequest(mediatypestring, requestedmembername, mediaisknownboolean):
    disk = diskWithName(requestedmembername)
    if disk and mountDisk(disk):
            defaults[DEFAULT_LAST_USED_NAME] = requestedmembername
            defaults[DEFAULT_LAST_USED_DATE] = NSDate.date()
            growlNotify(NOTIFICATION_MEDIA, requestedmembername,
                        'Disk is in use: please DO NOT swap')
    else:
        sendMail("IMPORTANT: Can't find disk %s" % requestedmembername,
                 "Retrospect can't find the disk '%s'.  Please eject the old disk, if any, and check that the correct disk is inserted." % requestedmembername)

def mediarequesttimedout(numberofsecondswaited): pass

def scriptend(scriptname, scripterrormessage, errorcount):
    disk = lastUsedDisk()
    if disk and ejectDisk(disk):
        growlNotify(NOTIFICATION_MEDIA, disk.volumeName(),
                    'Disk no longer in use: OK to swap')
    addEvent(scriptname + ': script completed',
             '%s; %s error(s)' % (scripterrormessage, errorcount))

def scriptcheckfailed(scriptname, failuremessage, scheduledate):
    failuremessage = failuremessage.replace('\r', '\n').decode('macroman')
    m = re.search(u'\u201c([^\u201d]+)\u201d', failuremessage)
    if not m:
	addEvent(scriptname + ': check failed', failuremessage)
	return
    if not diskWithName(m.group(1)):
        sendMail("Please swap backup media before %s" % scheduledate.ctime(),
                 failuremessage + "\n\nEject the old disk, if any, and check that the correct disk is inserted.")

def retrospectquit():
    # can use launch() in newer appscript versions without CommandError
    app(id='com.apple.Backup').activate()
    stopeventloop()

def handleEvent(eventName, params):
    eventHandler = globals().get(eventName)
    params = dict(zip(params[::2], params[1::2]))
    for k in params.keys():
        if params[k] == '': del params[k]
    eventStr = '%s(%s)' % (eventName, ', '.join(['%s=%r' % (k, v) for k, v
                                                 in params.iteritems()]))
    if not eventHandler:
        addEvent('%s: unhandled event' % eventName, eventStr)
        if DEBUG: NSLog('unhandled event: ' + eventStr)
        return
    try:
        eventHandler(**params)
    except:
        s = StringIO()
        print_exc(file=s)
        addEvent('%s: event handler failed' % eventName,
                 '%s\n\n%s' % (eventStr, s.getvalue()), True)
        if DEBUG:
            NSLog('event handler failed for %s:\n%s' % (eventStr, s.getvalue()))

installeventhandler(handleEvent, 'ascrpsbr',
                    ('snam', 'eventName', kAE.typeUnicodeText),
                    ('usrf', 'params', ArgListOf(kAE.typeWildCard)))

if __name__ == '__main__':
    growl.register(
        as_application=GROWL_APP_NAME,
        all_notifications=NOTIFICATIONS_ALL,
        default_notifications=NOTIFICATIONS_ALL)

    starteventloop()

# example events:

# retrospectstart(autolaunchboolean=False)
# scriptstart(scriptname='Boston Backup', startdate=datetime.datetime(2005, 11, 23, 2, 31, 51))
# volumestart(volumename='Backup Clients:Babs')

# volumeend(startdate=datetime.datetime(2005, 11, 23, 2, 31, 51), kbcopied=0, enddate=datetime.datetime(2005, 11, 23, 2, 32, 5), backuptypestring='Normal', volumename='Backup Clients:Babs', clientname='Babs', destinationname='Boston backup A', scriptname='Boston Backup', volumeerrormessage='client is not visible on network', backupdate=datetime.datetime(2005, 11, 23, 2, 32, 5), durationinseconds=0, volumeerrorcode=-1028, fileerrorcount=0, remotename='Babs', filecount=0, zonename='10.0.0.9')
# volumestart(volumename=' Local Desktop:Bookworm')
# volumestart(volumename='Backup Clients:Concord:Mac OS X', remotetname='Backup Clients:Concord', clientname='Backup Clients:Concord')
# mediarequest: appears even if media is available
# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
# volumeend(startdate=datetime.datetime(2005, 11, 23, 2, 31, 51), kbcopied=2291359, enddate=datetime.datetime(2005, 11, 23, 2, 56, 23), backuptypestring='Normal', volumename=' Local Desktop:Bookworm', clientname='Local Desktop', destinationname='Boston backup A', scriptname='Boston Backup', volumeerrormessage='successful', backupdate=datetime.datetime(2005, 11, 23, 2, 31, 51), durationinseconds=1066, volumeerrorcode=0, fileerrorcount=15, remotename='Local Desktop', filecount=72717)
# scriptend(scriptname='Boston Backup', scripterrormessage='client is not visible on network', errorcount=18)
# scriptcheckfailed(scriptname='Boston Backup', failuremessage='Appending to disk \xd21-Boston backup B\xd3\r\t149.0 G available (80%) of 186.2 G total capacity', scheduledate=datetime.datetime(2005, 11, 24, 1, 0))
# retrospectquit()
