const settingsDb = require('./settings.db')
const brand = require('../../config/brand')
const { parseJsonSetting } = require('../../utils/settingsJson')
const { resolveThemeTokens } = require('../../utils/themeResolve')
const { resolveBrandAssets } = require('../../utils/brandAssets')

// Keys safe to expose on the public site.
const PUBLIC_KEYS = [
  'site_mode',
  'maintenance_message',
  'status_message',
  'homepage_teaser',
  'contact_email',
  'site_title',
  'hero_layout', // portal hero composition (JSON). Draft key stays admin-only.
  'theme_visual', // preset/custom colors, fonts, radii (JSON). See THEMING_AND_NAV.md §6.1.
  'brand_assets', // uploaded logo/hero/favicon overrides (JSON). §6.3.
  'nav_public', // public site nav overrides (JSON). §6.4.
]

// Admin-configurable theming & navigation (docs/website/THEMING_AND_NAV.md).
// All five are JSON strings and all five are ABSENT by default — no migration
// seeds them. Absence of the row, not an empty value, is what makes a surface
// fall back to BRAND_* env / the hardcoded theme.css / the hardcoded NAV arrays.
//
// nav_admin and nav_player are deliberately not public: an anonymous visitor has
// no use for either, and the admin nav's labels describe the shape of the admin
// surface. They are read by their owners through GET /api/v1/settings/nav (§4.2).
const THEMING_KEYS = ['theme_visual', 'brand_assets', 'nav_public', 'nav_admin', 'nav_player']

// Keys a reset may delete. An explicit allowlist, not "any key": DELETE on an
// arbitrary key would let a bad request drop site_mode or the uo-link config,
// whose absence means something else entirely. hero_layout_draft is included
// because discarding a draft is the same operation.
const DELETABLE_KEYS = [...THEMING_KEYS, 'hero_layout_draft']

// Player self-registration mode. Stored under the 'player_registration' key.
// NOTE: the raw value is never exposed publicly — getPublic() derives boolean
// availability flags from it instead (see below).
const REGISTRATION_KEY = 'player_registration'
const REGISTRATION_MODES = ['disabled', 'password', 'sso', 'both']

// Resolve the registration mode, defaulting to 'disabled' (and coercing any
// unexpected stored value back to 'disabled' so a bad row can't open sign-up).
async function getRegistrationMode() {
  const value = await settingsDb.get(REGISTRATION_KEY)
  return REGISTRATION_MODES.includes(value) ? value : 'disabled'
}

// Derived, public-safe availability flags for the register page.
function registrationFlags(mode) {
  return {
    password: mode === 'password' || mode === 'both',
    sso: mode === 'sso' || mode === 'both',
  }
}

// Android App Links opt-in (M9 follow-up). When on, the shard auto-serves
// /.well-known/assetlinks.json and the mobile SSO bridge additionally accepts the
// self-origin https://<host>/mobile/callback redirect. Stored as the string
// 'true'/'false'; default off. See docs/android/APP_LINKS.md.
const MOBILE_APP_LINKS_KEY = 'mobile_app_links_enabled'

// Fail-closed: any read error (e.g. DB unavailable) reports "disabled" so a
// transient fault can never open the https redirect path or serve assetlinks.json.
async function isMobileAppLinksEnabled() {
  try {
    return String(await settingsDb.get(MOBILE_APP_LINKS_KEY)) === 'true'
  } catch {
    return false
  }
}

/**
 * This instance's name, resolved exactly as `getPublic().brand.name` resolves it —
 * the admin-editable site title wins over BRAND_NAME. Anything that has to *speak*
 * the instance's name outside the settings payload must use this rather than
 * `brand.name`, or an install that set only the site title gets two different names
 * on two different pages.
 *
 * Never throws: a name is always better than an error, so a DB fault falls back to
 * the env value.
 */
async function getInstanceName() {
  try {
    return (await settingsDb.get('site_title')) || brand.name
  } catch {
    return brand.name
  }
}

async function get(key) {
  return settingsDb.get(key)
}

async function set(key, value, updatedBy = null) {
  return settingsDb.set(key, value, updatedBy)
}

async function remove(key) {
  return settingsDb.remove(key)
}

async function setMany(obj, updatedBy = null) {
  for (const [key, value] of Object.entries(obj)) {
    await settingsDb.set(key, value, updatedBy)
  }
}

async function getAll() {
  const rows = await settingsDb.getAll()
  return rows.reduce((acc, row) => {
    acc[row.key] = row.value
    return acc
  }, {})
}

