import { KWLName, KeywordPayload, SysName, VSocket, Watcher, enforce_nonnull } from "vscript";

interface LivenessCanaryPayload {
  lastUpdate: Date | null;
  shouldCheck: () => Promise<boolean>;
  suspend: boolean;
  lastValue: KeywordPayload;
  watcher: Watcher;
  timeoutMS: number;
}

export class LivenessCanary {
  constructor(private payload: LivenessCanaryPayload) {}

  public watcher() {
    return this.payload.watcher;
  }

  public disable() {
    this.payload.suspend = true;
  }

  public enable() {
    this.payload.suspend = false;
  }
}

export class LivenessCanaries {
  private static TICK_INTERVAL_MS = 1000;
  private mTickTimer: any | undefined;
  private mLivenessState: Map<
    VSocket,
    Map<KWLName<"full">, Map<SysName /* kw */, LivenessCanaryPayload>>
  > = new Map();

  constructor(readonly exit_on_error: boolean) {}

  // FIXME: track enclosing row masks, suspend liveness canary on deallocation
  public async push(pars: {
    kwl: KWLName<"full">;
    kw: SysName;
    socket: VSocket;
    timeoutMS?: number;
    shouldCheck?: () => Promise<boolean>;
  }): Promise<LivenessCanary> {
    if (this.mLivenessState.has(pars.socket)) {
      const maybeEntry = this.mLivenessState.get(pars.socket)?.get(pars.kwl)?.get(pars.kw);
      if (maybeEntry) {
        maybeEntry.suspend = false;
        return new LivenessCanary(maybeEntry);
      }
    }
    if (!this.mLivenessState.has(pars.socket)) {
      this.mLivenessState.set(pars.socket, new Map());
    }
    const socketMap = this.mLivenessState.get(pars.socket)!;
    if (!socketMap.has(pars.kwl)) {
      socketMap.set(pars.kwl, new Map());
    }
    const kwlMap = socketMap.get(pars.kwl)!;
    console.assert(!kwlMap.has(pars.kw));
    const payload = {
      lastUpdate: null,
      lastValue: null,
      shouldCheck: pars.shouldCheck ?? (async () => true),
      suspend: false,
      timeoutMS: pars.timeoutMS ?? 5000,
      watcher: await pars.socket.watch({ kwl: pars.kwl, kw: pars.kw }, (v) => {
        if (this.mTickTimer !== undefined) {
          const x = kwlMap.get(pars.kw)!;
          x.lastValue = v;
          x.lastUpdate = new Date();
        }
      }),
    };
    kwlMap.set(pars.kw, payload);
    return new LivenessCanary(payload);
  }

  public arm() {
    if (this.mTickTimer === undefined) {
      this.mTickTimer = setInterval(() => {
        this.tick();
      }, LivenessCanaries.TICK_INTERVAL_MS);
    }
  }

  public disarm() {
    if (this.mTickTimer !== undefined) {
      clearInterval(this.mTickTimer);
      this.mTickTimer = undefined;
    }
  }

  public armed() {
    return this.mTickTimer !== undefined;
  }

  public remove(canary: LivenessCanary) {
    const watcher = canary.watcher();
    watcher.unwatch();
    for (const [socket, kwlMap] of this.mLivenessState) {
      if (kwlMap.has(watcher.path.kwl)) {
        if (
          enforce_nonnull(kwlMap.get(watcher.path.kwl)).has(watcher.path.kw) &&
          enforce_nonnull(kwlMap.get(watcher.path.kwl)).get(watcher.path.kw)!.watcher === watcher
        ) {
          enforce_nonnull(kwlMap.get(watcher.path.kwl)).delete(watcher.path.kw);
          if (enforce_nonnull(kwlMap.get(watcher.path.kwl)).size === 0) {
            kwlMap.delete(watcher.path.kwl);
            if (kwlMap.size === 0) {
              this.mLivenessState.delete(socket);
            }
          }
          return;
        }
      }
    }
    console.assert(false);
  }

  public clear() {
    this.disarm();
    for (const [_, kwlMap] of this.mLivenessState) {
      for (const [__, kwMap] of kwlMap) {
        for (const [___, x] of kwMap) {
          x.watcher.unwatch();
        }
      }
    }
    this.mLivenessState = new Map();
  }

  public iter(
    f: (pars: {
      kwl: KWLName<"full">;
      kw: SysName;
      socket: VSocket;
    }) => "suspend" | "enable" | "leave-unchanged",
  ) {
    for (const [socket, kwlMap] of this.mLivenessState) {
      for (const [kwl, kwMap] of kwlMap) {
        for (const [kw, x] of kwMap) {
          switch (f({ kwl, kw, socket })) {
            case "suspend":
              x.suspend = true;
              x.lastValue = null;
              x.lastUpdate = null;
              break;
            case "enable":
              x.lastValue = null;
              x.suspend = false;
              x.lastUpdate = null;
              break;
          }
        }
      }
    }
  }

  private async tick() {
    const queue: Array<Promise<void>> = [];
    for (const [socket, kwlMap] of this.mLivenessState) {
      for (const [kwl, kwMap] of kwlMap) {
        for (const [kw, x] of kwMap) {
          queue.push(
            (async () => {
              if (x.suspend || !(await x.shouldCheck())) {
                x.lastUpdate = null;
                return;
              }
              const now = new Date();
              if (x.lastUpdate === null) {
                x.lastValue = await socket.read({ kwl, kw });
                x.lastUpdate = now;
              } else if (now.valueOf() > x.lastUpdate.valueOf() + x.timeoutMS) {
                const msg = `${kwl}.${kw} @ ${
                  socket.ip
                } has not been updated for more than ${Math.floor(
                  x.timeoutMS / 1000,
                )}s (last received value: ${x.lastValue})`;
                throw new Error(msg);
              }
            })(),
          );
        }
      }
    }
    await Promise.all(queue);
  }
}
