import type { DatabaseSync } from 'node:sqlite';
import type { EventEmitter } from 'node:events';
import { listFeedsDue } from '../db/repos/feeds.ts';
import { fetchAndStore } from '../services/ingest.ts';
import type { UrlGuard } from '../services/ingest.ts';
import type { FeedRow } from '../types/db.js';
import type { BusEvents } from '../events.ts';

const GLOBAL_CAP: HostLimit = 5;
const HOST_CAP: HostLimit = 2;

type FeedDue = FeedRow;
type HostLimit = number;

export interface PollerOptions {
  guard?: UrlGuard;
}

export interface Poller {
  tick: () => void;
  pollOnce: (nowIso?: string) => Promise<void>;
  start: (intervalMs?: number) => void;
  stop: () => void;
  inFlight: Set<number>;
}

export function createPoller(
  db: DatabaseSync,
  bus: EventEmitter<BusEvents>,
  options: PollerOptions = {},
): Poller {
  const guard = options.guard;
  const inFlight = new Set<number>();
  const pending: FeedDue[] = [];
  const pendingIds = new Set<number>();
  let globalActive = 0;
  const hostActive = new Map<string, number>();

  function hostOf(url: string): string {
    return new URL(url).hostname;
  }

  async function processFeed(feed: FeedDue): Promise<void> {
    inFlight.add(feed.id);
    const h = hostOf(feed.url);
    globalActive += 1;
    hostActive.set(h, (hostActive.get(h) || 0) + 1);
    try {
      await fetchAndStore(db, feed, guard);
    } catch {
      // fetchAndStore records failures; swallow unexpected errors
    } finally {
      globalActive -= 1;
      hostActive.set(h, (hostActive.get(h) || 0) - 1);
      inFlight.delete(feed.id);
      pump();
    }
  }

  function pump(): void {
    for (let i = 0; i < pending.length;) {
      const feed = pending[i];
      if (inFlight.has(feed.id)) {
        i += 1;
        continue;
      }
      const h = hostOf(feed.url);
      if (globalActive >= GLOBAL_CAP) break;
      if ((hostActive.get(h) || 0) >= HOST_CAP) {
        i += 1;
        continue;
      }
      pending.splice(i, 1);
      pendingIds.delete(feed.id);
      processFeed(feed);
    }
  }

  function enqueueDue(nowIso = new Date().toISOString()): void {
    const due = listFeedsDue(db, nowIso);
    for (const feed of due) {
      if (inFlight.has(feed.id) || pendingIds.has(feed.id)) continue;
      pending.push(feed);
      pendingIds.add(feed.id);
    }
    pump();
  }

  function tick(): void {
    enqueueDue();
  }

  function pollOnce(nowIso?: string): Promise<void> {
    enqueueDue(nowIso);
    return new Promise((resolve) => {
      const check = setInterval(() => {
        if (pending.length === 0 && globalActive === 0) {
          clearInterval(check);
          resolve();
        }
      }, 10);
    });
  }

  let handle: NodeJS.Timeout | null = null;
  function start(intervalMs = 30000): void {
    if (handle) return;
    handle = setInterval(() => tick(), intervalMs);
    tick();
  }
  function stop(): void {
    if (handle) clearInterval(handle);
    handle = null;
  }

  return { tick, pollOnce, start, stop, inFlight };
}
