"""formfill-sb daemon — connects to adampowell.pro's SSE command stream,
dispatches commands to SeleniumBase flow runners, acks results back.

Layout:
  main thread  → SSE reader loop (blocking, reconnects silently)
  worker       → dispatched flow (one at a time; abort kills the driver)

Commands (type from server):
  - ext_ping                 → pong
  - ext_spotify_signup       → signup only (standalone browser)
  - ext_trikatuka_transfer   → trikatuka only
  - ext_smurfmarkt_upgrade   → smurfmarkt only
  - ext_full_flow            → composite signup → trikatuka → smurfmarkt
  - abort                    → kill any running driver + set abort flag
"""
import sys
import threading
import time
import traceback
from typing import Any, Callable, Dict

import abort
import config
import flows
import vault
from utils.logger import child, setup
from utils.monitor import place_window

log = child("daemon")


def _sb_factory(cfg: config.Config) -> Callable:
    """Return a zero-arg callable that yields a fresh SB context.

    Uses UC mode, incognito, browser-configurable window size. We set the
    position onto the configured monitor the moment the browser is up.
    """
    def make():
        from seleniumbase import SB
        ww, wh = cfg.window_size
        kwargs = dict(
            uc=True,
            incognito=True,
            headless=cfg.headless,
            window_size=f"{ww},{wh}",
            test=False,
            locale_code="en-US",
        )
        if cfg.proxy:
            kwargs["proxy"] = cfg.proxy
            log.info(f"launching SB with proxy: {cfg.proxy.split('@')[-1]}")
        sb_cm = SB(**kwargs)
        class _Wrap:
            def __enter__(_self):
                _self.sb = sb_cm.__enter__()
                try:
                    place_window(_self.sb.driver, cfg.monitor_index, cfg.window_size)
                except Exception as e:
                    log.warning(f"place_window: {e}")
                return _self.sb
            def __exit__(_self, *a):
                try:
                    return sb_cm.__exit__(*a)
                except Exception:
                    return False
        return _Wrap()
    return make


# Only one flow may run at a time. Re-entrant commands get rejected with a
# busy ack so the PWA surfaces it clearly instead of spinning up a parallel
# browser that races the first.
_flow_lock = threading.Lock()


def _dispatch(cmd: Dict[str, Any], cfg: config.Config) -> None:
    t = cmd.get("type") or ""
    command_id = cmd.get("commandId") or cmd.get("command_id") or ""
    log.info(f"dispatch type={t} id={command_id}")

    if t == "abort":
        log.warning("ABORT command received")
        abort.set_flag()
        abort.nuke_driver()
        return

    if t == "ext_ping":
        vault.ack(cfg, command_id, True, notes=f"pong {int(time.time())}")
        return

    abort.clear()
    runner = flows.get(t)
    if runner is None:
        vault.ack(cfg, command_id, False, error=f"unknown command type: {t}")
        return

    if not _flow_lock.acquire(blocking=False):
        log.warning(f"rejecting concurrent {t} — another flow is already running")
        vault.ack(cfg, command_id, False, error="busy: another flow is running; abort first")
        return

    factory = _sb_factory(cfg)

    def _worker():
        result: Dict[str, Any] = {"ok": False, "error": "worker did not produce result"}
        try:
            result = runner(factory, cmd, cfg)
        except abort.AbortedError:
            result = {"ok": False, "error": "aborted"}
        except Exception as e:
            tb = traceback.format_exc()
            log.error(f"flow {t} crashed: {e}\n{tb}")
            result = {"ok": False, "error": f"{e}", "log": tb[-1500:]}
        finally:
            _flow_lock.release()
            if command_id:
                vault.ack(
                    cfg,
                    command_id,
                    bool(result.get("ok")),
                    log_text=result.get("log"),
                    error=result.get("error"),
                    email=result.get("email"),
                    password=result.get("password"),
                    save_as_site=result.get("save_as_site"),
                )
            log.info(f"flow {t} done ok={result.get('ok')} err={result.get('error')}")

    thread = threading.Thread(target=_worker, name=f"flow-{t}", daemon=True)
    thread.start()


def main() -> int:
    cfg = config.load()
    setup(cfg.log_file)
    log.info(f"formfill-sb starting; server={cfg.server_root} monitor={cfg.monitor_index}")
    log.info(f"config: {cfg.config_path}")

    # Validate 2captcha key presence (warn only — signup has manual fallback paths)
    if not cfg.captcha2_key:
        log.warning("captcha2_key not set — reCAPTCHA pages will require manual solve")

    while True:
        try:
            for cmd in vault.stream_commands(cfg):
                try:
                    _dispatch(cmd, cfg)
                except Exception as e:
                    log.error(f"dispatch crashed: {e}\n{traceback.format_exc()}")
        except KeyboardInterrupt:
            log.info("keyboard interrupt — exiting")
            return 0
        except Exception as e:
            log.error(f"main loop error: {e}; restarting SSE in 5s")
            time.sleep(5)


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