#!/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-2020 - 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


DB_FILE = '/var/lib/tuptime/tuptime.db'
DATE_FORMAT = '%X %x'
__version__ = '4.1.0'

# 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 at this startup number'
    )
    parser.add_argument(
        '-c', '--csv',
        dest='csv',
        action='store_true',
        default=False,
        help='csv output'
    )
    parser.add_argument(
        '-d', '--date',
        dest='date_format',
        default=DATE_FORMAT,
        action='store',
        help='date format output'
    )
    parser.add_argument(
        '--decp',
        dest='decp',
        default=2,
        metavar='DECIMALS',
        action='store',
        type=int,
        help='number of decimals in percentages'
    )
    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(
        '-k', '--kernel',
        dest='kernel',
        action='store_true',
        default=False,
        help='print kernel information'
    )
    group.add_argument(
        '-l', '--list',
        dest='lst',
        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=['e', 'd', 'k', 'u', 'r', 's'],
        help='order enumerate by [<e|d|k|u|r|s>]'
    )
    parser.add_argument(
        '-p', '--power',
        dest='power',
        default=False,
        action='store_true',
        help='print 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=0,
        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='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=0,
        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__))

    # Check combination of operator requirements
    if arg.reverse or arg.order:
        if not arg.table and not arg.lst:
            parser.error('Used operators must be combined with [-t|--table] or [-l|--list].')
        if arg.order == 'k':
            if not arg.kernel:
                logging.info('Auto enable option = kernel')
                arg.kernel = True
        if arg.order == 'r' or arg.order == 's':
            if not arg.power:
                logging.info('Auto enable option = power')
                arg.power = True

    # Wrap 'at' over since and until
    if arg.at is not None:
        if arg.at < 0:
            arg.at = arg.at + 1
        arg.since = -1
        arg.until = arg.at

    if arg.power or arg.tat:
        if arg.ts or arg.tu:
            # Power states report accumulated time across an uptime range, is not possible to
            # know if the state was running or sleeping between specific points insde it.
            # Narrow ranges can cut the btime and offbtime info, it doesn't make sense with
            # 'at' argument report.
            parser.error('Operator can\'t be combined with [--tsince] or [--tuntil]')

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


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

    btime = None
    uptime = None
    rntime = None
    slptime = None
    ex_user = None
    kernel = None

    def os_bsd(btime, uptime, rntime):
        """Get values from BSD"""

        logging.info('System = BSD')

        try:
            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))
            for path in os.environ["PATH"].split(os.pathsep):
                sysctl_bin = os.path.join(path, 'sysctl')
                if os.path.isfile(sysctl_bin) and os.access(sysctl_bin, os.X_OK):
                    break
            sysctl_out = os.popen(sysctl_bin + ' -n kern.boottime').read()
            # Some BSDs report the value assigned to 'sec', others do it directly
            if 'sec' in sysctl_out:  # FreeBSD, Darwin
                btime = sysctl_out.split(' sec = ')[1].split(',')[0]
            else:  # OpenBSD, NetBSD
                btime = sysctl_out

        try:
            # Time since some unspecified starting point. Contains sleep time on BSDs.
            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 = uptime - uptime_raw
                # Time the system have been running. Not contains sleep time on OSX.
                rntime_raw = time.clock_gettime(time.CLOCK_UPTIME_RAW)
                rntime = rntime_raw + raw_diff
            else:
                # Time the system have been running. Not contains sleep time on BSDs.
                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')
            uptime = time.time() - btime
            rntime = uptime

        return btime, uptime, rntime

    def os_linux(btime, uptime, rntime):
        """Get values from Linux"""

        logging.info('System = Linux')

        try:
            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'):
                        btime = line.split()[1]

        try:  # uptime and rntime be toghether to avoid time mismatch between them
            # Time since some unspecified starting point. Contains sleep time on linux.
            uptime = time.clock_gettime(time.CLOCK_BOOTTIME)
            # Time since some unspecified starting point. Not contains sleep time on linux.
            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:
                uptime = fl1.readline().split()[0]
            rntime = uptime

        return btime, uptime, rntime

    # Linux
    if sys.platform.startswith(('linux')):
        btime, uptime, rntime = os_linux(btime, uptime, rntime)
    # BSDs
    elif sys.platform.startswith(('freebsd', 'darwin', 'dragonfly', 'openbsd', 'netbsd')):
        btime, uptime, rntime = os_bsd(btime, uptime, rntime)
    # elif:
    #     other_os()
    else:
        logging.error('System = %s not supported', sys.platform)
        sys.exit(-1)

    # All OS values as integer
    btime = int(round(float(btime), 0))
    uptime = int(round(float(uptime), 0))
    rntime = int(round(float(rntime), 0))

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

    # Get sleep time from runtime
    slptime = uptime - rntime

    ex_user = os.getuid()
    kernel = platform.platform()
    logging.info('Python = %s', str(platform.python_version()))

    try:
        logging.info('Current locale = %s', str(locale.getlocale()))
    except Exception:
        pass
    logging.info('Uptime = %s', str(uptime))
    logging.info('Rntime = %s', str(rntime))
    logging.info('Spdtime = %s', str(slptime))
    logging.info('Btime = %s', str(btime))
    logging.info('Kernel = %s', str(kernel))
    logging.info('Execution user = %s', str(ex_user))

    # Check right allocation of variables before continue and fix when possible
    for osvarkey, osvarvalue in {'btime': btime, 'uptime': uptime, 'rntime': rntime, 'slptime': slptime, 'ex_user': ex_user, 'kernel': kernel}.items():
        if osvarvalue is None:
            logging.error('%s value unallocate from system. Can\'t continue.', str(osvarkey))
            sys.exit(-1)
        if osvarkey in {'uptime', 'rntime', 'slptime'} and osvarvalue < 0:
            logging.warning('Invalid %s value \"%s\". Reset to \"0\"', str(osvarkey), str(osvarvalue))
            if osvarkey == 'uptime': uptime = 0
            if osvarkey == 'rntime': rntime = 0
            if osvarkey == 'slptime': slptime = 0

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

    return btime, uptime, rntime, slptime, kernel


