import { aggregatePoolPages, isLiquidityPayload, type LiquidityPayload, type RawPage } from "./liquidity.ts";
import { CHAINS, getChain, type ChainId } from "./chains.ts";
import { DISCOVERY_MS, type FeedStatus } from "./feed-config.ts";
import { fetchPoolBatch, providerConfig, ProviderError, type FeedEnvironment } from "./liquidity-provider.ts";

type Collection = { mode: "discovery" | "quotes"; pages: RawPage[]; addresses: string[]; universeUpdatedAt: string | null; batchSize: number };
type CollectorRow = { chain: string; state: string | null; provider: string; next_attempt_at: number; failures: number; last_status: number; lease_until: number; last_success_at: number };
const BUDGET = "__upstream__";
const empty = (batchSize: number): Collection => ({ mode: "discovery", pages: [], addresses: [], universeUpdatedAt: null, batchSize });

export async function readSharedSnapshot(db: D1Database, chain: ChainId): Promise<LiquidityPayload | null> {
  const row = await db.prepare("SELECT payload FROM liquidity_snapshots WHERE chain = ?").bind(chain).first<{ payload: string | null }>();
  if (!row?.payload) return null;
  const payload: unknown = JSON.parse(row.payload);
  return isLiquidityPayload(payload) && payload.chainId === chain ? payload : null;
}
export async function readSharedSnapshots(db: D1Database): Promise<Partial<Record<ChainId, LiquidityPayload>>> {
  const rows = await db.prepare("SELECT chain, payload FROM liquidity_snapshots WHERE payload IS NOT NULL").all<{ chain: string; payload: string }>();
  const result: Partial<Record<ChainId, LiquidityPayload>> = {};
  for (const row of rows.results) {
    const chain = getChain(row.chain);
    if (!chain) continue;
    const payload: unknown = JSON.parse(row.payload);
    if (isLiquidityPayload(payload) && payload.chainId === chain.id) result[chain.id] = payload;
  }
  return result;
}
export async function readFeedStatuses(db: D1Database, env: FeedEnvironment = {}): Promise<Record<ChainId, FeedStatus>> {
  const provider = providerConfig(env);
  const rows = await db.prepare("SELECT chain, state, provider, next_attempt_at, failures, last_status, lease_until, last_success_at FROM liquidity_collectors").all<CollectorRow>();
  const budget = rows.results.find(row => row.chain === BUDGET);
  return Object.fromEntries(CHAINS.map(chain => {
    const row = rows.results.find(row => row.chain === chain.id);
    const state: Collection = row?.state ? JSON.parse(row.state) : empty(provider.batchSize);
    return [chain.id, {
      provider: provider.name,
      state: budget?.last_status === 429 || budget?.last_status === 503 ? "rate-limited" : budget && budget.last_status >= 400 ? "unavailable" : state.universeUpdatedAt ? "healthy" : "collecting",
      nextAttemptAt: new Date(Math.max(Date.now(), budget?.next_attempt_at ?? 0)).toISOString(),
      requestIntervalSeconds: provider.interval / 1000,
      completed: state.pages.length,
      total: state.mode === "discovery" ? chain.poolPages : Math.ceil(state.addresses.length / state.batchSize),
      universeUpdatedAt: state.universeUpdatedAt,
    }];
  })) as Record<ChainId, FeedStatus>;
}

/** ONE global slot across every chain and visitor. Round-robin collection is
 * durable, bounded to one HTTP request, and independent of the selected chain. */
