<?php

defined( 'ABSPATH' ) || exit;

/**
 * Hide Login URL module.
 *
 * Replaces the standalone "WPS Hide Login" plugin. Serves the WordPress login
 * form at a custom slug instead of `/wp-login.php`, and 404s (or redirects)
 * the canonical login + admin URLs for unauthenticated visitors.
 *
 * Three concrete behaviours when active:
 *  1. Visiting the configured custom slug serves wp-login.php normally.
 *  2. Visiting `/wp-login.php` (any action) as a guest → blocked per setting
 *     (404 by default; `home_redirect` redirects to the homepage instead).
 *  3. Visiting `/wp-admin/...` as a guest → blocked the same way, with an
 *     allowlist for `admin-ajax.php` and `admin-post.php` (those endpoints
 *     are used by plugins from the front-end and must remain reachable).
 *
 * URL filters rewrite outgoing wp-login.php URLs to the custom slug so emails,
 * logout redirects, and password-reset links work transparently.
 *
 * Default OFF: ships with an empty slug. The module can be enabled on the
 * Modules tab, but the feature stays inert until a slug is configured. No
 * surprise lockouts on first install.
 *
 * Recovery from lockout: define `BW_DEV_HIDE_LOGIN_DISABLE` (any truthy value)
 * in `wp-config.php`. The module steps aside completely until you remove the
 * constant. SFTP recovery in 30 seconds.
 *
 * @package BW_Dev
 */

class BW_Dev_Module_Hide_Login implements BW_Dev_Module_Interface {

	const RECOVERY_CONSTANT = 'BW_DEV_HIDE_LOGIN_DISABLE';
	const ALLOWED_ACTIONS   = array( '404', 'home_redirect' );

	public function slug(): string {
		return 'hide_login';
	}

	public function label(): string {
		return __( 'Hide Login URL', 'bw-dev' );
	}

	public function group(): string {
		return 'security';
	}

	public function default_settings(): array {
		return array(
			'slug'             => '',
			'canonical_action' => '404',
		);
	}

	public function sanitize( array $data ): array {
		$slug_raw = isset( $data['slug'] ) ? (string) $data['slug'] : '';
		$slug     = sanitize_title( $slug_raw );
		// Restrict to a-z 0-9 _ - and 3–50 chars; anything else falls back to inactive.
		if ( ! preg_match( '/^[a-z0-9_-]{3,50}$/', $slug ) ) {
			$slug = '';
		}

		$action = isset( $data['canonical_action'] ) ? sanitize_key( (string) $data['canonical_action'] ) : '404';
		if ( ! in_array( $action, self::ALLOWED_ACTIONS, true ) ) {
			$action = '404';
		}

		return array(
			'slug'             => $slug,
			'canonical_action' => $action,
		);
	}

	public function register(): void {
		// Recovery hatch — bypass entirely if the constant is defined truthy.
		if ( defined( self::RECOVERY_CONSTANT ) && constant( self::RECOVERY_CONSTANT ) ) {
			return;
		}
		if ( '' === $this->configured_slug() ) {
			// Module enabled on Modules tab but no slug configured → feature inert.
			return;
		}

		add_action( 'plugins_loaded', array( $this, 'maybe_intercept' ), 1 );

		// URL filters: rewrite outgoing wp-login.php links/redirects.
		add_filter( 'site_url',         array( $this, 'filter_site_url' ),         10, 4 );
		add_filter( 'network_site_url', array( $this, 'filter_network_site_url' ), 10, 3 );
		add_filter( 'wp_redirect',      array( $this, 'filter_wp_redirect' ),      10, 1 );
	}

	/* ---------------------------------------------------------------------
	 * Setting accessors
	 * ------------------------------------------------------------------- */

	private function configured_slug(): string {
		$value = bw_dev()->settings()->get( $this->slug(), 'slug', '' );
		return is_string( $value ) ? $value : '';
	}

	private function configured_action(): string {
		$value = bw_dev()->settings()->get( $this->slug(), 'canonical_action', '404' );
		return in_array( (string) $value, self::ALLOWED_ACTIONS, true ) ? (string) $value : '404';
	}

	/* ---------------------------------------------------------------------
	 * Request interception
	 * ------------------------------------------------------------------- */

	public function maybe_intercept(): void {
		$path = strtolower( $this->current_request_path() );
		$slug = '/' . $this->configured_slug();

		// (1) Custom slug → serve wp-login.php.
		if ( $path === $slug || $path === $slug . '/' ) {
			$this->serve_login_form();
			return;
		}

		// (2) Canonical /wp-login.php → block for guests.
		if ( '/wp-login.php' === $path ) {
			if ( ! is_user_logged_in() ) {
				$this->block_canonical();
			}
			return;
		}

		// (3) Canonical /wp-admin/* → block for guests, with allowlist.
		if ( '/wp-admin' === $path || 0 === strpos( $path, '/wp-admin/' ) ) {
			if ( $this->is_admin_path_allowlisted( $path ) ) {
				return;
			}
			if ( ! is_user_logged_in() ) {
				$this->block_canonical();
			}
		}
	}

