interface Author {
  name?: string;
  photo?: string;
  url?: string;
}

export interface Mention {
  "wm-property": string;
  "wm-id": number;
  url: string;
  author?: Author;
  published?: string;
  content?: { text?: string; html?: string };
}

const WEBMENTION_IO_API = "https://webmention.io/api/mentions.jf2";
const SOCIAL_PROPERTIES = new Set([
  "like-of",
  "repost-of",
  "in-reply-to",
  "mention-of",
]);

const mentionsCache = new Map<
  string,
  { expiresAt: number; mentions: Mention[] }
>();
const sourceExistsCache = new Map<
  string,
  { expiresAt: number; exists: boolean }
>();

const MENTIONS_CACHE_TTL_MS = 60_000;
const SOURCE_EXISTS_TRUE_CACHE_TTL_MS = 10 * 60_000;
const SOURCE_EXISTS_FALSE_CACHE_TTL_MS = 60_000;
const MAX_CONCURRENCY = 6;
const FETCH_TIMEOUT_MS = 8_000;

type MastodonInteractionCheck = {
  kind: "mastodon";
  origin: string;
  statusId: string;
  actorId: string;
  action: "favourited_by" | "reblogged_by";
};

type MastodonStatusRef = {
  kind: "mastodon";
  origin: string;
  statusId: string;
};

function now() {
  return Date.now();
}

function cacheSourceExists(
  sourceUrl: string,
  exists: boolean,
  ttlMs = exists
    ? SOURCE_EXISTS_TRUE_CACHE_TTL_MS
    : SOURCE_EXISTS_FALSE_CACHE_TTL_MS,
) {
  sourceExistsCache.set(sourceUrl, {
    exists,
    expiresAt: now() + ttlMs,
  });
}

function getHostname(rawUrl: string) {
  try {
    return new URL(rawUrl).hostname.toLowerCase();
  } catch {
    return "";
  }
}

function isLikelySocialSource(url: string) {
  const host = getHostname(url);
  if (!host) return false;
  if (host.includes("mastodon")) return true;
  if (host.endsWith(".social")) return true;
  return false;
}

function shouldValidateSource(mention: Mention) {
  if (!mention.url) return false;
  if (!SOCIAL_PROPERTIES.has(mention["wm-property"])) return false;
  return isLikelySocialSource(mention.url);
}

function withTimeout(ms: number) {
  return AbortSignal.timeout(ms);
}

function getLinkHeaderNextUrl(linkHeader: string | null) {
  if (!linkHeader) return null;
  const match = linkHeader.match(/<([^>]+)>;\s*rel="next"/i);
  return match?.[1] ?? null;
}