async function getPublic() {
  const all = await getAll()
  const out = PUBLIC_KEYS.reduce((acc, key) => {
    if (all[key] !== undefined) acc[key] = all[key]
    return acc
  }, {})
  // Derived registration availability (never the raw mode). Lets the register
  // page show/hide the password form and SSO buttons.
  const mode = REGISTRATION_MODES.includes(all[REGISTRATION_KEY]) ? all[REGISTRATION_KEY] : 'disabled'
  out.registration = registrationFlags(mode)
  // `gameAccountSignup` was derived here until Phase 3 slice 3. It said whether
  // the site offers GAME-account creation, which is a question about a shard —
  // its own SignupMode has to agree — so it left with the rest of core's UO
  // prose. The `game_account_signup` row is unchanged and module-uo reads it
  // through ctx.settings; the derived flag is on its `/public/shard/features`.
  //
  // The effective CSS custom properties for the admin's theme, or absent when
  // no theme_visual row exists (or nothing in it was usable). The SPA writes
  // these onto <html>; absence means it writes nothing and theme.css's :root
  // stands, which is what keeps an untouched instance byte-for-byte as today.
  // Resolution — :root ← preset ← custom — happens here rather than in CSS so
  // there is one authority and brand.accent below can report the same value the
  // site actually paints. See THEMING_AND_NAV.md §6.
  const theme = resolveThemeTokens(all.theme_visual)
  if (theme) out.theme = theme
  // Uploaded brand-asset overrides (§6.3), resolved here so every consumer of
  // the brand block — the SPA, the Android app, the Discord bot — picks them up
  // through the one contract. Forgiving on read like the theme: a slot holding
  // something we would not emit as a URL is dropped and its neighbours kept.
  const brandAssets = resolveBrandAssets(parseJsonSetting(all.brand_assets))
  // Instance branding (BRAND_* env defaults). The admin-editable settings —
  // site title, contact email, and now the theme accent and uploaded assets —
  // override the env value when set, so existing installs keep their
  // DB-configured name; everything else comes from env.
  //
  // brand.accent is a CROSS-REPO CONTRACT: the Android app themes its whole
  // Material palette from it (BrandDto → RunicGatewayTheme) and the Discord bot
  // colors its embeds from it. Resolving the effective accent here is what lets
  // both track admin theming with no client change.
  out.brand = {
    name: out.site_title || brand.name,
    shortName: brand.shortName,
    tagline: brand.tagline,
    description: brand.description,
    contactEmail: out.contact_email || brand.contactEmail,
    url: brand.url,
    accent: theme?.['--accent'] || brand.accent,
    logo: brandAssets.logo || brand.logo,
    hero: brandAssets.hero || brand.hero,
    favicon: brandAssets.favicon || brand.favicon,
  }
  // Push-notification relay (M7). The client-facing ntfy base URL the app's
  // embedded distributor registers its device topic against; null when push is
  // not configured for this shard, in which case the app simply shows push as
  // unavailable. The publisher's own NTFY_BASE_URL may be an internal
  // compose-network address, so a distinct NTFY_PUBLIC_URL is preferred; failing
  // that we use the first NTFY_ALLOWED_ORIGINS entry (a device endpoint must sit
  // on an allowed origin anyway). Never NTFY_BASE_URL — it may be internal-only.
  out.push = { ntfyUrl: publicNtfyUrl() }
  // Whether this shard has opted into Android App Links (M9 follow-up). Lets a
  // native client tell whether it may request the https App Link redirect_uri
  // before doing so (the server would otherwise reject an unallowlisted one). The
  // custom-scheme callback works regardless of this flag.
  out.mobileAppLinks = String(all[MOBILE_APP_LINKS_KEY]) === 'true'
  return out
}

/**
 * What the HTML shell needs, resolved exactly as getPublic() resolves it: the
 * effective favicon and logo, plus the theme token map for the boot <style>
 * block. Kept here rather than in utils/htmlShell.js so there is one authority
 * for "which asset wins", and so the shell can never disagree with the payload
 * the SPA fetches a moment later.
 *
 * Throws on a DB fault — the caller (utils/htmlShell.js) decides what a failure
 * means for the page, and for it the answer is "serve the env-only shell".
 *
 * @returns {Promise<{logo: string, favicon: string, theme: object|null}>}
 */
async function getShellBrand() {
  const all = await getAll()
  const assets = resolveBrandAssets(parseJsonSetting(all.brand_assets))
  return {
    logo: assets.logo || brand.logo,
    favicon: assets.favicon || brand.favicon,
    theme: resolveThemeTokens(all.theme_visual),
  }
}

// The two nav-override keys their own audiences need but cannot read from
// GET /admin/settings (admin-only, while AdminLayout renders for editors and
// moderators and PlayerPortalLayout renders for players — THEMING_AND_NAV.md
// §4.2). Values are returned as stored: raw JSON strings, or null when the
// admin never overrode that nav.
async function getNav() {
  const all = await getAll()
  return {
    nav_admin: all.nav_admin ?? null,
    nav_player: all.nav_player ?? null,
  }
}

// The client-facing ntfy base URL (no trailing slash), or null when unset.
function publicNtfyUrl() {
  const explicit = (process.env.NTFY_PUBLIC_URL || '').trim()
  if (explicit) return explicit.replace(/\/+$/, '')
  const firstOrigin = (process.env.NTFY_ALLOWED_ORIGINS || '')
    .split(',')
    .map((s) => s.trim())
    .filter(Boolean)[0]
  return firstOrigin ? firstOrigin.replace(/\/+$/, '') : null
}

module.exports = {
  get,
  set,
  remove,
  setMany,
  getAll,
  getPublic,
  getShellBrand,
  getNav,
  getInstanceName,
  PUBLIC_KEYS,
  THEMING_KEYS,
  DELETABLE_KEYS,
  REGISTRATION_KEY,
  REGISTRATION_MODES,
  getRegistrationMode,
  registrationFlags,
  MOBILE_APP_LINKS_KEY,
  isMobileAppLinksEnabled,
}
