import { Agent } from "@atproto/api";
import { BrowserOAuthClient, type OAuthSession } from "@atproto/oauth-client-browser";
import type {
  SessionInfo,
  FeedRequest,
  FeedSignal,
  FeedAuthor,
  FeedImportPayload,
  FeedItem,
  PublishFeedRequest,
  BlueskyList,
  ListMember,
  FollowsAnalysisResult,
  OAuthConfig,
} from "../types";

let sessionInfo: SessionInfo = {
  isAuthenticated: false,
  did: null,
  handle: null,
  pdsUrl: null,
};

let clientPromise: Promise<BrowserOAuthClient> | null = null;
let oauthConfigPromise: Promise<OAuthConfig> | null = null;

async function resolvePdsUrl(did: string): Promise<string | null> {
  try {
    // Resolve DID to get PDS endpoint
    const didDocUrl = did.startsWith("did:plc:")
      ? `https://plc.directory/${did}`
      : `https://resolver.identity.foundation/1.0/identifiers/${did}`;
    
    const response = await fetch(didDocUrl, {
      headers: { Accept: "application/json" },
    });
    
    if (!response.ok) return null;
    
    const doc = await response.json();
    // Look for atproto PDS service
    const services = doc.service || [];
    const pdsService = services.find(
      (s: any) => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer"
    );
    
    return pdsService?.serviceEndpoint || null;
  } catch (err) {
    console.error("[bluesky] Failed to resolve PDS:", err);
    return null;
  }
}

async function toSessionInfo(session: OAuthSession): Promise<SessionInfo> {
  const did = session.sub;
  const handle = session.handle ?? did;
  
  // Try to get PDS from session audience or resolve from DID
  let pdsUrl = session.aud ?? null;
  if (!pdsUrl || pdsUrl.startsWith("did:")) {
    pdsUrl = await resolvePdsUrl(did);
  }
  
  return {
    isAuthenticated: true,
    did,
    handle,
    pdsUrl,
  };
}

async function getAgent() {
  const client = await getClient();

  if (!sessionInfo.did) {
    throw new Error("No active Bluesky session.");
  }

  const session = await client.restore(sessionInfo.did);
  if (!session) {
    throw new Error("Unable to restore Bluesky session.");
  }

  sessionInfo = await toSessionInfo(session);
  return new Agent(session);
}

function isUnauthorizedError(error: unknown) {
  if (!error || typeof error !== "object") {
    return false;
  }

  const maybeStatus = (error as { status?: number }).status;
  if (maybeStatus === 401) {
    return true;
  }

  const maybeMessage = (error as { message?: string }).message;
  return typeof maybeMessage === "string" && maybeMessage.includes("401");
}

async function callWithAuthRetry<T>(operation: (agent: Agent) => Promise<T>) {
  let lastError: unknown;

  for (let attempt = 0; attempt < 2; attempt++) {
    try {
      const agent = await getAgent();
      return await operation(agent);
    } catch (error) {
      lastError = error;
      if (!isUnauthorizedError(error) || attempt === 1) {
        break;
      }

      await new Promise((resolve) => window.setTimeout(resolve, 300));
    }
  }

  throw lastError;
}

async function mapInBatches<TInput, TOutput>(
  items: TInput[],
  batchSize: number,
  mapper: (item: TInput) => Promise<TOutput>
) {
  const output: TOutput[] = [];

  for (let index = 0; index < items.length; index += batchSize) {
    const batch = items.slice(index, index + batchSize);
    const settled = await Promise.allSettled(batch.map(mapper));

    for (const result of settled) {
      if (result.status === "fulfilled") {
        output.push(result.value);
      }
    }
  }

  return output;
}

async function mapInBatchesDetailed<TInput, TOutput>(
  items: TInput[],
  batchSize: number,
  mapper: (item: TInput) => Promise<TOutput>
) {
  const fulfilled: TOutput[] = [];
  const rejected: unknown[] = [];

  for (let index = 0; index < items.length; index += batchSize) {
    const batch = items.slice(index, index + batchSize);
    const settled = await Promise.allSettled(batch.map(mapper));

    for (const result of settled) {
      if (result.status === "fulfilled") {
        fulfilled.push(result.value);
      } else {
        rejected.push(result.reason);
      }
    }
  }

  return { fulfilled, rejected };
}

