#!/usr/bin/python

import argparse, os, re, requests, subprocess, sys, time
from datetime import datetime, date, timedelta
import xml.etree.ElementTree as ET

def msg(cmdargs,msg):
    if cmdargs.silent is False:
        print("%s" %msg)

def mythshutdownlock_check(cmdargs,cursor):
    if (cmdargs.lock):
        msg(cmdargs,"    Checking mythshutdown for lock...")
        try:
            cursor.execute("select data from settings where value = 'MythShutdownLock'")
            results=cursor.fetchone()
        except:
            return True
        lock=results[0]
        if int(lock) == 0 :
            msg(cmdargs,"        mythshutdown is NOT locked.")
            return True
        else:
            msg(cmdargs,"        mythshutdown is locked.")
            return False
    else:
        return True

def dailywake_check(cmdargs,cursor):
    if (cmdargs.daily):
        msg(cmdargs,"    Checking if in a daily wake period...")
        dailyWake=False
        today = date.today()
        now = datetime.now()
        try:
            cursor.execute("select data from settings where value = 'DailyWakeupStartPeriod1'")
            results=cursor.fetchone()
            p1Start=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M")
            cursor.execute("select data from settings where value = 'DailyWakeupEndPeriod1'")
            results=cursor.fetchone()
            p1End=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M")
            cursor.execute("select data from settings where value = 'DailyWakeupStartPeriod2'")
            results=cursor.fetchone()
            p2Start=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M")
            cursor.execute("select data from settings where value = 'DailyWakeupEndPeriod2'")
            results=cursor.fetchone()
            p2End=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M")
        except:
            print("error")
            return True

        # Check for time periods that cross midnight
        if (p1End < p1Start):
            if (now > p1End):
                p1End = p1End + timedelta(days=1)
            else:
                p1Start = p1Start + timedelta(days=-1)
        if (p2End < p2Start):
            if (now > p2End):
                p2End = p2End + timedelta(days=1)
            else:
                p2Start = p2Start + timedelta(days=-1)

        #Check for one of the daily wakeup periods
        if (p1Start != p1End):
            if (now >= p1Start and now <= p1End):
                msg(cmdargs,"        Currently in daily wake period 1.")
                return False
        if (p2Start != p2End):
            if (now >= p2Start and now <= p2End):
                msg(cmdargs,"        Currently in daily wake period 2.")
                return False

        #Are we about to start a daily wakeup period using the -t TIME var
        if (p1Start != p1End):
            delta=p1Start-now
            if (delta.seconds >= 0 and delta.seconds <= cmdargs.time * 60):
                msg(cmdargs,"        Daily wake period 1 will start in less than %s minutes." %cmdargs.time)
                return False
        if (p2Start != p2End):
            delta=p2Start-now
            if (delta.seconds >= 0 and delta.seconds <= cmdargs.time * 60):
                msg(cmdargs,"        Daily wake period 2 will start in less than %s minutes." %cmdargs.time)
                return False

        msg(cmdargs,"        Currently NOT in a daily wake period.")
        return True
    else:
        return True

def schemalock_check(cmdargs,cursor):
    msg(cmdargs,"    Checking if the schema is locked...")
    try:
        cursor.execute("select count(*) from schemalock")
        results=cursor.fetchone()
    except:
        return True
    schemalock=results[0]
    if schemalock == 0:
        msg(cmdargs,"        The schema is NOT locked.")
        return True
    else:
        msg(cmdargs,"        The schema is locked.")
        return False

def in_use(cmdargs,cursor):
    msg(cmdargs,"    Checking if programs are in use...")
    try:
        cursor.execute("select count(*) from inuseprograms")
        results=cursor.fetchone()
    except:
        return True
    prginuse=results[0]
    if prginuse == 0 :
        msg(cmdargs,"        Programs are NOT in use.")
        return True
    else:
        msg(cmdargs,"        %s programs are in use." %prginuse)
        cursor.execute("select recusage,chanid,lastupdatetime from inuseprograms")
        results=cursor.fetchall()
        for i in results:
            msg(cmdargs,"            %s - %s - %s" %(i[0],i[1],i[2]))
        return False

def job_check(cmdargs,cursor):
    msg(cmdargs,"    Checking jobqueue for active jobs...")
    try:
        cursor.execute("select count(*) from jobqueue where status between 2 and 5")
        results=cursor.fetchone()
    except:
        return True
    jobs=results[0]
    if jobs == 0 :
        msg(cmdargs,"        No jobs are active.")
        return True
    else:
        msg(cmdargs,"        Jobs are active.")
        return False

