import type { NodeInfoResponse } from 'src/lib/login-preflight'
import { isPublicHttpsHost } from 'src/lib/url-safety'
import { getJsonWithTimeout } from 'src/requests'
import { appStorage } from 'src/state/storage'

// 24 h TTL — probed software rarely changes between releases.
const PROBE_TTL_MS = 24 * 60 * 60 * 1000

const probeKeyFor = (host: string) => `app.probe.${host}`

export type HostSoftware = { name: string; version: string }

type CachedProbe = {
  at: number
  software: HostSoftware | null
}

function readCache(host: string): CachedProbe | null {
  const raw = appStorage.getString(probeKeyFor(host))
  if (!raw) return null
  try {
    const parsed = JSON.parse(raw) as CachedProbe
    if (!parsed || typeof parsed.at !== 'number') return null
    return parsed
  } catch {
    return null
  }
}

function writeCache(host: string, software: HostSoftware | null): void {
  const entry: CachedProbe = { at: Date.now(), software }
  appStorage.set(probeKeyFor(host), JSON.stringify(entry))
}

export async function probeHostSoftware(host: string): Promise<HostSoftware | null> {
  const cached = readCache(host)
  if (cached && Date.now() - cached.at < PROBE_TTL_MS) {
    return cached.software
  }

  try {
    const res = await getJsonWithTimeout(
      `https://${host}/.well-known/nodeinfo`,
      undefined,
      false,
      undefined,
      5000
    )
    const index = (await res.json()) as { links?: Array<{ href?: string }> }
    const href = index?.links?.[index.links.length - 1]?.href
    if (!href) {
      writeCache(host, null)
      return null
    }
    // Second leg: the remote nodeinfo index fully controls `href`. Reject
    // anything that isn't HTTPS or doesn't point back at the same host —
    // the spec requires nodeinfo to be served from the same origin, so a
    // mismatch is hostile (SSRF bait pointing at the user's LAN, an
    // attacker domain trying to siphon a probe, …).
    let parsedHref: URL
    try {
      parsedHref = new URL(href)
    } catch {
      writeCache(host, null)
      return null
    }
    if (
      !isPublicHttpsHost(href) ||
      parsedHref.hostname.toLowerCase() !== host.toLowerCase()
    ) {
      writeCache(host, null)
      return null
    }
    const infoRes = await getJsonWithTimeout(
      parsedHref.toString(),
      undefined,
      false,
      undefined,
      5000
    )
    const info = (await infoRes.json()) as NodeInfoResponse
    const name = info?.software?.name
    const version = info?.software?.version
    if (!name || !version) {
      writeCache(host, null)
      return null
    }
    const software: HostSoftware = { name, version }
    writeCache(host, software)
    return software
  } catch {
    writeCache(host, null)
    return null
  }
}