async function fetchActorLikes(actor: string, maxItems: number) {
  const likedEntries: any[] = [];
  const seenCursors = new Set<string>();
  let cursor: string | undefined;

  while (likedEntries.length < maxItems) {
    const response = await callWithAuthRetry((agent) =>
      agent.app.bsky.feed.getActorLikes({
        actor,
        limit: Math.min(100, maxItems - likedEntries.length),
        cursor,
      })
    );

    likedEntries.push(...response.data.feed);
    cursor = response.data.cursor;

    if (!cursor || seenCursors.has(cursor)) {
      break;
    }
    seenCursors.add(cursor);
  }

  return likedEntries;
}

async function fetchPostLikers(uri: string, maxItems: number) {
  const likes: any[] = [];
  const seenCursors = new Set<string>();
  let cursor: string | undefined;

  while (likes.length < maxItems) {
    const response = await callWithAuthRetry((agent) =>
      agent.app.bsky.feed.getLikes({
        uri,
        limit: Math.min(100, maxItems - likes.length),
        cursor,
      })
    );

    likes.push(...response.data.likes);
    cursor = response.data.cursor;

    if (!cursor || seenCursors.has(cursor)) {
      break;
    }
    seenCursors.add(cursor);
  }

  return likes;
}

function isLoopbackOrigin(origin: string) {
  try {
    const url = new URL(origin);
    const hostname = url.hostname;
    // Standard loopback
    if (hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1" || hostname === "localhost") {
      return true;
    }
    // Any 127.x.x.x address
    if (hostname.startsWith("127.")) {
      return true;
    }
    // 0.0.0.0 (bind all)
    if (hostname === "0.0.0.0") {
      return true;
    }
    // Private IP ranges (common for local dev)
    if (hostname.startsWith("10.") || hostname.startsWith("192.168.")) {
      return true;
    }
    // 172.16-31.x.x
    if (hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)) {
      return true;
    }
    return false;
  } catch {
    return false;
  }
}

async function getOAuthConfig(): Promise<OAuthConfig> {
  if (!oauthConfigPromise) {
    oauthConfigPromise = (async () => {
      const response = await fetch("/api/oauth/config", {
        credentials: "same-origin",
      });

      if (!response.ok) {
        throw new Error("Unable to load Bluesky OAuth configuration.");
      }

      return (await response.json()) as OAuthConfig;
    })();
  }

  return oauthConfigPromise;
}

async function getClient(): Promise<BrowserOAuthClient> {
  if (!clientPromise) {
    clientPromise = (async () => {
      const config = await getOAuthConfig();
      console.log("[oauth] config:", config);
      const loopback = config.allowLocalDev && isLoopbackOrigin(config.currentOrigin);
      console.log("[oauth] loopback check:", config.currentOrigin, "=>", loopback);
      
      const isHttps = config.publicOrigin?.startsWith("https://");
      const isIpAddress = config.publicOrigin?.match(/:\/\/\d+\.\d+\.\d+\.\d+/) || config.publicOrigin?.includes("://localhost");
      
      const clientMetadata =
        isHttps && !isIpAddress
          ? {
              // Production HTTPS web client with valid hostname
              client_id: `${config.publicOrigin}/oauth/client-metadata.json`,
              client_name: "Bluesky Feed Dashboard",
              client_uri: config.publicOrigin,
              redirect_uris: [`${config.publicOrigin}/oauth/callback`],
              application_type: "web" as const,
              grant_types: ["authorization_code", "refresh_token"],
              response_types: ["code"],
              token_endpoint_auth_method: "none" as const,
              scope: "atproto transition:generic",
              dpop_bound_access_tokens: true,
            }
          : loopback
            ? {
                // Loopback native client (HTTP or IP addresses)
                client_id: `http://localhost?redirect_uri=${encodeURIComponent(`${config.currentOrigin}/oauth/callback`)}&scope=${encodeURIComponent("atproto transition:generic")}`,
                client_name: "Bluesky Feed Dashboard (Dev)",
                application_type: "native" as const,
                grant_types: ["authorization_code", "refresh_token"],
                response_types: ["code"],
                redirect_uris: [`${config.currentOrigin}/oauth/callback`],
                token_endpoint_auth_method: "none" as const,
                scope: "atproto transition:generic",
                dpop_bound_access_tokens: true,
              }
            : null;

      console.log("[oauth] clientMetadata:", clientMetadata);

      if (!clientMetadata) {
        throw new Error(
          "Bluesky OAuth requires either a public HTTPS hostname or loopback development on 127.0.0.1/localhost."
        );
      }

      const client = new BrowserOAuthClient({
        clientMetadata,
        handleResolver: "https://bsky.social",
      });

      const result = await client.init();
      console.log("[oauth] client.init() result:", result);
      if (result?.session) {
        sessionInfo = await toSessionInfo(result.session);
        console.log("[oauth] Session restored:", sessionInfo);
      } else {
        console.log("[oauth] No session to restore");
      }

      return client;
    })();
  }

  return clientPromise;
}

