#!/usr/bin/env python3
"""
維港帳房 (Victoria Ledger) sub-server launcher — run alongside diary_server.
SPEC: /opt/data/specs/SPEC_ledger.md

Spawns the Flask app (default port 8790). diary_server.py reverse-proxies
/ledger/* to it. Lives next to the app so the launcher file is writable
by the same user that owns the app dir.
"""
import os
import sys
import time
import signal
import socket
import subprocess

LEDGER_HOST = "127.0.0.1"
# 18790: high port outside Hermes gateway range (8788/8789/8790/9119) and MCP stdio ports.
# Default 8790 collides with the Hermes WebUI server bound by /opt/data/webui-v052/server.py.
LEDGER_PORT = int(os.getenv("LEDGER_PORT") or "18790")
LEDGER_DIR  = os.path.dirname(os.path.abspath(__file__))
LOG_FILE    = "/opt/data/logs/ledger_server.log"
PID_FILE    = "/opt/data/logs/ledger_server.pid"

# Flask already installed under /opt/data/.local on the prod container (uv --target).
# Append rather than setdefault so sandbox-tmp paths can't shadow the real site-packages.
os.environ["PYTHONPATH"] = ":".join(
    [p for p in ["/opt/data/.local", os.environ.get("PYTHONPATH")] if p]
)
os.environ.setdefault("LEDGER_DB_PATH", os.path.join(LEDGER_DIR, "ledger.db"))
os.environ.setdefault("LEDGER_HOST", LEDGER_HOST)
os.environ.setdefault("LEDGER_PORT", str(LEDGER_PORT))


def is_port_open(host: str, port: int) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(0.4)
        try:
            s.connect((host, port))
            return True
        except OSError:
            return False


def wait_healthy(host: str, port: int, timeout: float = 15.0) -> bool:
    deadline = time.time() + timeout
    while time.time() < deadline:
        if is_port_open(host, port):
            return True
        time.sleep(0.4)
    return False


def spawn() -> subprocess.Popen:
    os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
    log = open(LOG_FILE, "ab", buffering=0)
    return subprocess.Popen(
        [sys.executable, "app.py"],
        cwd=LEDGER_DIR,
        stdout=log,
        stderr=log,
        stdin=subprocess.DEVNULL,
        env=os.environ,
        preexec_fn=os.setsid,
    )


def main():
    os.chdir(LEDGER_DIR)
    with open(PID_FILE, "w") as f:
        f.write(str(os.getpid()))

    proc = spawn()

    def _shutdown(signum, _frame):
        try:
            os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
        except Exception:
            pass
        sys.exit(0)

    signal.signal(signal.SIGTERM, _shutdown)
    signal.signal(signal.SIGINT, _shutdown)

    if not wait_healthy(LEDGER_HOST, LEDGER_PORT):
        sys.stderr.write(f"[ledger_server] app failed to bind {LEDGER_HOST}:{LEDGER_PORT}; see {LOG_FILE}\n")
        try:
            os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
        except Exception:
            pass
        sys.exit(1)

    sys.stderr.write(f"[ledger_server] up on {LEDGER_HOST}:{LEDGER_PORT} (pid={proc.pid}) db={os.environ['LEDGER_DB_PATH']}\n")
    sys.stderr.flush()
    # Block until child dies; OS will reap if preexec/setsid misbehaves.
    try:
        proc.wait()
    except KeyboardInterrupt:
        _shutdown(0, None)


if __name__ == "__main__":
    main()
