<?php
/**
 * Admin-page capability policy.
 *
 * Encodes the §3.1 / §3.2 admin-ui-spec capability mappings
 * documented in `specifications/admin-ui-spec.md` so every
 * Phase-8 admin page resolves its required-capability literal
 * through one well-known seam rather than scattering capability
 * literals across the slice 8c-2 admin-service provider's
 * `add_menu_page()` / `add_submenu_page()` calls.
 *
 * The policy is expressed as a static lookup keyed by a stable
 * "page token" string (for example `'admin.dashboard'`,
 * `'admin.settings'`). Each page token resolves to a single
 * required capability literal, mirroring the §3.1 split:
 *
 *   - §3.1 編集者相当 ("editor-tier") pages — Dashboard, Products
 *     list, Products new — require {@see self::CAP_VIEW_PRODUCTS}
 *     (the §3.3 `edit_posts` fallback documented in
 *     `specifications/admin-ui-spec.md` §3.1 編集者相当). Editors
 *     reach these pages.
 *
 *   - §3.1 管理者相当 ("admin-tier") pages — Settings,
 *     Diagnostics — require {@see self::CAP_FALLBACK_MANAGE}
 *     (`manage_options`) per `specifications/admin-ui-spec.md`
 *     §3.1 管理者相当 / §3.2 ("編集者には `Settings` メニュー
 *     を表示しない"). Editors carrying only `edit_posts` are
 *     denied.
 *
 *   - Unknown page tokens deliberately deny by default to
 *     {@see self::CAP_FALLBACK_MANAGE} rather than throw —
 *     admin pages registered by future slices may legitimately
 *     introduce new tokens before this class is updated, and a
 *     deny-by-default response keeps the surface failing closed
 *     in the meantime.
 *
 * Mirror of {@see \Affilun\Presentation\Rest\Permission\CapabilityPolicy}:
 *
 *   - {@see self::CAP_MANAGE_SETTINGS} mirrors the REST
 *     `affilun_manage_settings` capability so a future
 *     custom-capability migration can swap
 *     {@see self::CAP_FALLBACK_MANAGE} for the canonical
 *     `affilun_*` literal in one place.
 *
 *   - {@see self::CAP_FALLBACK_MANAGE} / {@see self::CAP_VIEW_PRODUCTS}
 *     mirror the REST `manage_options` / `edit_posts` fallbacks.
 *
 * The policy is stateless and exposed entirely as static methods
 * so the slice 8c-2 admin-service provider, the per-page render
 * callbacks, and tests can resolve a page's required-capability
 * literal without instantiating anything.
 *
 * @package Affilun\Presentation\Admin
 */

declare(strict_types=1);

namespace Affilun\Presentation\Admin;

/**
 * Static §3.1 / §3.2 capability policy for the admin-page
 * surface.
 *
 * See the file-level docblock for the mapping to
 * `admin-ui-spec.md` §3.1 / §3.2, the editor-tier vs admin-tier
 * split, and the deny-by-default policy for unknown page tokens.
 */
final class AdminCapability {

	/**
	 * §3.3 candidate capability for managing plugin settings.
	 *
	 * Mirrors {@see \Affilun\Presentation\Rest\Permission\CapabilityPolicy::CAP_MANAGE_SETTINGS}
	 * so a future custom-capability migration can swap the
	 * {@see self::CAP_FALLBACK_MANAGE} literal for this one in
	 * one place.
	 */
	public const CAP_MANAGE_SETTINGS = 'affilun_manage_settings';

	/**
	 * §3.3 candidate capability for reading the diagnostics
	 * surface (Phase 2 KI-008).
	 *
	 * Mirrors {@see \Affilun\Presentation\Rest\Permission\CapabilityPolicy::CAP_VIEW_DIAGNOSTICS}
	 * so a future custom-capability migration can swap the
	 * {@see self::CAP_FALLBACK_MANAGE} fallback for this one in
	 * one place. Granting this capability to a non-administrator
	 * role (typically through Members / User Role Editor) lets
	 * the site authorize an editor to read the Diagnostics admin
	 * page without escalating to `manage_options`.
	 */
	public const CAP_VIEW_DIAGNOSTICS = 'affilun_view_diagnostics';

	/**
	 * §3.3 candidate capability for reading the operational-log
	 * surface (Phase 2 KI-008).
	 *
	 * Mirrors {@see \Affilun\Presentation\Rest\Permission\CapabilityPolicy::CAP_VIEW_LOGS}.
	 */
	public const CAP_VIEW_LOGS = 'affilun_view_logs';

