#!/usr/bin/env python3

"""
installation-birthday
Receive congratulations on system installation anniversary.

Copyright © 2017-2020 Chris Lamb <lamby@debian.org>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

import os
import sys
import logging
import argparse
import datetime
import distro
import socket
import subprocess

DESCRIPTION = "Receive congratulations on system installation anniversaries."

TEMPLATE = r"""
                  0   0
                  |   |
              ____|___|____
           0  |~ ~ ~ ~ ~ ~|   0
           |  |           |   |
        ___|__|___________|___|__
        |/\/\/\/\/\/\/\/\/\/\/\/|
    0   |       H a p p y       |   0
    |   |/\/\/\/\/\/\/\/\/\/\/\/|   |
   _|___|_______________________|___|__
  |/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/|
  |                                   |
  |         B i r t h d a y ! ! !     |
  | ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ |
  |___________________________________|


Congratulations, your {distname} system "{node}" was installed
{age} year(s) ago today!


Best wishes,

Your local system administrator
"""


class Birthday:
    def main(self):
        self.args = self.parse_arguments()
        self.setup_logging()

        now = datetime.datetime.now(datetime.UTC)
        install = self.get_installation_datetime()

        if install is None:
            self.log.debug("Could not determine installation date")
            return 1

        if self.args.puppet:
            print("installation_date={}".format(install.date()))
            return 0  # Don't do anything else

        self.log.debug("Today's date: %s", now.date())
        self.log.info("Installation date: %s", install.date())

        age = now.year - install.year

        if not self.args.force:
            if age == 0:
                self.log.debug("Not reporting for today's year")
                return 0

            if (install.month, install.day) != (now.month, now.day):
                self.log.debug("Dates do not match")
                return 0

        print(
            TEMPLATE.format(
                age=age,
                node=socket.gethostname(),
                distname=distro.id().title(),
            )
        )

        return 0

    def parse_arguments(self):
        parser = argparse.ArgumentParser(description=DESCRIPTION)

        parser.add_argument(
            "--verbosity",
            dest="verbosity",
            type=int,
            default=1,
            choices=(0, 1, 2),
            help="set log level",
        )

        parser.add_argument(
            "-p",
            "--puppet",
            dest="puppet",
            action="store_true",
            default=False,
            help="output result in Puppet 'fact' format",
        )

        parser.add_argument(
            "--force",
            dest="force",
            action="store_true",
            default=False,
            help="celebrate regardless of today's date",
        )

        return parser.parse_args()

    def setup_logging(self):
        self.log = logging.getLogger()

        self.log.setLevel(
            {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}[
                self.args.verbosity
            ]
        )

        handler = logging.StreamHandler(sys.stderr)
        handler.setFormatter(logging.Formatter("%(levelname).1s: %(message)s"))
        self.log.addHandler(handler)

    def check_output(self, *args, **kwargs):
        kwargs.setdefault("stderr", subprocess.DEVNULL)

        output = subprocess.check_output(*args, **kwargs).decode("utf-8")

        return output.splitlines()

    def get_block_device(self, path):
        self.log.debug("Determining block device for %s", path)

        lines = self.check_output(
            (
                "df",
                "--portability",
                "--local",
                "--type=ext2",
                "--type=ext3",
                "--type=ext4",
                path,
            )
        )

        return lines[1].split(" ")[0]

    def get_installation_datetime(self):
        dt = None

        for candidate in self.gen_installation_datetimes():
            # Use the oldest mtime we can find
            if dt is None or candidate < dt:
                self.log.debug("Preferring %s over %s", candidate, dt)
                dt = candidate

        return dt

    def gen_installation_datetimes(self):
        try:
            device = self.get_block_device("/")

            self.log.debug("Determining creation time for %s", device)
            lines = self.check_output(("/sbin/tune2fs", "-l", device))
        except (OSError, subprocess.CalledProcessError):
            pass
        else:
            lookup = {}
            for x in lines:
                if ":" in x:
                    k, v = x.split(":", 1)
                    lookup[k] = v.strip()

            try:
                dt = datetime.datetime.strptime(
                    lookup["Filesystem created"].strip(),
                    "%a %b %d %H:%M:%S %Y",
                )
                self.log.debug("Found mtime of %s -> %s", device, dt)
                yield dt
            except (KeyError, ValueError):
                self.log.debug("Could not get creation time of %s", device)

        for x in (
            "/var/log/installer",
            "/var/log/bootstrap.log",
            "/var/lib/vim",
            "/lost+found",
            "/root",
            "/etc/machine-id",
        ):

            try:
                dt = datetime.datetime.fromtimestamp(os.stat(x).st_mtime, datetime.UTC)
                self.log.debug("Found mtime of %s -> %s", x, dt)
                yield dt
            except Exception:
                self.log.debug("Failed to get mtime of %s", x)


if __name__ == "__main__":
    sys.exit(Birthday().main())
