const { pool, query } = require('../../utils/db')

// Raw SQL for the spawn atlas. Every table here is IMPORT-OWNED: `replaceAtlas`
// empties and refills all six inside one transaction, and nothing else in the
// codebase writes to them. There are no foreign keys, consistent with every
// other shard_* table.

const BATCH = 500

const ATLAS_TABLES = [
  'shard_spawn_point_types',
  'shard_spawn_points',
  'shard_spawn_creatures',
  'shard_regions',
  'shard_landmarks',
  'shard_champion_spawns',
]

async function insertBatched(conn, sql, rows) {
  for (let i = 0; i < rows.length; i += BATCH) {
    await conn.batch(sql, rows.slice(i, i + BATCH))
  }
  return rows.length
}

/**
 * Replace the entire atlas in one transaction.
 *
 * All-or-nothing on purpose: a failed reload must leave the previous atlas
 * intact rather than a half-loaded world, since a partially-imported atlas is
 * indistinguishable from a real one to anyone reading it.
 *
 * `DELETE`, not `TRUNCATE` — `TRUNCATE` is DDL in MariaDB and implicitly
 * commits, which would defeat exactly that guarantee. At ~7k rows the cost of
 * `DELETE` is irrelevant.
 */
async function replaceAtlas(atlas, art = {}) {
  const conn = await pool.getConnection()
  const counts = {}
  try {
    await conn.beginTransaction()

    for (const table of ATLAS_TABLES) await conn.query(`DELETE FROM ${table}`)

    counts.creatures = await insertBatched(
      conn,
      'INSERT INTO shard_spawn_creatures (slug, name, total, points, facets, art) VALUES (?,?,?,?,?,?)',
      atlas.creatures.map((c) => [
        c.slug,
        c.name,
        c.total ?? 0,
        c.points ?? 0,
        JSON.stringify(c.facets ?? {}),
        art[c.slug] ?? null,
      ]),
    )

    counts.regions = await insertBatched(
      conn,
      'INSERT INTO shard_regions (facet, name, type, priority, parent, rects) VALUES (?,?,?,?,?,?)',
      atlas.regions.map((r) => [
        r.facet,
        r.name,
        r.type || null,
        r.priority ?? 0,
        r.parent || null,
        JSON.stringify(r.rects ?? []),
      ]),
    )

    counts.landmarks = await insertBatched(
      conn,
      'INSERT INTO shard_landmarks (facet, name, grp, x, y, z) VALUES (?,?,?,?,?,?)',
      atlas.landmarks.map((l) => [
        l.facet,
        l.name,
        l.group || null,
        l.x ?? 0,
        l.y ?? 0,
        l.z ?? 0,
      ]),
    )

    counts.champions = await insertBatched(
      conn,
      'INSERT INTO shard_champion_spawns ' +
        '(slug, name, grp, type, random_type, facet, x, y, z, radius, label) ' +
        'VALUES (?,?,?,?,?,?,?,?,?,?,?)',
      atlas.champions.map((c) => [
        c.slug,
        c.name,
        c.group || null,
        c.type || null,
        c.randomType ? 1 : 0,
        c.facet,
        c.x ?? 0,
        c.y ?? 0,
        c.z ?? 0,
        c.radius ?? 0,
        c.label || null,
      ]),
    )

    // Point ids are assigned explicitly rather than left to AUTO_INCREMENT: the
    // join rows need to know them and `conn.batch()` reports no usable insertId
    // for a multi-row insert. Safe because this transaction just emptied the
    // table and nothing else writes to it.
    counts.points = await insertBatched(
      conn,
      'INSERT INTO shard_spawn_points ' +
        '(id, facet, name, x, y, width, height, spawn_range, max_count, min_delay, max_delay, ' +
        'tod_start, tod_end, tod_mode, region, landmark, label) ' +
        'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
      atlas.points.map((p, i) => [
        i + 1,
        p.facet,
        p.name,
        p.x,
        p.y,
        p.width ?? 0,
        p.height ?? 0,
        p.range ?? 0,
        p.maxCount ?? 0,
        p.minDelay ?? 0,
        p.maxDelay ?? 0,
        p.todStart ?? 0,
        p.todEnd ?? 0,
        p.todMode ?? 0,
        p.region,
        p.landmark,
        p.label || 'Wilderness',
      ]),
    )

    counts.pointTypes = await insertBatched(
      conn,
      'INSERT INTO shard_spawn_point_types (point_id, slug, max_count) VALUES (?,?,?)',
      atlas.pointTypes,
    )

    await conn.query(
      'INSERT INTO shard_atlas_meta (id, payload) VALUES (1, ?) ' +
        'ON DUPLICATE KEY UPDATE payload = VALUES(payload), imported_at = CURRENT_TIMESTAMP',
      [JSON.stringify({ ...atlas.meta, importedCounts: counts })],
    )

    // A completed import answers whatever was pending.
    await conn.query('DELETE FROM shard_atlas_pending')

    await conn.commit()
    return counts
  } catch (err) {
    await conn.rollback().catch(() => {})
    throw err
  } finally {
    conn.release()
  }
}