	/**
	 * §3.3 candidate capability for triggering sync / cache work
	 * from the admin Sync Jobs page (Phase 2 KI-008).
	 *
	 * Mirrors {@see \Affilun\Presentation\Rest\Permission\CapabilityPolicy::CAP_RUN_SYNC}.
	 */
	public const CAP_RUN_SYNC = 'affilun_run_sync';

	/**
	 * §3.3 initial-version fallback capability for
	 * administrator-tier surfaces (`manage_options`).
	 *
	 * Per `specifications/admin-ui-spec.md` §3.1 管理者相当 the
	 * Settings / Diagnostics pages require this capability.
	 */
	public const CAP_FALLBACK_MANAGE = 'manage_options';

	/**
	 * §3.3 initial-version fallback capability for editor-tier
	 * surfaces (`edit_posts`).
	 *
	 * Per `specifications/admin-ui-spec.md` §3.1 編集者相当 the
	 * Dashboard / Products list / Products new pages require
	 * this capability.
	 */
	public const CAP_VIEW_PRODUCTS = 'edit_posts';

	/**
	 * §3.1 page token: Dashboard.
	 */
	public const PAGE_TOKEN_DASHBOARD = 'admin.dashboard';

	/**
	 * §3.1 page token: Products list.
	 */
	public const PAGE_TOKEN_PRODUCTS_LIST = 'admin.products.list';

	/**
	 * §3.1 page token: Products "Add New".
	 */
	public const PAGE_TOKEN_PRODUCTS_NEW = 'admin.products.new';

	/**
	 * §3.1 page token: Settings.
	 */
	public const PAGE_TOKEN_SETTINGS = 'admin.settings';

	/**
	 * §3.1 page token: Diagnostics.
	 */
	public const PAGE_TOKEN_DIAGNOSTICS = 'admin.diagnostics';

	/**
	 * §3.1 page token: Logs (Phase 2 KI-008).
	 */
	public const PAGE_TOKEN_LOGS = 'admin.logs';

	/**
	 * §3.1 page token: Sync Jobs (Phase 2 KI-008).
	 */
	public const PAGE_TOKEN_SYNC_JOBS = 'admin.sync_jobs';

	/**
	 * §3.1 page token: Migration / growth scans.
	 */
	public const PAGE_TOKEN_MIGRATION = 'admin.migration';

	/**
	 * §3.1 page token: Click analytics.
	 */
	public const PAGE_TOKEN_CLICKS = 'admin.clicks';

	/**
	 * §3.1 page token: Audit Events (Phase 2 KI-006 follow-up).
	 *
	 * Capability gating mirrors the §12.4 / §12.5 REST surfaces
	 * (`audit.read` / `audit.summary`) which both map to the
	 * `affilun_view_diagnostics` OR-list with `manage_options`
	 * fallback. The audit admin page renders the same projected
	 * row shape the REST surfaces emit so an admin who can read
	 * one can read the other.
	 */
	public const PAGE_TOKEN_AUDIT = 'admin.audit';

	/**
	 * §3.1 / §3.2 mapping from page token to required capability
	 * literal.
	 *
	 * Held as a private constant so {@see self::requiredCapability()}
	 * and {@see self::validPageTokens()} share the same source of
	 * truth.
	 *
	 * @var array<string, string>
	 */
	private const PAGE_TOKEN_MAP = array(
		self::PAGE_TOKEN_DASHBOARD     => self::CAP_VIEW_PRODUCTS,
		self::PAGE_TOKEN_PRODUCTS_LIST => self::CAP_VIEW_PRODUCTS,
		self::PAGE_TOKEN_PRODUCTS_NEW  => self::CAP_VIEW_PRODUCTS,
		self::PAGE_TOKEN_SETTINGS      => self::CAP_FALLBACK_MANAGE,
		self::PAGE_TOKEN_DIAGNOSTICS   => self::CAP_VIEW_DIAGNOSTICS,
		self::PAGE_TOKEN_LOGS          => self::CAP_VIEW_LOGS,
		self::PAGE_TOKEN_SYNC_JOBS     => self::CAP_RUN_SYNC,
		self::PAGE_TOKEN_MIGRATION     => self::CAP_VIEW_DIAGNOSTICS,
		self::PAGE_TOKEN_CLICKS        => self::CAP_VIEW_DIAGNOSTICS,
		self::PAGE_TOKEN_AUDIT         => self::CAP_VIEW_DIAGNOSTICS,
	);

