#!/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-2019 - 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
from datetime import datetime


DB_FILE = '/var/lib/tuptime/tuptime.db'
DATE_FORMAT = '%X %x'
DEC = int(2)  # Default decimals for seconds
DECP = int(2)  # Default decimals for percentages
__version__ = '3.5.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)
locale.setlocale(locale.LC_ALL, '')


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

    parser = argparse.ArgumentParser()
    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=DECP,
        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',
        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'
    )
    parser.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'
    )
    parser.add_argument(
        '-o', '--order',
        dest='order',
        metavar='TYPE',
        default=False,
        action='store',
        type=str,
        choices=['e', 'd', 'k', 'u'],
        help='order enumerate by [<e|d|k|u>]'
    )
    parser.add_argument(
        '-r', '--reverse',
        dest='reverse',
        default=False,
        action='store_true',
        help='reverse order'
    )
    parser.add_argument(
        '-s', '--seconds',
        dest='seconds',
        default=None,
        action='store_true',
        help='output time in seconds and epoch'
    )
    parser.add_argument(
        '-S', '--since',
        dest='since',
        default=0,
        action='store',
        type=int,
        help='restrict since this register number'
    )
    parser.add_argument(
        '-t', '--table',
        dest='table',
        default=False,
        action='store_true',
        help='enumerate system life as table'
    )
    parser.add_argument(
        '--tsince',
        dest='ts',
        metavar='TIMESTAMP',
        default=None,
        action='store',
        type=int,
        help='restrict since 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',
        type=int,
        help='restrict until this register 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)

    # - 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 kernel option.')
                arg.kernel = True

    if arg.table and arg.lst:
        parser.error('Operators [-t|--table] and [-l|--list] can\'t be combined together.')

    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
    ex_user = None
    kernel = None

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

        import time

        logging.info('BSD system')
        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 = int(sysctl_out.split(' sec = ')[1].split(',')[0])
        else:  # OpenBSD, NetBSD
            btime = int(sysctl_out)
        uptime = round(float((time.time() - btime)), 2)

        return btime, uptime

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

        logging.info('Linux system')
        with open('/proc/uptime') as fl1:
            uptime = float(fl1.readline().split()[0])
        with open('/proc/stat') as fl2:
            for line in fl2:
                if line.startswith('btime'):
                    btime = int(line.split()[1])

        return btime, uptime

    # Linux and any OS with '/proc' filesystem
    if os.path.isfile('/proc/uptime') and os.path.isfile('/proc/stat'):
        btime, uptime = os_linux(btime, uptime)
    # BSDs with 'sec' in kern.boottime
    elif sys.platform.startswith(('freebsd', 'darwin', 'dragonfly',
                                  'openbsd', 'netbsd')):
        btime, uptime = os_bsd(btime, uptime)
    # elif:
    #     other_os()
    else:
        logging.error('Operating system %s not supported.', sys.platform)
        sys.exit(-1)

    ex_user = os.getuid()
    kernel = platform.platform()

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

    # Check right allocation of system variables before continue
    for osvarkey, osvarvalue in {'btime': btime, 'uptime': uptime, 'ex_user': ex_user, 'kernel': kernel}.items():
        if osvarvalue is None:
            if osvarkey == 'kernel':
                logging.warning('%s value keep default value: %s', str(osvarkey), str(osvarvalue))
            else:
                logging.error('%s value unallocate from system. Can\'t continue.', str(osvarkey))
                sys.exit(-1)

    return btime, uptime, kernel


def assure_state_db(btime, uptime, 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 real,'
                         'offbtime integer,'
                         'endst integer,'
                         'downtime real,'
                         'kernel text)')
            conn.execute('insert into tuptime values (?,?,?,?,?,?)',
                         (str(btime),
                          str(uptime),
                          str('-1'),
                          str(arg.endst),
                          str('0'),
                          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 control_drift(last_btime, btime, uptime):
    """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('Correcting drift...')

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

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

            btime = int(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

        else:
            if uptime < offset:
                logging.info('Drift is bigger than uptime. Skipping')
            if (uptime + offset) < 0:
                logging.info('Drift decreases uptime under 0. Skipping')

    return btime, uptime


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

    secs = round(secs, 0)

    # 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': int(0), 'days': int(0), 'hours': int(0), 'minutes': int(0), 'seconds': int(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 (int(dtm[key])) == 1:  # Not plural for 1 unit
                    human_dtm += str(int(dtm[key])) + ' ' + str(key[:-1]) + ', '
                else:
                    human_dtm += str(int(dtm[key])) + ' ' + str(key) + ', '
            else:
                human_dtm += str(int(dtm[key])).zfill(2) + ':'

    if large_hfmt:
        # Nice sentence end, remove comma
        if human_dtm.find('minutes, ') or human_dtm.find('minute, '):
            human_dtm = human_dtm.replace('minutes, ', 'minutes and ')
            human_dtm = human_dtm.replace('minute, ', 'minute and ')

        # 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, arg, last_startup_n):
    """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
        if arg.since < 0:
            logging.warning('Invalid "since" value. Reset to first.')
            arg.since = 0

    if arg.since > last_startup_n:  # Sanity check
        logging.warning('Option "since" can not be greater than last startup register. '
                        'Reset to: %s', str(last_startup_n))
        arg.since = last_startup_n

    # Remove row 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, arg, last_startup_n):
    """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
        if arg.until <= 0:
            logging.warning('Invalid "until" value. Reset to last. ')
            arg.until = db_rows[-1]['startup']

    if arg.until < arg.since:  # Sanity check
        logging.warning('Option "until" can not be lower than "since". '
                        'Reset to: %s', str(arg.since))
        arg.until = arg.since

    if arg.until > last_startup_n:  # Sanity check
        logging.warning('Option "until" can not be greater than last startup register. '
                        'Reset to: %s', str(last_startup_n))
        arg.until = last_startup_n

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

    return db_rows, arg


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

    '''
    Conventions:
        - Each row keeps its startup number
        - startup == 0 indicate any result, empty values
        - btime == -1 indicate that both btime and uptime are empty
        - btime == 0 indicate an empty btime only
        - offbtime == -1 indicate that offbtime, downtime and endst are empty
        - offbtime ==  0 indicate an empty offbtime only
    '''

    if arg.tu < 0:  # Sanity check
        logging.warning('Option "tuntil" lower than 0 - Not applying.')

    else:

        if arg.ts and arg.tu < arg.ts:  # Sanity check
            logging.info('Option "tuntil" lower than "tsince". Reset to: %s', str(arg.ts))
            arg.tu = arg.ts

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

            # Stop when offset is set
            if offset is None:

                # If it is equal to btime, finish
                if arg.tu == row['btime']:
                    offset = 0
                    db_rows[ind]['uptime'] = 0
                    db_rows[ind]['offbtime'] = -1
                    db_rows[ind]['downtime'] = 0
                    db_rows[ind]['endst'] = -1
                # If it is between btime and offbtime
                # (offbtime is calculated directly with round 0 to avoid problems with uptime decimals)
                elif arg.tu > row['btime'] and arg.tu < int(round(row['btime'] + row['uptime'], 0)):
                    offset = arg.tu - row['btime']
                    db_rows[ind]['uptime'] = offset
                    db_rows[ind]['offbtime'] = -1
                    db_rows[ind]['downtime'] = 0
                    db_rows[ind]['endst'] = -1
                else:
                    # If it is equal to offbtime, finish
                    if arg.tu == row['offbtime']:
                        offset = 0
                        db_rows[ind]['downtime'] = 0
                    # If it is between offbtime and nextbtime
                    # (next btime is calculated directly with round 0 to avoid problems with uptime decimals)
                    elif arg.tu > row['offbtime'] and arg.tu < int(round(row['offbtime'] + row['downtime'], 0)):
                        offset = arg.tu - row['offbtime']
                        db_rows[ind]['downtime'] = offset

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

            # If offset is set, remove rows
            else:
                db_rows.remove(row)

    # Report 0 if matches produce an empty db
    if not db_rows:
        db_rows = [{'kernel': '', 'uptime': 0, 'endst': -1, 'offbtime': -1, 'startup': 0, 'btime': -1, 'downtime': 0}]

    return db_rows, arg


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

    '''
    Conventions:
        Same as tuntil_opt
    '''

    if arg.ts < 0:  # Sanity check
        logging.warning('Option "tsince" lower than 0 - Not applying.')

    elif arg.ts <= db_rows[0]['btime']:  # Sanity check
        logging.info('Option "tsince" lower or equal than first startup timestamp: %s'
                     ' - Not applying.', str(db_rows[0]['btime']))

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

            # Stop when offset is set
            if offset is None:

                # If it is equal to btime, finish
                if arg.ts == row['btime']:
                    offset = 0
                # If it is between btime and offtime
                # (offbtime is calculated directly with round 0 to avoid problems with uptime decimals)
                elif arg.ts > row['btime'] and arg.ts < int(round(row['btime'] + row['uptime'], 0)):
                    offset = round(row['btime'] + row['uptime'] - arg.ts, 2)
                    db_rows[0]['btime'] = 0
                    db_rows[0]['uptime'] = offset
                else:
                    # If it is equal to offbtime, finish
                    if arg.ts == row['offbtime']:
                        offset = 0
                        db_rows[0]['btime'] = -1
                        db_rows[0]['uptime'] = 0
                    # If it is between offbtime and next btime
                    # (next btime is calculated directly with round 0 to avoid problems with uptime decimals)
                    elif arg.ts > row['offbtime'] and arg.ts < int(round(row['offbtime'] + row['downtime'], 0)):
                        offset = round(row['offbtime'] + row['downtime'] - arg.ts, 2)
                        db_rows[0]['btime'] = -1
                        db_rows[0]['uptime'] = 0
                        db_rows[0]['offbtime'] = 0
                        db_rows[0]['downtime'] = offset
                    # If nothing match, remove
                    else:
                        db_rows.remove(row)

    # Report 0 if matches produce an empty db
    if not db_rows:
        db_rows = [{'kernel': '', 'uptime': 0, 'endst': -1, 'offbtime': -1, 'startup': 0, 'btime': -1, 'downtime': 0}]

    return db_rows, arg


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

    # In the case of multiple matches, the order is: uptime > end > downtime > kernel
    if arg.order and (arg.order in ('e', 'd', 'k', 'u')):
        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')
        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"""

    '''
    Conventions:
        Same as tuntil_opt and tsince_opt
    '''

    remap = []  # To store processed list

    # Following the conventions defined in tsince_opt and tuntil_opt...
    for row in db_rows:

        if row['btime'] < 0:
            row['btime'] = ''
            row['uptime'] = ''

        elif row['btime'] == 0:
            row['btime'] = ''
            row['uptime'] = round(row['uptime'], DEC)
            if arg.seconds is None:
                row['uptime'] = time_conv(row['uptime'])
        else:
            row['uptime'] = round(row['uptime'], DEC)
            if arg.seconds is None:
                row['btime'] = datetime.fromtimestamp(row['btime']).strftime(arg.date_format)
                row['uptime'] = time_conv(row['uptime'])

        if row['offbtime'] < 0:
            row['offbtime'] = ''
            row['endst'] = ''
            row['downtime'] = ''

        else:
            if row['endst'] == 1:
                row['endst'] = 'OK'
            elif row['endst'] == 0:
                row['endst'] = 'BAD'

            if row['offbtime'] == 0:
                row['offbtime'] = ''
                row['downtime'] = round(row['downtime'], DEC)
                if arg.seconds is None:
                    row['downtime'] = time_conv(row['downtime'])
            else:
                row['downtime'] = round(row['downtime'], DEC)
                if arg.seconds is None:
                    row['offbtime'] = datetime.fromtimestamp(row['offbtime']).strftime(arg.date_format)
                    row['downtime'] = time_conv(row['downtime'])

        if arg.seconds is not None:  # Fixed number of decimals always
            if row['uptime'] != '':
                row['uptime'] = ('{0:.' + str(DEC) + 'f}').format(round(row['uptime'], DEC))

            if row['downtime'] != '':
                row['downtime'] = ('{0:.' + str(DEC) + 'f}').format(round(row['downtime'], DEC))

        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 arg.csv is False:  # 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 arg.csv is False:
                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 row_dict['offbtime']:
            print(sp0 + 'Shutdown' + sp2 + str(row_dict['endst']) + sp1 + 'at' + sp1 + str(row_dict['offbtime']) + sp0)

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

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

        if arg.csv is False:
            print('')


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

    def maxwidth(table, index):
        """Get the maximum width of the given column index"""
        return max([len(str(row[index])) for row in table])

    tbl = []  # Initialize table plus its header
    tbl.append(['No.', 'Startup Date', 'Uptime', 'Shutdown Date', 'End', 'Downtime', 'Kernel'])
    if arg.csv is False:   # 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['offbtime']),
                    str(row_dict['endst']),
                    str(row_dict['downtime']),
                    str(row_dict['kernel'])])

    if not arg.kernel:  # Delete kernel if it isnt used
        tbl_no_kern = []
        for elx in tbl:
            del elx[-1]
            tbl_no_kern.append(elx)
        tbl = tbl_no_kern

    if arg.csv is False:

        for i in range(len(tbl[0])):
            colpad.append(maxwidth(tbl, i))

        # 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 (4, 6):   # 'End' and 'Kernel' columns align to 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))  # 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_default(db_rows, cuptime, cbtime, 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['offbtime'] < 0:
                    dbr.remove(row)
        if key == 'uptime':
            for row in dbr[:]:
                if row['btime'] < 0:
                    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: int(x[key]))
        elif option == 'min' and dbr:
            row = min(dbr, key=lambda x: int(x[key]))
        else:
            # If the dict is empty, report 0 values.
            row = {}
            row['btime'] = -1
            row['uptime'] = 0
            row['offbtime'] = -1
            row['downtime'] = 0
            row['kernel'] = ''

        # Report based on the key requested
        if key == 'uptime':
            return float(round(row['uptime'], DEC)), row['btime'], row['kernel']

        return float(round(row['downtime'], DEC)), 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_btime > 0:
            max_tstamp = last_btime + db_rows[-1]['uptime'] + db_rows[-1]['downtime']
        elif last_offbtime > 0:
            max_tstamp = last_offbtime + db_rows[-1]['downtime']
        else:
            max_tstamp = 0

        # Get min timestamp available
        if arg.ts is not None:
            min_tstamp = arg.ts
        elif first_btime > 0:
            min_tstamp = first_btime
        elif first_offbtime > 0:
            min_tstamp = first_offbtime - db_rows[0]['uptime']
            # note that without offbtime, produce a negative result
        else:
            # If range is under any timestamp, use max_tstamp date if is possible
            # to avoid fall into 0
            if max_tstamp > 0:
                min_tstamp = max_tstamp
            else:
                min_tstamp = 0

        # If range is over any timestamp, use min_tstamp if is possible for avoid fall into 0
        if max_tstamp == 0:
            max_tstamp = min_tstamp

        return max_tstamp, min_tstamp

    # Initialize empty variables
    total_uptime = 0.0
    total_downtime = 0.0
    bad_shdown = 0
    ok_shdown = 0
    prev_shdown = -1
    shutdowns = 0
    kernel_cnt = []

    # Parse rows getting counters
    for row in db_rows:

        # Count endst if offbtime is valid (not 0 or -1)
        if row['offbtime'] >= 0:
            if row['endst'] == 0:
                bad_shdown += 1
            if row['endst'] == 1:
                ok_shdown += 1
            shutdowns += 1

        # Count totals
        total_uptime += row['uptime']
        total_downtime += row['downtime']

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

    # Get startups count:
    #   Each row is an startup unless startup register indicate empty values
    if db_rows[0]['startup'] == 0:
        startups = 0
    else:
        startups = len(db_rows)

    # Get state of previous shutdown if filters aren't used:
    if startups > 1:
        if not (arg.since or arg.until):
            if not (arg.ts or arg.tu):
                prev_shdown = db_rows[-2]['endst']

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

    # Get system life
    sys_life = round(total_uptime + total_downtime, DEC)

    # Current uptime with right decimals
    cuptime = round(cuptime, DEC)

    # 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:
        uprate = round((total_uptime * 100) / sys_life, arg.decp)
        downrate = round((total_downtime * 100) / sys_life, arg.decp)
    else:
        uprate = 0.0
        downrate = 0.0

    if startups > 0:
        average_up = round((total_uptime / startups), DEC)
    else:
        average_up = 0.0

    if shutdowns > 0:
        average_down = round((total_downtime / shutdowns), DEC)
    else:
        average_down = 0.0

    larg_up_uptime, larg_up_btime, larg_up_kern = extract_times(db_rows, 'max', 'uptime')
    shrt_up_uptime, shrt_up_btime, shrt_up_kern = extract_times(db_rows, 'min', 'uptime')
    larg_down_downtime, larg_down_offbtime, larg_down_kern = extract_times(db_rows, 'max', 'downtime')
    shrt_down_downtime, shrt_down_offbtime, shrt_down_kern = extract_times(db_rows, 'min', 'downtime')

    total_uptime = round(total_uptime, DEC)
    total_downtime = round(total_downtime, DEC)

    if arg.seconds is None:  # - Human readable style
        max_tstamp = datetime.fromtimestamp(max_tstamp).strftime(arg.date_format)
        min_tstamp = datetime.fromtimestamp(min_tstamp).strftime(arg.date_format)
        larg_up_uptime = time_conv(larg_up_uptime)
        if larg_up_btime > 0:
            larg_up_btime = datetime.fromtimestamp(larg_up_btime).strftime(arg.date_format)
        average_up = time_conv(average_up)
        shrt_up_uptime = time_conv(shrt_up_uptime)
        if shrt_up_btime > 0:
            shrt_up_btime = datetime.fromtimestamp(shrt_up_btime).strftime(arg.date_format)
        larg_down_downtime = time_conv(larg_down_downtime)
        if larg_down_offbtime > 0:
            larg_down_offbtime = datetime.fromtimestamp(larg_down_offbtime).strftime(arg.date_format)
        average_down = time_conv(average_down)
        shrt_down_downtime = time_conv(shrt_down_downtime)
        if shrt_down_offbtime > 0:
            shrt_down_offbtime = datetime.fromtimestamp(shrt_down_offbtime).strftime(arg.date_format)
        cuptime = time_conv(cuptime)
        cbtime = datetime.fromtimestamp(cbtime).strftime(arg.date_format)
        total_uptime = time_conv(total_uptime)
        total_downtime = time_conv(total_downtime)
        sys_life = time_conv(sys_life)

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

    # Set how was previous shutdown
    if prev_shdown == 1:
        sp6 = '<-'
    elif prev_shdown == 0:
        sp6 = '->'
    else:
        sp6 = '-'

    if arg.tu or arg.until:
        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(ok_shdown) + sp4 + 'ok' + sp5 + sp6 + sp5 + str(bad_shdown) + sp4 + 'bad' + sp0)
    print(sp0 + 'System uptime' + sp3 + str(uprate) + ' %' + sp5 + '-' + sp5 + str(total_uptime) + sp0)
    print(sp0 + 'System downtime' + sp2 + str(downrate) + ' %' + sp5 + '-' + sp5 + str(total_downtime) + sp0)
    print(sp0 + 'System life' + sp3 + str(sys_life) + sp0)
    if arg.kernel:
        print(sp0 + 'System kernels' + sp2 + str(kernel_cnt) + sp0)
    if arg.csv is False:
        print('')
    if isinstance(larg_up_btime, str) or larg_up_btime > 0:
        print(sp0 + 'Largest uptime' + sp2 + str(larg_up_uptime) + sp5 + 'from' + sp5 + str(larg_up_btime) + sp0)
    else:
        print(sp0 + 'Largest uptime' + sp2 + str(larg_up_uptime) + sp7)
    if arg.kernel:
        print(sp0 + '...with kernel' + sp2 + str(larg_up_kern) + sp0)
    if isinstance(shrt_up_btime, str) or shrt_up_btime > 0:
        print(sp0 + 'Shortest uptime' + sp1 + str(shrt_up_uptime) + sp5 + 'from' + sp5 + str(shrt_up_btime) + sp0)
    else:
        print(sp0 + 'Shortest uptime' + sp1 + str(shrt_up_uptime) + sp7)
    if arg.kernel:
        print(sp0 + sp8 + '...with kernel' + sp2 + str(shrt_up_kern) + sp0)
    print(sp0 + 'Average uptime' + sp2 + str(average_up) + sp0)
    if arg.csv is False:
        print('')
    if isinstance(larg_down_offbtime, str) or larg_down_offbtime > 0:
        print(sp0 + 'Largest downtime' + sp1 + str(larg_down_downtime) + sp5 + 'from' + sp5 + str(larg_down_offbtime) + sp0)
    else:
        print(sp0 + 'Largest downtime' + sp1 + str(larg_down_downtime) + sp7)
    if arg.kernel:
        print(sp0 + (sp8 * 2) + '...with kernel' + sp2 + str(larg_down_kern) + sp0)
    if isinstance(shrt_down_offbtime, str) or shrt_down_offbtime > 0:
        print(sp0 + 'Shortest downtime' + sp1 + str(shrt_down_downtime) + sp5 + 'from' + sp5 + str(shrt_down_offbtime) + sp0)
    else:
        print(sp0 + 'Shortest downtime' + sp1 + str(shrt_down_downtime) + sp7)
    if arg.kernel:
        print(sp0 + (sp8 * 3) + '...with kernel' + sp2 + str(shrt_down_kern) + sp0)
    print(sp0 + 'Average downtime' + sp2 + str(average_down) + sp0)
    if arg.update is True:
        if arg.csv is False:
            print('')
        print(sp0 + 'Current uptime' + sp2 + str(cuptime) + sp5 + 'since' + sp5 + str(cbtime) + sp0)
        if arg.kernel:
            print(sp0 + '...with kernel' + sp2 + str(db_rows[-1]['kernel']) + sp0)


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

    arg = get_arguments()

    btime, uptime, kernel = get_os_values()

    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)

    assure_state_db(btime, uptime, kernel, arg)

    db_conn = sqlite3.connect(arg.db_file)
    db_conn.row_factory = sqlite3.Row
    conn = db_conn.cursor()

    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_offbtime = int(round((last_btime + last_uptime), 0))
    logging.info('Last offbtime from db = %s', str(last_offbtime))

    # - 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 actual btime.
    #
    # In some particular cases the btime value from /proc/stat may change.
    # Testing only last_btime vs actual btime can produce a false startup register.
    # This issue usually happen on virtualized enviroments, servers with high load,
    # high disk I/O or when ntp is running.
    # 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://unix.stackexchange.com/questions/118631/how-can-i-measure-and-prevent-clock-drift
    #
    # To avoid problems, please be sure that the system have time sync enabled, the init/systemd script and
    # the cron line works as expected. A uptime record can be lost if the btime value goes backward more
    # than the difference between last_offbtime and the new btime.
    if arg.update is True:
        try:
            if last_offbtime < btime:
                logging.info('System was restarted')

                offbtime_db = last_offbtime
                downtime_db = btime - last_offbtime
                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('-1'),
                              str(arg.endst),
                              str('0'),
                              str(kernel)))
            else:
                # Adjust time drift. Check only when system wasn't restarted
                btime, uptime = control_drift(last_btime, btime, uptime)

                logging.info('System wasn\'t restarted. Updating db values...')
                conn.execute('update tuptime set uptime = ' + str(uptime) + ', endst = ' + str(arg.endst) +
                             ', kernel = \'' + str(kernel) + '\' where rowid = (select max(rowid) from tuptime)')

        except sqlite3.OperationalError:
            logging.info('Values not saved into db')

            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('Skipping update values')

    if not arg.silent:
        # - Get all rows for calculate print values
        conn.execute('select rowid as startup, * from tuptime')
        db_rows = conn.fetchall()

        last_startup_n = db_rows[-1]['startup']  # Number of last startup by rowid number
        if len(db_rows) != last_startup_n:  # Real startups are not equal to enumerate startups
            logging.info('Possible deleted rows in db')

        # Create list of dicts from sqlite row objects to allow item allocation
        db_rows = [dict(row) for row in db_rows]

        if arg.update is True:
            # 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]['endst'] = arg.endst
            db_rows[-1]['kernel'] = kernel
            db_rows[-1]['downtime'] = 0

        if arg.since:  # Parse since option
            db_rows, arg = since_opt(db_rows, arg, last_startup_n)

        if arg.until:  # Parse until option
            db_rows, arg = until_opt(db_rows, arg, last_startup_n)

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

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

        if arg.tu:  # Parse tuntil option
            db_rows, arg = tuntil_opt(db_rows, arg)

        if arg.ts:  # Parse tsince option
            db_rows, arg = tsince_opt(db_rows, arg)

    db_conn.commit()
    db_conn.close()

    #  Print values
    if arg.silent:
        logging.info('Silent mode')

    else:
        if arg.lst:
            print_list(db_rows, arg)
        elif arg.table:
            print_table(db_rows, arg)
        else:
            print_default(db_rows, uptime, btime, arg)


if __name__ == "__main__":
    main()
