<?php
/**
 * Version-keyed schema migration runner.
 *
 * Compares the installed schema version stored in wp_options against the
 * WRENCHPRESS_DB_VERSION constant and applies any pending migration callables
 * in order. Runs on plugins_loaded so plugin auto-updates (which do not fire
 * the activation hook) still apply schema changes without re-activation.
 *
 * @package WrenchPress
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class WrenchPress_Migrations
 */
class WrenchPress_Migrations {

	/**
	 * Current required state of the role/capability matrix.
	 *
	 * Tracked independently of the schema version (WRENCHPRESS_DB_VERSION)
	 * because role and capability grants and schema changes move on separate
	 * timelines. Activation grants capabilities through
	 * WrenchPress_Activator::register_roles_and_caps(), but a plugin
	 * auto-update does not fire the activation hook, so an upgraded (not
	 * reactivated) site would never gain capabilities added in a release.
	 * Bump this constant whenever the role/capability matrix changes so the
	 * upgrade runner re-applies the matrix idempotently.
	 *
	 * Version 1 covers the Phase 2 addition of wrenchpress_manage_leads, which
	 * gates the Leads admin menu and the Settings submenu. Without this
	 * re-grant on upgrade those surfaces never register for any role on a site
	 * upgraded from Phase 1.
	 *
	 * Version 2 covers the Phase 3 vehicle/work-order/mechanic capabilities
	 * (wrenchpress_view_vehicles, wrenchpress_create_vehicles,
	 * wrenchpress_edit_vehicles, wrenchpress_delete_vehicles,
	 * wrenchpress_manage_workorders, wrenchpress_change_workorder_status, and
	 * the now-granted wrenchpress_add_workorder_notes). Bumping to 2 re-applies
	 * the expanded matrix so an upgraded (not reactivated) site gains the new
	 * vehicle and work-order surfaces, with the mechanic split (status and notes
	 * but not pricing) intact.
	 *
	 * Version 3 covers the Phase 4 appointment capabilities
	 * (wrenchpress_manage_appointments granted to owner/manager and front desk;
	 * wrenchpress_view_appointments granted to all staff including the mechanic).
	 * Bumping to 3 re-applies the matrix so an upgraded (not reactivated) site
	 * gains the appointment view/manage split.
	 *
	 * Version 4 covers the Phase 5 service-reminder capability
	 * (wrenchpress_manage_reminders granted to owner/manager and front desk; the
	 * mechanic works the job, not the follow-up, so it is withheld). Bumping to 4
	 * re-applies the matrix so an upgraded (not reactivated) site gains the
	 * reminder worklist surface for the two front-of-house roles.
	 */
	const CAPS_VERSION = 4;

	/**
	 * Option key (in wp_options) tracking the granted caps version.
	 *
	 * Deliberately separate from WRENCHPRESS_DB_VERSION_OPTION so the schema
	 * migration and the capability re-grant each re-run only when their own
	 * version moves, and neither short-circuits the other.
	 */
	const CAPS_VERSION_OPTION = 'wrenchpress_caps_version';

