#!/usr/bin/env npx tsx
/**
 * Social Cache Updater
 *
 * Finds social accounts (Mastodon, Twitter, Bluesky) for GitHub stargazers using multiple strategies:
 * 1. Manual overrides (highest priority)
 * 2. Parse GitHub bio for handles
 * 3. Parse GitHub blog URL for profile links
 * 4. Search username on known platforms
 *
 * Supports incremental updates (only new stargazers) and weekly full refresh.
 */

import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";

// ES Module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// ─────────────────────────────────────────────────────────────────────────────
// Configuration
// ─────────────────────────────────────────────────────────────────────────────

const OWNER = "phranck";
const REPO = "TUIkit";
const GITHUB_API = "https://api.github.com";

/** Known Mastodon instances to search for username matches. */
const KNOWN_MASTODON_INSTANCES = [
  "mastodon.social",
  "mastodon.online",
  "mastodon.world",
  "mstdn.social",
  "mas.to",
  "fosstodon.org",
  "hachyderm.io",
  "infosec.exchange",
  "techhub.social",
  "iosdev.space",
  "indieweb.social",
  "chaos.social",
  "ruby.social",
  "phpc.social",
  "social.linux.pizza",
  "toot.community",
  "det.social",
  "mastodon.art",
  "social.coop",
  "aus.social",
  "nrw.social",
];

/** Delay between API requests to avoid rate limiting (ms). */
const REQUEST_DELAY = 200;

// ─────────────────────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────────────────────

interface SocialInfo {
  handle: string;
  url: string;
  source: "github" | "bio" | "blog" | "username-match" | "manual" | "keybase" | "aboutme" | "linktree";
  verified: boolean;
}

interface SocialCacheEntry {
  login: string;
  mastodon?: SocialInfo;
  twitter?: SocialInfo;
  bluesky?: SocialInfo;
  updatedAt: string;
}

interface SocialCache {
  generatedAt: string | null;
  entries: Record<string, SocialCacheEntry>;
}

interface SocialOverrides {
  overrides: Record<string, {
    mastodon?: { handle: string; url: string };
    twitter?: { handle: string; url: string };
    bluesky?: { handle: string; url: string };
  }>;
}

interface GitHubUser {
  login: string;
  avatar_url: string;
  html_url: string;
  bio?: string | null;
  blog?: string | null;
  twitter_username?: string | null;
}

interface GitHubStargazer {
  login: string;
  avatar_url: string;
  html_url: string;
}

/**
 * Domains that use `/@username` URL patterns but are NOT Mastodon instances.
 * Prevents false positive matches from link-in-bio services and social platforms.
 */
const NON_MASTODON_DOMAINS = [
  "twitter.com", "x.com", "github.com", "linkedin.com",
  "facebook.com", "instagram.com", "youtube.com", "reddit.com",
  "bento.me", "linktr.ee", "carrd.co", "bio.link", "beacons.ai",
  "campsite.bio", "solo.to", "tap.bio", "withkoji.com", "milkshake.app",
  "later.com", "snipfeed.co", "hoo.be", "allmylinks.com", "lnk.bio",
  "medium.com", "dev.to", "hashnode.dev", "substack.com",
  "codepen.io", "dribbble.com", "behance.net",
  "threads.net", "bsky.app",
];

/** Cache for validated Mastodon/ActivityPub instances (domain → boolean). */
const mastodonInstanceCache = new Map<string, boolean>();

/**
 * Checks whether a domain is a Mastodon/ActivityPub instance by querying its
 * `.well-known/nodeinfo` endpoint. Results are cached for the script lifetime.
 */