def assure_state_db(btime, uptime, rntime, slptime, kernel, arg):
    """Assure state of db file and related directories"""

    if arg.db_file == DB_FILE:  # If db_file keeps default value
        # Check for DB environment variable
        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.path.isdir(os.path.dirname(arg.db_file)):
            logging.info('Directory exists = %s', str(os.path.dirname(arg.db_file)))
        else:
            logging.info('Creating path = %s', str(os.path.dirname(arg.db_file)))
            os.makedirs(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('Creating 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'
                         '(btime integer, uptime integer, rntime integer, slptime integer,'
                         'offbtime integer, endst integer, downtime integer, kernel text)')
            conn.execute('insert into tuptime values (?,?,?,?,?,?,?,?)',
                         (str(btime), str(uptime), str(rntime), str(slptime), None, str(arg.endst), None, str(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)


def upgrade_db(db_conn, conn, arg):
    """Upgrade db to from 3.x to 4.x 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:
        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()
    except Exception as exp_db:
        logging.error('Upgrading DB format failed. "%s"', str(exp_db))
        sys.exit(-1)

    logging.warning('Upgraded')


def control_drift(last_btime, btime, uptime, rntime, slptime):
    """Check time drift due inconsistencies with system clock"""

    offset = btime - last_btime  # Calculate time offset
    logging.info('Drift over btime = %s', str(offset))

    # If previous btime doesn't match
    if last_btime != btime:
        logging.info('Fixing drift...')

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

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

            rntime = rntime + offset
            if rntime < 1:
                slptime = slptime + rntime
                if slptime < 0:
                    logging.info('Drift decrease slptime under 0. Impossible')
                    slptime = 0
                logging.info('Drift decrease rntime under 1. Impossible')
                rntime = 1
            logging.info('Fixed rntime = %s', str(rntime))
            logging.info('Fixed slptime = %s', str(slptime))

            btime = btime - offset
            logging.info('Fixed btime = %s', str(btime))
            logging.info('Fixed timestamp = %s', str(btime + 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
            btime = last_btime
            logging.info('Keep last btime from db = %s', str(btime))
            if uptime <= offset:
                logging.info('Drift is bigger or equal than uptime. Skipping')
            if (uptime + offset) <= 0:
                logging.info('Drift decreases uptime under 1. Skipping')

    return btime, uptime, rntime, slptime


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

    # Human style time counter format:
    #  Large --> 1 hour, 48 minutes and 55 seconds
    #  Short --> 01:48:55
    large_hfmt = True

    # Dict to store values
    dtm = {'years': 0, 'days': 0, 'hours': 0, 'minutes': 0, 'seconds': 0}
    human_dtm = ''

    # Calculate values
    dtm['minutes'], dtm['seconds'] = divmod(secs, 60)
    dtm['hours'], dtm['minutes'] = divmod(dtm['minutes'], 60)
    dtm['days'], dtm['hours'] = divmod(dtm['hours'], 24)
    dtm['years'], dtm['days'] = divmod(dtm['days'], 365)

    # Construct date sentence
    for key in ('years', 'days', 'hours', 'minutes', 'seconds'):

        # Avoid print empty values at the beginning
        if (dtm[key] == 0) and (human_dtm == '') and (key != 'seconds'):
            continue
        else:
            if large_hfmt:
                if (dtm[key]) == 1:  # Not plural for 1 unit
                    human_dtm += str(dtm[key]) + ' ' + str(key[:-1]) + ', '
                else:
                    human_dtm += str(dtm[key]) + ' ' + str(key) + ', '
            else:
                human_dtm += str(dtm[key]).zfill(2) + ':'

    if large_hfmt:
        # Return without last comma and space character
        return str(human_dtm[:-2])
    else:
        # Return without last semicolon character
        return str(human_dtm[:-1])


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

    if arg.since < 0:  # Negative value start from bottom
        arg.since = db_rows[-1]['startup'] + arg.since + 1

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

    if not db_rows:
        db_rows = empty_row

    return db_rows, arg


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

    if arg.until < 0:  # Negative value start from bottom
        arg.until = db_rows[-1]['startup'] + arg.until

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

    if not db_rows:
        db_rows = empty_row

    return db_rows, arg


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

    Conventions:
        - Each row keeps its startup number
        - Empty values are False
    """

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

    # Find a match along all rows and get the offset
    offset = None
    for ind, row in enumerate(db_rows[:]):

        if offset is not None:
            db_rows.remove(row)

        elif arg.tu > row['offbtime'] and arg.tu <= (row['offbtime'] + row['downtime']):
            offset = arg.tu - row['offbtime']
            db_rows[ind]['downtime'] = offset

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

        elif arg.tu <= row['btime']:
            offset = True
            db_rows.remove(row)

    # Report 0 if matches produce an empty db
    if not db_rows:
        db_rows = empty_row

    return db_rows, arg


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

    Conventions:
        - Each row keeps its startup number
        - Empty values are False
    """

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

    # Find a match along all rows and get the offset
    offset = None
    for row in db_rows[:]:

        if arg.ts <= row['btime']:
            offset = True

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

        elif arg.ts == row['offbtime']:
            offset = True
            db_rows[0]['btime'] = False
            db_rows[0]['uptime'] = False
            db_rows[0]['rntime'] = db_rows[0]['slptime'] = False

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

        elif offset is None:
            db_rows.remove(row)

    # Report 0 if matches produce an empty db
    if not db_rows:
        db_rows = empty_row

    return db_rows, arg


def ordering_output(db_rows, arg):
    """Order output"""

    if arg.order and (arg.order in ('e', 'd', 'k', 'u', 'r', 's')):
        key_lst = []
        arg.reverse = not arg.reverse
        if arg.order == 'u':
            key_lst.append('uptime')
        if arg.order == 'e':
            key_lst.append('endst')
        if arg.order == 'd':
            key_lst.append('downtime')
        if arg.order == 'k':
            key_lst.append('kernel')
        if arg.order == 'r':
            key_lst.append('rntime')
        if arg.order == 's':
            key_lst.append('slptime')
        db_rows = sorted(db_rows, key=lambda x: tuple(x[i] for i in key_lst), reverse=arg.reverse)
    else:
        if arg.reverse:
            db_rows = list(reversed(db_rows))

    return db_rows


def for_print(db_rows, arg):
    """Prepare values for print"""

    remap = []  # To store processed list

    # Based if the value is False or not, set the right content format
    for row in db_rows:

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

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

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

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

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

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

        remap.append(row)
    return remap


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

    for row_dict in for_print(db_rows, arg):

        if not arg.csv:  # Define content/spaces between values
            sp0, sp1 = '', '  '
            sp2, sp3, sp4 = ': ', ':  ', ':   '

        else:
            sp0 = '"'
            sp4 = sp3 = sp2 = sp1 = '","'

        if row_dict['btime']:
            print(sp0 + 'Startup' + sp3 + str(row_dict['startup']) + sp1 + 'at' + sp1 + str(row_dict['btime']) + sp0)
        else:
            if not arg.csv:
                print(sp0 + 'Startup' + sp3 + str(row_dict['startup']) + sp0)
            else:  # Consistent csv output, always with the same number of values
                print(sp0 + 'Startup' + sp3 + str(row_dict['startup']) + sp1 + '' + sp1 + '' + sp0)

        if row_dict['uptime']:
            print(sp0 + 'Uptime' + sp4 + str(row_dict['uptime']) + sp0)

            if arg.power:
                print(sp0 + 'Running' + sp3 + str(row_dict['rntime']) + sp0)
                print(sp0 + 'Sleeping' + sp3 + 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']:
            if not arg.csv:
                print(sp0 + 'Shutdown' + sp2 + str(row_dict['endst']) + sp0)
            else:
                print(sp0 + 'Shutdown' + sp2 + str(row_dict['endst']) + sp1 + '' + sp1 + '' + sp0)

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

        if arg.kernel and row_dict['kernel']:
            print(sp0 + 'Kernel' + sp4 + str(row_dict['kernel']) + sp0)

        if not arg.csv:
            print('')


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

    tbl = [['No.', 'Startup Date', 'Uptime', 'Running', 'Sleeping', 'Shutdown Date', 'End', 'Downtime', 'Kernel']]
    if not arg.csv:   # Add empty brake up line if csv is not used
        tbl.append([''] * len(tbl[0]))
    colpad = []
    side_spaces = 3

    db_rows = ordering_output(db_rows, arg)

    # Build table for print
    for row_dict in for_print(db_rows, arg):
        tbl.append([str(row_dict['startup']),
                    str(row_dict['btime']),
                    str(row_dict['uptime']),
                    str(row_dict['rntime']),
                    str(row_dict['slptime']),
                    str(row_dict['offbtime']),
                    str(row_dict['endst']),
                    str(row_dict['downtime']),
                    str(row_dict['kernel'])])

    if not arg.power:  # Delete runinng and sleep if it isnt used
        tbl = [x[:3] + x[5:] for x in tbl]

    # Position of columns aligned to left
    al_left = (tbl[0].index('End'), tbl[0].index('Kernel'))

    if not arg.kernel:  # Delete kernel if it isnt used
        tbl = [x[:-1] for x in tbl]

    if not arg.csv:
        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:
            sys.stdout.write(str(row[0]).ljust(colpad[0]))  # First col print
            for i in range(1, len(row)):
                if i in al_left:  # Aligned side
                    col = (side_spaces * ' ') + str(row[i]).ljust(colpad[i])
                else:
                    col = str(row[i]).rjust(colpad[i] + side_spaces)
                sys.stdout.write(str('' + col))  # Other col print
            print('')
    else:
        for row in tbl:
            for key, value in enumerate(row):
                sys.stdout.write('"' + value + '"')
                if (key + 1) != len(row):
                    sys.stdout.write(',')
            print("")


def print_tat(db_rows, btime, uptime, arg):
    """Report system status at specific timestamp"""

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

    report = {'at': False, 'startup': False, 'status': False, 'time': False, 'time_fwd': False, 'time_total': False}

    for row in db_rows:
        report['at'] = arg.tat
        report['startup'] = row['startup']

        # 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 report['time'] is not False:

        perctg_1 = round(report['time'] * 100 / report['time_total'], arg.decp)
        perctg_2 = round(report['time_fwd'] * 100 / report['time_total'], arg.decp)

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

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

        print(sp0 + 'Target status' + sp2  + str(report['status']) + sp5 + 'at' + sp5 + str(report['at']) + sp5 + 'on' + sp5 + str(report['startup']) + sp0)
        print(sp0 + (sp8 * 3) + 'elapsed in' + sp2 + str(perctg_1) + '%' + sp5 + '-' + sp5 + str(report['time']) + sp0)
        print(sp0 + sp8 + 'remaining in' + sp2 + str(perctg_2) + '%' + sp5 + '-' + sp5 + str(report['time_fwd']) + sp0)


def print_default(db_rows, cbtime, cuptime, crntime, cslptime, ckernel, arg):
    """Print values as default output"""

    def extract_times(db_rows, option, key):
        """Extract max/min values for uptime/downtime"""

        # Work with a fresh copy of the list of dicts
        dbr = db_rows[:]

        # Remove empty startup and downtime dates to avoid reporting them
        if key == 'downtime':
            for row in dbr[:]:
                if row['downtime'] is False:
                    dbr.remove(row)
        if key == 'uptime':
            for row in dbr[:]:
                if row['uptime'] is False:
                    dbr.remove(row)

        # Extract max/min values from the complete time rows only if
        # the dict keep 1 row or more
        if option == 'max' and dbr:
            row = max(dbr, key=lambda x: x[key])
        elif option == 'min' and dbr:
            row = min(dbr, key=lambda x: x[key])
        else:
            # If the dict is empty, report nothing
            row = {'btime': False, 'uptime': 0, 'rntime': 0, 'slptime': 0, 'offbtime': False, 'downtime': 0, 'kernel': None}

        # Report based on the key requested
        if key == 'uptime':
            return row['uptime'], row['rntime'], row['slptime'], row['btime'], row['kernel']

        return row['downtime'], row['offbtime'], row['kernel']

    def extract_max_min_tst(db_rows, arg):
        """Extract max and min timestamps values available"""

        last_btime = db_rows[-1]['btime']
        last_offbtime = db_rows[-1]['offbtime']
        first_btime = db_rows[0]['btime']
        first_offbtime = db_rows[0]['offbtime']

        # Get max timestamp available
        if arg.tu is not None:
            max_tstamp = arg.tu
        elif last_offbtime is not False:
            max_tstamp = last_offbtime + db_rows[-1]['downtime']
        elif last_btime is not False:
            max_tstamp = last_btime + db_rows[-1]['uptime']
        else:
            max_tstamp = None

        # Get min timestamp available
        if arg.ts is not None:
            min_tstamp = arg.ts
        elif first_btime is not False:
            min_tstamp = first_btime
        elif first_offbtime is not False:
            min_tstamp = first_offbtime - db_rows[0]['uptime']
        else:
            min_tstamp = max_tstamp

        return max_tstamp, min_tstamp

    # Initialize empty variables
    cal = {'lar': {}, 'sho': {}}  # For calculated times: average, large, short, total
    cal.update({'tot': {'uptime': 0, 'rntime': 0, 'slptime': 0, 'downtime': 0}})
    cal.update({'ave': {'uptime': 0, 'rntime': 0, 'slptime': 0, 'downtime': 0}})
    rate = {'up': 0.0, 'rn': 0.0, 'spd': 0.0, 'down': 0.0}  # For rated values
    shdown = {'ok': 0, 'bad': 0}  # For shutdown states
    startups = 0
    shutdowns = 0
    kernel_cnt = []

    # Unless startup register indicate empty
    if db_rows[0]['startup']:
        for row in db_rows:

            # Count startups
            startups += 1

            # Count shutdowns when endst is set
            if row['endst'] is not False:
                if any((row['offbtime'], row['downtime'])):
                    if row['endst'] == 0:
                        shdown['bad'] += 1
                    if row['endst'] == 1:
                        shdown['ok'] += 1
                    shutdowns += 1

            # Count totals
            cal['tot']['uptime'] += row['uptime']
            cal['tot']['rntime'] += row['rntime']
            cal['tot']['slptime'] += row['slptime']
            cal['tot']['downtime'] += row['downtime']

            # List with kernel names
            kernel_cnt.append(row['kernel'])

    # Get kernel count:
    #   Remove duplicate and empty elements
    kernel_cnt = len(set(filter(None, kernel_cnt)))

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

    # Get max/min timestamp
    max_tstamp, min_tstamp = extract_max_min_tst(db_rows, arg)

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

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

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

    cal['lar']['up_uptime'], cal['lar']['up_rntime'], cal['lar']['up_slptime'], \
    cal['lar']['up_btime'], cal['lar']['up_kern'] = extract_times(db_rows, 'max', 'uptime')

    cal['sho']['up_uptime'], cal['sho']['up_rntime'], cal['sho']['up_slptime'], \
    cal['sho']['up_btime'], cal['sho']['up_kern'] = extract_times(db_rows, 'min', 'uptime')

    cal['lar']['down_downtime'], cal['lar']['down_offbtime'], cal['lar']['down_kern'] = extract_times(db_rows, 'max', 'downtime')
    cal['sho']['down_downtime'], cal['sho']['down_offbtime'], cal['sho']['down_kern'] = extract_times(db_rows, 'min', 'downtime')

    if not arg.seconds:  # Human readable style
        if max_tstamp is not None:
            max_tstamp = datetime.fromtimestamp(max_tstamp).strftime(arg.date_format)

        if min_tstamp is not None:
            min_tstamp = datetime.fromtimestamp(min_tstamp).strftime(arg.date_format)

        # Look into the keys to set right values
        for k in cal:
            for key in cal[k]:
                if key in ['up_kern', 'down_kern']:
                    continue
                elif key in ['up_btime', 'down_offbtime']:
                    if cal[k][key] is not False:
                        cal[k][key] = datetime.fromtimestamp(cal[k][key]).strftime(arg.date_format)
                else:
                    cal[k][key] = time_conv(cal[k][key])

        cuptime = time_conv(cuptime)
        crntime = time_conv(crntime)
        cslptime = time_conv(cslptime)
        cbtime = datetime.fromtimestamp(cbtime).strftime(arg.date_format)
        sys_life = time_conv(sys_life)

    if not arg.csv:  # Define content/spaces between values
        sp0 = sp7 = ''
        sp1, sp2, sp3 = ':\t', ': \t', ': \t\t'
        sp4 = sp8 = ' '
        sp5 = '   '
    else:
        sp0, sp7, sp8 = '"', '","",""', ''
        sp1 = sp2 = sp3 = sp4 = sp5 = '","'

    if arg.power:
        pw1 = sp4 + '(r:' + str(cal['lar']['up_rntime']) + ' + s:' + str(cal['lar']['up_slptime']) + ')'
        pw2 = sp4 + '(r:' + str(cal['sho']['up_rntime']) + ' + s:' + str(cal['sho']['up_slptime']) + ')'
        pw3 = sp4 + '(r:' + str(cal['ave']['rntime']) + ' + s:' + str(cal['ave']['slptime']) + ')'
        pw4 = sp4 + '(r:' + str(crntime) + ' + s:' + str(cslptime) + ')'
    else:
        pw1 = pw2 = pw3 = pw4 = ''

    if (arg.tu is not None or arg.until) and max_tstamp:
        print(sp0 + 'System startups' + sp1 + str(startups) + sp5 + 'since' + sp5 + str(min_tstamp) + sp5 + 'until' + sp5 + str(max_tstamp) + sp0)
    else:
        print(sp0 + 'System startups' + sp1 + str(startups) + sp5 + 'since' + sp5 + str(min_tstamp) + sp7)
    print(sp0 + 'System shutdowns' + sp1 + str(shdown['ok']) + sp4 + 'ok' + sp5 + '-' + sp5 + str(shdown['bad']) + sp4 + 'bad' + sp0)
    print(sp0 + 'System uptime' + sp3 + str(rate['up']) + '%' + sp5 + '-' + sp5 + str(cal['tot']['uptime']) + sp0)
    if arg.power:
        print(sp0 + 'System power state' + sp1 + 'r:' + str(rate['rn']) + '% + s:' + str(rate['spd'])  + '%' + sp5 + '-' + sp5 + 'r:' + str(cal['tot']['rntime']) + ' + s:' + str(cal['tot']['slptime']) + sp0)
    print(sp0 + 'System downtime' + sp2 + str(rate['down']) + '%' + sp5 + '-' + sp5 + str(cal['tot']['downtime']) + sp0)
    print(sp0 + 'System life' + sp3 + str(sys_life) + sp0)
    if arg.kernel:
        print(sp0 + 'System kernels' + sp2 + str(kernel_cnt) + sp0)
    if not arg.csv:
        print('')

    if isinstance(cal['lar']['up_btime'], str) or cal['lar']['up_btime'] is not False:
        print(sp0 + 'Largest uptime' + sp2 + str(cal['lar']['up_uptime']) + pw1 + sp5 + 'from' + sp5 + str(cal['lar']['up_btime']) + sp0)
    else:
        print(sp0 + 'Largest uptime' + sp2 + str(cal['lar']['up_uptime']) + pw1 + sp7)
    if arg.kernel:
        print(sp0 + '...with kernel' + sp2 + str(cal['lar']['up_kern']) + sp0)

    if isinstance(cal['sho']['up_btime'], str) or cal['sho']['up_btime'] is not False:
        print(sp0 + 'Shortest uptime' + sp1 + str(cal['sho']['up_uptime']) + pw2 + sp5 + 'from' + sp5 + str(cal['sho']['up_btime']) + sp0)
    else:
        print(sp0 + 'Shortest uptime' + sp1 + str(cal['sho']['up_uptime']) + pw2 + sp7)
    if arg.kernel:
        print(sp0 + sp8 + '...with kernel' + sp2 + str(cal['sho']['up_kern']) + sp0)
    print(sp0 + 'Average uptime' + sp2 + str(cal['ave']['uptime']) + pw3 + sp0)
    if not arg.csv:
        print('')

    if isinstance(cal['lar']['down_offbtime'], str) or cal['lar']['down_offbtime'] is not False:
        print(sp0 + 'Largest downtime' + sp1 + str(cal['lar']['down_downtime']) + sp5 + 'from' + sp5 + str(cal['lar']['down_offbtime']) + sp0)
    else:
        print(sp0 + 'Largest downtime' + sp1 + str(cal['lar']['down_downtime']) + sp7)
    if arg.kernel:
        print(sp0 + (sp8 * 2) + '...with kernel' + sp2 + str(cal['lar']['down_kern']) + sp0)
    if isinstance(cal['sho']['down_offbtime'], str) or cal['sho']['down_offbtime'] is not False:
        print(sp0 + 'Shortest downtime' + sp1 + str(cal['sho']['down_downtime']) + sp5 + 'from' + sp5 + str(cal['sho']['down_offbtime']) + sp0)
    else:
        print(sp0 + 'Shortest downtime' + sp1 + str(cal['sho']['down_downtime']) + sp7)
    if arg.kernel:
        print(sp0 + (sp8 * 3) + '...with kernel' + sp2 + str(cal['sho']['down_kern']) + sp0)
    print(sp0 + 'Average downtime' + sp2 + str(cal['ave']['downtime']) + sp0)
    if arg.update:
        if not arg.csv:
            print('')
        print(sp0 + 'Current uptime' + sp2 + str(cuptime) + pw4 + sp5 + 'since' + sp5 + str(cbtime) + sp0)
        if arg.kernel:
            print(sp0 + '...with kernel' + sp2 + str(ckernel) + sp0)


def main():
    """main entry point, core logic and database manage"""

    arg = get_arguments()

    btime, uptime, rntime, slptime, kernel = get_os_values()

    assure_state_db(btime, uptime, rntime, slptime, kernel, arg)

    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' not in columns:
        logging.warning('DB format outdated')
        upgrade_db(db_conn, conn, arg)

    conn.execute('select btime, uptime from tuptime where rowid = (select max(rowid) from tuptime)')
    last_btime, last_uptime = conn.fetchone()
    logging.info('Last btime from db = %s', str(last_btime))
    logging.info('Last uptime from db = %s', str(last_uptime))
    last_buptime = last_btime + last_uptime
    logging.info('Last buptime from db = %s', str(last_buptime))

    # - Test if system was resterted
    # How tuptime does it:
    #    Checking if the value resultant from last_btime plus last_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 last_uptime
    #
    # In some particular cases the btime value from /proc/stat or from the system clock functions may change.
    # It is affected by discontinuous jumps in the system time (e.g., if the system administrator
    # manually changes the clock), and by the incremental adjustments performed by adjtime(3) and NTP.
    # Testing only last_btime vs actual btime can produce a false startup register.
    # This issue also happen on virtualized enviroments, servers with high load or with high disk I/O.
    # Also related to kernel system clock frequency, computation of jiffies / HZ and the problem
    # of lost ticks.
    # More info:
    #    https://tools.ietf.org/html/rfc1589
    #    https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=119971
    #    http://man7.org/linux/man-pages/man2/clock_gettime.2.html
    #    http://unix.stackexchange.com/questions/118631/how-can-i-measure-and-prevent-clock-drift
    #
    # To avoid lost an uptime record, please be sure that the system have time sync enabled, the init/systemd
    # script and the cron tasks works as expected.

    if arg.update:
        try:
            if (last_buptime < btime) or (last_buptime == btime and uptime < last_uptime):
                logging.info('System restarted = True')

                offbtime_db = last_buptime
                downtime_db = btime - last_buptime
                logging.info('Recording offbtime into db = %s', str(offbtime_db))
                logging.info('Recording downtime into db = %s', str(downtime_db))

                # Save downtimes for previous boot
                conn.execute('update tuptime set offbtime = ' + str(offbtime_db) + ', downtime = ' + str(downtime_db) +
                             ' where rowid = (select max(rowid) from tuptime)')
                # Create entry for new boot
                conn.execute('insert into tuptime values (?,?,?,?,?,?,?,?)',
                             (str(btime), str(uptime), str(rntime), str(slptime), None, str(arg.endst), None, str(kernel)))
                logging.info('DB info = insert OK')

            else:
                # Adjust time drift. Check only when system wasn't restarted
                btime, uptime, rntime, slptime = control_drift(last_btime, btime, uptime, rntime, slptime)

                logging.info('System restarted = False')
                conn.execute('update tuptime set uptime = ' + str(uptime) + ', rntime = ' + str(rntime) +
                             ', slptime = ' + str(slptime) + ', endst = ' + str(arg.endst) + ', kernel = \'' + str(kernel) +
                             '\' where rowid = (select max(rowid) from tuptime)')
                logging.info('DB info = update OK')

        except sqlite3.OperationalError:
            logging.info('DB info = write PASS')

            if 'offbtime_db' in locals() and 'downtime_db' in locals():
                # 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 are not saved into db.')
                logging.error('Tuptime execution user can\'t write into db file: %s', str(arg.db_file))
                sys.exit(-1)
    else:
        logging.info('DB info = write SKIP by option')

    db_conn.commit()

    # Exit if silent is enabled. Avoid wasting time in print operations
    if arg.silent:
        db_conn.close()
        logging.info('Silent mode')
        sys.exit(0)

    # Get all rows to determine print values
    conn.execute('select rowid as startup, * from tuptime')
    db_rows = conn.fetchall()

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

    # Create list of dicts from sqlite row objects to allow item allocation
    db_rows = [dict(row) for row in db_rows]
    empty_row = [{'kernel': None, 'uptime': False, 'rntime': False, 'slptime': False, 'endst': False, 'offbtime': False, 'startup': 0, 'btime': False, 'downtime': False}]

    if arg.update:
        # If the user can only read db, the previous select return outdated numbers in last row
        # because the db was not updated previously. The following snippet update that in memmory
        db_rows[-1]['uptime'] = uptime
        db_rows[-1]['rntime'] = rntime
        db_rows[-1]['slptime'] = slptime
        db_rows[-1]['endst'] = arg.endst
        db_rows[-1]['kernel'] = 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

    # Parse since until arguments
    if arg.until:
        db_rows, arg = until_opt(db_rows, empty_row, arg)
    if arg.since:
        db_rows, arg = since_opt(db_rows, empty_row, arg)

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

    # Print values
    if arg.lst:
        print_list(db_rows, arg)
    elif arg.table:
        print_table(db_rows, arg)
    elif arg.tat is not None:
        print_tat(db_rows, btime, uptime, arg)
    else:
        print_default(db_rows, btime, uptime, rntime, slptime, kernel, arg)


if __name__ == "__main__":
    main()