	/**
	 * Allow admin-ajax.php and admin-post.php through unconditionally —
	 * front-end plugins and forms hit these endpoints as guests routinely
	 * (Contact Form 7, Gravity Forms, Heartbeat, REST fallbacks, etc.).
	 */
	private function is_admin_path_allowlisted( string $path ): bool {
		if ( 0 === strpos( $path, '/wp-admin/admin-ajax.php' ) ) {
			return true;
		}
		if ( 0 === strpos( $path, '/wp-admin/admin-post.php' ) ) {
			return true;
		}
		return false;
	}

	private function serve_login_form(): void {
		// Pretend the request was for wp-login.php so the script behaves normally.
		$query                  = $this->current_query_string();
		$_SERVER['SCRIPT_NAME'] = '/wp-login.php';
		$_SERVER['REQUEST_URI'] = '/wp-login.php' . $query;
		$GLOBALS['pagenow']     = 'wp-login.php';

		require ABSPATH . 'wp-login.php';
		exit;
	}

	private function block_canonical(): void {
		if ( 'home_redirect' === $this->configured_action() ) {
			wp_safe_redirect( home_url( '/' ), 302 );
			exit;
		}
		// Default: a clean, theme-less 404. We're pre-init so no theme template is loaded.
		status_header( 404 );
		nocache_headers();
		header( 'Content-Type: text/html; charset=UTF-8' );
		echo "<!DOCTYPE html>\n<html><head><title>404 Not Found</title><meta name=\"robots\" content=\"noindex\"></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>";
		exit;
	}

	/* ---------------------------------------------------------------------
	 * URL rewriting
	 * ------------------------------------------------------------------- */

	public function filter_site_url( $url, $path, $scheme, $blog_id ) {
		unset( $path, $scheme, $blog_id );
		return $this->rewrite_wp_login( $url );
	}

	public function filter_network_site_url( $url, $path, $scheme ) {
		unset( $path, $scheme );
		return $this->rewrite_wp_login( $url );
	}

	public function filter_wp_redirect( $url ) {
		return $this->rewrite_wp_login( $url );
	}

	private function rewrite_wp_login( $url ) {
		if ( ! is_string( $url ) || false === strpos( $url, 'wp-login.php' ) ) {
			return $url;
		}
		return str_replace( '/wp-login.php', '/' . $this->configured_slug(), $url );
	}

	/* ---------------------------------------------------------------------
	 * REQUEST_URI helpers
	 * ------------------------------------------------------------------- */

	private function current_request_path(): string {
		$uri  = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
		$path = wp_parse_url( (string) $uri, PHP_URL_PATH );
		return is_string( $path ) ? $path : '';
	}

	private function current_query_string(): string {
		$uri   = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
		$query = wp_parse_url( (string) $uri, PHP_URL_QUERY );
		return is_string( $query ) && '' !== $query ? '?' . $query : '';
	}

	/* ---------------------------------------------------------------------
	 * Settings tab
	 * ------------------------------------------------------------------- */