	/**
	 * Ordered map of schema version => callable.
	 *
	 * Add a new entry whenever WRENCHPRESS_DB_VERSION is bumped. Additive
	 * changes (new column, new index, new table) go through dbDelta via the
	 * schema class and are idempotent; destructive changes need explicit,
	 * guarded ALTER/DROP statements in their own callable.
	 *
	 * Version 2 is a real additive change: the canonical schema gained the
	 * vehicles.engine column and its index, so re-running create_tables applies
	 * that column to an existing install via dbDelta with no re-activation.
	 *
	 * Version 3 is also additive: the canonical schema gained the
	 * wrenchpress_leads table, so re-running create_tables creates that table on
	 * an existing install via dbDelta with no re-activation.
	 *
	 * Version 4 is again additive: the canonical schema gained the
	 * wrenchpress_work_order_items and wrenchpress_events tables plus the
	 * work_orders.tax_rate and vehicles.status columns and the vehicles status
	 * index, so re-running create_tables applies all of those to an existing
	 * install via dbDelta with no re-activation.
	 *
	 * Version 5 is additive: the appointments table gained work_order_id and
	 * notes columns plus the work_order_id index, so re-running create_tables
	 * applies those to an existing install via dbDelta with no re-activation.
	 *
	 * Version 6 is additive: the canonical schema gained the
	 * vehicles.engine_number column and its index, so re-running create_tables
	 * applies that column to an existing install via dbDelta with no
	 * re-activation. It is a NEW column and not a rename: vehicles.engine keeps
	 * its own meaning (the engine SPEC, e.g. "2.0L Turbo") and its existing
	 * values, while engine_number holds the engine SERIAL number. Existing rows
	 * take the '' default; nothing is dropped, retyped, or rewritten.
	 *
	 * Version 7 is additive but does NOT reuse create_tables: the canonical
	 * schema gained the wrenchpress_documents and wrenchpress_document_counters
	 * tables, and the counters table needs one starting row per document type.
	 * Creating the tables is idempotent through dbDelta, but seeding a counter is
	 * not, so version 7 points at the dedicated create_documents callable, which
	 * runs create_tables and then inserts each counter row only if it is absent.
	 * That insert-if-absent guard means a re-run never rewinds a live sequence.
	 * The version-7 migration only fires once WRENCHPRESS_DB_VERSION is bumped to
	 * 7, which happens when the full document feature is wired.
	 *
	 * Version 8 is additive: the canonical schema gained the
	 * wrenchpress_customer_sessions table (the single store holding both pending
	 * magic-link tokens and active portal sessions, distinguished by a status
	 * column), so re-running create_tables creates that table on an existing
	 * install via dbDelta with no re-activation. Nothing is dropped or retyped.
	 *
	 * Most map entries point at create_tables because those changes are purely
	 * additive and dbDelta is idempotent; a version needing a non-idempotent step
	 * (like the version-7 counter seed) gets its own dedicated callable instead.
	 *
	 * @return array<int, callable> Version-keyed migration callables.
	 */
	public static function migrations() {
		return array(
			1 => array( 'WrenchPress_Schema', 'create_tables' ),
			2 => array( 'WrenchPress_Schema', 'create_tables' ),
			3 => array( 'WrenchPress_Schema', 'create_tables' ),
			4 => array( 'WrenchPress_Schema', 'create_tables' ),
			5 => array( 'WrenchPress_Schema', 'create_tables' ),
			6 => array( 'WrenchPress_Schema', 'create_tables' ),
			7 => array( 'WrenchPress_Schema', 'create_documents' ),
			8 => array( 'WrenchPress_Schema', 'create_tables' ),
		);
	}

	/**
	 * Apply any migrations newer than the installed version, then record the
	 * current schema version. Independently re-grant roles and capabilities
	 * when the installed caps version is behind.
	 *
	 * The caps re-grant is evaluated independently of the schema early-return
	 * so a site whose schema is already current (DB version up to date) but
	 * whose capabilities were never granted on upgrade still gains them. This
	 * closes the case where the Leads menu and Settings submenu silently fail
	 * to register on a site upgraded, rather than reactivated, from Phase 1.
	 *
	 * @return void
	 */
	public static function maybe_upgrade() {
		self::maybe_upgrade_caps();

		$installed = (int) get_option( WRENCHPRESS_DB_VERSION_OPTION, 0 );

		if ( $installed >= WRENCHPRESS_DB_VERSION ) {
			return;
		}

		foreach ( self::migrations() as $version => $callable ) {
			if ( $version > $installed ) {
				call_user_func( $callable );
			}
		}

		update_option( WRENCHPRESS_DB_VERSION_OPTION, WRENCHPRESS_DB_VERSION );
	}

	/**
	 * Re-grant roles and capabilities on the upgrade path when the installed
	 * caps version is behind CAPS_VERSION.
	 *
	 * Capability grants normally run only at activation, but a plugin
	 * auto-update does not fire the activation hook, so an upgraded site would
	 * never gain newly added capabilities. This calls the already-idempotent
	 * WrenchPress_Activator::register_roles_and_caps() (which grants
	 * wrenchpress_manage_leads to administrator, owner/manager, and front desk
	 * and leaves mechanic without it) and is guarded by a dedicated
	 * caps-version option so it runs at most once per caps version rather than
	 * on every plugins_loaded request.
	 *
	 * @return void
	 */
	private static function maybe_upgrade_caps() {
		$installed_caps = (int) get_option( self::CAPS_VERSION_OPTION, 0 );

		if ( $installed_caps >= self::CAPS_VERSION ) {
			return;
		}

		WrenchPress_Activator::register_roles_and_caps();

		update_option( self::CAPS_VERSION_OPTION, self::CAPS_VERSION );
	}
}