async function getMeta() {
  const rows = await query('SELECT payload, imported_at FROM shard_atlas_meta WHERE id = 1')
  if (rows.length === 0) return null
  const payload = typeof rows[0].payload === 'string' ? JSON.parse(rows[0].payload) : rows[0].payload
  return { ...payload, importedAt: rows[0].imported_at }
}

/** Facet names currently loaded, used to detect a facet disappearing. */
async function getFacets() {
  const rows = await query('SELECT DISTINCT facet FROM shard_spawn_points ORDER BY facet')
  return rows.map((row) => row.facet)
}

// ── Pending review ─────────────────────────────────────────────────────────

async function getPending() {
  const rows = await query('SELECT payload, status, detected_at FROM shard_atlas_pending WHERE id = 1')
  if (rows.length === 0) return null
  const payload = typeof rows[0].payload === 'string' ? JSON.parse(rows[0].payload) : rows[0].payload
  return { ...payload, status: rows[0].status, detectedAt: rows[0].detected_at }
}

async function setPending(payload, status = 'pending') {
  return query(
    'INSERT INTO shard_atlas_pending (id, status, payload) VALUES (1, ?, ?) ' +
      'ON DUPLICATE KEY UPDATE status = VALUES(status), payload = VALUES(payload), ' +
      'detected_at = CURRENT_TIMESTAMP',
    [status, JSON.stringify(payload)],
  )
}

async function clearPending() {
  return query('DELETE FROM shard_atlas_pending')
}

// ── Reads (the public /atlas surface) ──────────────────────────────────────
//
// Every read here is a plain indexed query over ~7k rows and is served entirely
// from MariaDB: the atlas is static shard content, so nothing on this path
// touches the sidecar and nothing degrades when the shard is down.
//
// A facet filter is expressed as EXISTS over the points, never as a JSON path
// built from caller input. `shard_spawn_creatures.facets` is a JSON object keyed
// by facet name, and matching a key means either concatenating the name into a
// path or handing it to JSON_SEARCH — whose search string treats `%` and `_` as
// wildcards, so `?facet=%` would quietly match everything. The join is exact and
// uses the indexes that already exist.
const CREATURE_FACET_EXISTS = `EXISTS (
  SELECT 1 FROM shard_spawn_point_types t
    JOIN shard_spawn_points p ON p.id = t.point_id
   WHERE t.slug = c.slug AND p.facet = ?
)`

// Build the WHERE for a creature search. `q` is a substring match on the display
// name — a LIKE scan, which is free at ~800 rows and, unlike FULLTEXT, has no
// minimum token length to break a search for "orc".
function creatureWhere({ q, facet }) {
  const where = []
  const params = []
  if (q) {
    where.push('c.name LIKE ?')
    params.push(`%${q}%`)
  }
  if (facet) {
    where.push(CREATURE_FACET_EXISTS)
    params.push(facet)
  }
  return { sql: where.length ? `WHERE ${where.join(' AND ')}` : '', params }
}

async function countCreatures({ q = '', facet = '' } = {}) {
  const { sql, params } = creatureWhere({ q, facet })
  const rows = await query(`SELECT COUNT(*) AS n FROM shard_spawn_creatures c ${sql}`, params)
  return rows[0] ? Number(rows[0].n) : 0
}

function listCreatures({ q = '', facet = '', limit = 50, offset = 0 } = {}) {
  const { sql, params } = creatureWhere({ q, facet })
  return query(
    `SELECT c.slug, c.name, c.total, c.points, c.facets, c.art
       FROM shard_spawn_creatures c
       ${sql}
      ORDER BY c.total DESC, c.name ASC
      LIMIT ? OFFSET ?`,
    [...params, limit, offset],
  )
}