def upcoming_check(cmdargs,mythBE):
    msg(cmdargs,"    Checking for recordings in the next %s minutes..." %cmdargs.time)
    try:
        upcoming = mythBE.getUpcomingRecordings()
    except:
        msg(cmdargs,"        Could not get upcoming recordings.")
        return True
    time_diff=10000
    r=0
    for i in upcoming:
        r += 1
        if r > 1:
            break
        show=str(i)
        show=show.strip()
        showtime=re.split("[-+]\\d\\d:\\d\\d",str(i.starttime))[0]
        now=time.time()
        rec_time=time.strptime( showtime ,"%Y-%m-%d %H:%M:%S" )
        r=time.mktime(rec_time)
        time_diff = ( r - now ) / 60

    if ( time_diff > cmdargs.time) :
        msg(cmdargs,"        No recordings starting in %s minutes." %cmdargs.time)
        return True
    else:
        msg(cmdargs,"        A recording is starting in %s minutes." %int(time_diff))
        return False

def mfd_check(cmdargs):
    msg(cmdargs,"    Checking if mythfilldatabase is running...")
    with open(os.devnull, "w") as fnull:
        mythfilldatabase_ret = subprocess.call(["pidof", "mythfilldatabase"], stdout=fnull)
    if mythfilldatabase_ret == 0 :
        msg(cmdargs,"        mythfilldatabase is running.")
        return False
    else:
        msg(cmdargs,"        mythfilldatabase is NOT running.")
        return True

def mythtvsetup_check(cmdargs):
    msg(cmdargs,"    Checking if mythtv-setup is running...")
    with open(os.devnull, "w") as fnull:
        mythsetup_ret = subprocess.call(["pidof", "mythtv-setup"], stdout=fnull)
    if mythsetup_ret == 0 :
        msg(cmdargs,"        mythtv-setup is running.")
        return False
    else:
        msg(cmdargs,"        mythtv-setup is NOT running.")
        return True

def userlogins_check(cmdargs):
    if (cmdargs.logins):
        u=False
        msg(cmdargs,"    Checking for users logged in...")
        users=subprocess.check_output("who")
        names=([x.split() for x in users.splitlines()])
        for i in names:
            if (i[0] == "mythtv" and i[4] == "(:0)"):
                msg(cmdargs,"        Ignoring %s %s" %(i[0],i[4]))
            else:
                msg(cmdargs,"        User logged in: %s %s" %(i[0],i[4]))
                u=True
        if u:
            return False
        else:
            return True
    else:
        return True

def sambafiles_check(cmdargs):
    if (cmdargs.sambafiles):
        msg(cmdargs,"    Checking if Samba files are in use...")
        try:
            smbstatus=subprocess.check_output(["smbstatus", "-L"])
        except:
            smbstatus="No locked files"
        if "No locked files" in smbstatus:
            msg(cmdargs,"        Samba files are NOT in use.")
            return True
        else:
            msg(cmdargs,"        Samba files are in use.")
            return False
    else:
        return True

def mythfe_check(cmdargs,cursor,mythDB):
    #checks to see if a frontend is considered idle
    # True means FE is idle

    if ( cmdargs.runningfe ):
        msg(cmdargs,"    Checking for running and playing mythfrontends...")
    else:
        msg(cmdargs,"    Checking for playing mythfrontends...")
    try:
        cursor.execute("select distinct hostname from settings where hostname is not null;")
        frontends=cursor.fetchall()
    except:
        return True

    for i in frontends:
        try:
            URL = f"http://{i[0]}:6547/Frontend/GetStatus"
            msg(cmdargs,"        Checking %s's mythfrontend status..." %i)
            response = requests.get(URL, timeout=3)
            root = ET.fromstring(response.text)
            # Find the current location and state in the State element
            state_element = root.find('State')
            if state_element is not None:
                location = state_element.find(".//String[Key='currentlocation']/Value")
                state = state_element.find(".//String[Key='state']/Value")

            if ( cmdargs.runningfe ):
                msg(cmdargs,"            %s's mythfrontend is RUNNING." %i)
                return False

            if location is not None and location.text == "standbymode":
                msg(cmdargs,"            %s's mythfrontend is in Standby Mode." %i)
                continue

            if state is not None and state.text.startswith('Watching'):
                msg(cmdargs,"            %s's mythfrontend is PLAYING." %i)
                return False
            else:
                msg(cmdargs,"            %s's mythfrontend is NOT playing." %i)

            if location is not None and '.xml' in location.text or 'mainmenu' in location.text:
                msg(cmdargs,"            %s's mythfrontend is in MENUS." %i)
            else:
                #FE is not in menus, so it must be active in a plugin
                msg(cmdargs,"            %s's mythfrontend is NOT in menus." %i)
                return False
        except:
            msg(cmdargs,"            Could not connect to %s's mythfrontend." %i)

    if ( cmdargs.runningfe ):
        msg(cmdargs,"        mythfrontends are not running or playing or are in menus.")
    else:
        msg(cmdargs,"        mythfrontends are not playing or are in menus.")

    return True

