<?php
/**
 * Migration: create `affilun_embed_items` table (schema version 4).
 *
 * Installs the embed/product junction table described in
 * `specifications/database-schema.md` §7. The table holds the ordered
 * association between a reusable embed definition (`affilun_embeds`) and
 * one or more products (`affilun_products`) for single-card and grid
 * display modes. Per §7.1, the initial Gutenberg block does NOT need
 * to reference this table directly: blocks keep `productId` /
 * `productIds` in the post body, and this junction is used only when
 * a reusable embed definition is materialized through the shortcode
 * path or future shared-display surfaces.
 *
 * This migration targets {@see SchemaVersion::of(4)} — the fourth step
 * in affilun's schema pipeline, installed after
 * `affilun_products` (v1), `affilun_product_meta` (v2), and `affilun_embeds` (v3).
 *
 * Scope of this migration:
 *
 *   - Creates `{$prefix}affilun_embed_items` with the full column set
 *     defined in §7.2 (five columns: surrogate primary key, references
 *     to `affilun_embeds.id` and `affilun_products.id`, display sort order,
 *     and a creation timestamp — there is no `updated_at` because the
 *     row is replaced rather than mutated when an embed's item list
 *     changes).
 *
 *   - Adds the unique constraint on `(embed_id, product_id)` (§7.3)
 *     that prevents registering the same product twice on the same
 *     embed.
 *
 *   - Adds the three secondary indexes listed in §7.3
 *     (`idx_embed_id`, `idx_product_id`, `idx_embed_sort`).
 *
 *   - Uses the charset/collation returned by
 *     `$wpdb->get_charset_collate()` (per §17.2) and relies on
 *     `InnoDB` as the storage engine (§17.1).
 *
 * Scope explicitly NOT covered here:
 *
 *   - Foreign keys to `affilun_embeds.id` / `affilun_products.id`. Per §11.2
 *     and §13.4, AFFILUN does not declare foreign keys at the DB level;
 *     referential integrity is enforced by the application layer when
 *     an embed is deleted (§14.2) or when an item list is rewritten.
 *
 *   - `items.length` integrity constraints (`type = 'single'` →
 *     exactly one item, `type = 'grid'` → 1..24 items). §7.4 names
 *     application-layer validation (REST and write-path checks) as
 *     the canonical enforcement point, matching the approach used
 *     for the `type` enum on `affilun_embeds`.
 *
 *   - `DEFAULT CURRENT_TIMESTAMP` on `created_at`. Per §13.4,
 *     environment-sensitive default clauses are avoided; timestamps
 *     are supplied by the application layer on insert.
 *
 *   - Any DML. This migration creates the empty table only; seed data
 *     and backfills belong in later migrations or runtime code paths.
 *
 * The shared DDL-assembly and installer-dispatch skeleton lives in
 * {@see AbstractCreateTableMigration}; this subclass only declares the
 * bits that actually differ between tables (target version,
 * description, logical name, column manifest, index manifest). See the
 * base class's file-level docblock for the idempotency and
 * failure-propagation guarantees inherited here.
 *
 * @package Affilun\Infrastructure\Persistence\Migrations
 */

declare(strict_types=1);

namespace Affilun\Infrastructure\Persistence\Migrations;

use Affilun\Infrastructure\Persistence\SchemaVersion;

/**
 * Creates the `affilun_embed_items` table at schema version 4.
 *
 * Concrete {@see AbstractCreateTableMigration} subclass. See the
 * file-level docblock for the scope of this migration and the columns
 * and indexes it installs; see the base class for the formatting rules
 * and failure semantics that apply to every AFFILUN table-creation
 * migration.
 */
final class CreateEmbedItemsTable extends AbstractCreateTableMigration {

	/**
	 * Logical, unprefixed AFFILUN table name installed by this migration.
	 *
	 * Kept as a class-level constant so tests can reference the exact
	 * name without duplicating the string literal, and so a future
	 * rename has a single location to update.
	 */
	public const TABLE_LOGICAL_NAME = 'affilun_embed_items';

	/**
	 * Target schema version installed by this migration.
	 *
	 * Returns version 4 — the revision that adds the embed/product
	 * junction table on top of `affilun_products` (v1),
	 * `affilun_product_meta` (v2), and `affilun_embeds` (v3). See
	 * `database-schema.md` §13.2 for the canonical version numbering.
	 */
	public function targetVersion(): SchemaVersion {
		return SchemaVersion::of( 4 );
	}

	/**
	 * Short, human-readable description of this migration.
	 *
	 * Used by the diagnostics surface and migration logs. Kept terse
	 * and operator-friendly per the
	 * {@see \Affilun\Infrastructure\Persistence\Migration::description()}
	 * contract.
	 */
	public function description(): string {
		return 'create affilun_embed_items table';
	}

	/**
	 * Logical, unprefixed AFFILUN table name installed by this migration.
	 *
	 * Consumed by {@see AbstractCreateTableMigration::up()} and
	 * {@see AbstractCreateTableMigration::tableName()} to resolve the
	 * physical, prefix-applied name through the injected
	 * {@see \Affilun\Infrastructure\Persistence\TableNameResolver}.
	 */
	protected function logicalTableName(): string {
		return self::TABLE_LOGICAL_NAME;
	}

	/**
	 * Column definitions for `affilun_embed_items`, in the canonical order
	 * defined by `database-schema.md` §7.2.
	 *
	 * The table intentionally has no `updated_at` column: per §7.1 the
	 * junction row is replaced (delete-then-insert) when an embed's
	 * item list changes, rather than updated in place, so only the
	 * creation timestamp is meaningful.
	 *
	 * Each entry is a single column definition without its trailing
	 * comma; the base class adds the indent and inter-column commas.
	 *
	 * @return list<string>
	 */
	protected function columnDefinitions(): array {
		return array(
			'id bigint unsigned NOT NULL AUTO_INCREMENT',
			'embed_id bigint unsigned NOT NULL',
			'product_id bigint unsigned NOT NULL',
			'sort_order int unsigned NOT NULL DEFAULT 0',
			'created_at datetime NOT NULL',
		);
	}

	/**
	 * Index definitions for `affilun_embed_items`, in the canonical order
	 * defined by `database-schema.md` §7.3.
	 *
	 * The `PRIMARY KEY` entry uses the two-space separator that
	 * `dbDelta()` relies on to detect the primary key line — this is
	 * a WordPress-specific formatting quirk, not a typo.
	 *
	 * The unique constraint on `(embed_id, product_id)` prevents
	 * duplicate registration of the same product on a single embed
	 * (§7.3 "理由"); the `idx_embed_sort` composite index accelerates
	 * grid-order retrieval.
	 *
	 * @return list<string>
	 */
	protected function indexDefinitions(): array {
		return array(
			'PRIMARY KEY  (id)',
			'UNIQUE KEY uq_embed_product (embed_id, product_id)',
			'KEY idx_embed_id (embed_id)',
			'KEY idx_product_id (product_id)',
			'KEY idx_embed_sort (embed_id, sort_order)',
		);
	}
}
