#!/usr/bin/python3
"""
Run a backup, creating a snapshot in the process.

Command-Line Arguments
======================
* -c, --use-checksums   use rsync's checksum feature to detect file changes
* -d, --dry-run         make this backup a dry run
* --debug               show debug messages
* -n, --name            name to give to the backup snapshot
* -s, --run-post-sync   run sync syscall after backup
* -v, --verbose         show info messages

On each run of this script, a new snapshot is made and any unchanged
files are hardlinked into the new snapshot.
"""
import argparse
import logging
import os
import sys
from subprocess import CalledProcessError

from rbackup.rsync import rsync
from rbackup.struct.repository import Repository

# ========== Constants ==========
LOGFORMAT = "==> %(levelname)s %(message)s"
RSYNC_DEFAULT_OPTS = [
    "--acls",
    "--archive",
    "--backup",
    "--backup-dir=backup",
    "--hard-links",
    "--ignore-missing-args",
    "--prune-empty-dirs",
    "--suffix=.old",
    "--recursive",
    "--xattrs",
]
EXTRA_RSYNC_OPTS = {
    "dry_run": "--dry-run",
    "delete": "--delete-after",
    "checksum": "--checksum",
    "update": "--update",
}

# ----- File Options -----
CONFIG_DIR = "/etc/rbackup"
FILE_OPTS = [
    f"--files-from={CONFIG_DIR}/include-paths.conf",
    f"--exclude-from={CONFIG_DIR}/home-exclude.conf",
]


# ----- Error Codes -----
E_FAILED_PROCESS = 1
E_INVALID_SNAPSHOT_NAME = 2

# ========== Logging Setup ==========
console_formatter = logging.Formatter(LOGFORMAT)
syslog = logging.getLogger("rbackup")
syslog.setLevel(logging.DEBUG)

stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(logging.INFO)
stdout_handler.setFormatter(console_formatter)
stdout_handler.addFilter(lambda record: record.levelno <= logging.INFO)

stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setLevel(logging.WARNING)
stderr_handler.setFormatter(console_formatter)

syslog.addHandler(stdout_handler)
syslog.addHandler(stderr_handler)


# ========== Functions ==========
def parse_cmdline_arguments(*args, **kwargs):
    """Parse command line arguments passed to the script.
        All args and kwargs are passed to ArgumentParser.parse_args()

    :rtype: argparse.Namespace object
    """
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-c",
        "--use-checksums",
        action="store_const",
        dest="extra_rsync_opts",
        const=EXTRA_RSYNC_OPTS["checksum"],
        help="use rsync's checksumming feature to look for changed files",
    )
    parser.add_argument(
        "-d",
        "--dry-run",
        action="append_const",
        dest="extra_rsync_opts",
        const=EXTRA_RSYNC_OPTS["dry_run"],
        help="pass --dry-run to rsync",
    )
    parser.add_argument("--debug", action="store_true", help="log debug messages")
    parser.add_argument(
        "-n", "--name", default=None, help="name to give to the snapshot"
    )
    parser.add_argument(
        "-s",
        "--run-post-sync",
        action="store_true",
        help="run sync operation after backup is complete",
    )
    parser.add_argument(
        "-v", "--verbose", action="store_true", help="log info messages"
    )
    parser.add_argument("repository", help="repository to back up to", metavar="repo")

    return parser.parse_args(*args, **kwargs)


# ========== Main Script ==========
if __name__ == "__main__":
    args = parse_cmdline_arguments()
    repo = Repository(args.repository)

    rsync_opts = RSYNC_DEFAULT_OPTS.copy()

    if args.extra_rsync_opts is not None:
        rsync_opts.extend(args.extra_rsync_opts)

    if args.verbose:
        stdout_handler.setLevel(logging.INFO)

    if args.debug:
        stdout_handler.setLevel(logging.DEBUG)

    # We want to iterate through the repository and create the --link-dest
    # options before creating the new snapshot
    link_dests = tuple(f"--link-dest={s.path}" for s in repo)

    curr_snapshot = None

    try:
        curr_snapshot = repo.create_snapshot(args.name)
        rsync(*rsync_opts, *FILE_OPTS, *link_dests, "/", str(curr_snapshot.path))
    except ValueError as e:
        syslog.critical(e)
        exit(E_INVALID_SNAPSHOT_NAME)
    except CalledProcessError as e:
        syslog.critical("Backup process failed")
        syslog.critical(f"Failing command: {e.cmd}")
        exit(E_FAILED_PROCESS)

    snapshot_symlink = repo.path / "current"

    try:
        snapshot_symlink.unlink()
    except FileNotFoundError:
        pass

    snapshot_symlink.symlink_to(curr_snapshot, target_is_directory=True)

    if args.run_post_sync:
        os.sync()