import * as Nostr from "nostr-typedef";
import { filter, Observable, Subject } from "rxjs";

import type { FilledRxNostrConfig } from "../config/index.js";
import { evalFilters } from "../lazy-filter.js";
import { Nip11Registry } from "../nip11.js";
import { isFiltered } from "../nostr/filter.js";
import { EventPacket, LazyREQ } from "../packet.js";
import { AuthProxy } from "./auth.js";
import { RelayConnection } from "./relay.js";
import { CounterSubject } from "./utils.js";

export interface FinPacket {
  from: string;
  subId: string;
}

export class SubscribeProxy {
  // maxSubscriptions: number | null = undefined;
  // maxFilters: number | null;
  // maxLimit: number | null;
  private relay: RelayConnection;
  private authProxy: AuthProxy | null;
  private config: FilledRxNostrConfig;
  private subs = new Map<string, SubRecord>();
  private authRequiredSubs = new Set<string>();
  private fin$ = new Subject<FinPacket>();
  private disposed = false;
  private queue: SubQueue;

  constructor(params: {
    relay: RelayConnection;
    authProxy: AuthProxy | null;
    config: FilledRxNostrConfig;
  }) {
    this.relay = params.relay;
    this.authProxy = params.authProxy;
    this.config = params.config;

    this.queue = new SubQueue(this.relay.url, this.config);

    // Dequeuing
    this.queue.getActivationObservable().subscribe((activated) => {
      for (const { req } of activated) {
        this.sendREQ(req);
      }
    });

    // Recovering
    this.relay.getReconnectedObservable().subscribe(() => {
      for (const { req } of this.queue.ongoings) {
        this.sendREQ(req);
      }
    });

    // Auto closing
    this.relay.getEOSEObservable().subscribe(({ subId }) => {
      if (this.subs.get(subId)?.autoclose) {
        this.unsubscribe(subId);
      }
    });

    // Mark as closed
    this.relay.getCLOSEDObservable().subscribe(async ({ subId, notice }) => {
      const sub = this.subs.get(subId);
      if (!sub) {
        return;
      }

      if (this.authProxy && notice?.startsWith("auth-required:")) {
        this.authRequiredSubs.add(subId);
      } else {
        this.fin(subId);
      }
    });

    this.authProxy?.getAuthResultObservable().subscribe((ok) => {
      if (ok) {
        for (const subId of this.authRequiredSubs) {
          const req = this.subs.get(subId)?.req;
          if (req) {
            this.sendREQ(req);
          }
        }
      } else {
        for (const subId of this.authRequiredSubs) {
          this.fin(subId);
        }
      }

      this.authRequiredSubs.clear();
    });
  }

  subscribe(req: LazyREQ, autoclose: boolean): void {
    if (this.disposed) {
      return;
    }

    const subId = req[1];
    const sub: SubRecord = {
      subId,
      req,
      autoclose,
    };

    this.subs.set(subId, sub);
    this.queue.enqueue(sub);
  }
  unsubscribe(subId: string): void {
    if (this.disposed) {
      return;
    }

    if (this.subs.has(subId)) {
      this.sendCLOSE(subId);
    }
    this.fin(subId);
  }

  isOngoingOrQueued(subId: string): boolean {
    return this.subs.has(subId);
  }

  getEventObservable(): Observable<EventPacket> {
    return this.relay.getEVENTObservable().pipe(
      filter(({ subId, event }) => {
        const filters = this.subs.get(subId)?.filters;
        if (!filters) {
          return false;
        }

        return (
          this.config.skipValidateFilterMatching || isFiltered(event, filters)
        );
      }),
    );
  }
  getFinObservable(): Observable<FinPacket> {
    return this.fin$.asObservable();
  }
  getLogicalConnectionSizeObservable(): Observable<number> {
    return this.queue.getSizeObservable();
  }

  dispose() {
    this[Symbol.dispose]();
  }

  [Symbol.dispose](): void {
    if (this.disposed) {
      return;
    }

    this.disposed = true;

    const subjects = [this.fin$];
    for (const sub of subjects) {
      sub.complete();
    }

    this.queue.dispose();
  }

  private sendREQ([, subId, ...lazyFilters]: LazyREQ) {
    const filters = evalFilters(lazyFilters);
    const sub = this.subs.get(subId);
    if (!sub) {
      return;
    }

    sub.filters = filters;
    this.relay.send(["REQ", subId, ...filters]);
  }
  private sendCLOSE(subId: string) {
    this.relay.send(["CLOSE", subId]);
  }
  private fin(subId: string) {
    this.subs.delete(subId);
    this.queue.drop(subId);
    this.fin$.next({
      from: this.relay.url,
      subId,
    });
  }
}

interface SubRecord {
  subId: string;
  req: LazyREQ;
  filters?: Nostr.Filter[];
  autoclose: boolean;
}

class SubQueue {
  private _queuings: SubRecord[] = [];
  private _ongoings: SubRecord[] = [];
  private activated$ = new Subject<SubRecord[]>();
  private count$ = new CounterSubject();

  get queuings(): SubRecord[] {
    return this._queuings;
  }
  private set queuings(v: SubRecord[]) {
    this._queuings = v;
  }
  get ongoings(): SubRecord[] {
    return this._ongoings;
  }
  private set ongoings(v: SubRecord[]) {
    this._ongoings = v;
  }

  constructor(
    private url: string,
    private config: FilledRxNostrConfig,
  ) {}

  enqueue(v: SubRecord): void {
    this.queuings = [...this.queuings, v];
    this.count$.increment();

    this.shift();
  }
  drop(subId: string): void {
    const remove = (arr: SubRecord[], subId: string): [SubRecord[], number] => {
      const prevLength = arr.length;
      const filtered = arr.filter((e) => e.subId !== subId);
      const removed = prevLength - filtered.length;

      return [filtered, removed];
    };

    const [queuings, droppedX] = remove(this.queuings, subId);
    const [ongoings, droppedY] = remove(this.ongoings, subId);
    this.queuings = queuings;
    this.ongoings = ongoings;
    this.count$.next((v) => v - (droppedX + droppedY));

    this.shift();
  }

  getActivationObservable() {
    return this.activated$.asObservable();
  }
  getSizeObservable() {
    return this.count$.asObservable();
  }

  dispose() {
    const subjects = [this.activated$, this.count$];

    for (const sub of subjects) {
      sub.complete();
    }
  }

  private async shift() {
    const capacity = await this.capacity();

    const concated = [...this.ongoings, ...this.queuings];
    const ongoings = concated.slice(0, capacity);
    const queuings = concated.slice(capacity);
    const activated = this.queuings.slice(0, capacity - this.ongoings.length);

    this.ongoings = ongoings;
    this.queuings = queuings;

    if (activated.length > 0) {
      this.activated$.next(activated);
    }
  }

  private async capacity() {
    const capacity = await Nip11Registry.getValue(
      this.url,
      (data) => data.limitation?.max_subscriptions,
      {
        skipFetch: this.config.skipFetchNip11,
      },
    );
    return capacity ?? Infinity;
  }
}