export async function refreshSharedSnapshots(db: D1Database, env: FeedEnvironment = {}): Promise<LiquidityPayload | null> {
  const now = Date.now(), token = crypto.randomUUID(), provider = providerConfig(env);
  const budget = await db.prepare(`INSERT INTO liquidity_collectors (chain, provider, lease_until, lease_token, next_attempt_at, last_attempt_at)
    VALUES (?, ?, ?, ?, ?, ?)
    ON CONFLICT(chain) DO UPDATE SET provider = excluded.provider, lease_until = excluded.lease_until,
      lease_token = excluded.lease_token, next_attempt_at = excluded.next_attempt_at, last_attempt_at = excluded.last_attempt_at
    WHERE liquidity_collectors.lease_until <= ? AND (liquidity_collectors.next_attempt_at <= ? OR liquidity_collectors.provider != ?)
    RETURNING chain, state, provider, next_attempt_at, failures, last_status, lease_until, last_success_at`)
    .bind(BUDGET, provider.id, now + 10_000, token, now + provider.interval, now, now, now, provider.id).first<CollectorRow>();
  if (!budget) return null;
  const cursor: number = budget.state ? JSON.parse(budget.state).cursor ?? 0 : 0;
  const chain = CHAINS[cursor % CHAINS.length];
  const nextCursor = (cursor + 1) % CHAINS.length;
  const row = await db.prepare("SELECT state FROM liquidity_collectors WHERE chain = ?").bind(chain.id).first<{ state: string | null }>();
  let state: Collection = row?.state ? JSON.parse(row.state) : empty(provider.batchSize);
  if (state.batchSize !== provider.batchSize) state = empty(provider.batchSize);
  if (!state.pages.length && (!state.universeUpdatedAt || now - Date.parse(state.universeUpdatedAt) >= DISCOVERY_MS)) state.mode = "discovery";
  try {
    const expected = state.mode === "quotes" ? state.addresses.slice(state.pages.length * state.batchSize, (state.pages.length + 1) * state.batchSize) : null;
    const page = await fetchPoolBatch(chain.id, expected ? { addresses: expected } : { page: state.pages.length + 1 }, env);
    if (expected && expected.some(address => !page.data.some(pool => pool.attributes.address.toLowerCase() === address.toLowerCase()))) {
      state = empty(provider.batchSize); // Rediscover delisted pools, without publishing partial metrics.
    } else state.pages.push(page);
    const complete = state.pages.length === (state.mode === "discovery" ? chain.poolPages : Math.ceil(state.addresses.length / state.batchSize));
    let payload: LiquidityPayload | null = null;
    if (complete) {
      const observedAt = new Date(Math.min(...state.pages.map(p => Date.parse(p.observedAt!)))).toISOString();
      payload = aggregatePoolPages(state.pages, observedAt, chain.id);
      if (!isLiquidityPayload(payload)) throw new ProviderError("Invalid aggregated snapshot", 502);
      if (state.mode === "discovery") {
        state.addresses = [...new Set(state.pages.flatMap(p => p.data.map(pool => pool.attributes.address)))];
        state.universeUpdatedAt = observedAt;
      }
      payload.coverage.pages = chain.poolPages;
      state.mode = "quotes"; state.pages = [];
    }
    const mutations = [
      db.prepare(`INSERT INTO liquidity_collectors (chain, state, provider, last_status, last_success_at)
        SELECT ?, ?, ?, 200, ? WHERE EXISTS (SELECT 1 FROM liquidity_collectors WHERE chain = ? AND lease_token = ?)
        ON CONFLICT(chain) DO UPDATE SET state = excluded.state, provider = excluded.provider, last_status = 200, last_success_at = excluded.last_success_at`)
        .bind(chain.id, JSON.stringify(state), provider.id, Date.now(), BUDGET, token),
    ];
    if (payload) mutations.push(db.prepare(`INSERT INTO liquidity_snapshots (chain, payload, observed_at)
      SELECT ?, ?, ? WHERE EXISTS (SELECT 1 FROM liquidity_collectors WHERE chain = ? AND lease_token = ?)
      ON CONFLICT(chain) DO UPDATE SET payload = excluded.payload, observed_at = excluded.observed_at
      WHERE liquidity_snapshots.observed_at < excluded.observed_at`).bind(chain.id, JSON.stringify(payload), Date.parse(payload.updatedAt), BUDGET, token));
    mutations.push(db.prepare("UPDATE liquidity_collectors SET state = ?, failures = 0, last_status = 200, lease_until = 0, last_success_at = ? WHERE chain = ? AND lease_token = ?")
      .bind(JSON.stringify({ cursor: nextCursor }), Date.now(), BUDGET, token));
    await db.batch(mutations);
    return payload;
  } catch (error) {
    const status = error instanceof ProviderError ? error.status : 502;
    const failures = budget.failures + 1;
    const backoff = Math.min(15 * 60_000, 60_000 * 2 ** Math.min(failures - 1, 4));
    const retryAt = Math.max(now + provider.interval, Date.now() + backoff, error instanceof ProviderError ? error.retryAt : 0);
    // Quota errors cool down the whole provider; chain-specific failures rotate fairly.
    await db.prepare("UPDATE liquidity_collectors SET state = ?, failures = ?, last_status = ?, next_attempt_at = ?, lease_until = 0 WHERE chain = ? AND lease_token = ?")
      .bind(JSON.stringify({ cursor: status === 429 || status === 503 ? cursor : nextCursor }), failures, status, retryAt, BUDGET, token).run();
    console.warn(`Liquidity collector deferred: ${chain.id} HTTP ${status}; retry in ${Math.ceil((retryAt - Date.now()) / 1000)}s`);
    return null;
  }
}
