// ── Player: game-account linking + reads ───────────────────────────────────
//
// The player-facing surface for the uo-link integration. A logged-in player
// runs [link in game, gets a one-time code, and enters it here — the server
// confirms it with the sidecar (which permanently tags the game account with the
// website user id) and mirrors the link locally. Roster/vendor reads are
// ownership-checked against that mirror so a player can only see accounts they
// have linked. The sidecar token stays server-side throughout.

const uoLinkClient = require('../../../utils/uoLinkClient')
const shardLinks = require('../../../model/shardLinks/shardLinks.model')
const shardState = require('../../../model/shardState/shardState.model')
const settings = require('../../../model/settings/settings.model')
const { salesForAccounts } = require('../../../utils/shardSales')
const activity = require('../../../model/activity/activity.model')

const log = require('../../../utils/logger')('player-shard')

const SERIAL_RE = /^0x[0-9a-fA-F]+$/

// Decorate a char.profile with cross-links from our own board data: the guild the
// character leads and any city governorship on its account. Best-effort — a
// failure here never fails the profile (it's a nicety, not the sheet).
async function enrichCharProfile(profile) {
  if (!profile) return profile
  try {
    const guild = await shardState.findGuildForActor({ serial: profile.serial, acct: profile.acct })
    if (guild) profile.guild = guild
    if (profile.acct) {
      const govs = await shardState.listGovernorshipsForAccounts([profile.acct])
      if (govs.length) profile.governorOf = govs.map((g) => g.city)
    }
  } catch (err) {
    log.warn('enrichCharProfile failed', { serial: profile.serial, message: err.message })
  }
  return profile
}

