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

// SQL for installed_modules — the module system's record of what is installed and
// what happened to it on the last boot (db/schema.sql, docs/website/MODULE_SYSTEM.md
// §2.4). Rows are keyed by module id. All state rules live in modules.model.js;
// this file only moves rows.

const COLS = `id, name, version, state, failure_stage, failure_reason,
              source, sha256, installed_at, started_at, updated_at`

const listAll = () => query(`SELECT ${COLS} FROM installed_modules ORDER BY id`)

const getOne = (id) => query(`SELECT ${COLS} FROM installed_modules WHERE id = ?`, [id])

// Write (or refresh) the row for an installed module. A re-install or an upgrade
// updates the metadata and deliberately leaves `state` alone: upgrading an enabled
// module must not silently disable it, and re-installing a disabled one must not
// silently switch it back on. A brand-new row lands in `installed`, the transient
// state the next restart resolves.
// Provenance is COALESCEd, and that is the whole difference between this
// working and not.
//
// `lifecycle.boot()` re-records every module it scanned with no source and no
// sha256 — a directory placed on the volume by hand genuinely has neither, and
// the boot has no way to know where one came from. With a plain
// `source = VALUES(source)` that refresh overwrote both columns with NULL on
// EVERY boot, so an admin-panel install's provenance survived exactly until the
// restart that install asked for. Nothing could catch it before Phase 4: until
// then no caller ever passed a non-null value, and lifecycle.js's comment
// asserting that "recordInstalled leaves what it is not given" was a description
// of an intention rather than of this statement.
//
// COALESCE makes that comment true: a value overwrites, a NULL leaves what is
// there. The cost is that re-placing a DIFFERENT bundle by hand over a row that
// was once installed from a URL keeps the old provenance — which is wrong but
// stale, and strictly better than the alternative, which was wrong and blank.
const upsert = ({ id, name, version, source, sha256 }) =>
  query(
    `INSERT INTO installed_modules (id, name, version, source, sha256, state)
     VALUES (?, ?, ?, ?, ?, 'installed')
     ON DUPLICATE KEY UPDATE
       name    = VALUES(name),
       version = VALUES(version),
       source  = COALESCE(VALUES(source), source),
       sha256  = COALESCE(VALUES(sha256), sha256)`,
    [id, name, version, source ?? null, sha256 ?? null],
  )

// Move one row to a new state. `failureStage`/`failureReason` are written on every
// call — a non-failing transition passes nulls, which is what clears a stale reason
// off a module that has since come up. `stampStarted` sets started_at to now.
const setState = ({ id, state, failureStage = null, failureReason = null, stampStarted = false }) =>
  query(
    `UPDATE installed_modules
        SET state = ?, failure_stage = ?, failure_reason = ?
            ${stampStarted ? ', started_at = CURRENT_TIMESTAMP' : ''}
      WHERE id = ?`,
    [state, failureStage, failureReason, id],
  )

// Boot reset: every row the operator has not disabled goes back to `enabled` with
// no failure recorded, so the load that follows writes this boot's outcome rather
// than leaving the last one on display. `disabled` is untouched — it is a decision,
// not an outcome.
const resetForBoot = () =>
  query(
    `UPDATE installed_modules
        SET state = 'enabled', failure_stage = NULL, failure_reason = NULL
      WHERE state <> 'disabled'`,
  )

// Drop the row entirely. Only the explicit purge does this (§2.5); a plain
// uninstall disables the module and keeps its row and its data.
const remove = (id) => query('DELETE FROM installed_modules WHERE id = ?', [id])

module.exports = { listAll, getOne, upsert, setState, resetForBoot, remove }