	/**
	 * §3.3 OR-list extension: per-page-token map of secondary
	 * (fallback) capabilities accepted alongside the primary
	 * capability returned by {@see self::requiredCapability()}.
	 *
	 * Mirrors the REST {@see \Affilun\Presentation\Rest\Permission\CapabilityPolicy}
	 * `[primary, fallback]` shape so a site that has not yet
	 * granted the dedicated `affilun_*` capability still authorizes
	 * `manage_options` administrators by default. Pages whose
	 * primary capability already IS the fallback (`manage_options`
	 * Settings, `edit_posts` Products) need no entry here.
	 *
	 * @var array<string, list<string>>
	 */
	private const FALLBACK_MAP = array(
		self::PAGE_TOKEN_DIAGNOSTICS => array( self::CAP_FALLBACK_MANAGE ),
		self::PAGE_TOKEN_LOGS        => array( self::CAP_FALLBACK_MANAGE ),
		self::PAGE_TOKEN_SYNC_JOBS   => array( self::CAP_FALLBACK_MANAGE ),
		self::PAGE_TOKEN_MIGRATION   => array( self::CAP_FALLBACK_MANAGE ),
		self::PAGE_TOKEN_CLICKS      => array( self::CAP_FALLBACK_MANAGE ),
		self::PAGE_TOKEN_AUDIT       => array( self::CAP_FALLBACK_MANAGE ),
	);

	/**
	 * Return the required capability literal for the given page
	 * token.
	 *
	 * Unknown page tokens deliberately deny by default to
	 * {@see self::CAP_FALLBACK_MANAGE} rather than throw — admin
	 * pages registered by future slices may legitimately
	 * introduce new tokens before this class is updated, and a
	 * deny-by-default response keeps the surface failing closed
	 * in the meantime.
	 *
	 * @param string $pageToken Stable page token (case-sensitive).
	 */
	public static function requiredCapability( string $pageToken ): string {
		return self::PAGE_TOKEN_MAP[ $pageToken ] ?? self::CAP_FALLBACK_MANAGE;
	}

	/**
	 * Return the OR-list of capability literals that authorize
	 * access to the supplied page token (Phase 2 KI-008).
	 *
	 * Order: primary capability first (the one returned by
	 * {@see self::requiredCapability()}), followed by zero or more
	 * fallback capabilities the policy accepts. Mirrors the REST
	 * {@see \Affilun\Presentation\Rest\Permission\CapabilityPolicy}
	 * OR-list contract so a site that has not yet granted the
	 * dedicated `affilun_*` capability still authorizes
	 * `manage_options` administrators by default.
	 *
	 * Unknown page tokens deny by default to a single-element list
	 * containing {@see self::CAP_FALLBACK_MANAGE}, matching
	 * {@see self::requiredCapability()}'s deny-by-default policy.
	 *
	 * @param string $pageToken Stable page token (case-sensitive).
	 *
	 * @return list<string>
	 */
	public static function requiredCapabilities( string $pageToken ): array {
		$primary  = self::requiredCapability( $pageToken );
		$fallback = self::FALLBACK_MAP[ $pageToken ] ?? array();

		$ordered = array_merge( array( $primary ), $fallback );

		// De-duplicate while preserving order so a primary that is
		// itself the fallback (Settings, Products) does not appear
		// twice in the returned list.
		$seen   = array();
		$result = array();
		foreach ( $ordered as $cap ) {
			if ( array_key_exists( $cap, $seen ) ) {
				continue;
			}
			$seen[ $cap ] = true;
			$result[]     = $cap;
		}

		return $result;
	}

	/**
	 * Return the full closed set of page tokens this policy
	 * currently recognizes, in declaration order.
	 *
	 * Used by tests to assert that every documented page token is
	 * mapped, and by diagnostics surfaces that want to enumerate
	 * the known admin pages.
	 *
	 * @return list<string>
	 */
	public static function validPageTokens(): array {
		return array_values( array_keys( self::PAGE_TOKEN_MAP ) );
	}

	/**
	 * Report whether the supplied capability probe authorizes
	 * access to the given page token.
	 *
	 * The probe closure carries the per-request capability check
	 * (typically a thin wrapper around `current_user_can()`); its
	 * signature is `function (string $cap): bool`. Returns true
	 * when the probe returns true for the page token's required
	 * capability literal.
	 *
	 * @param callable $capProbe  Per-request capability probe.
	 * @param string   $pageToken Stable page token (case-sensitive).
	 *
	 * @phpstan-param callable(string): bool $capProbe
	 */
	public static function isAuthorized( callable $capProbe, string $pageToken ): bool {
		foreach ( self::requiredCapabilities( $pageToken ) as $cap ) {
			if ( (bool) $capProbe( $cap ) ) {
				return true;
			}
		}

		return false;
	}
}
