#!/usr/bin/env python3
"""
watchdog.py — Keeps web/app.py running and auto-restarts it when the
APScheduler gets stuck (detected via consecutive "maximum number of running
instances reached" warnings without a successful refresh completing).

Usage:
    python watchdog.py            # prematch only (port 5000)
    python watchdog.py --live     # live only     (port 5001)
    python watchdog.py --both     # both processes
"""
import os
import sys
import subprocess
import time
import argparse

ROOT   = os.path.dirname(os.path.abspath(__file__))
PYTHON = sys.executable

MAX_SKIPS     = 2   # restart after this many back-to-back scheduler skips
RESTART_DELAY = 5   # seconds between kill and re-launch


def launch(script: str) -> subprocess.Popen:
    return subprocess.Popen(
        [PYTHON, script],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        bufsize=1,
        cwd=ROOT,
    )


def watch(script: str, label: str):
    """
    Monitor one app process. Restart it when:
      - it exits unexpectedly, or
      - MAX_SKIPS consecutive scheduler skips occur without a successful refresh.
    Runs forever until KeyboardInterrupt.
    """
    proc = launch(script)
    skips = 0
    print(f'[watchdog:{label}] started (pid {proc.pid})', flush=True)

    try:
        while True:
            line = proc.stdout.readline()

            # Process ended unexpectedly
            if not line:
                code = proc.wait()
                print(
                    f'[watchdog:{label}] exited (code {code}), '
                    f'restarting in {RESTART_DELAY}s…',
                    flush=True,
                )
                time.sleep(RESTART_DELAY)
                proc = launch(script)
                skips = 0
                print(f'[watchdog:{label}] restarted (pid {proc.pid})', flush=True)
                continue

            print(line, end='', flush=True)

            if 'maximum number of running instances reached' in line:
                skips += 1
                print(f'[watchdog:{label}] scheduler skip #{skips}/{MAX_SKIPS}', flush=True)
                if skips >= MAX_SKIPS:
                    print(
                        f'[watchdog:{label}] stuck — killing and restarting…',
                        flush=True,
                    )
                    proc.kill()
                    proc.wait()
                    time.sleep(RESTART_DELAY)
                    proc = launch(script)
                    skips = 0
                    print(f'[watchdog:{label}] restarted (pid {proc.pid})', flush=True)

            elif '── Done' in line or 'Odds refresh started' in line:
                # Successful refresh cycle — reset stuck counter
                skips = 0

    except KeyboardInterrupt:
        print(f'\n[watchdog:{label}] interrupted — stopping', flush=True)
        proc.kill()
        proc.wait()
        raise


def main():
    parser = argparse.ArgumentParser()
    group = parser.add_mutually_exclusive_group()
    group.add_argument('--live', action='store_true', help='Watch live_app.py only')
    group.add_argument('--both', action='store_true', help='Watch both app.py and live_app.py')
    args = parser.parse_args()

    prematch_script = os.path.join(ROOT, 'web', 'app.py')
    live_script     = os.path.join(ROOT, 'web', 'live_app.py')

    if args.both:
        import threading
        threads = [
            threading.Thread(target=watch, args=(prematch_script, 'prematch'), daemon=True),
            threading.Thread(target=watch, args=(live_script,     'live'),     daemon=True),
        ]
        for t in threads:
            t.start()
        try:
            for t in threads:
                t.join()
        except KeyboardInterrupt:
            print('\n[watchdog] stopped', flush=True)
    elif args.live:
        try:
            watch(live_script, 'live')
        except KeyboardInterrupt:
            pass
    else:
        try:
            watch(prematch_script, 'prematch')
        except KeyboardInterrupt:
            pass


if __name__ == '__main__':
    main()
