// Login preflight: classify a server before the OAuth round-trip starts.
//
// Lives above `src/requests.ts` (the network-primitive leaf) and above
// `src/lib/capabilities.ts` (the Mastodon version-floor probe) so it can pull
// from both without the requests↔capabilities cycle that existed when
// `loginPreflightCheck` lived inside `requests.ts`.

import type { AccountType } from 'src/state/accountStore'
import { compareSemver } from 'src/utils'
import {
  isMastodonApiVersionAtOrAboveFloor,
  MASTODON_MIN_VERSION,
  probeMastodonApiVersion,
} from './capabilities'
import { discoverNodeInfo, type NodeInfoResponse } from './nodeinfo'

// Re-exported so callers that imported `NodeInfoResponse` from this module
// keep working after Phase A factored discovery into `src/lib/nodeinfo.ts`.
export type { NodeInfoResponse }

// Typed reasons let callers (handleLogin, AuthProvider) decide how to surface
// the failure instead of the network layer popping an RN Alert. The message
// strings live here so both code paths stay in sync.
//
// Mastodon Pass Phase 1 §2.2 — preflight now classifies the server, not just
// the Pixelfed-or-not boolean it used to. Two new reasons replace
// `'not-pixelfed'`:
//   - `'unsupported-server'` — software.name fell outside the strict
//     allow-list (Pixelfed / Mastodon). The error UX names the actual
//     reported software so users understand why (plan §1.2 / §6.1).
//   - `'mastodon-too-old'` — a Mastodon instance below the 4.4.0 floor
//     (plan §1.1). The minimum version is part of the rejection message.
export type LoginPreflightReason =
  | 'unreachable'
  | 'invalid-software'
  | 'unsupported-server'
  | 'outdated'
  | 'mastodon-too-old'

export type LoginPreflightSuccess = {
  ok: true
  type: AccountType
  // Mastodon Pass Phase 1 §2.1 — populated only on the Mastodon path. The
  // login-completion path passes this on to AccountStore.upsert so the
  // registry entry's `apiVersionsMastodon` is set at first sign-in.
  apiVersionsMastodon: number | null
  nodeinfo: NodeInfoResponse
}

export type LoginPreflightFailure = {
  ok: false
  reason: LoginPreflightReason
  nodeinfo?: NodeInfoResponse
  // Surfaced for `'unsupported-server'` so the toast can name the actual
  // reported software.name (plan §1.2: "This server reports as 'hometown'…").
  softwareName?: string
  // Surfaced for `'mastodon-too-old'` so the toast can include the minimum
  // version users would need.
  minVersion?: string
}

export type LoginPreflightResult = LoginPreflightSuccess | LoginPreflightFailure

// Static fallback copy when no `server` is in scope (the legacy
// `loginPreflightMessages` record below). Reasons that need runtime data
// (`'unsupported-server'`, `'mastodon-too-old'`) are formatted in
// `formatLoginPreflightMessage` instead — all login copy lives in this
// module so callers never template-string their own.
const STATIC_LOGIN_PREFLIGHT_MESSAGES: Record<
  Exclude<LoginPreflightReason, 'unsupported-server' | 'mastodon-too-old'>,
  string
> = {
  unreachable: 'This server is not compatible or is unavailable.',
  'invalid-software': 'Cannot reach server. Invalid software.',
  outdated: 'Invalid or outdated version, please ask your admin to update.',
}

/**
 * Format a user-facing message for a preflight failure.
 *
 * `server` is optional — when present, `unreachable` and `invalid-software`
 * messages name the actual hostname so users can spot a typo or pick a
 * different instance. Reasons that already embed runtime data (the rejected
 * software name, the minimum Mastodon version) ignore `server`.
 */
export function formatLoginPreflightMessage(
  failure: LoginPreflightFailure,
  server?: string
): string {
  if (failure.reason === 'unsupported-server') {
    const name = failure.softwareName ?? 'an unsupported server type'
    return `This server reports as '${name}'. Halflight currently only supports official Pixelfed and official Mastodon servers.`
  }
  if (failure.reason === 'mastodon-too-old') {
    const min = failure.minVersion ?? MASTODON_MIN_VERSION
    return `This Mastodon server is older than ${min}. Halflight requires Mastodon ${min} or newer.`
  }
  if (server) {
    if (failure.reason === 'unreachable') {
      return `We couldn't reach ${server}. Check the spelling and your connection, then try again.`
    }
    if (failure.reason === 'invalid-software') {
      return `${server} doesn't look like a Pixelfed or Mastodon server.`
    }
  }
  return STATIC_LOGIN_PREFLIGHT_MESSAGES[failure.reason]
}