async function isMastodonInstance(domain: string): Promise<boolean> {
  if (NON_MASTODON_DOMAINS.some((blocked) => domain.includes(blocked))) return false;
  if (KNOWN_MASTODON_INSTANCES.includes(domain)) return true;

  const cached = mastodonInstanceCache.get(domain);
  if (cached !== undefined) return cached;

  try {
    const response = await fetch(`https://${domain}/.well-known/nodeinfo`, {
      headers: { "User-Agent": "TUIKit-Social-Lookup" },
      signal: AbortSignal.timeout(5000),
      redirect: "follow",
    });
    if (!response.ok) {
      mastodonInstanceCache.set(domain, false);
      return false;
    }
    const data = (await response.json()) as { links?: Array<{ rel: string }> };
    // NodeInfo is used by Mastodon, Pleroma, Misskey, etc.
    const isValid = Array.isArray(data.links) && data.links.some(
      (link) => link.rel?.includes("nodeinfo"),
    );
    mastodonInstanceCache.set(domain, isValid);
    if (isValid) {
      console.log(`    Validated ${domain} as ActivityPub instance ✓`);
    }
    return isValid;
  } catch {
    mastodonInstanceCache.set(domain, false);
    return false;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Avatar Comparison
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Fetches an image and returns a hash for comparison.
 * Uses djb2 algorithm with file size for better collision resistance.
 */
async function fetchAvatarHash(url: string): Promise<string | null> {
  try {
    const response = await fetch(url, {
      headers: { "User-Agent": "TUIKit-Social-Lookup" },
      signal: AbortSignal.timeout(5000),
    });
    if (!response.ok) return null;

    const buffer = await response.arrayBuffer();
    const bytes = new Uint8Array(buffer);

    // djb2 hash with denser sampling for better uniqueness
    let hash = 5381;
    for (let i = 0; i < bytes.length; i += 50) {
      hash = ((hash << 5) + hash) ^ bytes[i];
    }
    // Include file size for additional collision resistance
    return `${bytes.length.toString(16)}-${(hash >>> 0).toString(16)}`;
  } catch {
    return null;
  }
}

/**
 * Compares two avatar URLs to check if they're likely the same image.
 * Returns true if avatars match, false otherwise.
 */
async function avatarsMatch(url1: string, url2: string): Promise<boolean> {
  const [hash1, hash2] = await Promise.all([
    fetchAvatarHash(url1),
    fetchAvatarHash(url2),
  ]);

  if (!hash1 || !hash2) return false;
  return hash1 === hash2;
}

// ─────────────────────────────────────────────────────────────────────────────
// Profile Lookup Services
// ─────────────────────────────────────────────────────────────────────────────

interface ProfileSocialAccounts {
  mastodon?: SocialInfo;
  twitter?: SocialInfo;
  bluesky?: SocialInfo;
}

// ─────────────────────────────────────────────────────────────────────────────
// About.me Profile Lookup
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Fetches social accounts from about.me profile by username.
 * Scrapes the profile page for social links.
 */
async function fetchSocialFromAboutMe(username: string): Promise<ProfileSocialAccounts> {
  const accounts: ProfileSocialAccounts = {};

  try {
    const url = `https://about.me/${username}`;
    const response = await fetch(url, {
      headers: { "User-Agent": "TUIKit-Social-Lookup" },
      redirect: "follow",
      signal: AbortSignal.timeout(5000),
    });

    if (!response.ok) return accounts;

    const html = await response.text();

    // Look for Twitter/X links
    const twitterMatch = html.match(/href="https?:\/\/(?:twitter\.com|x\.com)\/([a-zA-Z0-9_]+)"/i);
    if (twitterMatch) {
      accounts.twitter = {
        handle: `@${twitterMatch[1]}`,
        url: `https://x.com/${twitterMatch[1]}`,
        source: "aboutme",
        verified: true,
      };
      console.log(`    Found Twitter from about.me: @${twitterMatch[1]}`);
    }

    // Look for Mastodon links (exclude twitter/x.com false positives)
    const mastodonMatch = html.match(/href="(https?:\/\/([a-zA-Z0-9.-]+)\/@([a-zA-Z0-9_]+))"/i);
    if (mastodonMatch && !mastodonMatch[2].includes("twitter") && !mastodonMatch[2].includes("x.com")) {
      accounts.mastodon = {
        handle: `@${mastodonMatch[3]}@${mastodonMatch[2]}`,
        url: mastodonMatch[1],
        source: "aboutme",
        verified: true,
      };
      console.log(`    Found Mastodon from about.me: @${mastodonMatch[3]}@${mastodonMatch[2]}`);
    }

    // Look for Bluesky links
    const bskyMatch = html.match(/href="https?:\/\/bsky\.app\/profile\/([^"]+)"/i);
    if (bskyMatch) {
      accounts.bluesky = {
        handle: `@${bskyMatch[1]}`,
        url: `https://bsky.app/profile/${bskyMatch[1]}`,
        source: "aboutme",
        verified: true,
      };
      console.log(`    Found Bluesky from about.me: @${bskyMatch[1]}`);
    }
  } catch {
    // about.me lookup failed, continue
  }

  await new Promise((resolve) => setTimeout(resolve, REQUEST_DELAY));
  return accounts;
}

// ─────────────────────────────────────────────────────────────────────────────
// Keybase Profile Lookup
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Fetches social accounts from Keybase by GitHub username.
 * Keybase has cryptographically verified identities.
 */
async function fetchSocialFromKeybase(githubUsername: string): Promise<ProfileSocialAccounts> {
  const accounts: ProfileSocialAccounts = {};

  try {
    // Keybase API to find user by GitHub proof
    const url = `https://keybase.io/_/api/1.0/user/lookup.json?github=${githubUsername}`;
    const response = await fetch(url, {
      headers: { "User-Agent": "TUIKit-Social-Lookup" },
      signal: AbortSignal.timeout(5000),
    });

    if (!response.ok) return accounts;

    const data = (await response.json()) as {
      them?: Array<{
        proofs_summary?: {
          all?: Array<{
            proof_type: string;
            nametag: string;
            service_url: string;
          }>;
        };
      }>;
    };

    const proofs = data.them?.[0]?.proofs_summary?.all;
    if (!proofs) return accounts;

    for (const proof of proofs) {
      if (proof.proof_type === "twitter") {
        accounts.twitter = {
          handle: `@${proof.nametag}`,
          url: proof.service_url || `https://x.com/${proof.nametag}`,
          source: "keybase",
          verified: true, // Keybase proofs are cryptographically verified!
        };
        console.log(`    Found Twitter from Keybase: @${proof.nametag} (verified ✓)`);
      }

      if (proof.proof_type === "mastodon" || proof.proof_type.includes("mastodon")) {
        const mastodonMatch = proof.service_url?.match(/https?:\/\/([^/]+)\/@?([^/]+)/);
        if (mastodonMatch) {
          accounts.mastodon = {
            handle: `@${mastodonMatch[2]}@${mastodonMatch[1]}`,
            url: proof.service_url,
            source: "keybase",
            verified: true,
          };
          console.log(`    Found Mastodon from Keybase: @${mastodonMatch[2]}@${mastodonMatch[1]} (verified ✓)`);
        }
      }
    }
  } catch {
    // Keybase lookup failed, continue
  }

  await new Promise((resolve) => setTimeout(resolve, REQUEST_DELAY));
  return accounts;
}

// ─────────────────────────────────────────────────────────────────────────────
// Linktree Profile Lookup
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Fetches social accounts from Linktree by username.
 * Scrapes the public profile page for social links.
 */
async function fetchSocialFromLinktree(username: string): Promise<ProfileSocialAccounts> {
  const accounts: ProfileSocialAccounts = {};

  try {
    const url = `https://linktr.ee/${username}`;
    const response = await fetch(url, {
      headers: { "User-Agent": "TUIKit-Social-Lookup" },
      redirect: "follow",
      signal: AbortSignal.timeout(5000),
    });

    if (!response.ok) return accounts;

    const html = await response.text();

    // Look for Twitter/X links
    const twitterMatch = html.match(/href="https?:\/\/(?:twitter\.com|x\.com)\/([a-zA-Z0-9_]+)"/i);
    if (twitterMatch) {
      accounts.twitter = {
        handle: `@${twitterMatch[1]}`,
        url: `https://x.com/${twitterMatch[1]}`,
        source: "linktree",
        verified: true,
      };
      console.log(`    Found Twitter from Linktree: @${twitterMatch[1]}`);
    }

    // Look for Mastodon links (various instances)
    const mastodonMatch = html.match(/href="(https?:\/\/([a-zA-Z0-9.-]+)\/@([a-zA-Z0-9_]+))"/i);
    if (mastodonMatch && !mastodonMatch[2].includes("twitter") && !mastodonMatch[2].includes("x.com")) {
      accounts.mastodon = {
        handle: `@${mastodonMatch[3]}@${mastodonMatch[2]}`,
        url: mastodonMatch[1],
        source: "linktree",
        verified: true,
      };
      console.log(`    Found Mastodon from Linktree: @${mastodonMatch[3]}@${mastodonMatch[2]}`);
    }

    // Look for Bluesky links
    const bskyMatch = html.match(/href="https?:\/\/bsky\.app\/profile\/([^"]+)"/i);
    if (bskyMatch) {
      accounts.bluesky = {
        handle: `@${bskyMatch[1]}`,
        url: `https://bsky.app/profile/${bskyMatch[1]}`,
        source: "linktree",
        verified: true,
      };
      console.log(`    Found Bluesky from Linktree: @${bskyMatch[1]}`);
    }
  } catch {
    // Linktree lookup failed, continue
  }

  await new Promise((resolve) => setTimeout(resolve, REQUEST_DELAY));
  return accounts;
}

