#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""tuptime - Report the historical and statistical real time of the system,
keeping it between restarts"""
# Copyright (C) 2011-2021 - Ricardo F.

# 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 2 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 sys, os, argparse, locale, platform, signal, logging, sqlite3, time
from datetime import datetime
# On os_bsd(): import subprocess


DB_FILE = '/var/lib/tuptime/tuptime.db'
DATETM_FORMAT = '%X %x'
__version__ = '5.0.2'

# Terminate when SIGPIPE signal is received
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

# Set locale to the user’s default settings (LANG env. var)
try:
    locale.setlocale(locale.LC_ALL, '')
except Exception:
    pass  # Fast than locale.setlocale(locale.LC_ALL, 'C')


def get_arguments():
    """Get arguments from command line"""

    parser = argparse.ArgumentParser()
    group = parser.add_mutually_exclusive_group()
    parser.add_argument(
        '-A', '--at',
        dest='at',
        default=None,
        action='store',
        metavar='STARTUP',
        type=int,
        help='restrict to this startup number'
    )
    parser.add_argument(
        '-b', '--bootid',
        dest='bootid',
        action='store_true',
        default=False,
        help='show boot identifier'
    )
    parser.add_argument(
        '-c', '--csv',
        dest='csv',
        action='store_true',
        default=False,
        help='csv output'
    )
    parser.add_argument(
        '-d', '--date',
        dest='dtm_format',
        metavar='DATETM_FORMAT',
        default=DATETM_FORMAT,
        action='store',
        help='datetime/timestamp format output'
    )
    parser.add_argument(
        '-e', '--dec',
        dest='dec',
        default=2,
        metavar='DECIMALS',
        action='store',
        type=int,
        help='number of decimals in percentages'
    )
    parser.add_argument(
        '--decp',
        dest='decp',
        default=None,
        action='store',
        type=int,
        help=argparse.SUPPRESS
    )
    parser.add_argument(
        '-f', '--filedb',
        dest='db_file',
        default=DB_FILE,
        action='store',
        help='database file (' + DB_FILE + ')',
        metavar='FILE'
    )
    parser.add_argument(
        '-g', '--graceful',
        dest='endst',
        action='store_const',
        default=int(0),
        const=int(1),
        help='register a graceful shutdown'
    )
    parser.add_argument(
        '-i', '--invert',
        dest='invert',
        action='store_true',
        default=False,
        help='startup number in reverse count'
    )
    parser.add_argument(
        '-k', '--kernel',
        dest='kernel',
        action='store_true',
        default=False,
        help='show kernel version'
    )
    group.add_argument(
        '-l', '--list',
        dest='list',
        default=False,
        action='store_true',
        help='enumerate system life as list'
    )
    parser.add_argument(
        '-n', '--noup',
        dest='update',
        default=True,
        action='store_false',
        help='avoid update values into DB'
    )
    parser.add_argument(
        '-o', '--order',
        dest='order',
        metavar='TYPE',
        default=False,
        action='store',
        type=str,
        choices=['u', 'r', 's', 'e', 'd', 'k'],
        help='order enumerate by [u|r|s|e|d|k]'
    )
    parser.add_argument(
        '-p', '--power',
        dest='power',
        default=False,
        action='store_true',
        help='show power states run + sleep'
    )
    parser.add_argument(
        '-r', '--reverse',
        dest='reverse',
        default=False,
        action='store_true',
        help='reverse order in list or table output'
    )
    parser.add_argument(
        '-s', '--seconds',
        dest='seconds',
        default=False,
        action='store_true',
        help='output time in seconds and epoch'
    )
    parser.add_argument(
        '-S', '--since',
        dest='since',
        default=None,
        action='store',
        metavar='STARTUP',
        type=int,
        help='restrict from this startup number'
    )
    group.add_argument(
        '-t', '--table',
        dest='table',
        default=False,
        action='store_true',
        help='enumerate system life as table'
    )
    group.add_argument(
        '--tat',
        dest='tat',
        metavar='TIMESTAMP',
        default=None,
        action='store',
        type=int,
        help='system status at epoch timestamp'
    )
    parser.add_argument(
        '--tsince',
        dest='ts',
        metavar='TIMESTAMP',
        default=None,
        action='store',
        type=int,
        help='restrict from this epoch timestamp'
    )
    parser.add_argument(
        '--tuntil',
        dest='tu',
        metavar='TIMESTAMP',
        default=None,
        action='store',
        type=int,
        help='restrict until this epoch timestamp'
    )
    parser.add_argument(
        '-U', '--until',
        dest='until',
        default=None,
        action='store',
        metavar='STARTUP',
        type=int,
        help='restrict up until this startup number'
    )
    parser.add_argument(
        '-v', '--verbose',
        dest='verbose',
        default=False,
        action='store_true',
        help='verbose output'
    )
    parser.add_argument(
        '-V', '--version',
        action='version',
        version='tuptime version ' + (__version__),
        help='show version'
    )
    parser.add_argument(
        '-x', '--silent',
        dest='silent',
        default=False,
        action='store_true',
        help='update values into DB without output'
    )
    arg = parser.parse_args()

    # Check enable verbose
    if arg.verbose:
        logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
        logging.info('Version = %s', (__version__))

    if (arg.power and (arg.ts or arg.tu or arg.tat)) or (arg.tat and (arg.ts or arg.tu)):
        # - power states report accumulated time across an uptime range, it isn't possible to
        # know if the state was running or sleeping between specific points inside it.
        # - tat work within startups numbers, not in narrow ranges.
        parser.error('Used operators can\'t be combined together')

    # Wrap 'at' over since and until
    if arg.at is not None:
        arg.since = arg.until = arg.at

    if arg.decp:
        arg.dec = arg.decp
        logging.warning('Argument \'--decp\' is deprecated in favour of \'-e\' or \'--dec\'.' )

    logging.info('Arguments = %s', str(vars(arg)))
    return arg


def get_os_values():
    """Get values from each type of operating system"""

    sis = {'bootid': None, 'btime': None, 'uptime': None, 'rntime': None, 'slptime': None, 'offbtime': None, 'downtime': None, 'kernel': None}

    def os_bsd(sis):
        """Get values from BSD"""

        logging.info('System = BSD')
        import subprocess

        try:
            sis['btime'] = time.clock_gettime(time.CLOCK_REALTIME) - time.clock_gettime(time.CLOCK_MONOTONIC)
        except Exception as exp:
            logging.info('Old btime assignment. %s', str(exp))
            sysctl_out = subprocess.run(['sysctl', '-n', 'kern.boottime'], stdout=subprocess.PIPE, text=True, check=True).stdout
            # Some BSDs report the value assigned to 'sec', others do it directly
            if 'sec' in sysctl_out:  # FreeBSD, Darwin
                sis['btime'] = sysctl_out.split(' sec = ')[1].split(',')[0]
            else:  # OpenBSD, NetBSD
                sis['btime'] = sysctl_out

        try:
            # Time since some unspecified starting point. Contains sleep time on BSDs
            sis['uptime'] = time.clock_gettime(time.CLOCK_MONOTONIC)
            if sys.platform.startswith(('darwin')):
                # OSX > 10.12 have only UPTIME_RAW. Avoid compare it with non _RAW
                # counters. Their reference here is CLOCK_REALTIME, so remove the raw drift:
                uptime_raw = time.clock_gettime(time.CLOCK_MONOTONIC_RAW)
                raw_diff = sis['uptime'] - uptime_raw
                # Time the system have been running. Not contains sleep time on OSX
                rntime_raw = time.clock_gettime(time.CLOCK_UPTIME_RAW)
                sis['rntime'] = rntime_raw + raw_diff
            else:
                # Time the system have been running. Not contains sleep time on BSDs
                sis['rntime'] = time.clock_gettime(time.CLOCK_UPTIME)
        except Exception as exp:
            logging.info('Old uptime/rntime assignment. %s', str(exp))
            logging.info('Power states disabled, values assigned from uptime')
            sis['uptime'] = time.time() - sis['btime']
            sis['rntime'] = sis['uptime']

        try:
            sysctl_out = subprocess.run(['sysctl', '-xn', 'kern.boot_id'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, check=False).stdout
            if 'Dump' in sysctl_out:
                sis['bootid'] = sysctl_out.split('Dump:')[-1].rstrip()
            else:
                raise ValueError
        except Exception as exp:
            logging.info('BSD boot_id not assigned')

        return sis

    def os_linux(sis):
        """Get values from Linux"""

        logging.info('System = Linux')

        try:
            sis['btime'] = time.clock_gettime(time.CLOCK_REALTIME) - time.clock_gettime(time.CLOCK_BOOTTIME)
        except Exception as exp:
            logging.info('Old btime assignment. %s', str(exp))
            with open('/proc/stat') as fl2:
                for line in fl2:
                    if line.startswith('btime'):
                        sis['btime'] = line.split()[1]

        try:  # uptime and rntime must be toghether to avoid time mismatch between them
            # Time since some unspecified starting point. Contains sleep time on linux
            sis['uptime'] = time.clock_gettime(time.CLOCK_BOOTTIME)
            # Time since some unspecified starting point. Not contains sleep time on linux
            sis['rntime'] = time.clock_gettime(time.CLOCK_MONOTONIC)
        except Exception as exp:
            logging.info('Old uptime/rntime assignment. %s', str(exp))
            logging.info('Power states disabled, values assigned from uptime')
            with open('/proc/uptime') as fl1:
                sis['uptime'] = fl1.readline().split()[0]
            sis['rntime'] = sis['uptime']

        try:
            with open('/proc/sys/kernel/random/boot_id') as fl3:
                sis['bootid'] = fl3.readline().split()[0]
        except Exception as exp:
            logging.info('Linux boot_id not assigned')

        return sis

    # Linux
    if sys.platform.startswith('linux'):
        sis = os_linux(sis)
    # BSD and related
    elif sys.platform.startswith(('freebsd', 'darwin', 'dragonfly', 'openbsd', 'netbsd', 'sunos')):
        sis = os_bsd(sis)
    # elif:
    #     other_os()
    else:
        logging.error('System = %s not supported', sys.platform)
        sys.exit(-1)

    # Check right allocation of core variables before continue
    for key in sis:
        if key in ('btime', 'uptime', 'rntime'):
            if sis[key] is None:
                logging.error('"%s" value unallocate from system. Can\'t continue.', str(key))
                sys.exit(-1)
        if key in ('uptime', 'rntime') and float(sis[key]) < 0:
            logging.warning('Reset invalid "%s" value "%s"', str(key), str(sis[key]))
            if key == 'uptime': sis['uptime'] = 1
            if key == 'rntime': sis['rntime'] = 1

    # Set number OS values as integer
    sis['btime'] = int(round(float(sis['btime']), 0))
    sis['uptime'] = int(round(float(sis['uptime']), 0))
    sis['rntime'] = int(round(float(sis['rntime']), 0))

    # Avoid missmatch whith elapsed time between getting counters and/or rounded values,
    # whit less than 1 seconds, values are equal
    if (sis['uptime'] - 1) <= sis['rntime'] <= (sis['uptime'] + 1):
        sis['rntime'] = sis['uptime']

    # Get sleep time from runtime
    sis['slptime'] = sis['uptime'] - sis['rntime']

    # Set text OS values
    sis['bootid'] = str(sis['bootid'])
    sis['kernel'] = str(platform.platform())

    logging.info('Python = %s', str(platform.python_version()))
    try:
        logging.info('Current locale = %s', str(locale.getlocale()))
    except Exception:
        logging.info('Current locale = None')
    logging.info('Boot ID = %s', str(sis['bootid']))
    logging.info('Uptime = %s', str(sis['uptime']))
    logging.info('Rntime = %s', str(sis['rntime']))
    logging.info('Slptime = %s', str(sis['slptime']))
    logging.info('Btime = %s', str(sis['btime']))
    logging.info('Kernel = %s', str(sis['kernel']))
    logging.info('Execution user = %s', str(os.getuid()))

    # Avoid executing when OS clock is too out of phase
    if sis['btime'] < 946684800:   # 01/01/2000 00:00
        logging.error('Epoch boot time value is too old \'%s\'. Check system clock sync.', str(sis['btime']))
        logging.error('Tuptime execution can\'t continue.')
        sys.exit(-1)

    return sis


def gain_db(sis, arg):
    """Assure DB state and get DB connection"""

    # If db_file keeps default value, check for DB environment variable
    if arg.db_file == DB_FILE:
        if os.environ.get('TUPTIME_DBF'):
            arg.db_file = os.environ.get('TUPTIME_DBF')
            logging.info('DB environ var = %s', str(arg.db_file))

    # Test path
    arg.db_file = os.path.abspath(arg.db_file)  # Get absolute or relative path
    try:
        if os.makedirs(os.path.dirname(arg.db_file), exist_ok=True):
            logging.info('Making path = %s', str(os.path.dirname(arg.db_file)))
    except Exception as exp_path:
        logging.error('Checking DB path "%s": %s', str(os.path.dirname(arg.db_file)), str(exp_path))
        sys.exit(-1)

    # Test and create DB with the initial values
    try:
        if os.path.isfile(arg.db_file):
            logging.info('DB file exists = %s', str(arg.db_file))
        else:
            logging.info('Making DB file = %s', str(arg.db_file))
            db_conn = sqlite3.connect(arg.db_file)
            conn = db_conn.cursor()
            conn.execute('create table if not exists tuptime'
                         '(bootid text, btime integer, uptime integer, rntime integer, slptime integer,'
                         'offbtime integer, endst integer, downtime integer, kernel text)')
            conn.execute('insert into tuptime values (?,?,?,?,?,?,?,?,?)',
                         (str(sis['bootid']), str(sis['btime']), str(sis['uptime']), str(sis['rntime']),
                          str(sis['slptime']), None, str(arg.endst), None, str(sis['kernel'])))
            db_conn.commit()
            db_conn.close()
    except Exception as exp_file:
        logging.error('Checking DB file "%s": %s', str(arg.db_file), str(exp_file))
        sys.exit(-1)

    # Get DB connection
    db_conn = sqlite3.connect(arg.db_file)
    db_conn.row_factory = sqlite3.Row
    conn = db_conn.cursor()

    # Check if DB have the old format
    columns = [i[1] for i in conn.execute('PRAGMA table_info(tuptime)')]
    if 'rntime' and 'slptime' and 'bootid' not in columns:
        logging.warning('DB format outdated')
        upgrade_db(db_conn, conn, columns, arg)

    return db_conn, conn


def upgrade_db(db_conn, conn, columns, arg):
    """Upgrade DB to current format"""

    if not os.access(arg.db_file, os.W_OK):
        logging.error('"%s" file not writable by execution user.', str(arg.db_file))
        sys.exit(-1)
    logging.warning('Upgrading DB file = %s', str(arg.db_file))

    try:

        if 'rntime' and 'slptime' not in columns:  # new in tuptime v4
            logging.warning('Upgrading DB with power states')
            conn.execute('create table if not exists tuptimeNew'
                         '(btime integer, uptime integer, rntime integer, slptime integer,'
                         'offbtime integer, endst integer, downtime integer, kernel text)')
            conn.execute('update tuptime set uptime = cast(round(uptime) as int)')
            conn.execute('update tuptime set offbtime = cast(round(offbtime) as int)')
            conn.execute('update tuptime set downtime = cast(round(downtime) as int)')
            conn.execute('insert into tuptimeNew '
                         '(btime, uptime, offbtime, endst, downtime, kernel) '
                         'SELECT btime, uptime, offbtime, endst, downtime, kernel '
                         'FROM tuptime')
            conn.execute('update tuptimeNew set rntime = uptime')
            conn.execute('update tuptimeNew set slptime = 0')
            conn.execute('drop table tuptime')
            conn.execute('alter table tuptimeNew RENAME TO tuptime')
            db_conn.commit()

        if 'bootid' not in columns:  # new in tuptime v5
            logging.warning('Upgrading DB with boot ID')
            conn.execute('create table if not exists tuptimeNew'
                         '(bootid text, btime integer, uptime integer, rntime integer, slptime integer,'
                         'offbtime integer, endst integer, downtime integer, kernel text)')
            conn.execute('insert into tuptimeNew '
                         '(btime, uptime, rntime, slptime, offbtime, endst, downtime, kernel) '
                         'SELECT btime, uptime, rntime, slptime, offbtime, endst, downtime, kernel '
                         'FROM tuptime')
            conn.execute('update tuptimeNew set bootid = "None"')
            conn.execute('update tuptimeNew set kernel = "None" where kernel = ""')
            conn.execute('drop table tuptime')
            conn.execute('alter table tuptimeNew RENAME TO tuptime')
            db_conn.commit()

    except Exception as exp_db:
        logging.error('Upgrading DB format failed. "%s"', str(exp_db))
        sys.exit(-1)

    logging.warning('Upgraded')


def control_drift(prev, sis):
    """Check time drift due inconsistencies with system clock"""

    offset = sis['btime'] - prev['btime']  # Calculate time offset
    logging.info('Drift over btime = %s', str(offset))

    if offset:
        logging.info('Fixing drift...')

        # Apply offset to btime, uptime and related
        if (sis['uptime'] + offset) > 0:
            logging.info('System timestamp = %s', str(sis['btime'] + sis['uptime']))

            sis['uptime'] = sis['uptime'] + offset
            logging.info('Fixed uptime = %s', str(sis['uptime']))

            # Apply offset to rntime if it have room for it, else, to slptime too
            sis['rntime'] = sis['rntime'] + offset
            if sis['rntime'] < 1:
                sis['slptime'] = sis['slptime'] + sis['rntime'] - 1
                if sis['slptime'] < 0:
                    logging.info('Drift decrease slptime under 0. Impossible')
                    sis['slptime'] = 0
                logging.info('Drift decrease rntime under 1. Impossible')
                sis['rntime'] = 1  # Always keep almost 1 second
            logging.info('Fixed rntime = %s', str(sis['rntime']))
            logging.info('Fixed slptime = %s', str(sis['slptime']))

            sis['btime'] = sis['btime'] - offset
            logging.info('Fixed btime = %s', str(sis['btime']))
            logging.info('Fixed timestamp = %s', str(sis['btime'] + sis['uptime']))
            # Fixed timestamp must be equal to system timestamp after drift values
            # Fixed btime must be equal to last btime from DB

        else:
            # Keep btime from DB with current uptime until it can be fixed
            sis['btime'] = prev['btime']
            logging.info('Drift decreases uptime under 1. Skipping fix')
            logging.info('Unfixed btime = %s', str(sis['btime']))

    return sis


def time_conv(secs):
    """Convert seconds to human readable syle"""

    dtm = {'yr': 0, 'd': 0, 'h': 0, 'm': 0, 's': 0}
    line = ''

    # Get human values from seconds
    dtm['m'], dtm['s'] = divmod(secs, 60)
    dtm['h'], dtm['m'] = divmod(dtm['m'], 60)
    dtm['d'], dtm['h'] = divmod(dtm['h'], 24)
    dtm['yr'], dtm['d'] = divmod(dtm['d'], 365)

    # Build datetime sentence with this order
    for key in ('yr', 'd', 'h', 'm', 's'):

        # Avoid print empty values at the beginning, except seconds
        if (dtm[key] == 0) and (line == '') and (key != 's'):
            continue
        else:
            line += str(dtm[key]) + key + ' '

    # Return without last space char
    return str(line[:-1])


def since_opt(db_rows, last_st, arg):
    """Get rows since a given row startup number"""

    if arg.since <= 0:  # Negative value start from bottom
        arg.since = last_st + arg.since

    # Remove rows if the startup is lower
    for row in db_rows[:]:
        if arg.since > row['startup']:
            db_rows.remove(row)

    return db_rows, arg


def until_opt(db_rows, last_st, arg):
    """Get rows until a given row startup number"""

    if arg.until <= 0:  # Negative value start from bottom
        arg.until = last_st + arg.until

    # Remove row if the startup is greater
    for row in db_rows[:]:
        if arg.until < row['startup']:
            db_rows.remove(row)

    return db_rows, arg


def tuntil_opt(db_rows, sis, arg):
    """Split and report rows until a given timestamp

    Conventions:
        - Keep startup number, boot ID and kernel
        - Empty values are False
    """

    # Negative value decrease actual timestamp
    if arg.tu < 0:
        arg.tu = sis['btime'] + sis['uptime'] + arg.tu

    # Parse rows trying to look for the rightmost (older) value
    remap = []
    for row in db_rows:

        if arg.tu > row['offbtime'] and arg.tu <= (row['offbtime'] + row['downtime']):
            row['downtime'] = arg.tu - row['offbtime']
            remap.append(row)
            break

        elif arg.tu > row['btime'] and arg.tu <= (row['btime'] + row['uptime']):
            row['uptime'] = arg.tu - row['btime']
            row['rntime'] = row['slptime'] = False
            row['offbtime'] = False
            row['endst'] = False
            row['downtime'] = False
            remap.append(row)
            break

        elif arg.tu <= row['btime']:
            break

        else:
            remap.append(row)

    return remap, arg


def tsince_opt(db_rows, sis, arg):
    """Split and report rows since a given timestamp

    Conventions:
        - Keep startup number, boot ID and kernel
        - Empty values are False
    """

    # Negative value decrease actual timestamp
    if arg.ts < 0:
        arg.ts = sis['btime'] + sis['uptime'] + arg.ts

    # Parse rows trying to look for the leftmost (newer) value
    remap = []
    for row in db_rows:

        if arg.ts <= row['btime']:
            remap.append(row)

        elif arg.ts > row['btime'] and arg.ts < (row['btime'] + row['uptime']):
            row['uptime'] = row['btime'] + row['uptime'] - arg.ts
            row['btime'] = False
            row['rntime'] = row['slptime'] = False
            remap.append(row)

        elif arg.ts == row['offbtime']:
            row['btime'] = False
            row['uptime'] = False
            row['rntime'] = row['slptime'] = False
            remap.append(row)

        elif arg.ts > row['offbtime'] and arg.ts < (row['offbtime'] + row['downtime']):
            row['downtime'] = row['offbtime'] + row['downtime'] - arg.ts
            row['btime'] = False
            row['uptime'] = False
            row['rntime'] = row['slptime'] = False
            row['offbtime'] = False
            row['endst'] = False
            remap.append(row)

        else:
            continue

    return remap, arg


def ordering_output(db_rows, arg, last_st):
    """Order and/or revert and/or invert output"""

    if db_rows:

        if arg.order:
            match_value = {'u': 'uptime', 'r': 'rntime', 's': 'slptime', 'e': 'endst', 'd': 'downtime', 'k': 'kernel'}
            db_rows = sorted(db_rows, key=lambda x: (x[match_value[arg.order]]))

        if arg.reverse:
            db_rows = list(reversed(db_rows))

        if arg.invert:
            for ind, _ in enumerate(db_rows):
                db_rows[ind]['startup'] = db_rows[ind]['startup'] - last_st

    else:
        # Return default empty row
        db_rows = [{'startup': 0, 'bootid': False, 'uptime': False, 'rntime': False, 'slptime': False,
                    'endst': False, 'offbtime': False, 'btime': False, 'downtime': False, 'kernel': False}]

    return db_rows


def format_output(db_rows, arg):
    """Set the right output format"""

    remap = []
    for row in db_rows:

        if row['bootid'] is False:
            row['bootid'] = ''

        if row['btime'] is False:
            row['btime'] = ''
        else:
            if not arg.seconds:
                row['btime'] = datetime.fromtimestamp(row['btime']).strftime(arg.dtm_format)

        if row['uptime'] is False:
            row['uptime'] = row['rntime'] = row['slptime'] = ''
        else:
            if not arg.seconds:
                row['uptime'] = time_conv(row['uptime'])
                row['rntime'] = time_conv(row['rntime'])
                row['slptime'] = time_conv(row['slptime'])

        if row['endst'] is False:
            row['endst'] = ''
        else:
            if row['offbtime'] is False or row['downtime'] is False:
                row['endst'] = ''
            else:
                if row['endst'] == 1:
                    row['endst'] = 'OK'
                elif row['endst'] == 0:
                    row['endst'] = 'BAD'

        if row['offbtime'] is False:
            row['offbtime'] = ''
        else:
            if not arg.seconds:
                row['offbtime'] = datetime.fromtimestamp(row['offbtime']).strftime(arg.dtm_format)

        if row['downtime'] is False:
            row['downtime'] = ''
        else:
            if not arg.seconds:
                row['downtime'] = time_conv(row['downtime'])

        if row['kernel'] is False:
            row['kernel'] = ''

        remap.append(row)
    return remap


def print_list(db_rows, last_st, arg):
    """Print values as list"""

    db_rows = ordering_output(db_rows, arg, last_st)

    if arg.csv:  # Define content/spaces between values
        sp0, sp8 = '"', ''
        sp1 = sp2 = '","'
    else:
        sp1, sp2 = '  ', ': '
        sp8, sp0 = ' ', ''

    for row_dict in format_output(db_rows, arg):

        if row_dict['btime']:
            print(sp0 + 'Startup' + sp2 + sp8 + str(row_dict['startup']) + sp1 + 'at' + sp1 + str(row_dict['btime']) + sp0)
        else:
            print(sp0 + 'Startup' + sp2 + sp8 + str(row_dict['startup']) + sp0)

        if arg.bootid and row_dict['bootid']:
            print(sp0 + 'Boot ID' + sp2 + sp8 + str(row_dict['bootid']) + sp0)

        if row_dict['uptime']:
            print(sp0 + 'Uptime' + sp2 + (sp8 * 2) + str(row_dict['uptime']) + sp0)

            if arg.power:
                print(sp0 + 'Running' + sp2 + sp8 + str(row_dict['rntime']) + sp0)
                print(sp0 + 'Sleeping' + sp2 + str(row_dict['slptime']) + sp0)

        if row_dict['offbtime'] and row_dict['endst']:
            print(sp0 + 'Shutdown' + sp2 + str(row_dict['endst']) + sp1 + 'at' + sp1 + str(row_dict['offbtime']) + sp0)
        elif row_dict['endst']:
            print(sp0 + 'Shutdown' + sp2 + str(row_dict['endst']) + sp0)

        if row_dict['downtime']:
            print(sp0 + 'Downtime' + sp2 + str(row_dict['downtime']) + sp0)

        if arg.kernel and row_dict['kernel']:
            print(sp0 + 'Kernel' + sp2 + (sp8 * 2) + str(row_dict['kernel']) + sp0)

        if not arg.csv:
            print('')


def print_table(db_rows, last_st, arg):
    """Print values as a table"""

    tbl = [['No.', 'Boot ID', 'Startup T.', 'Uptime', 'Running', 'Sleeping', 'Shutdown T.', 'End', 'Downtime', 'Kernel']]
    align_left, colpad = [], []
    side_spaces = 3

    # Define header row, remove unused optional cols
    if not arg.bootid:
        tbl[0].remove('Boot ID')

    if not arg.power:
        tbl[0].remove('Running')
        tbl[0].remove('Sleeping')

    if not arg.kernel:
        tbl[0].remove('Kernel')

    if not arg.csv:   # Add empty brake up line if csv is not used
        tbl.append([''] * len(tbl[0]))

    db_rows = ordering_output(db_rows, arg, last_st)

    # Build table for print
    for row_dict in format_output(db_rows, arg):
        rowd = ([str(row_dict['startup']), str(row_dict['btime']), str(row_dict['uptime']),
                 str(row_dict['offbtime']), str(row_dict['endst']), str(row_dict['downtime'])])

        # Add optional cols if exist on header
        if 'Boot ID' in tbl[0]:
            rowd.insert(tbl[0].index('Boot ID'), str(row_dict['bootid']))

        if 'Running' in tbl[0]:
            rowd.insert(tbl[0].index('Running'), str(row_dict['rntime']))
            rowd.insert(tbl[0].index('Sleeping'), str(row_dict['slptime']))

        if 'Kernel' in tbl[0]:
            rowd.append(str(row_dict['kernel']))

        tbl.append(rowd)

    if arg.csv:
        for row in tbl:
            for key, value in enumerate(row):
                sys.stdout.write('"' + str(value) + '"')
                if (key + 1) != len(row):
                    sys.stdout.write(',')
            print('')

    else:
        # Get index position of elements left aligned
        for i in ('End', 'Kernel'):
            if i in tbl[0]:
                align_left.extend([tbl[0].index(i)])

        for i in range(len(tbl[0])):
            # Get the maximum width of the given column index
            colpad.append(max([len(str(row[i])) for row in tbl]))

        # Print cols by row
        for row in tbl:

            # First in raw and next ones with side spaces
            sys.stdout.write(str(row[0]).rjust(colpad[0]))
            for i in range(1, len(row)):
                if i in align_left:
                    col = (side_spaces * ' ') + str(row[i]).ljust(colpad[i])
                else:
                    col = str(row[i]).rjust(colpad[i] + side_spaces)
                sys.stdout.write(str('' + col))
            print('')


def print_tat(db_rows, sis, last_st, arg):
    """Report system status at specific timestamp"""

    # Negative value decrease actual timestamp
    if arg.tat < 0:
        arg.tat = sis['btime'] + sis['uptime'] + arg.tat

    report = {'at': arg.tat, 'status': None}

    for row in db_rows:
        report['startup'] = row['startup']
        report['bootid'] = row['bootid']
        report['kernel'] = row['kernel']

        # Report UP if tat fall into btime + uptime range
        if (arg.tat >= row['btime']) and (arg.tat < (row['btime'] + row['uptime'])):
            report['status'] = 'UP'
            report['time'] = arg.tat - row['btime']
            report['time_fwd'] = row['uptime'] - report['time']
            report['time_total'] = row['uptime']
            break

        # Report DOWN if tat fall into offbtime + downtime range
        elif (arg.tat >= row['offbtime']) and (arg.tat < (row['offbtime'] + row['downtime'])):
            report['time'] = arg.tat - row['offbtime']
            report['time_fwd'] = row['downtime'] - report['time']
            if row['endst'] == 1:
                report['status'] = 'DOWN-OK'
            elif row['endst'] == 0:
                report['status'] = 'DOWN-BAD'
            else:
                report['status'] = 'DOWN'
            report['time_total'] = row['downtime']
            break

    # If status keep their default value, no match, clean all other variables
    if report['status'] is None:
        report['startup'] = report['time'] = report['time_fwd'] = 0
        report['bootid'] = report['kernel'] = 'None'
        perctg_1 = perctg_2 = 0.0
    else:
        perctg_1 = round(report['time'] * 100 / report['time_total'], arg.dec)
        perctg_2 = round(report['time_fwd'] * 100 / report['time_total'], arg.dec)

        if arg.invert:
            report['startup'] = report['startup'] - last_st

    if not arg.seconds:
        report['at'] = datetime.fromtimestamp(report['at']).strftime(arg.dtm_format)
        report['time'] = time_conv(report['time'])
        report['time_fwd'] = time_conv(report['time_fwd'])

    if arg.csv:  # Define content/spaces between values
        sp0, sp8 = '"', ''
        sp2 = sp5 = sp9 = '","'
    else:
        sp0, sp2, sp9 = '', ':\t', '  =  '
        sp8, sp5 = ' ', '  '

    print(sp0 + 'Timestamp status' + sp2 + str(report['status']) + sp5 + 'at' + sp5 + str(report['at']) + sp5 + 'on' + sp5 + str(report['startup']) + sp0)
    if arg.bootid:
        print(sp0 + (sp8 * 6) + '...boot ID' + sp2 + str(report['bootid']) + sp0)
    if arg.kernel:
        print(sp0 + (sp8 * 2) + '...with kernel' + sp2 + str(report['kernel']) + sp0)
    print(sp0 + (sp8 * 6) + 'elapsed in' + sp2 + str(perctg_1) + '%' + sp9 + str(report['time']) + sp0)
    print(sp0 + (sp8 * 4) + 'remaining in' + sp2 + str(perctg_2) + '%' + sp9 + str(report['time_fwd']) + sp0)


def print_default(db_rows, sis, arg):
    """Print values with default output"""

    def parse_rows(db_rows, cal, shdown, updown, cnt):
        """Loop along all DB rows"""

        for row in db_rows:

            # Sum totals
            if row['btime'] is not False:
                updown['startups'] += 1
            if row['offbtime'] is not False:
                if row['endst'] == 0:
                    shdown['bad'] += 1
                if row['endst'] == 1:
                    shdown['ok'] += 1
                updown['shutdowns'] += 1
            cal['tot']['uptime'] += row['uptime']
            cal['tot']['rntime'] += row['rntime']
            cal['tot']['slptime'] += row['slptime']
            cal['tot']['downtime'] += row['downtime']

            # Get lists with all values
            cnt['bootid'].append(row['bootid'])
            if row['uptime'] is not False:
                cnt['uptime'].append(row['uptime'])
            if row['downtime'] is not False:
                cnt['downtime'].append(row['downtime'])
            cnt['kernel'].append(row['kernel'])

        return cal, shdown, updown, cnt

    cal = {'tot': {'uptime': 0, 'rntime': 0, 'slptime': 0, 'downtime': 0},
           'ave': {'uptime': 0, 'rntime': 0, 'slptime': 0, 'downtime': 0}}
    rate = {'up': 0.0, 'rn': 0.0, 'slp': 0.0, 'down': 0.0}
    shdown = {'ok': 0, 'bad': 0}
    updown = {'startups': 0, 'shutdowns': 0}
    tstamp = {'min': arg.ts, 'max': arg.tu}  # Args as init values
    cnt = {'bootid': [], 'uptime': [], 'downtime': [], 'kernel': []}
    sys_life = 0

    if db_rows:

        # Get values from all DB rows
        cal, shdown, updown, cnt = parse_rows(db_rows, cal, shdown, updown, cnt)

        # Max timestamp - until datetime
        # Get rightmost (older) value from last row if arg.tu is not used
        if tstamp['max'] is None:
            if db_rows[-1]['offbtime'] is not False:
                tstamp['max'] = db_rows[-1]['offbtime'] + db_rows[-1]['downtime']
            elif db_rows[-1]['btime'] is not False:
                tstamp['max'] = db_rows[-1]['btime'] + db_rows[-1]['uptime']

        # Min timestamp - since datetime
        # Get leftmost (newer) value, always btime, from first row value if arg.ts is not used
        if tstamp['min'] is None:
            tstamp['min'] = db_rows[0]['btime']

    # Get system life
    sys_life = cal['tot']['uptime'] + cal['tot']['downtime']

    # Get rates and average uptime / downtime
    if sys_life > 0:
        rate['up'] = round((cal['tot']['uptime'] * 100) / sys_life, arg.dec)
        rate['rn'] = round((cal['tot']['rntime'] * 100) / sys_life, arg.dec)
        rate['slp'] = round((cal['tot']['slptime'] * 100) / sys_life, arg.dec)
        rate['down'] = round((cal['tot']['downtime'] * 100) / sys_life, arg.dec)

    if len(cnt['uptime']) > 0:
        cal['ave']['uptime'] = int(round(float(cal['tot']['uptime'] / len(cnt['uptime'])), 0))
        cal['ave']['rntime'] = int(round(float(cal['tot']['rntime'] / len(cnt['uptime'])), 0))
        cal['ave']['slptime'] = int(round(float(cal['tot']['slptime'] / len(cnt['uptime'])), 0))

    if len(cnt['downtime']) > 0:
        cal['ave']['downtime'] = int(round(float(cal['tot']['downtime'] / len(cnt['downtime'])), 0))

    # Get kernel and boot_id counters, remove duplicate and empty elements
    cnt['kernel'] = len(set(cnt['kernel']))
    cnt['bootid'] = len(set(cnt['bootid']))

    # Ouput style: Apply human or keep seconds
    if not arg.seconds:
        if tstamp['max'] is not None:
            tstamp['max'] = datetime.fromtimestamp(tstamp['max']).strftime(arg.dtm_format)

        if tstamp['min'] is not None:
            tstamp['min'] = datetime.fromtimestamp(tstamp['min']).strftime(arg.dtm_format)

        # Look into the keys to set right values
        for k in cal:
            for key in cal[k]:
                cal[k][key] = time_conv(cal[k][key])

        sis['uptime'] = time_conv(sis['uptime'])
        sis['rntime'] = time_conv(sis['rntime'])
        sis['slptime'] = time_conv(sis['slptime'])
        sis['btime'] = datetime.fromtimestamp(sis['btime']).strftime(arg.dtm_format)
        sys_life = time_conv(sys_life)

    # Finally print all
    if arg.csv:  # Define content/spaces between values
        sp0, sp8 = '"', ''
        sp1 = sp4 = sp5 = sp7 = sp9 = '","'
    else:
        sp1, sp5 = ': \t', '  '
        sp9, sp7, sp0 = '  =  ', '  +  ', ''
        sp4 = sp8 = ' '

    uptime = {'average': str(cal['ave']['uptime']), 'current': str(sis['uptime']), 'sys_rate': str(rate['up']) + '%', 'sys_time': str(cal['tot']['uptime'])}

    if arg.power:
        uptime['average'] += str(sp4 + '(rn: ' + str(cal['ave']['rntime']) + ' + slp: ' + str(cal['ave']['slptime']) + ')')
        uptime['current'] += str(sp4 + '(rn: ' + str(sis['rntime']) + ' + slp: ' + str(sis['slptime']) + ')')
        uptime['sys_rate'] += str(sp4 + '(rn: ' + str(rate['rn']) + '% + slp: ' + str(rate['slp']) + '%)')
        uptime['sys_time'] += str(sp4 + '(rn: ' + str(cal['tot']['rntime']) + ' + slp: ' + str(cal['tot']['slptime']) + ')')

    if arg.tu or arg.until:
        print(sp0 + 'System startups' + sp1 + str(updown['startups']) + sp5 + 'since' + sp5 + str(tstamp['min']) + sp5 + 'until' + sp5 + str(tstamp['max']) + sp0)
    else:
        print(sp0 + 'System startups' + sp1 + str(updown['startups']) + sp5 + 'since' + sp5 + str(tstamp['min']) + sp0)
    print(sp0 + 'System shutdowns' + sp1 + str(shdown['ok']) + sp4 + 'ok' + sp7 + str(shdown['bad']) + sp4 + 'bad' + sp0)
    print(sp0 + 'System life' + sp1 + (sp8 * 8) + str(sys_life) + sp0)
    if arg.bootid:
        print(sp0 + 'System boot IDs' + sp1 + str(cnt['bootid']) + sp0)
    if arg.kernel:
        print(sp0 + 'System kernels' + sp1 + str(cnt['kernel']) + sp0)
    if not arg.csv:
        print('')

    print(sp0 + 'System uptime' + sp1 + (sp8 * 8) + uptime['sys_rate'] + sp9 + uptime['sys_time'] + sp0)
    print(sp0 + 'System downtime' + sp1 + str(rate['down']) + '%' + sp9 + str(cal['tot']['downtime']) + sp0)
    if not arg.csv:
        print('')

    print(sp0 + 'Average uptime' + sp1 + uptime['average'] + sp0)
    print(sp0 + 'Average downtime' + sp1 + str(cal['ave']['downtime']) + sp0)
    if arg.update and not (arg.until or arg.tu):
        if not arg.csv:
            print('')
        print(sp0 + 'Current uptime' + sp1 + uptime['current'] + sp5 + 'since' + sp5 + str(sis['btime']) + sp0)
        if arg.bootid:
            print(sp0 + (sp8 * 4) + '...boot ID' + sp1 + str(sis['bootid']) + sp0)
        if arg.kernel:
            print(sp0 + '...with kernel' + sp1 + str(sis['kernel']) + sp0)


def output_hub(db_rows, sis, arg):
    """Manage values for print"""

    last_st = db_rows[-1]['startup']
    if len(db_rows) != last_st:
        logging.info('Real startups are not equal to enumerate startups. Possible deleted rows in DB')

    if arg.update:
        # If the user can only read DB, the select query over DB return outdated numbers in last row
        # because the DB was not updated previously. The following snippet update them in memmory
        # If the user wrote into DB, the values are the same
        db_rows[-1]['uptime'] = sis['uptime']
        db_rows[-1]['rntime'] = sis['rntime']
        db_rows[-1]['slptime'] = sis['slptime']
        db_rows[-1]['endst'] = arg.endst
        db_rows[-1]['kernel'] = sis['kernel']
        db_rows[-1]['offbtime'] = False
        db_rows[-1]['downtime'] = False
    else:
        # Convert last line None sqlite registers to False
        for key in db_rows[-1].keys():
            if db_rows[-1][key] is None:
                db_rows[-1][key] = False

    # Pass rows to arguments that can trim the range
    if arg.until is not None:
        db_rows, arg = until_opt(db_rows, last_st, arg)
    if arg.since is not None:
        db_rows, arg = since_opt(db_rows, last_st, arg)

    if arg.tu is not None:
        db_rows, arg = tuntil_opt(db_rows, sis, arg)
    if arg.ts is not None:
        db_rows, arg = tsince_opt(db_rows, sis, arg)

    # Print values with the chosen output
    if arg.list:
        print_list(db_rows, last_st, arg)
    elif arg.table:
        print_table(db_rows, last_st, arg)
    elif arg.tat is not None:
        print_tat(db_rows, sis, last_st, arg)
    else:
        print_default(db_rows, sis, arg)


def check_new_boot(prev, sis):
    """Test if system have new boot"""

    # How tuptime does it:
    #
    #    If boot id exists (only on Linux and FreeBSD nowadays), checking if its value has changed
    #
    #    If not exists, checking if the value resultant from previous btime plus previous uptime (both
    #    saved into DB) is lower than current btime or (to catch shutdowns lower than a second) equal
    #    to current btime and current uptime is lower than previous uptime
    #
    # Checking boot id is the most secure way to detect a new boot. Working with time values is not 100% relialible.
    # In some particular cases the btime value from /proc/stat or from the system clock functions may change.
    # When tuptime doesn't register a new boot, only an update of the records, it tries to fix the drift.
    #
    # To avoid lost an uptime record, please be sure that the system have time sync enabled, the init/systemd
    # script and the cron task works as expected.

    if prev['bootid'] != 'None' and sis['bootid'] != 'None':
        if prev['bootid'] != sis['bootid']:
            logging.info('System restarted = True from bootid')
            return True
        else:
            logging.info('System restarted = False from bootid')
            return False

    elif prev['buptime'] < sis['btime'] or (prev['buptime'] == sis['btime'] and sis['uptime'] < prev['uptime']):
        logging.info('System restarted = True from btime')
        return True

    else:
        logging.info('System restarted = False')
        return False


def main():
    """Main entry point, core logic"""

    arg = get_arguments()
    sis = get_os_values()
    db_conn, conn = gain_db(sis, arg)

    conn.execute('select bootid, btime, uptime, endst from tuptime where rowid = (select max(rowid) from tuptime)')
    prev = dict(zip(['bootid', 'btime', 'uptime', 'endst'], conn.fetchone()))
    prev['buptime'] = prev['btime'] + prev['uptime']

    logging.info('Last bootid from DB = %s', str(prev['bootid']))
    logging.info('Last btime from DB = %s', str(prev['btime']))
    logging.info('Last uptime from DB = %s', str(prev['uptime']))
    logging.info('Last buptime from DB = %s', str(prev['buptime']))
    logging.info('Last endst from DB = %s', str(prev['endst']))

    # Check if system was restarted
    if arg.update and check_new_boot(prev, sis):

        sis['offbtime'] = prev['buptime']
        if sis['offbtime'] > sis['btime']:  # Assure btime. Never lower than shutdown
            sis['btime'] = sis['offbtime']
        sis['downtime'] = sis['btime'] - prev['buptime']
        logging.info('Recording offbtime into DB = %s', str(sis['offbtime']))
        logging.info('Recording downtime into DB = %s', str(sis['downtime']))

        try:
            # Save downtimes for previous boot
            conn.execute('update tuptime set \
                         offbtime = ' + str(sis['offbtime']) + ', downtime = ' + str(sis['downtime']) +
                         ' where rowid = (select max(rowid) from tuptime)')

            # Create a new boot register
            conn.execute('insert into tuptime values (?,?,?,?,?,?,?,?,?)',
                         (str(sis['bootid']), str(sis['btime']), str(sis['uptime']), str(sis['rntime']),
                          str(sis['slptime']), None, str(arg.endst), None, str(sis['kernel'])))
            logging.info('DB info = insert ok')

        except Exception:
            # If you see this error, maybe the systemd script isn't executed at startup
            # or the DB file (DB_FILE) have wrong permissions
            logging.error('Detected a new system startup but the values have not been saved into DB.')
            logging.error('Tuptime execution user can\'t write into DB file: %s', str(arg.db_file))
            sys.exit(-1)

    elif arg.update:
        # Adjust time drift. Check only when system wasn't restarted
        sis = control_drift(prev, sis)

        # If a graceful shutdown was just registered before, let 5 seconds to next update to avoid being overlaped
        # with regular schedule execution (it can happen at shutdown)
        if prev['endst'] and (prev['uptime'] + 5 > sis['uptime']) and not arg.endst:
            logging.info('DB info = graceful pass')
        else:
            try:
                # Update current boot records
                conn.execute('update tuptime set uptime = ' + str(sis['uptime']) +
                             ', rntime = ' + str(sis['rntime']) + ', slptime = ' + str(sis['slptime']) +
                             ', endst = ' + str(arg.endst) + ', kernel = \'' + str(sis['kernel']) +
                             '\' where rowid = (select max(rowid) from tuptime)')
                logging.info('DB info = update ok')

            except sqlite3.OperationalError:
                logging.info('DB info = update skip')

    else:
        logging.info('DB info = skip by arg.update')

    db_conn.commit()

    if arg.silent:
        db_conn.close()
        logging.info('Silent mode')

    else:
        # Get all rows to determine print values. Convert from sqlite row object to dict to allow item allocation
        conn.execute('select rowid as startup, * from tuptime')
        db_rows = [dict(row) for row in conn.fetchall()]
        db_conn.close()

        output_hub(db_rows, sis, arg)


if __name__ == "__main__":
    main()