async function getCreature(slug) {
  const rows = await query(
    'SELECT slug, name, total, points, facets, art FROM shard_spawn_creatures WHERE slug = ?',
    [slug],
  )
  return rows[0] || null
}

/**
 * Where a creature spawns, grouped by resolved place.
 *
 * This is the answer the atlas exists to give — "lizardman → Shrines,
 * Isamu-Jima, Yew" — so it is aggregated in SQL rather than by summing 6,455
 * point rows in Node.
 */
function listCreaturePlaces(slug, { facet = '' } = {}) {
  const params = [slug]
  let facetSql = ''
  if (facet) {
    facetSql = 'AND p.facet = ?'
    params.push(facet)
  }
  return query(
    `SELECT p.facet, p.label, COUNT(*) AS spawners, SUM(t.max_count) AS max_alive
       FROM shard_spawn_point_types t
       JOIN shard_spawn_points p ON p.id = t.point_id
      WHERE t.slug = ? ${facetSql}
      GROUP BY p.facet, p.label
      ORDER BY spawners DESC, p.facet ASC, p.label ASC`,
    params,
  )
}

/** The individual spawners for a creature, newest-largest first. Bounded. */
function listCreaturePoints(slug, { facet = '', limit = 200 } = {}) {
  const params = [slug]
  let facetSql = ''
  if (facet) {
    facetSql = 'AND p.facet = ?'
    params.push(facet)
  }
  params.push(limit)
  return query(
    `SELECT p.id, p.facet, p.name, p.x, p.y, p.width, p.height, p.spawn_range,
            p.min_delay, p.max_delay, p.tod_start, p.tod_end, p.tod_mode,
            p.region, p.landmark, p.label, t.max_count
       FROM shard_spawn_point_types t
       JOIN shard_spawn_points p ON p.id = t.point_id
      WHERE t.slug = ? ${facetSql}
      ORDER BY t.max_count DESC, p.facet ASC, p.label ASC, p.id ASC
      LIMIT ?`,
    params,
  )
}

/** Every other creature sharing a spawner with this one. */
function listCreatureCompanions(slug, { limit = 24 } = {}) {
  return query(
    `SELECT o.slug, c.name, COUNT(*) AS shared
       FROM shard_spawn_point_types t
       JOIN shard_spawn_point_types o ON o.point_id = t.point_id AND o.slug <> t.slug
       JOIN shard_spawn_creatures c ON c.slug = o.slug
      WHERE t.slug = ?
      GROUP BY o.slug, c.name
      ORDER BY shared DESC, c.name ASC
      LIMIT ?`,
    [slug, limit],
  )
}

function listRegions({ facet = '', q = '' } = {}) {
  const where = []
  const params = []
  if (facet) {
    where.push('facet = ?')
    params.push(facet)
  }
  if (q) {
    where.push('name LIKE ?')
    params.push(`%${q}%`)
  }
  return query(
    `SELECT facet, name, type, priority, parent, rects
       FROM shard_regions
       ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
      ORDER BY facet ASC, name ASC`,
    params,
  )
}

function listLandmarks({ facet = '', q = '' } = {}) {
  const where = []
  const params = []
  if (facet) {
    where.push('facet = ?')
    params.push(facet)
  }
  if (q) {
    where.push('(name LIKE ? OR grp LIKE ?)')
    params.push(`%${q}%`, `%${q}%`)
  }
  return query(
    `SELECT facet, name, grp, x, y, z
       FROM shard_landmarks
       ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
      ORDER BY facet ASC, grp ASC, name ASC`,
    params,
  )
}

function listChampions({ facet = '' } = {}) {
  const params = []
  let where = ''
  if (facet) {
    where = 'WHERE facet = ?'
    params.push(facet)
  }
  return query(
    `SELECT slug, name, grp, type, random_type, facet, x, y, z, radius, label
       FROM shard_champion_spawns
       ${where}
      ORDER BY facet ASC, name ASC`,
    params,
  )
}

module.exports = {
  replaceAtlas,
  getMeta,
  getFacets,
  getPending,
  setPending,
  clearPending,
  countCreatures,
  listCreatures,
  getCreature,
  listCreaturePlaces,
  listCreaturePoints,
  listCreatureCompanions,
  listRegions,
  listLandmarks,
  listChampions,
}
