#!/usr/bin/python3
"""
Run a backup.

Command-Line Arguments
======================
* -c, --use-checksums   use rsync's checksum feature to detect file changes
* -d, --dry-run         make this backup a dry run
* -s, --run-post-sync   run sync syscall after backup
* -v, --verbose         increase script verbosity

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

from rbackup.hierarchy.repository import Repository
from rbackup.rsync import rsync
from subprocess import CalledProcessError

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

CONFIG_DIR = "/etc/rbackup"
ETC_INCLUDE_FILE = f"{CONFIG_DIR}/etc-include.conf"
HOME_EXCLUDE_FILE = f"{CONFIG_DIR}/home-exclude.conf"

# ----- Error Codes -----
E_FAILED_PROCESS = 1

# ========== 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 backup_all(s, prev, rsync_opts):
    """Run all backup functions.

    :param s: snapshot to back up to
    :type s: Snapshot object
    :param prev: previous snapshot for hardlinking
    :type prev: Snapshot object or None
    :param opts: options to pass to rsync
    :type opts: list
    """
    raise NotImplementedError


def backup_etc(s, prev, rsync_opts):
    """Create a backup of /etc.

    :param s: snapshot to back up to
    :type s: Snapshot object
    :param prev: previous snapshot for hardlinking
    :type prev: Snapshot object or None
    :param opts: options to pass to rsync
    :type opts: list
    """
    local_opts = rsync_opts
    local_opts.append(f"--files-from={ETC_INCLUDE_FILE}")

    if prev is not None:
        rsync_opts.append(f"--link-dest={prev_snapshot.etc_dir}")

    syslog.debug("Creating directory {s.etc_dir}")
    s.etc_dir.mkdir(parents=True, exist_ok=False)
    rsync(*local_opts, "/etc/", str(s.etc_dir))


def backup_home(s, prev, rsync_opts):
    """Create a backup of /home.

    :param s: snapshot to back up to
    :type s: Snapshot object
    :param prev: previous snapshot for hardlinking
    :type prev: Snapshot object or None
    :param opts: options to pass to rsync
    :type opts: list
    """
    local_opts = rsync_opts
    local_opts.append(f"--exclude-from={HOME_EXCLUDE_FILE}")

    if prev is not None:
        rsync_opts.append(f"--link-dest={prev_snapshot.home_dir}")

    syslog.debug("Creating directory {s.home_dir}")
    s.home_dir.mkdir(parents=True, exist_ok=False)
    rsync(*local_opts, "/home/", str(s.home_dir))


def backup_pkgmanager(s, prev, rsync_opts):
    """Create a backup of package manager files.

    :param s: snapshot to back up to
    :type s: Snapshot object
    :param prev: previous snapshot for hardlinking
    :type prev: Snapshot object or None
    :param opts: options to pass to rsync
    :type opts: list
    """
    raise NotImplementedError


def backup_root_home(s, prev, rsync_opts):
    """Create a backup of /root.

    :param s: snapshot to back up to
    :type s: Snapshot object
    :param prev: previous snapshot for hardlinking
    :type prev: Snapshot object or None
    :param opts: options to pass to rsync
    :type opts: list
    """
    local_opts = rsync_opts
    local_opts.append(f"--exclude-from={HOME_EXCLUDE_FILE}")

    if prev is not None:
        rsync_opts.append(f"--link-dest={prev_snapshot.root_home_dir}")

    syslog.debug("Creating directory {s.root_home_dir}")
    s.root_home_dir.mkdir(parents=True, exist_ok=False)
    rsync(*local_opts, "/root/", str(s.root_home_dir))


def backup_system():
    """Create a backup of system directories.

    :param s: snapshot to back up to
    :type s: Snapshot object
    :param prev: previous snapshot for hardlinking
    :type prev: Snapshot object or None
    :param opts: options to pass to rsync
    :type opts: list
    """
    raise NotImplementedError


DISPATCHER = {
    "all": backup_all,
    "etc": backup_etc,
    "home": backup_home,
    "system": backup_system,
}


# ========== Main Script ==========
if __name__ == "__main__":
    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(
        "-s",
        "--run-post-sync",
        action="store_true",
        help="run sync operation after backup is complete",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="increase script verbosity",
    )
    parser.add_argument(
        "operation",
        choices=["all", "etc", "home", "pkg", "system"],
        help="the operation to perform",
        metavar="op",
    )
    parser.add_argument(
        "repository", help="repository to back up to", metavar="repo"
    )

    args = parser.parse_args()

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

    args = parser.parse_args()
    repo = Repository(args.repository)

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

    if repo.empty:
        prev_snapshot = None
    else:
        prev_snapshot = repo.curr_snapshot

    repo.create_snapshot()
    curr_snapshot = repo.curr_snapshot

    rsync_opts.append("--backup-dir=backup")

    try:
        DISPATCHER[args.operation](curr_snapshot, prev_snapshot, rsync_opts)
    except CalledProcessError as e:
        syslog.error("Backup process failed")
        syslog.info(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.path, target_is_directory=True)

    if args.run_post_sync:
        os.sync()