#
#    Copyright (c) 2009-2023 Tom Keffer <tkeffer@gmail.com>
#
#    See the file LICENSE.txt for your rights.
#
"""Entry point to the weewx weather system."""
import importlib
import locale
import logging
import os
import os.path
import platform
import signal
import sys
import time
from optparse import OptionParser

import configobj

import weecfg
import weedb
import weeutil.logger
import weewx.engine
from weeutil.weeutil import to_bool
from weewx import daemon

log = logging.getLogger(__name__)

usagestr = """Usage: %prog --help
       %prog --version
       %prog  [CONFIG_FILE|--config=CONFIG_FILE]
              [--daemon] [--pidfile=PIDFILE]
              [--exit]   [--loop-on-init]
              [--log-label=LABEL]
           
  Entry point to the weewx weather program. Can be run directly, or as a daemon
  by specifying the '--daemon' option.

Arguments:
    CONFIG_FILE: The weewx configuration file to be used. Optional.
"""


# ===============================================================================
#                       Main entry point
# ===============================================================================

def main():
    parser = OptionParser(usage=usagestr)
    parser.add_option("--config", dest="config_path", type=str,
                      metavar="CONFIG_FILE",
                      help="Use configuration file CONFIG_FILE.")
    parser.add_option("-d", "--daemon", action="store_true", dest="daemon", help="Run as a daemon")
    parser.add_option("-p", "--pidfile", type="string", dest="pidfile",
                      help="Store the process ID in PIDFILE",
                      default="/var/run/weewx.pid", metavar="PIDFILE")
    parser.add_option("-v", "--version", action="store_true", dest="version",
                      help="Display version number then exit")
    parser.add_option("-x", "--exit", action="store_true", dest="exit",
                      help="Exit on I/O and database errors instead of restarting")
    parser.add_option("-r", "--loop-on-init", action="store_true", dest="loop_on_init",
                      help="Retry forever if device is not ready on startup")
    parser.add_option("-n", "--log-label", type="string", dest="log_label",
                      help="Label to use in syslog entries",
                      default="weewx", metavar="LABEL")

    # Get the command line options and arguments:
    options, args = parser.parse_args()

    if options.version:
        print(weewx.__version__)
        sys.exit(0)

    if args and options.config_path:
        print("Specify CONFIG_PATH as an argument, or by using --config, but not both",
              file=sys.stderr)
        sys.exit(weewx.CMD_ERROR)

    # Read the configuration file
    try:
        # Pass in a copy of the command line arguments. read_config() will change it.
        config_path, config_dict = weecfg.read_config(options.config_path, list(args))
    except (IOError, configobj.ConfigObjError) as e:
        print("Error parsing config file: %s" % e, file=sys.stderr)
        weeutil.logger.log_traceback(log.critical, "    ****  ")
        sys.exit(weewx.CMD_ERROR)

    # Now that we have the configuration dictionary, we can add the path to the user
    # directory to PYTHONPATH.
    weewx.add_user_path(config_dict)
    # Now we can import user extensions
    importlib.import_module('user.extensions')

    weewx.debug = int(config_dict.get('debug', 0))

    # Now that we have the config_dict and debug setting, we can customize the
    # logging with user additions
    weeutil.logger.setup(options.log_label, config_dict)

    # Log key bits of information.
    log.info("Initializing weewx version %s", weewx.__version__)
    log.info("Using Python %s", sys.version)
    log.info("Located at %s", sys.executable)
    log.info("Platform %s", platform.platform())
    log.info("Locale is '%s'", locale.setlocale(locale.LC_ALL))
    log.info("Entry path: %s", __file__)
    log.info("Using configuration file %s", config_path)
    log.info("Debug is %s", weewx.debug)

    # If no command line --loop-on-init was specified, look in the config file.
    if options.loop_on_init is None:
        loop_on_init = to_bool(config_dict.get('loop_on_init', False))
    else:
        loop_on_init = options.loop_on_init

    # Save the current working directory. A service might
    # change it. In case of a restart, we need to change it back.
    cwd = os.getcwd()

    # Make sure the system time is not out of date (a common problem with the Raspberry Pi).
    # Do this by making sure the system time is later than the creation time of this file.
    sane = os.stat(__file__).st_ctime
    n = 0
    while weewx.launchtime_ts < sane:
        # Log any problems every minute.
        if n % 120 == 0:
            log.info("Waiting for sane time. Current time is %s",
                     weeutil.weeutil.timestamp_to_string(weewx.launchtime_ts))
        n += 1
        time.sleep(0.5)
        weewx.launchtime_ts = time.time()

    # Set up a handler for a termination signal
    signal.signal(signal.SIGTERM, sigTERMhandler)

    if options.daemon:
        log.info("PID file is %s", options.pidfile)
        daemon.daemonize(pidfile=options.pidfile)

    # Main restart loop
    while True:

        os.chdir(cwd)

        try:
            log.debug("Initializing engine")

            # Create and initialize the engine
            engine = weewx.engine.StdEngine(config_dict)

            log.info("Starting up weewx version %s", weewx.__version__)

            # Start the engine. It should run forever unless an exception
            # occurs. Log it if the function returns.
            engine.run()
            log.critical("Unexpected exit from main loop. Program exiting.")

        # Catch any console initialization error:
        except weewx.engine.InitializationError as e:
            # Log it:
            log.critical("Unable to load driver: %s", e)
            # See if we should loop, waiting for the console to be ready.
            # Otherwise, just exit.
            if loop_on_init:
                log.critical("    ****  Waiting 60 seconds then retrying...")
                time.sleep(60)
                log.info("retrying...")
            else:
                log.critical("    ****  Exiting...")
                sys.exit(weewx.IO_ERROR)

        # Catch any recoverable weewx I/O errors:
        except weewx.WeeWxIOError as e:
            # Caught an I/O error. Log it, wait 60 seconds, then try again
            log.critical("Caught WeeWxIOError: %s", e)
            if options.exit:
                log.critical("    ****  Exiting...")
                sys.exit(weewx.IO_ERROR)
            log.critical("    ****  Waiting 60 seconds then retrying...")
            time.sleep(60)
            log.info("retrying...")

        # Catch any database connection errors:
        except (weedb.CannotConnectError, weedb.DisconnectError) as e:
            # No connection to the database server. Log it, wait 60 seconds, then try again
            log.critical("Database connection exception: %s", e)
            if options.exit:
                log.critical("    ****  Exiting...")
                sys.exit(weewx.DB_ERROR)
            log.critical("    ****  Waiting 60 seconds then retrying...")
            time.sleep(60)
            log.info("retrying...")

        except weedb.OperationalError as e:
            # Caught a database error. Log it, wait 120 seconds, then try again
            log.critical("Database OperationalError exception: %s", e)
            if options.exit:
                log.critical("    ****  Exiting...")
                sys.exit(weewx.DB_ERROR)
            log.critical("    ****  Waiting 2 minutes then retrying...")
            time.sleep(120)
            log.info("retrying...")

        except OSError as e:
            # Caught an OS error. Log it, wait 10 seconds, then try again
            log.critical("Caught OSError: %s", e)
            weeutil.logger.log_traceback(log.critical, "    ****  ")
            log.critical("    ****  Waiting 10 seconds then retrying...")
            time.sleep(10)
            log.info("retrying...")

        except Terminate:
            log.info("Terminating weewx version %s", weewx.__version__)
            weeutil.logger.log_traceback(log.debug, "    ****  ")
            signal.signal(signal.SIGTERM, signal.SIG_DFL)
            os.kill(0, signal.SIGTERM)

        # Catch any keyboard interrupts and log them
        except KeyboardInterrupt:
            log.critical("Keyboard interrupt.")
            # Reraise the exception (this should cause the program to exit)
            raise

        # Catch any non-recoverable errors. Log them, exit
        except Exception as ex:
            # Caught unrecoverable error. Log it, exit
            log.critical("Caught unrecoverable exception:")
            log.critical("    ****  %s" % ex)
            # Include a stack traceback in the log:
            weeutil.logger.log_traceback(log.critical, "    ****  ")
            log.critical("    ****  Exiting.")
            # Reraise the exception (this should cause the program to exit)
            raise


# ==============================================================================
#                       Signal handlers
# ==============================================================================

class Terminate(Exception):
    """Exception raised when terminating the engine."""


def sigTERMhandler(signum, _frame):
    log.info("Received signal TERM (%s).", signum)
    raise Terminate


if __name__ == "__main__":
    # Start up the program
    main()