/**
 * Backwards-compatibility shim — earlier code reached into a static record
 * keyed by reason. Two reasons now carry runtime detail, so the canonical
 * call is `formatLoginPreflightMessage(failure)`. The record below is kept
 * for any caller that only has a reason string in hand and renders best-
 * effort copy without the per-call detail. Prefer the function form.
 */
export const loginPreflightMessages: Record<LoginPreflightReason, string> = {
  ...STATIC_LOGIN_PREFLIGHT_MESSAGES,
  'unsupported-server':
    "This server isn't supported. Halflight currently only supports official Pixelfed and official Mastodon servers.",
  'mastodon-too-old': `This Mastodon server is older than ${MASTODON_MIN_VERSION}. Halflight requires Mastodon ${MASTODON_MIN_VERSION} or newer.`,
}

/**
 * Map a (lowercased) nodeinfo `software.name` to the supported-account-type
 * union, or `null` for "rejected at the allow-list" (plan §1.2 / §6.1).
 *
 * Glitch-soc deliberately reports `software.name === 'mastodon'` and is the
 * one fork that passes silently — no special-casing, no UX disclosure (plan
 * §1.2). Hometown, Pleroma, Akkoma, GoToSocial, Iceshrimp, Sharkey, Firefish,
 * Kmyblue, Fedibird, etc. all fall through to `null` and are rejected.
 */
export function detectAccountType(softwareName: string): AccountType | null {
  const name = softwareName.toLowerCase()
  if (name === 'pixelfed') return 'pixelfed'
  if (name === 'mastodon') return 'mastodon'
  return null
}

export async function loginPreflightCheck(server: string): Promise<LoginPreflightResult> {
  // Phase A of the Mastodon follow-up plan: discover nodeinfo via the
  // `/.well-known/nodeinfo` contract that both servers honour, instead of
  // hitting Pixelfed's `/api/nodeinfo/2.0.json` directly (which 404s on
  // Mastodon and surfaced the misleading "unreachable" toast). The helper
  // returns null on any failure — non-OK statuses are treated as unreachable
  // because the previous unconditional `res.json()` blew up with a SyntaxError
  // when a 404 returned an HTML error page.
  const json = await discoverNodeInfo(server)

  if (!json) {
    return { ok: false, reason: 'unreachable' }
  }

  if (!json.software || !json.software.name || !json.software.version) {
    return { ok: false, reason: 'invalid-software', nodeinfo: json }
  }

  const softwareNameRaw = json.software.name
  const detected = detectAccountType(softwareNameRaw)

  // Halflight profile flag short-circuits the Pixelfed check — any server
  // exposing `halflight.profile = true` is an accepted target as a Pixelfed
  // account. Accept both the spec path (`metadata.config.features.halflight
  // .*`) and the path emitted by current Halflight-patched server builds
  // (`metadata.halflight.features.*`). The Halflight profile shim is a
  // Pixelfed-shaped extension; classifying it as the Mastodon transport
  // would silently strip `_pe=1` and break the very feature it advertises.
  const hl =
    json.metadata?.halflight?.features ?? json.metadata?.config?.features?.halflight
  if (hl?.profile === true) {
    return { ok: true, type: 'pixelfed', apiVersionsMastodon: null, nodeinfo: json }
  }

  if (detected === null) {
    return {
      ok: false,
      reason: 'unsupported-server',
      softwareName: softwareNameRaw,
      nodeinfo: json,
    }
  }

  if (detected === 'pixelfed') {
    const validVersion = compareSemver(json.software.version, '0.12.3')
    if (validVersion === -1) {
      return { ok: false, reason: 'outdated', nodeinfo: json }
    }
    return { ok: true, type: 'pixelfed', apiVersionsMastodon: null, nodeinfo: json }
  }

  // Mastodon path. Plan §2.2 / §1.1 — the only authoritative version floor
  // signal is the integer at `/api/v2/instance.api_versions.mastodon`. We
  // can't compare nodeinfo's `software.version` with `compareSemver`
  // because the integer-to-version mapping is deliberately not published
  // by Mastodon (the docs treat the integer as opaque), and forks routinely
  // fork the version string while staying API-compatible.
  const apiVersion = await probeMastodonApiVersion(server)
  if (!isMastodonApiVersionAtOrAboveFloor(apiVersion)) {
    return {
      ok: false,
      reason: 'mastodon-too-old',
      minVersion: MASTODON_MIN_VERSION,
      nodeinfo: json,
    }
  }
  return {
    ok: true,
    type: 'mastodon',
    apiVersionsMastodon: apiVersion,
    nodeinfo: json,
  }
}
