import { IgRateLimitError } from '@insta-monitor/collector';
import type { Database } from '@insta-monitor/db';
import { collectAccount, abortableSleep, isInactiveChannelResult, RATE_LIMIT_ABORT, FAIL_ABORT } from './collect.js';
import type { CollectOptions, CollectResult, CollectPacing, CollectProgress, ProfileSource } from './collect.js';

const BACKFILL_GIVE_UP = 3;
// Inactive (not_found/private) channels get a cheap liveness re-probe once a week — a
// temporarily deactivated or now-public account would otherwise stay excluded forever.
const INACTIVE_RETRY_DAYS = 7;
// Cap probes per pass BELOW FAIL_ABORT so still-inactive probes (appended last) can never
// abort a pass by themselves.
const INACTIVE_RETRY_CAP = 3;

/**
 * Light-refresh auto-collect accounts whose last refresh is older than ~1 day.
 *
 * Capped at `limit` accounts per run (oldest-refresh first — listRefreshDue orders
 * NULLs then by last_refresh_at). The client wants up to 500 channels refreshed
 * EVERY day (competitor does 500/day slowly and stays unbanned), so the default is
 * 500 and a <=500 fleet collects fully each day (no rotation). Only the excess above
 * 500 staggers across days. Every channel costs a real
 * timeline scrape on the one session (IG no longer serves web_profile_info posts),
 * so ~500/day is a long human-paced pass — ban-safety comes from slow pacing + the
 * >=7d range floor, not from under-collecting.
 */
export async function runDailyLight(
  db: Database,
  source: ProfileSource,
  opts: CollectOptions,
  pacing?: CollectPacing,
  longTailLimit = 500,
  onProgress?: CollectProgress,
  signal?: AbortSignal,
  onAfterEach?: (r: CollectResult) => void | Promise<void>,
): Promise<CollectResult[]> {
  // Priority (pinned) channels collect EVERY run — not cutoff-gated — so they get
  // consecutive daily snapshots (working 1d/7d metrics). The long tail fills the
  // remaining budget, oldest-first, excluding the priority ones already taken.
  const cutoff = new Date(new Date(opts.collectedAt).getTime() - 86_400_000).toISOString();
  const priority = db.accounts.listPriority();
  const longTail = db.accounts
    .listRefreshDue(cutoff, true)
    .filter((a) => !a.priority)
    .slice(0, Math.max(0, longTailLimit));
  // Weekly liveness probe for inactive channels, appended LAST: real collection always runs
  // first, and a success revives the account (collectAccount resets not_found/private → pending).
  const probeCutoff = new Date(new Date(opts.collectedAt).getTime() - INACTIVE_RETRY_DAYS * 86_400_000).toISOString();
  const inactiveProbes = db.accounts.listInactiveRetryDue(probeCutoff).slice(0, INACTIVE_RETRY_CAP);
  const targets = [...priority, ...longTail, ...inactiveProbes];
  const results: CollectResult[] = [];
  onProgress?.(0, targets.length); // emit total up front so the UI shows 0/N immediately
  let rlStreak = 0;
  let failStreak = 0;
  for (let i = 0; i < targets.length; i += 1) {
    if (signal?.aborted) break; // user Stop: exit before the next account
    if (i > 0 && pacing) await abortableSleep(Math.floor(pacing.minMs + Math.random() * (pacing.maxMs - pacing.minMs)), signal);
    if (signal?.aborted) break; // abort during the pacing gap
    const r = await collectAccount(db, source, targets[i]!.username, opts, 'light', signal);
    if (signal?.aborted) break; // stopped mid-scrape: drop this result (partial kept in DB)
    results.push(r);
    onProgress?.(results.length, targets.length);
    // Fire 떡상 alerts per-channel while the reel is freshest, not once at end of a multi-hour
    // pass (see collectMany). Awaited → serialized; notifyNewlyViral is idempotent so it's cheap.
    if (onAfterEach) await onAfterEach(r);
    // not_found/private: neutral to the rl streak, still count toward FAIL_ABORT — see collectMany.
    rlStreak = isInactiveChannelResult(r) ? rlStreak : r.rateLimited ? rlStreak + 1 : 0;
    failStreak = r.status === 'error' ? failStreak + 1 : 0;
    if (rlStreak >= RATE_LIMIT_ABORT || failStreak >= FAIL_ABORT) {
      console.warn(`[light] aborting pass after ${failStreak} consecutive failures (rate-limited streak ${rlStreak}) — collection is failing; backing off instead of deepening the flag`);
      break;
    }
  }
  return results;
}

/** Deep-collect ONE backfill-pending account. Never throws; reports rate-limit so the caller can cool down. */
export async function backfillNext(
  db: Database,
  source: ProfileSource,
  opts: CollectOptions,
): Promise<{ ran: boolean; username?: string; rateLimited: boolean }> {
  const pending = db.accounts.listBackfillPending();
  if (pending.length === 0) return { ran: false, rateLimited: false };
  const acct = pending[0]!;
  try {
    const res = await collectAccount(db, source, acct.username, opts, 'deep');
    if (res.status === 'ok') {
      db.accounts.setBackfillState(acct.id, 'done');
      return { ran: true, username: acct.username, rateLimited: false };
    }
    // collectAccount swallowed the error into status:'error' + recorded the failure +
    // set res.rateLimited (typed, not string-matched). Don't give up on a rate-limit
    // (it's transient); after 3 real failures, mark unavailable so we stop scraping it.
    // An inactive (not_found/private) result already got a stronger state — don't downgrade it.
    if (!res.rateLimited && !isInactiveChannelResult(res) && db.accounts.getById(acct.id)!.consecutiveFailures >= BACKFILL_GIVE_UP) {
      db.accounts.setBackfillState(acct.id, 'unavailable');
    }
    return { ran: true, username: acct.username, rateLimited: res.rateLimited ?? false };
  } catch (err) {
    // collectAccount shouldn't throw, but guard: treat IgRateLimitError as cooldown signal.
    const rateLimited = err instanceof IgRateLimitError;
    return { ran: true, username: acct.username, rateLimited };
  }
}