// POST /player/shard/link — confirm an in-game link code.
async function link(req, res) {
  const { code } = req.body
  try {
    const result = await uoLinkClient.confirmLink(code, req.user.id)

    if (result.ok && result.data && result.data.kind === 'link.ok') {
      const account = result.data.account
      await shardLinks.link({ account, userId: req.user.id, charName: result.data.char || null })
      await activity.log({ req, action: 'uoLink.account.link', detail: { account } })
      log.info('player linked game account', { user: req.user.username, account })
      return res.json({ linked: true, account })
    }

    // Sidecar reports bad/expired codes as 400 link.error or 404.
    if (result.status === 400 || result.status === 404) {
      return res.status(400).json({ message: 'That code is unknown or has expired. Run [link in game for a new one.' })
    }
    if (result.status === 503 || result.status === 0) {
      return res.status(503).json({ message: 'The shard is unavailable right now — try again shortly.' })
    }
    return res.status(502).json({ message: 'Could not confirm the link with the shard.' })
  } catch (err) {
    log.error('player.shard.link', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

// GET /player/shard/accounts — the caller's linked game accounts.
async function listAccounts(req, res) {
  try {
    return res.json(await shardLinks.listForUser(req.user.id))
  } catch (err) {
    log.error('player.shard.listAccounts', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

// Admins may view any character's data; everyone else is limited to accounts
// they have personally linked. The same handlers back /player/shard (role
// `player`, never admin) and /admin/shard (staff), so this bypass only ever
// widens access for genuine admins.
const isAdmin = (req) => req.user && req.user.role === 'admin'

// Shared ownership gate + live round-trip for roster/vendors. `fetcher` is the
// uoLinkClient method to call with the account.
async function ownedRoundTrip(req, res, fetcher, label) {
  const { account } = req.params
  try {
    const owns = isAdmin(req) || (await shardLinks.ownsAccount(account, req.user.id))
    if (!owns) return res.status(403).json({ message: 'That account is not linked to your profile.' })

    const result = await fetcher(account)
    if (result.ok) return res.json(result.data)
    if (result.status === 404) return res.status(404).json({ message: 'Not found.' })
    if (result.status === 503 || result.status === 0) {
      return res.status(503).json({ message: 'The shard is unavailable right now — try again shortly.' })
    }
    return res.status(502).json({ message: 'Could not reach the shard.' })
  } catch (err) {
    log.error(`player.shard.${label}`, err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

// GET /player/shard/roster/:account — characters on a linked account.
const roster = (req, res) => ownedRoundTrip(req, res, uoLinkClient.getRoster, 'roster')

// GET /player/shard/vendors/:account — player vendors on a linked account.
const vendors = (req, res) => ownedRoundTrip(req, res, uoLinkClient.getVendors, 'vendors')

// GET /player/shard/char/:serial — a character sheet, but ONLY if the character's
// account is linked to the caller. The sidecar returns the owning account in the
// profile, which we check against the caller's links before returning anything.
async function getChar(req, res) {
  const { serial } = req.params
  if (!SERIAL_RE.test(serial)) return res.status(400).json({ message: 'Invalid serial.' })
  try {
    const result = await uoLinkClient.getCharBySerial(serial)
    if (result.ok) {
      // Admins see any character; others only characters on an account they linked.
      if (!isAdmin(req)) {
        const acct = result.data && result.data.acct
        const owns = acct ? await shardLinks.ownsAccount(acct, req.user.id) : false
        if (!owns) return res.status(403).json({ message: 'That character is not on an account linked to you.' })
      }
      return res.json(await enrichCharProfile(result.data))
    }
    if (result.status === 404) return res.status(404).json({ message: 'Character not found.' })
    if (result.status === 503 || result.status === 0) {
      return res.status(503).json({ message: 'The game server is restarting — try again shortly.' })
    }
    return res.status(502).json({ message: 'Could not reach the shard.' })
  } catch (err) {
    log.error('player.shard.getChar', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

// GET /player/shard/sales — recent player-vendor sales for the caller's linked
// accounts only (as seller/owner). Read from the site's own event log.
async function getSales(req, res) {
  try {
    const links = await shardLinks.listForUser(req.user.id)
    const accounts = links.map((l) => l.account)
    return res.json(await salesForAccounts(accounts))
  } catch (err) {
    log.error('player.shard.getSales', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

// GET /player/shard/houses — the caller's OWN houses (home status), scoped to
// their linked accounts. A player sees their own decay/IDOC standing; never
// anyone else's. Full detail is fine here — it's their property.
async function getHouses(req, res) {
  try {
    const links = await shardLinks.listForUser(req.user.id)
    const accounts = links.map((l) => l.account)
    return res.json(await shardState.listHousesForAccounts(accounts))
  } catch (err) {
    log.error('player.shard.getHouses', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

// Map a failed uoLinkClient.createAccount result to a user-facing HTTP response.
// The password is never echoed anywhere; only the mapped reason is returned.
function mapCreateAccountError(res, result) {
  const reason = (result.data && result.data.reason) || ''
  switch (result.status) {
    case 409:
      return res.status(409).json({ message: 'That account name is already taken.' })
    case 429:
      return res.status(429).json({ message: 'The account limit for your network has been reached.' })
    case 403:
      return res.status(403).json({ message: 'Game-account signups are not available on this shard right now.' })
    case 400:
      return res.status(400).json({ message: reason || 'The account name or password was not accepted.' })
    case 503:
    case 0:
      return res.status(503).json({ message: 'The game server is unavailable — try again shortly.' })
    default:
      return res.status(502).json({ message: 'Could not reach the shard to create the account.' })
  }
}

// POST /player/shard/account — provision a GAME account for the signed-in website
// user and auto-link it (Protocol 2.0 hybrid). Used by self-serve signup and the
// invite-accept "create game account" step alike (both act as the signed-in user).
// actor + websiteUserId are stamped from the session; the browser IP (req.ip,
// trust-proxy configured) is forwarded for the shard's per-IP cap; the password is
// never logged. Gated by the game_account_signup setting AND the shard's own mode.
async function createGameAccount(req, res) {
  const { account, password } = req.body
  try {
    if (!(await settings.isGameAccountSignupEnabled())) {
      return res.status(403).json({ message: 'Game-account signup is not available right now.' })
    }
    const result = await uoLinkClient.createAccount({
      actor: req.user.username,
      account,
      password,
      websiteUserId: req.user.id,
      ip: req.ip,
    })
    if (result.ok) {
      // Mirror the link locally so the portal lists the account immediately.
      await shardLinks.link({ account, userId: req.user.id })
      await activity.log({ req, userId: req.user.id, action: 'shard.account.create', detail: { account } })
      log.info('game account created', { account, userId: req.user.id, ip: req.ip })
      return res.status(201).json({ account, linked: true })
    }
    return mapCreateAccountError(res, result)
  } catch (err) {
    log.error('player.shard.createGameAccount', err)
    return res.status(500).json({ message: 'Internal Server Error' })
  }
}

module.exports = { link, listAccounts, roster, vendors, getChar, getSales, getHouses, createGameAccount }