def usage():
    line = '''
    idle.py checks if the system is idle.
    Use idle.py -h to see options.

    idle.py checks these parts of the system in this order to
    determine if it is idle:
    - (option -g) users are logged in return busy
        ignores mythtv (:0) for busy
    - (option -f) Samba files are in use return busy
    - (option -l) mythshutdown is locked return busy
    - (option -d) in a daily wake period or
        about to start a daily wake period return busy
        checks the next 15 minutes. -t TIME changes time
    - schema is locked return busy
    - there are in use programs return busy
    - there are active jobs in the job queue return busy
    - mythfilldatabase is running return busy
    - mythtv-setup is running return busy
    - there are upcoming recordings return busy
        checks the next 15 minutes. -t TIME changes time
    - (option -r) mythfrontends running return busy
    - mythfrontends playing back a recording or video return busy
    - mythfrontends not in menus return busy

    idle.py stops checking and returns false (busy) when the first busy is found.
    '''
    print(line)
    sys.exit(0)

def main(args=[False]):
    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--daily', action='store_true', help='Include daily wake & about to start wake in system busy. (default: daily wake & about to start wake is system idle)')
    parser.add_argument('-g', '--logins', action='store_true', help='Include user logins in system busy. Ignores mythtv (:0) in system busy.')
    parser.add_argument('-f', '--sambafiles', action='store_true', help='Include Samba files in use in system busy.')
    parser.add_argument('-l', '--lock', action='store_true', help='Include mythshutdown lock in system busy. (default: mythshutdown lock is system idle)')
    parser.add_argument('-r', '--runningfe', action='store_true', help='Include running mythfrontends in system busy. (default: running mythfrontends are system idle)')
    parser.add_argument('-s', '--silent', action='store_true', help='Run without printing output. Recommended for use in cron jobs or scripts.')
    parser.add_argument('-t', '--time', type=int, default=15, help='Minutes of idle time needed to return idle for upcoming recordings and daily wake.')
    parser.add_argument('-u', '--usage', action='store_true', help='Print usage instructions.')
    if args[0] is False:
        cmdargs = parser.parse_args()
    else:
        cmdargs = parser.parse_args(args)

    if cmdargs.usage:
        usage()
    idle=True
    msg(cmdargs,"Checking system idle...")

    if (userlogins_check(cmdargs)):
        idle = True
    else:
        idle = False

    if (idle and sambafiles_check(cmdargs)):
        idle = True
    else:
        idle = False

    try:
        from MythTV import MythDB
        mythDB = MythDB()
        cursor = mythDB.cursor()
        db_conn=True
    except:
        msg(cmdargs,"Couldn't connect to MythTV database.")
        db_conn=False

    try:
        from MythTV import MythBE
        mythBE = MythBE()
        be_conn=True
    except:
        msg(cmdargs,"Couldn't connect to MythTV backend.")
        be_conn=False

    if ( db_conn and idle ):
        if (mythshutdownlock_check(cmdargs,cursor) and dailywake_check(cmdargs,cursor) and schemalock_check(cmdargs,cursor) and in_use(cmdargs,cursor) and job_check(cmdargs,cursor)):
            idle=True
        else:
            idle=False

    if ( be_conn and idle ):
        if (mfd_check(cmdargs) and mythtvsetup_check(cmdargs) and upcoming_check(cmdargs,mythBE)):
            idle=True
        else:
            idle=False

    if ( db_conn and idle ):
        if (mythfe_check(cmdargs,cursor,mythDB)):
            idle=True
        else:
            idle=False

    if ( idle ):
        msg(cmdargs,"System is idle.")
    else:
        msg(cmdargs,"System is busy.")
    return idle

if __name__ == "__main__":
    idle=main()
    if ( idle ):
        exit(0)
    else:
        exit(1)