function normalizePost(entry: any): FeedItem {
  const post = entry.post ?? entry;
  const author = post.author ?? {};
  const record = post.record ?? {};

  return {
    subjectUri: post.uri,
    authorDid: author.did,
    authorHandle: author.handle,
    authorDisplayName: author.displayName ?? author.handle ?? author.did,
    text: record.text ?? "",
    origin: "app.bsky.feed.getTimeline",
    labels: (post.labels ?? []).map((label: any) => label.val),
    createdAt: record.createdAt ?? post.indexedAt ?? null,
  };
}

function normalizeLikedPost(entry: any): FeedItem {
  const post = entry.post ?? entry;
  const author = post.author ?? {};
  const record = post.record ?? {};
  return {
    subjectUri: post.uri,
    authorDid: author.did,
    authorHandle: author.handle ?? author.did,
    authorDisplayName: author.displayName ?? author.handle ?? author.did,
    text: record.text ?? "",
    origin: "liked-seed",
    labels: (post.labels ?? []).map((label: any) => label.val),
    createdAt: record.createdAt ?? post.indexedAt ?? null,
  };
}

async function fetchAuthorPosts(author: FeedAuthor & { source: "first-order" | "second-order" }, limit: number) {
  const authorFeed = await callWithAuthRetry((agent) =>
    agent.app.bsky.feed.getAuthorFeed({
      actor: author.did,
      filter: "posts_no_replies",
      limit,
    })
  );

  return authorFeed.data.feed.map((entry: any) => ({
    ...normalizePost(entry),
    origin:
      author.source === "first-order"
        ? `liked-author:${author.handle}`
        : `second-order:${author.handle}`,
  }));
}

function isAtUri(value: string | undefined) {
  return !!value && value.startsWith("at://");
}

