"""Restart the live maker safely. Use this instead of doing it by hand.

WHY THIS EXISTS. Restarting by hand meant: write the kill file, sleep a fixed
30s, delete the lock file, launch. That deletes the single-instance lock -- the
one guard built specifically to stop two makers running -- and on 2026-08-17 the
old process had not finished exiting when the new one started. Two makers ran
against the same account, every leg went to two resting orders, and 26
duplicates accumulated before it was caught. Same failure as 2026-08-16, caused
by the fix for it being switched off by the procedure meant to use it.

The lock is never touched here. A cleanly exiting maker releases it itself, and
if it dies without doing so the heartbeat expires on its own.

    python main\\restart_maker.py -- --live --books 80 --minutes 1440
"""
import json
import os
import subprocess
import sys
import time
from pathlib import Path

sys.path.insert(0, r'E:\soccer\main')
import kalshi_trader as kt
import kalshi_maker as km

ROOT = Path(r'E:\soccer')
KILL = ROOT / 'STOP_MAKER'
LOCK = ROOT / 'research' / '.mm_maker_live.lock'
PY = sys.executable
WAIT_EXIT_S = 120
LOCK_STALE_S = 120        # must match mm_maker_live.LOCK_STALE_S


def running():
    """PIDs of live maker processes, via WMI (no psutil dependency)."""
    out = subprocess.run(
        ['powershell', '-NoProfile', '-Command',
         "Get-CimInstance Win32_Process -Filter \"Name like '%python%'\" | "
         # 'mm_maker_live' is a PREFIX of 'mm_maker_live_polyus', so the old
         # substring match killed the Poly-US maker every time this ran --
         # observed 2026-08-22 ("stopping 2 maker process(es)"). The two venues
         # are separate processes precisely so one cannot take the other down,
         # and a loose regex quietly undid that. Match the filename exactly.
         "Where-Object { $_.CommandLine -match 'mm_maker_live\\.py' } | "
         "ForEach-Object { $_.ProcessId }"],
        capture_output=True, text=True, timeout=60)
    pids = [int(x) for x in out.stdout.split() if x.strip().isdigit()]
    return pids or _lock_holder()


def _pid_alive(pid):
    """Alive check that does not lie about processes owned by SYSTEM.

    Deliberately duplicated from mm_maker_live rather than imported: the tool
    that restarts the maker must not depend on importing the maker.
    ERROR_ACCESS_DENIED (5) means it exists and is not ours; 87 means no such
    pid; anything else fails safe to alive.
    """
    try:
        import ctypes
        k = ctypes.windll.kernel32
        h = k.OpenProcess(0x1000, False, int(pid))
        if h:
            k.CloseHandle(h)
            return True
        return k.GetLastError() != 87
    except Exception:
        return True


def _lock_holder():
    """PID from the lock file, when its heartbeat is fresh and the pid lives.

    WMI IS NOT ENOUGH. Win32_Process.CommandLine comes back EMPTY for a process
    owned by another user unless this shell is elevated, and the maker normally
    runs as SYSTEM under the guard. So running() matched nothing, printed "no
    maker running", and this script launched a second maker alongside a healthy
    one -- observed 2026-08-29. The lock file is written by the maker itself and
    is readable whoever owns the process, so it sees what WMI hides.
    """
    try:
        d = json.loads(LOCK.read_text(encoding="utf-8"))
    except (OSError, ValueError, TypeError):
        return []
    pid = d.get("pid")
    age = time.time() - float(d.get("heartbeat") or 0)
    if pid and age < LOCK_STALE_S and _pid_alive(pid):
        print(f"  lock file reports a live maker (pid {pid}, heartbeat "
              f"{age:.0f}s ago) that WMI could not see")
        return [int(pid)]
    return []