function parseMastodonInteraction(
  sourceUrl: string,
): MastodonInteractionCheck | null {
  try {
    const parsed = new URL(sourceUrl);
    const fragment = decodeURIComponent(parsed.hash.replace(/^#/, ""));
    if (!fragment) return null;

    const likeMatch = fragment.match(/^(?:favorited|favourited)-by-(\d+)$/i);
    const repostMatch = fragment.match(/^reblogged-by-(\d+)$/i);
    if (!likeMatch && !repostMatch) return null;

    const statusIdMatch = parsed.pathname.match(/\/(\d+)\/?$/);
    const statusId = statusIdMatch?.[1];
    if (!statusId) return null;

    const likedActorId = likeMatch?.[1];
    const repostActorId = repostMatch?.[1];

    if (likedActorId) {
      return {
        kind: "mastodon",
        origin: parsed.origin,
        statusId,
        actorId: likedActorId,
        action: "favourited_by",
      };
    }

    if (!repostActorId) return null;
    return {
      kind: "mastodon",
      origin: parsed.origin,
      statusId,
      actorId: repostActorId,
      action: "reblogged_by",
    };
  } catch {
    return null;
  }
}

function parseSocialInteraction(sourceUrl: string) {
  return parseMastodonInteraction(sourceUrl);
}

function parseMastodonStatusRef(sourceUrl: string): MastodonStatusRef | null {
  try {
    const parsed = new URL(sourceUrl);
    const statusIdMatch = parsed.pathname.match(/\/(\d+)\/?$/);
    const statusId = statusIdMatch?.[1];
    if (!statusId) return null;
    if (parsed.hash && parsed.hash.length > 1) return null;

    const host = parsed.hostname.toLowerCase();
    if (!host.includes("mastodon") && !host.endsWith(".social")) return null;

    return {
      kind: "mastodon",
      origin: parsed.origin,
      statusId,
    };
  } catch {
    return null;
  }
}

function parseSocialPostRef(sourceUrl: string): MastodonStatusRef | null {
  return parseMastodonStatusRef(sourceUrl);
}

async function checkMastodonInteractionExists(
  interaction: MastodonInteractionCheck,
) {
  let nextUrl: string | null =
    `${interaction.origin}/api/v1/statuses/${interaction.statusId}/${interaction.action}?limit=80`;

  for (let page = 0; page < 20 && nextUrl; page++) {
    const res = await fetch(nextUrl, {
      signal: withTimeout(FETCH_TIMEOUT_MS),
      headers: {
        "User-Agent": "adrian-altner.com webmentions verifier",
      },
    });

    if (res.status === 404 || res.status === 410) return false;
    if (!res.ok) return true;

    const accounts = await res.json();
    if (!Array.isArray(accounts)) return true;
    if (
      accounts.some(
        (account) => String(account?.id ?? "") === interaction.actorId,
      )
    ) {
      return true;
    }

    nextUrl = getLinkHeaderNextUrl(res.headers.get("link"));
  }

  return !!nextUrl;
}

async function checkMastodonStatusExists(status: MastodonStatusRef) {
  const res = await fetch(
    `${status.origin}/api/v1/statuses/${status.statusId}`,
    {
      signal: withTimeout(FETCH_TIMEOUT_MS),
      headers: {
        "User-Agent": "adrian-altner.com webmentions verifier",
      },
    },
  );

  if (res.status === 404 || res.status === 410) return false;
  if (!res.ok) return true;
  return true;
}

async function socialPostStillExists(postRef: MastodonStatusRef) {
  return checkMastodonStatusExists(postRef);
}

async function socialInteractionStillExists(
  interaction: MastodonInteractionCheck,
) {
  return checkMastodonInteractionExists(interaction);
}

async function sourceStillExists(sourceUrl: string) {
  const cached = sourceExistsCache.get(sourceUrl);
  if (cached && cached.expiresAt > now()) return cached.exists;

  try {
    const interaction = parseSocialInteraction(sourceUrl);
    if (interaction) {
      const exists = await socialInteractionStillExists(interaction);
      cacheSourceExists(sourceUrl, exists);
      return exists;
    }

    const postRef = parseSocialPostRef(sourceUrl);
    if (postRef) {
      const exists = await socialPostStillExists(postRef);
      cacheSourceExists(sourceUrl, exists);
      return exists;
    }

    const res = await fetch(sourceUrl, {
      redirect: "follow",
      signal: withTimeout(FETCH_TIMEOUT_MS),
      headers: {
        "User-Agent": "adrian-altner.com webmentions verifier",
      },
    });

    const statusGone = res.status === 404 || res.status === 410;
    if (statusGone) {
      cacheSourceExists(sourceUrl, false);
      return false;
    }

    if (!res.ok) {
      // Fail open on transient upstream errors.
      cacheSourceExists(sourceUrl, true, 60_000);
      return true;
    }

    const host = getHostname(sourceUrl);
    const contentType = res.headers.get("content-type") ?? "";
    const isHtml = contentType.includes("text/html");
    if (isHtml && host.includes("mastodon")) {
      const body = (await res.text()).toLowerCase();
      const isDeletedMastodon =
        host.includes("mastodon") &&
        (body.includes("status not found") ||
          body.includes("this status does not exist"));
      if (isDeletedMastodon) {
        cacheSourceExists(sourceUrl, false);
        return false;
      }
    }

    cacheSourceExists(sourceUrl, true);
    return true;
  } catch {
    // Fail open on network/runtime problems.
    cacheSourceExists(sourceUrl, true, 60_000);
    return true;
  }
}

async function runWithConcurrencyLimit<T, R>(
  items: T[],
  limit: number,
  worker: (item: T) => Promise<R>,
) {
  const output = new Array<R | undefined>(items.length);
  let nextIndex = 0;

  async function runWorker() {
    while (nextIndex < items.length) {
      const index = nextIndex++;
      const item = items[index];
      if (item === undefined) continue;
      output[index] = await worker(item);
    }
  }

  const workers = Array.from(
    { length: Math.max(1, Math.min(limit, items.length)) },
    () => runWorker(),
  );
  await Promise.all(workers);
  return output as R[];
}

async function fetchRawWebmentions(target: string) {
  const apiUrl = `${WEBMENTION_IO_API}?target=${encodeURIComponent(target)}&per-page=100`;
  const res = await fetch(apiUrl, {
    signal: withTimeout(FETCH_TIMEOUT_MS),
    headers: {
      "User-Agent": "adrian-altner.com webmentions fetcher",
    },
  });

  if (!res.ok) return [];
  const data = await res.json();
  return (data.children ?? []) as Mention[];
}

export async function getFilteredWebmentions(target: string) {
  const cached = mentionsCache.get(target);
  if (cached && cached.expiresAt > now()) {
    return cached.mentions;
  }

  const mentions = await fetchRawWebmentions(target);
  if (mentions.length === 0) {
    mentionsCache.set(target, {
      mentions: [],
      expiresAt: now() + MENTIONS_CACHE_TTL_MS,
    });
    return [];
  }

  const checks = await runWithConcurrencyLimit(
    mentions,
    MAX_CONCURRENCY,
    async (mention) => {
      if (!shouldValidateSource(mention)) return true;
      return sourceStillExists(mention.url);
    },
  );

  const filtered = mentions.filter((_, idx) => checks[idx]);
  mentionsCache.set(target, {
    mentions: filtered,
    expiresAt: now() + MENTIONS_CACHE_TTL_MS,
  });
  return filtered;
}