export const blueskyService = {
  async getSession(): Promise<SessionInfo> {
    await getClient();
    console.log("[blueskyService] getSession returning:", sessionInfo);
    return sessionInfo;
  },

  async beginLogin(identifier: string): Promise<void> {
    if (!identifier) {
      return;
    }

    const client = await getClient();
    void client.signIn(identifier, { scope: "atproto transition:generic" });
  },

  async fetchFeed(request: FeedRequest): Promise<FeedItem[]> {
    const source = request.source?.trim();

    if (!source || source === "home") {
      const timeline = await callWithAuthRetry((agent) =>
        agent.app.bsky.feed.getTimeline({ limit: request.limit })
      );
      return timeline.data.feed.map(normalizePost);
    }

    if (isAtUri(source)) {
      const feed = await callWithAuthRetry((agent) =>
        agent.app.bsky.feed.getFeed({ feed: source, limit: request.limit })
      );
      return feed.data.feed.map(normalizePost);
    }

    const authorFeed = await callWithAuthRetry((agent) =>
      agent.app.bsky.feed.getAuthorFeed({ actor: source, limit: request.limit })
    );
    return authorFeed.data.feed.map(normalizePost);
  },

  async fetchLikedNetwork(): Promise<FeedImportPayload> {
    const actor = sessionInfo.did;
    if (!actor) {
      throw new Error("No active Bluesky session.");
    }
    console.log("[import] Starting liked-network crawl for", actor);
    const t0 = performance.now();

    console.log("[import] Fetching actor likes (max 300)…");
    const likedEntries = await fetchActorLikes(actor, 300);
    console.log("[import] Got", likedEntries.length, "liked entries in", Math.round(performance.now() - t0), "ms");

    const likedPosts = likedEntries.map(normalizeLikedPost);
    const seedItems = likedPosts.map((liked) => ({
      subjectUri: liked.subjectUri,
      authorDid: liked.authorDid,
      authorHandle: liked.authorHandle,
      authorDisplayName: liked.authorDisplayName,
      text: liked.text,
      origin: "liked-seed",
      labels: liked.labels,
      createdAt: liked.createdAt,
    }));
    const authorMap = new Map<string, FeedAuthor>();

    for (const liked of likedPosts) {
      const existing = authorMap.get(liked.authorDid);
      authorMap.set(liked.authorDid, {
        did: liked.authorDid,
        handle: liked.authorHandle,
        displayName: liked.authorDisplayName,
        likeCount: (existing?.likeCount ?? 0) + 1,
        source: "first-order",
      });
    }

    const topAuthors = [...authorMap.values()]
      .sort((left, right) => right.likeCount - left.likeCount)
      .slice(0, 25);

    const firstOrderAuthors: FeedAuthor[] = topAuthors.map((author) => ({ ...author, source: "first-order" }));
    console.log("[import] Top first-order authors:", firstOrderAuthors.length);

    const secondOrderSeedPosts = seedItems.slice(0, 40);
    console.log("[import] Crawling second-order likers for", secondOrderSeedPosts.length, "seed posts…");
    const secondOrderResults = await mapInBatchesDetailed(secondOrderSeedPosts, 4, async (seedItem) => {
      const likes = await fetchPostLikers(seedItem.subjectUri, 40);
      return likes.map((like: any) => ({
        seedPostUri: seedItem.subjectUri,
        seedWeight: 1,
        actor: like.actor,
      }));
    });
    const secondOrderLikes = secondOrderResults.fulfilled;
    console.log("[import] Second-order: fulfilled =", secondOrderLikes.length, "rejected =", secondOrderResults.rejected.length, "in", Math.round(performance.now() - t0), "ms");

    if (secondOrderResults.rejected.length > 0) {
      const errorSummary = secondOrderResults.rejected
        .slice(0, 3)
        .map((error) => (error instanceof Error ? error.message : String(error)))
        .join(" | ");
      throw new Error(
        `Second-order co-liker crawl failed for ${secondOrderResults.rejected.length} seed post(s). ` +
          `Seed count=${secondOrderSeedPosts.length}. Errors: ${errorSummary}`
      );
    }

    const rawSecondOrderRows = secondOrderLikes.flat();
    if (secondOrderSeedPosts.length > 0 && rawSecondOrderRows.length === 0) {
      throw new Error(
        `Second-order co-liker crawl returned zero rows. ` +
          `Seed posts=${secondOrderSeedPosts.length}`
      );
    }

    const secondOrderAuthorMap = new Map<string, FeedAuthor>();
    let droppedAsSelf = 0;
    let droppedAsAlreadyFirstOrder = 0;
    for (const discovered of rawSecondOrderRows) {
      const likedActor = discovered.actor ?? {};
      const likedActorDid = likedActor.did;
      if (!likedActorDid) {
        continue;
      }

      if (likedActorDid === actor) {
        droppedAsSelf += 1;
        continue;
      }

      if (authorMap.has(likedActorDid)) {
        droppedAsAlreadyFirstOrder += 1;
        continue;
      }

      const existing = secondOrderAuthorMap.get(likedActorDid);
      secondOrderAuthorMap.set(likedActorDid, {
        did: likedActorDid,
        handle: likedActor.handle ?? likedActorDid,
        displayName: likedActor.displayName ?? likedActor.handle ?? likedActorDid,
        likeCount: (existing?.likeCount ?? 0) + discovered.seedWeight,
        source: "second-order",
      });
    }

    const secondOrderAuthors = [...secondOrderAuthorMap.values()]
      .sort((left, right) => right.likeCount - left.likeCount)
      .slice(0, 30);
    console.log("[import] Second-order authors:", secondOrderAuthors.length);

    if (rawSecondOrderRows.length > 0 && secondOrderAuthors.length === 0) {
      throw new Error(
        `Second-order crawl collapsed to zero authors after filtering. ` +
          `Raw rows=${rawSecondOrderRows.length}, dropped as self=${droppedAsSelf}, dropped as already-first-order=${droppedAsAlreadyFirstOrder}.`
      );
    }

    console.log("[import] Fetching author feeds (first-order:", firstOrderAuthors.length, "second-order:", secondOrderAuthors.length, ")…");
    const fetchedAuthorFeeds = await mapInBatches(firstOrderAuthors, 4, async (author) =>
      fetchAuthorPosts(author, 6)
    );
    const fetchedSecondOrderFeeds = await mapInBatches(secondOrderAuthors, 4, async (author) =>
      fetchAuthorPosts(author, 4)
    );

    const feedItems = [...fetchedAuthorFeeds.flat(), ...fetchedSecondOrderFeeds.flat()];

    const deduped = Array.from(new Map(feedItems.map((item) => [item.subjectUri, item])).values());
    const combinedAuthors = [...firstOrderAuthors, ...secondOrderAuthors].sort((left, right) => right.likeCount - left.likeCount);
    const result = {
      account: {
        did: sessionInfo.did!,
        handle: sessionInfo.handle ?? sessionInfo.did!,
        pdsUrl: sessionInfo.pdsUrl,
      },
      authors: combinedAuthors,
      items: deduped,
      seedItems,
    } satisfies FeedImportPayload;
    const jsonSize = new Blob([JSON.stringify(result)]).size;
    console.log("[import] Crawl complete:", deduped.length, "posts,", combinedAuthors.length, "authors in", Math.round(performance.now() - t0), "ms — payload ~" + Math.round(jsonSize / 1024) + " KB");
    return result;
  },

  async writeSignal(signal: FeedSignal): Promise<void> {
    await callWithAuthRetry((agent) =>
      agent.com.atproto.repo.putRecord({
        repo: agent.assertDid,
        collection: signal.collection,
        rkey: crypto.randomUUID(),
        record: {
          $type: signal.collection,
          subjectUri: signal.subjectUri,
          signal: signal.signal,
          weight: signal.weight,
          generatorId: signal.generatorId,
          metadata: signal.metadata,
          createdAt: signal.createdAt,
        },
      })
    );
  },

  async publishFeedGenerator(request: PublishFeedRequest): Promise<void> {
    await callWithAuthRetry((agent) =>
      agent.com.atproto.repo.putRecord({
        repo: request.ownerDid,
        collection: "app.bsky.feed.generator",
        rkey: request.feedKey,
        record: {
          $type: "app.bsky.feed.generator",
          did: request.serviceDid,
          displayName: request.displayName,
          description: request.description,
          createdAt: new Date().toISOString(),
        },
      })
    );
  },

  async fetchAuthorFollows(actor: string, maxFollows: number) {
    const follows: any[] = [];
    const seenCursors = new Set<string>();
    let cursor: string | undefined;

    while (follows.length < maxFollows) {
      const response = await callWithAuthRetry((agent) =>
        agent.app.bsky.graph.getFollows({
          actor,
          limit: Math.min(100, maxFollows - follows.length),
          cursor,
        })
      );

      follows.push(...(response.data.follows || []));
      cursor = response.data.cursor;

      if (!cursor || seenCursors.has(cursor)) {
        break;
      }
      seenCursors.add(cursor);
    }

    return follows;
  },

  async fetchUserLists(): Promise<BlueskyList[]> {
    const actor = sessionInfo.did;
    if (!actor) {
      throw new Error("No active Bluesky session.");
    }

    console.log("[lists] Fetching lists for", actor);
    const lists: BlueskyList[] = [];
    const seenCursors = new Set<string>();
    let cursor: string | undefined;

    while (true) {
      const response = await callWithAuthRetry((agent) =>
        agent.app.bsky.graph.getLists({
          actor,
          limit: 100,
          cursor,
        })
      );

      const listsData = response.data.lists || [];
      for (const list of listsData) {
        lists.push({
          uri: list.uri,
          name: list.name,
          description: list.description,
          creator: {
            did: list.creator.did,
            handle: list.creator.handle,
            displayName: list.creator.displayName,
          },
          indexedAt: list.indexedAt,
        });
      }

      cursor = response.data.cursor;
      if (!cursor || seenCursors.has(cursor)) {
        break;
      }
      seenCursors.add(cursor);
    }

    console.log("[lists] Found", lists.length, "lists");
    return lists;
  },

  async fetchListMembers(listUri: string, maxItems: number): Promise<ListMember[]> {
    console.log("[lists] Fetching members for", listUri);
    const members: ListMember[] = [];
    const seenCursors = new Set<string>();
    let cursor: string | undefined;

    while (members.length < maxItems) {
      const response = await callWithAuthRetry((agent) =>
        agent.app.bsky.graph.getList({
          list: listUri,
          limit: Math.min(100, maxItems - members.length),
          cursor,
        })
      );

      const items = response.data.items || [];
      for (const item of items) {
        if (item.subject) {
          members.push({
            did: item.subject.did,
            handle: item.subject.handle,
            displayName: item.subject.displayName || item.subject.handle,
          });
        }
      }

      cursor = response.data.cursor;
      if (!cursor || seenCursors.has(cursor)) {
        break;
      }
      seenCursors.add(cursor);
    }

    console.log("[lists] Found", members.length, "members");
    return members;
  },

  async analyzeListFollows(request: { listUri?: string; maxFollowsPerUser: number }): Promise<FollowsAnalysisResult> {
    const actor = sessionInfo.did;
    if (!actor) {
      throw new Error("No active Bluesky session.");
    }

    console.log("[list-analysis] Starting follows analysis for", actor, request.listUri ? `list: ${request.listUri}` : "liked network");
    const t0 = performance.now();

    let sourceAuthors: { did: string; handle: string; displayName: string }[];

    if (request.listUri) {
      // Fetch members from a specific list
      console.log("[list-analysis] Fetching list members...");
      const members = await this.fetchListMembers(request.listUri, 200);
      sourceAuthors = members.map(m => ({
        did: m.did,
        handle: m.handle,
        displayName: m.displayName,
      }));
      console.log("[list-analysis] Found", sourceAuthors.length, "list members");
    } else {
      // Fall back to imported liked network - need to fetch it first
      console.log("[list-analysis] Fetching liked network for analysis...");
      const likedNetwork = await this.fetchLikedNetwork();
      sourceAuthors = likedNetwork.authors
        .filter(a => a.source === "first-order")
        .map(a => ({
          did: a.did,
          handle: a.handle,
          displayName: a.displayName,
        }));
      console.log("[list-analysis] Found", sourceAuthors.length, "source authors from liked network");
    }

    if (sourceAuthors.length === 0) {
      return {
        sourceCount: 0,
        analyzedCount: 0,
        topFollowed: [],
      };
    }

    // Fetch follows for each source author
    console.log("[list-analysis] Fetching follows for each source author...");
    const followsResults = await mapInBatchesDetailed(
      sourceAuthors,
      3,
      async (author) => {
        const follows = await this.fetchAuthorFollows(author.did, request.maxFollowsPerUser);
        return {
          sourceDid: author.did,
          sourceHandle: author.handle,
          follows: follows.map((f: any) => ({
            did: f.did,
            handle: f.handle,
            displayName: f.displayName || f.handle,
          })),
        };
      }
    );

    console.log(
      "[list-analysis] Fetched follows:",
      followsResults.fulfilled.length,
      "succeeded,",
      followsResults.rejected.length,
      "failed in",
      Math.round(performance.now() - t0),
      "ms"
    );

    // Aggregate follows and count mutuals
    const followedMap = new Map<string, { did: string; handle: string; displayName: string; followerCount: number; followedBy: string[] }>();

    for (const result of followsResults.fulfilled) {
      for (const followed of result.follows) {
        const existing = followedMap.get(followed.did);
        if (existing) {
          existing.followerCount++;
          if (!existing.followedBy.includes(result.sourceHandle)) {
            existing.followedBy.push(result.sourceHandle);
          }
        } else {
          followedMap.set(followed.did, {
            did: followed.did,
            handle: followed.handle,
            displayName: followed.displayName,
            followerCount: 1,
            followedBy: [result.sourceHandle],
          });
        }
      }
    }

    // Sort by follower count and take top 50
    const topFollowed = [...followedMap.values()]
      .sort((a, b) => b.followerCount - a.followerCount)
      .slice(0, 50);

    console.log(
      "[list-analysis] Analysis complete:",
      followedMap.size,
      "unique follows found,",
      topFollowed.length,
      "top followed in",
      Math.round(performance.now() - t0),
      "ms"
    );

    return {
      sourceCount: sourceAuthors.length,
      analyzedCount: followsResults.fulfilled.length,
      topFollowed,
    };
  },
};
An unhandled error has occurred. Reload 🗙