#!/usr/bin/env python
# coding=utf8

import argparse
from binascii import hexlify
from datetime import datetime
from getpass import getpass
from itertools import chain
import msgpack
import os
import tarfile
import time
import sys

import logbook
import progressbar

from ministryofbackup import Database, backend_url, DATA_PROGRESS_BAR,\
                             create_backend
from ministryofbackup.fds import FileDescriptorRegistry
from ministryofbackup.archive import create_output_chain, DEFAULT_BUFSIZE

log = logbook.Logger('mob')

parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
parser.set_defaults(loglevel=logbook.NOTICE)
parser.add_argument('directory')
parser.add_argument('destination', type=backend_url)
parser.add_argument('--db', default='fingerprints.db')
parser.add_argument('-b', '--bufsize', default=DEFAULT_BUFSIZE, type=int)
parser.add_argument('-d', '--debug', action='count', default=0)
parser.add_argument('-p', '--password', default=None)

logargs = parser.add_mutually_exclusive_group()
logargs.add_argument('-v', '--verbose', const=logbook.INFO,
                     action='store_const', dest='loglevel')
logargs.add_argument('-q', '--quiet', const=logbook.WARNING,
                     action='store_const', dest='loglevel')

args = parser.parse_args()

# set up logging
logbook.NullHandler().push_application()
logbook.StderrHandler(
    level=logbook.DEBUG if args.debug else args.loglevel,
    format_string='{record.channel}[{record.process}]: {record.message}'
).push_application()

fdreg = FileDescriptorRegistry.get_global_instance()

# prompt for password
password = args.password if args.password != None\
                         else getpass('Enter archive password: ')

# set up database
base = os.path.abspath(args.directory)
log.debug("Base directory: %s" % base)

if os.path.exists(args.db):
    log.notice("Loading fingerprint database '%s'" % args.db)
    with open(args.db, 'rb') as f:
        db = Database.load(base, f)
else:
    log.notice("New fingerprint database")
    db = Database(base)

if args.debug>1:
    log.debug("META, CONTENT, RELNAME")
    for rel_name, meta_print in db.meta_prints.iteritems():
        log.debug('%s %s %s' % (hexlify(meta_print),\
              hexlify(db.content_prints[rel_name]),\
              rel_name))

# collect filenames on filesystem
db.load_meta()

log.notice("Collected %d files in %d directories" % (len(db.files),
                                                   len(db.dirs)))

new, updated = db.get_new_and_updated_files()

# altered file checks with progress-bar
altered = []
if updated:  # could also force checking all files here with cmdline arg?
    pbar = progressbar.ProgressBar(widgets=DATA_PROGRESS_BAR,
                                   maxval=db.get_sizes_of(updated))
    pbar.start()
    altered = db.get_altered_files(updated, progress=pbar.update)
    pbar.finish()
deleted = db.get_deleted_files()

log.notice("Found %d new files, %d updated, %d altered and %d deleted files"\
         % (len(new), len(updated), len(altered), len(deleted)))

if args.loglevel >= logbook.INFO:
    for rel_name in new:
        log.info("N %s" % rel_name)
    for rel_name in updated:
        log.info("U %s" % rel_name)
    for rel_name in altered:
        log.info("A %s" % rel_name)
    for rel_name in deleted:
        log.info("D %s" % rel_name)

# metadata
current_time = datetime.utcnow()
backup_id = '%s@%s' % (
    db.series_id,
    current_time.strftime('%Y-%m-%d-%H-%M-%S')
)
log.info('Backup id is %s' % backup_id)
uncompressed_size = db.get_sizes_of()
meta = {
    'timestamp': current_time.timetuple(),
    'backup-id': backup_id,
    'uncompressed_size': uncompressed_size
}

backend = create_backend(args.destination)

# set up compression and encryption
storagefd = backend.open_backup_archive(backup_id, uncompressed_size)
fdreg.add_fd(storagefd)

tarpipe_r, tarpipe_w = fdreg.pipe()
log.debug(str(fdreg))

# keep tarpipe_w, as we're writing to it

ps = create_output_chain(fdreg,
                         tarpipe_r,
                         storagefd,
                         args.password,
                         args.bufsize)

with os.fdopen(tarpipe_w, 'wb') as tar_w,\
tarfile.open(mode='w|', fileobj=tar_w) as archive:
    for rel_name in chain(new, altered):
        fm = db.files[rel_name]
        if args.debug>1:
            log.debug('adding %r to archive' % fm.path)
        tarinfo = archive.gettarinfo(fm.path, rel_name)
        r = fm.open_read()
        archive.addfile(tarinfo, r)

        # tarinfo reads stats bytes, trigger end-of-file detection
        assert '' == r.read()

fdreg.close_all_except()

log.debug('Waiting for processes to finish...')
for p in ps:
    p.join()
log.debug('Compression and encryption finished, waiting for backend')
backend.wait_for_completion()
log.debug('Finshed storing archive')

# we already have new and altered files, need to add metadata of changed files
meta['deleted'] = deleted
meta['updated'] = {}
for fn in updated:
    meta['updated'][fn] = db.files[fn].meta_tuple

metapipe_r, metapipe_w = fdreg.pipe()
metastoragefd = backend.open_backup_meta(backup_id)
fdreg.add_fd(metastoragefd)

ps = create_output_chain(fdreg,
                         metapipe_r,
                         metastoragefd,
                         args.password,
                         args.bufsize)

with os.fdopen(metapipe_w, 'wb') as m:
    log.debug('Writing metadata archive')
    # write header
    m.write('metamob1')
    msgpack.dump(meta, m)
    log.debug(repr(meta))

fdreg.close_all_except()
log.debug('Waiting for processes to finish...')
for p in ps:
    p.join()
log.debug('Compression and encryption finished, waiting for backend')
backend.wait_for_completion()
log.debug('Finshed storing metadata')

# transition over
log.notice("Updating database")
db.update_meta()

log.debug("Writing to database")

with open(args.db, 'wb') as f:
    db.dump(f)