// ─────────────────────────────────────────────────────────────────────────────
// GitHub API
// ─────────────────────────────────────────────────────────────────────────────

function getGitHubHeaders(): Record<string, string> {
  const headers: Record<string, string> = {
    Accept: "application/vnd.github+json",
    "User-Agent": "TUIKit-Social-Lookup",
  };
  const token = process.env.GITHUB_TOKEN;
  if (token) {
    headers.Authorization = `Bearer ${token}`;
  }
  return headers;
}

async function fetchStargazers(): Promise<GitHubStargazer[]> {
  const stargazers: GitHubStargazer[] = [];
  let page = 1;
  const perPage = 100;

  while (true) {
    const url = `${GITHUB_API}/repos/${OWNER}/${REPO}/stargazers?per_page=${perPage}&page=${page}`;
    const response = await fetch(url, { headers: getGitHubHeaders() });

    if (!response.ok) {
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const data = (await response.json()) as GitHubStargazer[];
    stargazers.push(...data);

    if (data.length < perPage) break;
    page++;
  }

  console.log(`Fetched ${stargazers.length} stargazers from GitHub`);
  return stargazers;
}

async function fetchUserDetails(login: string): Promise<GitHubUser | null> {
  try {
    const url = `${GITHUB_API}/users/${login}`;
    const response = await fetch(url, { headers: getGitHubHeaders() });

    if (!response.ok) {
      console.warn(`  Failed to fetch user ${login}: ${response.status}`);
      return null;
    }

    return (await response.json()) as GitHubUser;
  } catch (error) {
    console.warn(`  Error fetching user ${login}:`, error);
    return null;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// GitHub Social Accounts API
// ─────────────────────────────────────────────────────────────────────────────

interface GitHubSocialAccount {
  provider: string;
  url: string;
}

/**
 * Fetches social accounts from GitHub's `/users/{login}/social_accounts` endpoint.
 * These are user-provided and authoritative: highest trust after manual overrides.
 */
async function fetchGitHubSocialAccounts(login: string): Promise<ProfileSocialAccounts> {
  const accounts: ProfileSocialAccounts = {};

  try {
    const url = `${GITHUB_API}/users/${login}/social_accounts`;
    const response = await fetch(url, {
      headers: getGitHubHeaders(),
      signal: AbortSignal.timeout(5000),
    });

    if (!response.ok) return accounts;

    const data = (await response.json()) as GitHubSocialAccount[];

    for (const entry of data) {
      if (entry.provider === "twitter" && !accounts.twitter) {
        const handle = entry.url.match(/(?:twitter\.com|x\.com)\/([a-zA-Z0-9_]+)/i);
        if (handle) {
          accounts.twitter = {
            handle: `@${handle[1]}`,
            url: `https://x.com/${handle[1]}`,
            source: "github",
            verified: true,
          };
          console.log(`    Found Twitter from GitHub social accounts: @${handle[1]}`);
        }
      }

      if (entry.provider === "mastodon" && !accounts.mastodon) {
        const match = entry.url.match(/https?:\/\/([^/]+)\/@?([^/]+)/);
        if (match) {
          accounts.mastodon = {
            handle: `@${match[2]}@${match[1]}`,
            url: entry.url,
            source: "github",
            verified: true,
          };
          console.log(`    Found Mastodon from GitHub social accounts: @${match[2]}@${match[1]}`);
        }
      }

      if (entry.provider === "bluesky" && !accounts.bluesky) {
        const match = entry.url.match(/bsky\.app\/profile\/([a-zA-Z0-9.-]+)/);
        if (match) {
          accounts.bluesky = {
            handle: `@${match[1]}`,
            url: `https://bsky.app/profile/${match[1]}`,
            source: "github",
            verified: true,
          };
          console.log(`    Found Bluesky from GitHub social accounts: @${match[1]}`);
        }
      }
    }
  } catch {
    // Social accounts endpoint failed, continue
  }

  return accounts;
}

// ─────────────────────────────────────────────────────────────────────────────
// Social Detection - Mastodon
// ─────────────────────────────────────────────────────────────────────────────

/** Matches @username@instance.tld or username@instance.tld in text. */
const MASTODON_HANDLE_REGEX = /@?([a-zA-Z0-9_]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;

/** Matches Mastodon profile URLs. */
const MASTODON_URL_REGEX = /https?:\/\/([a-zA-Z0-9.-]+)\/(@|users\/)?([a-zA-Z0-9_]+)\/?$/i;

async function parseMastodonFromBio(bio: string): Promise<SocialInfo | null> {
  const matches = [...bio.matchAll(MASTODON_HANDLE_REGEX)];
  if (matches.length === 0) return null;

  const [fullMatch, username, instance] = matches[0];
  const hasLeadingAt = fullMatch.startsWith("@") && fullMatch.indexOf("@", 1) > 0;

  // Bare `user@domain` without leading `@` is almost always an email.
  // Only accept it if the domain is a validated ActivityPub instance.
  if (!hasLeadingAt) {
    const isInstance = await isMastodonInstance(instance);
    if (!isInstance) {
      console.log(`    Skipping bio match ${username}@${instance} (no leading @, not a known instance)`);
      return null;
    }
  } else {
    // Even with leading `@`, validate the domain
    const isInstance = await isMastodonInstance(instance);
    if (!isInstance) {
      console.log(`    Skipping bio match @${username}@${instance} (domain is not an ActivityPub instance)`);
      return null;
    }
  }

  return {
    handle: `@${username}@${instance}`,
    url: `https://${instance}/@${username}`,
    source: "bio",
    verified: false,
  };
}

async function parseMastodonFromBlog(blog: string): Promise<SocialInfo | null> {
  if (!blog) return null;

  const match = blog.match(MASTODON_URL_REGEX);
  if (!match) return null;

  const [, instance, , username] = match;

  // Validate the domain is actually an ActivityPub instance
  const isInstance = await isMastodonInstance(instance);
  if (!isInstance) {
    console.log(`    Skipping blog URL match ${instance}/@${username} (not an ActivityPub instance)`);
    return null;
  }

  return {
    handle: `@${username}@${instance}`,
    url: `https://${instance}/@${username}`,
    source: "blog",
    verified: false,
  };
}

async function searchMastodonByUsername(username: string, githubAvatarUrl: string): Promise<SocialInfo | null> {
  for (const instance of KNOWN_MASTODON_INSTANCES) {
    try {
      const url = `https://${instance}/api/v1/accounts/lookup?acct=${username}`;
      const response = await fetch(url, {
        headers: { "User-Agent": "TUIKit-Social-Lookup" },
        signal: AbortSignal.timeout(5000),
      });

      if (response.ok) {
        const data = (await response.json()) as { username: string; url: string; avatar: string };

        // Avatar match is a bonus signal: if it matches, we're confident immediately
        const avatarMatch = await avatarsMatch(githubAvatarUrl, data.avatar);
        if (avatarMatch) {
          console.log(`    Found Mastodon @${username} on ${instance} (avatar verified ✓)`);
          return {
            handle: `@${data.username}@${instance}`,
            url: data.url,
            source: "username-match",
            verified: true,
          };
        }

        // No avatar match: accept as unverified candidate.
        // Cross-verification will check for back-links and either verify or remove it.
        console.log(`    Found Mastodon @${username} on ${instance} (pending cross-verification)`);
        return {
          handle: `@${data.username}@${instance}`,
          url: data.url,
          source: "username-match",
          verified: false,
        };
      }
    } catch {
      // Instance unreachable or rate limited, continue
    }

    await new Promise((resolve) => setTimeout(resolve, REQUEST_DELAY));
  }

  return null;
}

// ─────────────────────────────────────────────────────────────────────────────
// Social Detection - Twitter/X
// ─────────────────────────────────────────────────────────────────────────────

/** Matches Twitter/X profile URLs. */
const TWITTER_URL_REGEX = /https?:\/\/(?:twitter\.com|x\.com)\/([a-zA-Z0-9_]+)\/?$/i;

function parseTwitterFromBio(bio: string): SocialInfo | null {
  // Look for explicit Twitter mentions or X.com links
  const urlMatch = bio.match(TWITTER_URL_REGEX);
  if (urlMatch) {
    const username = urlMatch[1];
    return {
      handle: `@${username}`,
      url: `https://x.com/${username}`,
      source: "bio",
      verified: false,
    };
  }
  return null;
}

function parseTwitterFromBlog(blog: string): SocialInfo | null {
  if (!blog) return null;

  const match = blog.match(TWITTER_URL_REGEX);
  if (!match) return null;

  const username = match[1];
  return {
    handle: `@${username}`,
    url: `https://x.com/${username}`,
    source: "blog",
    verified: false,
  };
}

function getTwitterFromGitHubProfile(user: GitHubUser): SocialInfo | undefined {
  if (user.twitter_username) {
    console.log(`  Found Twitter from GitHub profile: @${user.twitter_username}`);
    return {
      handle: `@${user.twitter_username}`,
      url: `https://x.com/${user.twitter_username}`,
      source: "github",
      verified: true, // GitHub's twitter_username field is user-provided and authoritative
    };
  }
  return undefined;
}

// ─────────────────────────────────────────────────────────────────────────────
// Social Detection - Bluesky
// ─────────────────────────────────────────────────────────────────────────────

/** Matches Bluesky handles like @user.bsky.social or user.bsky.social in text. */
const BLUESKY_HANDLE_REGEX = /@?([a-zA-Z0-9.-]+\.bsky\.social)/gi;

/** Matches Bluesky profile URLs. */
const BLUESKY_URL_REGEX = /https?:\/\/bsky\.app\/profile\/([a-zA-Z0-9.-]+)/i;

function parseBlueskyFromBio(bio: string): SocialInfo | null {
  // Check for bsky.app URLs first
  const urlMatch = bio.match(BLUESKY_URL_REGEX);
  if (urlMatch) {
    const handle = urlMatch[1];
    return {
      handle: `@${handle}`,
      url: `https://bsky.app/profile/${handle}`,
      source: "bio",
      verified: false,
    };
  }

  // Check for .bsky.social handles
  const handleMatches = [...bio.matchAll(BLUESKY_HANDLE_REGEX)];
  if (handleMatches.length > 0) {
    const handle = handleMatches[0][1];
    return {
      handle: `@${handle}`,
      url: `https://bsky.app/profile/${handle}`,
      source: "bio",
      verified: false,
    };
  }

  return null;
}

function parseBlueskyFromBlog(blog: string): SocialInfo | null {
  if (!blog) return null;

  const match = blog.match(BLUESKY_URL_REGEX);
  if (!match) return null;

  const handle = match[1];
  return {
    handle: `@${handle}`,
    url: `https://bsky.app/profile/${handle}`,
    source: "blog",
    verified: false,
  };
}

async function searchBlueskyByUsername(username: string, githubAvatarUrl: string): Promise<SocialInfo | null> {
  // Try Bluesky handle - use lowercase since handles are case-insensitive
  const handle = `${username.toLowerCase()}.bsky.social`;

  try {
    const url = `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${handle}`;
    const response = await fetch(url, {
      headers: { "User-Agent": "TUIKit-Social-Lookup" },
      signal: AbortSignal.timeout(5000),
    });

    if (response.ok) {
      const data = (await response.json()) as { handle: string; avatar?: string };

      // Avatar match is a bonus signal: if it matches, we're confident immediately
      if (data.avatar) {
        const avatarMatch = await avatarsMatch(githubAvatarUrl, data.avatar);
        if (avatarMatch) {
          console.log(`    Found Bluesky @${data.handle} (avatar verified ✓)`);
          return {
            handle: `@${data.handle}`,
            url: `https://bsky.app/profile/${data.handle}`,
            source: "username-match",
            verified: true,
          };
        }
      }

      // No avatar match: accept as unverified candidate.
      // Cross-verification will check for back-links and either verify or remove it.
      console.log(`    Found Bluesky @${data.handle} (pending cross-verification)`);
      return {
        handle: `@${data.handle}`,
        url: `https://bsky.app/profile/${data.handle}`,
        source: "username-match",
        verified: false,
      };
    }
  } catch {
    // API error, continue
  }

  return null;
}

// ─────────────────────────────────────────────────────────────────────────────
// Combined Search
// ─────────────────────────────────────────────────────────────────────────────

async function findSocialAccounts(user: GitHubUser): Promise<ProfileSocialAccounts> {
  const accounts: ProfileSocialAccounts = {};
  const githubAvatarUrl = user.avatar_url;

  // ── GitHub Social Accounts API first (user-provided, authoritative) ──
  const ghSocialAccounts = await fetchGitHubSocialAccounts(user.login);
  if (ghSocialAccounts.twitter) accounts.twitter = ghSocialAccounts.twitter;
  if (ghSocialAccounts.mastodon) accounts.mastodon = ghSocialAccounts.mastodon;
  if (ghSocialAccounts.bluesky) accounts.bluesky = ghSocialAccounts.bluesky;

  // ── Try Keybase (cryptographically verified, fills remaining gaps) ──
  if (!accounts.twitter || !accounts.mastodon || !accounts.bluesky) {
    const keybaseAccounts = await fetchSocialFromKeybase(user.login);
    if (!accounts.twitter && keybaseAccounts.twitter) accounts.twitter = keybaseAccounts.twitter;
    if (!accounts.mastodon && keybaseAccounts.mastodon) accounts.mastodon = keybaseAccounts.mastodon;
    if (!accounts.bluesky && keybaseAccounts.bluesky) accounts.bluesky = keybaseAccounts.bluesky;
  }

  // ── Try about.me profile (high quality source) ──
  if (!accounts.twitter || !accounts.mastodon || !accounts.bluesky) {
    const aboutMeAccounts = await fetchSocialFromAboutMe(user.login);
    if (!accounts.twitter && aboutMeAccounts.twitter) accounts.twitter = aboutMeAccounts.twitter;
    if (!accounts.mastodon && aboutMeAccounts.mastodon) accounts.mastodon = aboutMeAccounts.mastodon;
    if (!accounts.bluesky && aboutMeAccounts.bluesky) accounts.bluesky = aboutMeAccounts.bluesky;
  }

  // ── Try Linktree profile ──
  if (!accounts.twitter || !accounts.mastodon || !accounts.bluesky) {
    const linktreeAccounts = await fetchSocialFromLinktree(user.login);
    if (!accounts.twitter && linktreeAccounts.twitter) accounts.twitter = linktreeAccounts.twitter;
    if (!accounts.mastodon && linktreeAccounts.mastodon) accounts.mastodon = linktreeAccounts.mastodon;
    if (!accounts.bluesky && linktreeAccounts.bluesky) accounts.bluesky = linktreeAccounts.bluesky;
  }

  // ── Twitter (check GitHub profile, it's authoritative) ──
  if (!accounts.twitter) {
    accounts.twitter = getTwitterFromGitHubProfile(user);
  }
  if (!accounts.twitter && user.bio) {
    accounts.twitter = parseTwitterFromBio(user.bio) ?? undefined;
  }
  if (!accounts.twitter && user.blog) {
    accounts.twitter = parseTwitterFromBlog(user.blog) ?? undefined;
  }

  // ── Bluesky ──
  if (!accounts.bluesky && user.bio) {
    accounts.bluesky = parseBlueskyFromBio(user.bio) ?? undefined;
  }
  if (!accounts.bluesky && user.blog) {
    accounts.bluesky = parseBlueskyFromBlog(user.blog) ?? undefined;
  }
  if (!accounts.bluesky) {
    accounts.bluesky = (await searchBlueskyByUsername(user.login, githubAvatarUrl)) ?? undefined;
  }

  // ── Mastodon ──
  if (!accounts.mastodon && user.bio) {
    accounts.mastodon = (await parseMastodonFromBio(user.bio)) ?? undefined;
  }
  if (!accounts.mastodon && user.blog) {
    accounts.mastodon = (await parseMastodonFromBlog(user.blog)) ?? undefined;
  }
  if (!accounts.mastodon) {
    accounts.mastodon = (await searchMastodonByUsername(user.login, githubAvatarUrl)) ?? undefined;
  }

  // ── Cross-platform verification ──
  // If we found accounts, verify them by checking for back-links or matching data
  await crossPlatformVerify(accounts, user);

  return accounts;
}

// ─────────────────────────────────────────────────────────────────────────────
// Cross-Platform Verification
// ─────────────────────────────────────────────────────────────────────────────

/** Sources that are inherently authoritative and should never be downgraded. */
const AUTHORITATIVE_SOURCES = new Set(["github", "keybase", "manual"]);

/**
 * Cross-references found accounts to increase confidence.
 * Checks for:
 * - GitHub links in social profiles (back-link verification)
 * - Matching display names across platforms
 * - Consistent avatar usage
 *
 * Authoritative sources (github, keybase, manual) are never downgraded.
 */
async function crossPlatformVerify(accounts: ProfileSocialAccounts, user: GitHubUser): Promise<void> {
  const validationResults: string[] = [];

  // Verify Mastodon by checking if profile links back to GitHub
  if (accounts.mastodon && !accounts.mastodon.verified && !AUTHORITATIVE_SOURCES.has(accounts.mastodon.source)) {
    const verified = await verifyMastodonLinksToGitHub(accounts.mastodon.url, user.login);
    if (verified) {
      accounts.mastodon.verified = true;
      validationResults.push("Mastodon→GitHub ✓");
    } else if (accounts.mastodon.source === "username-match") {
      // Unverified username-match without back-link: remove to avoid false positives
      console.log(`    Removing unverified Mastodon match (no back-link to GitHub)`);
      delete accounts.mastodon;
    }
  }

  // Verify Bluesky by checking if profile links back to GitHub
  if (accounts.bluesky && !accounts.bluesky.verified && !AUTHORITATIVE_SOURCES.has(accounts.bluesky.source)) {
    const verified = await verifyBlueskyLinksToGitHub(accounts.bluesky.handle.replace("@", ""), user.login);
    if (verified) {
      accounts.bluesky.verified = true;
      validationResults.push("Bluesky→GitHub ✓");
    } else if (accounts.bluesky.source === "username-match") {
      // Unverified username-match without back-link: remove
      console.log(`    Removing unverified Bluesky match (no back-link to GitHub)`);
      delete accounts.bluesky;
    }
  }

  if (validationResults.length > 0) {
    console.log(`    Cross-verified: ${validationResults.join(", ")}`);
  }
}

/**
 * Checks if a Mastodon profile links back to the GitHub user.
 */
async function verifyMastodonLinksToGitHub(mastodonUrl: string, githubUsername: string): Promise<boolean> {
  try {
    // Extract instance and username from URL
    const match = mastodonUrl.match(/https?:\/\/([^/]+)\/@?([^/]+)/);
    if (!match) return false;

    const [, instance, username] = match;
    const apiUrl = `https://${instance}/api/v1/accounts/lookup?acct=${username}`;

    const response = await fetch(apiUrl, {
      headers: { "User-Agent": "TUIKit-Social-Lookup" },
      signal: AbortSignal.timeout(5000),
    });

    if (!response.ok) return false;

    const data = (await response.json()) as {
      note?: string;
      url?: string;
      fields?: Array<{ name: string; value: string; verified_at?: string | null }>;
    };

    // Check bio (note) for GitHub link
    const githubPattern = new RegExp(`github\\.com/${githubUsername}`, "i");

    if (data.note && githubPattern.test(data.note)) {
      return true;
    }

    // Check profile fields (Website, Homepage, etc.) for GitHub link
    if (data.fields) {
      for (const field of data.fields) {
        if (githubPattern.test(field.value) || githubPattern.test(field.name)) {
          return true;
        }
      }
    }

    return false;
  } catch {
    return false;
  }
}

/**
 * Checks if a Bluesky profile links back to the GitHub user.
 */
async function verifyBlueskyLinksToGitHub(handle: string, githubUsername: string): Promise<boolean> {
  try {
    const url = `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${handle}`;
    const response = await fetch(url, {
      headers: { "User-Agent": "TUIKit-Social-Lookup" },
      signal: AbortSignal.timeout(5000),
    });

    if (!response.ok) return false;

    const data = (await response.json()) as { description?: string };

    if (!data.description) return false;

    // Check bio for GitHub link
    const githubPattern = new RegExp(`github\\.com/${githubUsername}`, "i");
    return githubPattern.test(data.description);
  } catch {
    return false;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// File I/O
// ─────────────────────────────────────────────────────────────────────────────

const CACHE_PATH = path.join(__dirname, "../public/social-cache.json");
const OVERRIDES_PATH = path.join(__dirname, "../social-overrides.json");

function loadCache(): SocialCache {
  try {
    const content = fs.readFileSync(CACHE_PATH, "utf-8");
    return JSON.parse(content) as SocialCache;
  } catch {
    return { generatedAt: null, entries: {} };
  }
}

function saveCache(cache: SocialCache): void {
  cache.generatedAt = new Date().toISOString();
  fs.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2) + "\n");
  console.log(`Saved cache with ${Object.keys(cache.entries).length} entries`);
}

function loadOverrides(): SocialOverrides {
  try {
    const content = fs.readFileSync(OVERRIDES_PATH, "utf-8");
    return JSON.parse(content) as SocialOverrides;
  } catch {
    return { overrides: {} };
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────────────────────

async function main() {
  const isFullRefresh = process.argv.includes("--full");
  console.log(`Social Cache Update (${isFullRefresh ? "FULL REFRESH" : "incremental"})`);
  console.log("=".repeat(60));

  // Load existing data
  const cache = loadCache();
  const overrides = loadOverrides();

  let stargazers: GitHubStargazer[];
  try {
    stargazers = await fetchStargazers();
  } catch (err) {
    console.error("Failed to fetch stargazers, retrying in 5s...");
    await new Promise((r) => setTimeout(r, 5000));
    stargazers = await fetchStargazers();
  }

  // Determine which users to process
  const cachedLogins = new Set(Object.keys(cache.entries));
  const currentLogins = new Set(stargazers.map((s) => s.login));

  // Remove entries for users who unstarred
  for (const login of cachedLogins) {
    if (!currentLogins.has(login)) {
      console.log(`Removing unstarred user: ${login}`);
      delete cache.entries[login];
    }
  }

  // On full refresh, clear all entries so stale false positives don't survive
  if (isFullRefresh) {
    console.log("Full refresh: clearing all cached entries");
    cache.entries = {};
  }

  // Find new stargazers (not in cache)
  const toProcess = isFullRefresh
    ? stargazers
    : stargazers.filter((s) => !cachedLogins.has(s.login));

  console.log(`\nProcessing ${toProcess.length} users...`);

  for (const stargazer of toProcess) {
    const { login } = stargazer;
    console.log(`\nProcessing: ${login}`);

    // Check manual overrides first
    const override = overrides.overrides[login];
    if (override) {
      console.log(`  Using manual override`);
      const entry: SocialCacheEntry = {
        login,
        updatedAt: new Date().toISOString(),
      };
      if (override.mastodon) {
        entry.mastodon = { ...override.mastodon, source: "manual", verified: true };
      }
      if (override.twitter) {
        entry.twitter = { ...override.twitter, source: "manual", verified: true };
      }
      if (override.bluesky) {
        entry.bluesky = { ...override.bluesky, source: "manual", verified: true };
      }
      cache.entries[login] = entry;
      continue;
    }

    // Fetch user details and search for social accounts
    const userDetails = await fetchUserDetails(login);
    if (!userDetails) continue;

    const accounts = await findSocialAccounts(userDetails);

    if (accounts.mastodon || accounts.twitter || accounts.bluesky) {
      cache.entries[login] = {
        login,
        mastodon: accounts.mastodon,
        twitter: accounts.twitter,
        bluesky: accounts.bluesky,
        updatedAt: new Date().toISOString(),
      };

      const found = [
        accounts.mastodon ? `Mastodon: ${accounts.mastodon.handle}` : null,
        accounts.twitter ? `Twitter: ${accounts.twitter.handle}` : null,
        accounts.bluesky ? `Bluesky: ${accounts.bluesky.handle}` : null,
      ].filter(Boolean).join(", ");
      console.log(`  Found: ${found}`);
    } else {
      console.log("  No social accounts found");
    }
  }

  // Save updated cache
  saveCache(cache);

  console.log("\n" + "=".repeat(60));
  console.log("Done!");
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});
