import type { DatabaseSync } from 'node:sqlite';
import { getFeedByUrl, createFeed, updateFeed } from '../db/repos/feeds.ts';
import { insertItem } from '../db/repos/items.ts';
import { fetchFeed, isNotModified } from '../feeds/fetcher.ts';
import { parseFeed } from '../feeds/parser.ts';
import { normalizeFeed } from '../feeds/normalizer.ts';
import { discoverFeedUrl } from '../feeds/discover.ts';
import { bus } from '../events.ts';
import { createLogger } from '../logger.ts';
import type { FeedRow, ItemRow } from '../types/db.js';
import type { NormalizedItem } from '../types/feed.js';
const logger = createLogger(process.env.LOG_LEVEL || 'info');

const SUCCESS_INTERVAL_MS = 15 * 60 * 1000;

export type UrlGuard = (url: string) => Promise<void>;

export interface FetchAndStoreResult {
  inserted: number;
  items: ItemRow[];
  status: 200 | 304 | 'error';
  error?: string;
}

function nextPollDate(ms: number): string {
  return new Date(Date.now() + ms).toISOString();
}

function backoffMs(failureCount: number): number {
  const base = 5 * 60 * 1000;
  return Math.min(60 * 60 * 1000, base * Math.pow(2, Math.max(0, failureCount - 1)));
}

export async function ensureFeed(
  db: DatabaseSync,
  inputUrl: string,
  guard?: UrlGuard,
): Promise<FeedRow | null> {
  const feedUrl = await discoverFeedUrl(inputUrl, guard);
  let feed = getFeedByUrl(db, feedUrl);
  if (!feed) {
    const now = new Date().toISOString();
    feed = createFeed(db, { url: feedUrl, next_poll_at: now, created_at: now });
  }
  return feed;
}

export async function fetchAndStore(
  db: DatabaseSync,
  feedRow: FeedRow,
  guard?: UrlGuard,
): Promise<FetchAndStoreResult> {
  const now = new Date().toISOString();
  try {
    const res = await fetchFeed(
      feedRow.url,
      { etag: feedRow.etag ?? undefined, lastModified: feedRow.last_modified ?? undefined },
      guard || undefined,
    );
    if (isNotModified(res)) {
      updateFeed(db, feedRow.id, {
        last_fetched_at: now,
        last_success_at: now,
        failure_count: 0,
        last_error: null,
        next_poll_at: nextPollDate(SUCCESS_INTERVAL_MS),
      });
      return { inserted: 0, items: [], status: 304 };
    }
    const parsed = parseFeed(res.body, res.contentType);
    const normalized = normalizeFeed(parsed, res.url, now);
    updateFeed(db, feedRow.id, {
      url: res.url,
      title: normalized.title,
      description: normalized.description,
      site_url: normalized.siteUrl,
      etag: res.etag,
      last_modified: res.lastModified,
      last_fetched_at: now,
      last_success_at: now,
      failure_count: 0,
      last_error: null,
      next_poll_at: nextPollDate(SUCCESS_INTERVAL_MS),
    });
    const newItems: ItemRow[] = [];
    for (const it of normalized.items as NormalizedItem[]) {
      const row = insertItem(db, {
        feed_id: feedRow.id,
        dedup_key: it.dedupKey,
        guid: it.guid,
        url: it.link,
        title: it.title,
        content_html: it.contentHtml,
        content_text: it.contentText,
        author: it.author,
        published_at: it.publishedAt,
        fetched_at: now,
      });
      if (row) newItems.push(row);
    }
    if (newItems.length) bus.emit('items:new', { feedId: feedRow.id, items: newItems });
    return { inserted: newItems.length, items: newItems, status: 200 };
  } catch (e) {
    const message = e instanceof Error ? e.message : String(e);
    logger.error({ feedUrl: feedRow.url, err: message }, 'feed poll failed');
    const failureCount = (feedRow.failure_count || 0) + 1;
    updateFeed(db, feedRow.id, {
      last_fetched_at: now,
      failure_count: failureCount,
      last_error: message,
      next_poll_at: nextPollDate(backoffMs(failureCount)),
    });
    return { inserted: 0, items: [], status: 'error', error: message };
  }
}
