// Per-host nodeinfo classifier — Phase B §3.1 of the Mastodon follow-up plan.
//
// The Mastodon-active home feed has no `pf_type='event'` enrichment to lean
// on (Pixelfed adds that server-side; vanilla Mastodon strips the extension
// in federation). To still surface Mobilizon events as events, the client
// probes the author's instance nodeinfo once per host and caches the
// `software.name`. A subsequent render that sees the same author host knows
// — without any extra round-trip — whether the author is on a Mobilizon
// instance.
//
// Cache layout: `app.softwareByHost.<host>` → lowercased software name (or
// the empty string for "probe failed, don't retry until eviction"). Cached
// forever — instances rarely change software, and a wrong cache hit at most
// mis-renders one cell with an event badge. The plan §3.1 / §6 makes the
// trade-off explicit: cheap probe + opportunistic enrichment, no detection
// heuristics beyond the nodeinfo signal.
//
// Same `discoverNodeInfo` helper as the login preflight, so the codebase has
// exactly one canonical nodeinfo probe.

import { appStorage } from 'src/state/storage'
import { discoverNodeInfo } from './nodeinfo'

const CACHE_PREFIX = 'app.softwareByHost.'

// Hosts currently being probed. Coalesces concurrent calls (every cell that
// sees the same author on the first frame triggers one network request, not
// N) and short-circuits the synchronous read path while a probe is in
// flight, since a hit during probing would otherwise return the empty cache
// value and pin "default" until the next render fires after resolution.
const inflight = new Map<string, Promise<string | null>>()

function cacheKey(host: string): string {
  return `${CACHE_PREFIX}${host}`
}

/**
 * Synchronous read against the persistent cache. Returns:
 *   - the lowercased software name for hosts we've classified successfully
 *   - the empty string `''` for hosts where the probe failed (do-not-retry)
 *   - `undefined` for hosts we have not yet probed
 *
 * Callers that want to drive a probe-when-missing path use
 * `classifyRemoteHost` directly; this read exists for the synchronous
 * `useStatusKind` render path that needs to know "do we already know?"
 * without spawning a fetch.
 */
export function getCachedSoftwareName(host: string): string | undefined {
  const v = appStorage.getString(cacheKey(host))
  return v === undefined ? undefined : v
}

/**
 * Probe (or read the cache for) a remote host's `software.name`. Returns the
 * lowercased software name on success, or null when the nodeinfo probe
 * failed. Result is persisted forever — see module-level rationale.
 *
 * Concurrent calls for the same host share one network round-trip. Calls
 * after a previous failure return null without re-probing (the empty-string
 * sentinel in cache).
 */
export async function classifyRemoteHost(host: string): Promise<string | null> {
  const cached = getCachedSoftwareName(host)
  if (cached !== undefined) {
    return cached === '' ? null : cached
  }
  const existing = inflight.get(host)
  if (existing) return existing

  const probe = (async () => {
    const json = await discoverNodeInfo(host)
    const name = json?.software?.name?.toLowerCase()
    // Persist the empty string on failure so we don't re-probe forever. A
    // successful but unfamiliar software name is still cached — every
    // consumer is checking for a single canonical name, not enumerating.
    appStorage.set(cacheKey(host), name ?? '')
    return name ?? null
  })()
  inflight.set(host, probe)
  try {
    return await probe
  } finally {
    inflight.delete(host)
  }
}
