import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname } from "path";
import { DepositEvent } from "../types";
import { IDepositQueueStore, QueuedDepositEvent } from "./StateStoreContracts";

interface DepositQueueStore {
  queue: QueuedDepositEvent[];
  delivered: Record<string, string>;
}

export class DepositQueueService implements IDepositQueueStore {
  constructor(
    private filePath: string,
    private deliveredRetentionDays: number = 30
  ) {
    this.ensureStore();
  }

  async enqueue(events: DepositEvent[]): Promise<{ accepted: number; duplicates: number }> {
    const store = this.read();
    const queuedKeys = new Set(store.queue.map((event) => event.idempotency_key));
    let accepted = 0;
    let duplicates = 0;

    for (const event of events) {
      if (store.delivered[event.idempotency_key] || queuedKeys.has(event.idempotency_key)) {
        duplicates += 1;
        continue;
      }

      const now = new Date().toISOString();
      store.queue.push({
        ...event,
        attempts: 0,
        queued_at: now,
        updated_at: now,
        next_attempt_at: now,
      });
      queuedKeys.add(event.idempotency_key);
      accepted += 1;
    }

    this.write(store);
    console.info("deposit queue enqueue complete", { accepted, duplicates, queued: store.queue.length });
    return { accepted, duplicates };
  }

  async hasSeen(idempotencyKey: string): Promise<boolean> {
    const store = this.read();
    return Boolean(
      store.delivered[idempotencyKey] || store.queue.some((event) => event.idempotency_key === idempotencyKey)
    );
  }

  async getProcessable(maxBatch: number = 100, now: Date = new Date()): Promise<QueuedDepositEvent[]> {
    const store = this.read();
    const threshold = now.getTime();
    return store.queue
      .filter((event) => Date.parse(event.next_attempt_at) <= threshold)
      .slice(0, maxBatch);
  }

  async markDelivered(event: QueuedDepositEvent): Promise<void> {
    const store = this.read();
    store.queue = store.queue.filter((queued) => queued.idempotency_key !== event.idempotency_key);
    store.delivered[event.idempotency_key] = new Date().toISOString();
    this.write(store);
  }

  async markFailed(event: QueuedDepositEvent, errorMessage: string, now: Date = new Date()): Promise<void> {
    const store = this.read();
    const index = store.queue.findIndex((queued) => queued.idempotency_key === event.idempotency_key);
    if (index === -1) {
      return;
    }

    const attempts = store.queue[index].attempts + 1;
    const delayMs = this.calculateBackoffMs(attempts);
    store.queue[index] = {
      ...store.queue[index],
      attempts,
      updated_at: now.toISOString(),
      next_attempt_at: new Date(now.getTime() + delayMs).toISOString(),
      last_error: errorMessage,
    };
    this.write(store);
  }

  async stats(): Promise<{ queued: number; delivered: number }> {
    const store = this.read();
    return { queued: store.queue.length, delivered: Object.keys(store.delivered).length };
  }

  private calculateBackoffMs(attempts: number): number {
    const baseMs = 5000;
    const maxMs = 5 * 60 * 1000;
    return Math.min(baseMs * Math.max(1, 2 ** (attempts - 1)), maxMs);
  }

  private ensureStore(): void {
    if (!existsSync(dirname(this.filePath))) {
      mkdirSync(dirname(this.filePath), { recursive: true });
    }

    if (!existsSync(this.filePath)) {
      this.write({ queue: [], delivered: {} });
    }
  }

  private read(): DepositQueueStore {
    const parsed = JSON.parse(readFileSync(this.filePath, "utf8")) as DepositQueueStore;
    return this.pruneDelivered({
      queue: Array.isArray(parsed.queue) ? parsed.queue : [],
      delivered: parsed.delivered && typeof parsed.delivered === "object" ? parsed.delivered : {},
    });
  }

  private write(payload: DepositQueueStore): void {
    writeFileSync(this.filePath, JSON.stringify(this.pruneDelivered(payload), null, 2), "utf8");
  }

  private pruneDelivered(payload: DepositQueueStore): DepositQueueStore {
    const threshold = Date.now() - this.deliveredRetentionDays * 24 * 60 * 60 * 1000;
    const delivered = Object.entries(payload.delivered).reduce<Record<string, string>>((acc, [key, value]) => {
      const timestamp = Date.parse(value);
      if (!Number.isNaN(timestamp) && timestamp >= threshold) {
        acc[key] = value;
      }
      return acc;
    }, {});

    return {
      queue: payload.queue,
      delivered,
    };
  }
}