	public function render_tab(): void {
		$slug          = $this->configured_slug();
		$action        = $this->configured_action();
		$is_active     = '' !== $slug;
		$is_bypassed   = defined( self::RECOVERY_CONSTANT ) && constant( self::RECOVERY_CONSTANT );
		$slug_name     = BW_Dev_Settings::OPTION . '[' . $this->slug() . '][slug]';
		$action_name   = BW_Dev_Settings::OPTION . '[' . $this->slug() . '][canonical_action]';
		$current_url   = $is_active ? home_url( '/' . $slug . '/' ) : '';
		?>
		<p class="description">
			<?php esc_html_e( 'Move the WordPress login form to a custom URL and 404 the canonical /wp-login.php and /wp-admin/ URLs for unauthenticated visitors. Replaces the standalone "WPS Hide Login" plugin.', 'bw-dev' ); ?>
		</p>

		<?php if ( $is_bypassed ) : ?>
			<div style="background:#fff;border-left:4px solid #d63638;padding:12px 16px;margin:14px 0;max-width:720px;">
				<strong><?php esc_html_e( 'Recovery mode active', 'bw-dev' ); ?></strong>
				<p style="margin:6px 0 0;">
					<?php
					printf(
						/* translators: %s: PHP constant name */
						esc_html__( 'The %s constant is defined in wp-config.php, so this module is currently bypassed. Remove the constant to re-enable hiding the login URL.', 'bw-dev' ),
						'<code>' . esc_html( self::RECOVERY_CONSTANT ) . '</code>'
					);
					?>
				</p>
			</div>
		<?php elseif ( ! $is_active ) : ?>
			<div style="background:#fff;border-left:4px solid #dba617;padding:12px 16px;margin:14px 0;max-width:720px;">
				<strong><?php esc_html_e( 'Feature inactive', 'bw-dev' ); ?></strong>
				<p style="margin:6px 0 0;">
					<?php esc_html_e( 'No custom slug is configured, so /wp-login.php and /wp-admin/ work normally. Set a slug below to activate.', 'bw-dev' ); ?>
				</p>
			</div>
		<?php else : ?>
			<div style="background:#fff;border-left:4px solid #00a32a;padding:12px 16px;margin:14px 0;max-width:720px;">
				<strong><?php esc_html_e( 'Your login URL', 'bw-dev' ); ?></strong>
				<p style="margin:6px 0 0;">
					<a href="<?php echo esc_url( $current_url ); ?>"><code><?php echo esc_html( $current_url ); ?></code></a>
				</p>
				<p style="margin:6px 0 0;font-size:12px;color:#646970;">
					<?php esc_html_e( 'Bookmark this URL. The canonical /wp-login.php and /wp-admin/ pages now return 404 for visitors who are not logged in.', 'bw-dev' ); ?>
				</p>
			</div>
		<?php endif; ?>

		<table class="form-table" role="presentation">
			<tbody>
				<tr>
					<th scope="row">
						<label for="bw-dev-hide-login-slug"><?php esc_html_e( 'Custom login slug', 'bw-dev' ); ?></label>
					</th>
					<td>
						<code><?php echo esc_html( trailingslashit( home_url() ) ); ?></code>
						<input type="text" id="bw-dev-hide-login-slug" name="<?php echo esc_attr( $slug_name ); ?>" value="<?php echo esc_attr( $slug ); ?>" class="regular-text" placeholder="my-secret-login" />
						<p class="description">
							<?php esc_html_e( '3–50 characters, lowercase letters, numbers, hyphens and underscores only. Pick something hard to guess. Leave blank to disable the feature without disabling the module.', 'bw-dev' ); ?>
						</p>
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'When someone visits /wp-login.php or /wp-admin/', 'bw-dev' ); ?></th>
					<td>
						<fieldset>
							<label>
								<input type="radio" name="<?php echo esc_attr( $action_name ); ?>" value="404" <?php checked( $action, '404' ); ?> />
								<?php esc_html_e( 'Return 404 (recommended — no clue the site uses WordPress)', 'bw-dev' ); ?>
							</label><br />
							<label>
								<input type="radio" name="<?php echo esc_attr( $action_name ); ?>" value="home_redirect" <?php checked( $action, 'home_redirect' ); ?> />
								<?php esc_html_e( 'Redirect to the homepage', 'bw-dev' ); ?>
							</label>
						</fieldset>
					</td>
				</tr>
			</tbody>
		</table>

		<h3><?php esc_html_e( 'If you lock yourself out', 'bw-dev' ); ?></h3>
		<p>
			<?php esc_html_e( 'Connect via SFTP, edit wp-config.php, and add this line near the top (after the opening <?php):', 'bw-dev' ); ?>
		</p>
		<pre style="background:#f6f7f7;padding:10px 14px;max-width:720px;border-left:4px solid #2271b1;"><code>define( '<?php echo esc_html( self::RECOVERY_CONSTANT ); ?>', true );</code></pre>
		<p>
			<?php esc_html_e( 'The module will step aside completely. Log in via /wp-login.php as normal, fix the slug here, then remove the constant.', 'bw-dev' ); ?>
		</p>

		<h3><?php esc_html_e( 'What still works', 'bw-dev' ); ?></h3>
		<ul style="list-style:disc;margin-left:20px;">
			<li><?php esc_html_e( 'admin-ajax.php and admin-post.php (form submissions, Heartbeat, plugins) — always reachable.', 'bw-dev' ); ?></li>
			<li><?php esc_html_e( 'Logout, lost-password, and password-reset flows — wp-login.php URLs in WordPress emails are rewritten to your custom slug automatically.', 'bw-dev' ); ?></li>
			<li><?php esc_html_e( 'wp-cron.php — lives at the site root, not /wp-admin/, and is unaffected.', 'bw-dev' ); ?></li>
			<li><?php esc_html_e( 'REST API at /wp-json/ — unaffected (see Security Hardening for separate REST hardening options).', 'bw-dev' ); ?></li>
		</ul>
		<?php
	}

	public function uninstall(): void {
		// Settings live under bw_dev_settings — root option drop covers them.
	}
}