def main():
    args = sys.argv[1:]
    if args and args[0] == '--':
        args = args[1:]

    pids = running()
    if pids:
        print(f'stopping {len(pids)} maker process(es): {pids}')
        # 'restart', not an empty file: the maker reads this and keeps the
        # completing legs of held books resting on its way out. A bare
        # `touch STOP_MAKER` still means a deliberate STOP and cancels all.
        KILL.write_text('restart', encoding='utf-8')
        deadline = time.time() + WAIT_EXIT_S
        while time.time() < deadline:
            pids = running()
            if not pids:
                break
            time.sleep(3)
        else:
            print(f'  did NOT exit within {WAIT_EXIT_S}s -- force killing {pids}')
            for p in pids:
                subprocess.run(['taskkill', '/F', '/PID', str(p)],
                               capture_output=True)
            time.sleep(3)
        print('  all maker processes gone')
    else:
        print('no maker running')

    # Cancel anything left, and VERIFY, before the new process starts. A restart
    # that leaves orders resting hands them to the next instance as strays.
    # NEVER EXIT LEAVING THE KILL FILE ENGAGED (2026-09-03).
    #
    # KILL.touch() above is how this script stops the running maker, and until
    # today it was only removed on the SUCCESS path. Every failure -- refusing
    # to start, or an exception out of the cancel/verify below -- returned with
    # STOP_MAKER still on disk. mm_maker_live_guard.ps1 treats that file as an
    # intentional stop and exits immediately, INCLUDING its stall watchdog, so
    # nothing was left watching anything.
    #
    # 2026-09-02: the network dropped, get_open_orders raised a DNS error out of
    # this function, and the maker stayed down for 23 hours with every guard
    # cycle exiting on the kill file. Nothing was broken and nothing alerted.
    #
    # So the kill file is released on EVERY path out of here. Refusing to start
    # this cycle is still correct; staying dead forever is not. The guard retries
    # in five minutes, and mm_maker_live runs its own startup cancel-all, so a
    # transient failure now self-heals.
    def _release_kill():
        try:
            if KILL.exists():
                KILL.unlink()
        except Exception as e:
            print(f'  WARNING: could not remove {KILL}: {e}')
            print('  THE MAKER WILL NOT START until that file is deleted by hand')

    try:
        km.set_live(True)
        # Completing legs of held books survive the restart (see cancel_all);
        # verify that everything ELSE is gone. If positions are unreadable,
        # held_events() is None and nothing counts as kept -- fail closed.
        n = km.cancel_all('restart', keep_held=True)
        km.set_live(False)
        _held = km.held_events() or set()
        left = kept = None
        for _ in range(5):
            time.sleep(2)
            _all = (kt.get_open_orders() or {}).get('orders', []) or []
            kept = [o for o in _all
                    if str(o.get('ticker', '')).rsplit('-', 1)[0] in _held]
            left = [o for o in _all if o not in kept]
            if not left:
                break
        if kept:
            print(f'kept {len(kept)} completing leg(s) of held books resting: '
                  + ', '.join(sorted(o.get('ticker', '') for o in kept)))
    except Exception as e:
        # Cannot reach the exchange. Do not start -- we have no idea what is
        # resting -- but release the kill so the next guard cycle can retry.
        print(f'cancel/verify FAILED: {type(e).__name__}: {e}')
        print('REFUSING to start this cycle; releasing the kill file so the '
              'guard retries')
        _release_kill()
        return 1
    print(f'cancelled {n}; resting now {len(left) if left is not None else "?"}')
    if left:
        print('REFUSING to start: orders still resting')
        _release_kill()
        return 1

    _release_kill()
    # LOCK IS NOT TOUCHED. A clean exit released it; a crash lets the heartbeat
    # expire. Deleting it here would rebuild the exact hole this script closes.
    if LOCK.exists():
        print(f'  note: lock file present -- {LOCK.read_text(encoding="utf-8")}')
        print('  leaving it alone; the new process will refuse if it is live')

    cmd = [PY, '-u', str(ROOT / 'main' / 'mm_maker_live.py')] + args
    print('starting:', ' '.join(cmd[2:]))
    log = ROOT / 'logs' / 'mm_restart.log'
    with log.open('a', encoding='utf-8') as fh:
        subprocess.Popen(cmd, cwd=str(ROOT), stdout=fh, stderr=subprocess.STDOUT)
    time.sleep(8)
    print('running pids:', running())
    return 0


if __name__ == '__main__':
    sys.exit(main())
