<?php
/**
 * Content Locker — email-gate and password lock (milestone M8, FREE hook).
 *
 * Real, server-side gating: the protected content is NOT rendered into the HTML
 * until the visitor unlocks it. On unlock the cookie is set and the page
 * reloads, so the server renders the content only when the cookie is valid —
 * it can't be revealed with "view source".
 *
 * Shortcodes:
 *   [cshield_gate]…protected…[/cshield_gate]                 email gate (captures a lead)
 *   [cshield_lock password="secret"]…protected…[/cshield_lock]   password lock
 *
 * Leads are stored locally and exportable to CSV. Email-marketing integrations
 * (Mailchimp, etc.) and a paywall are reserved for PRO.
 *
 * @package Copyfend
 */

defined( 'ABSPATH' ) || exit;

/**
 * Registers the gate shortcodes, the unlock endpoint and the leads store.
 */
class CShield_Locker {

	const DB_VERSION = '1';

	/**
	 * Settings handler.
	 *
	 * @var CShield_Settings
	 */
	private $settings;

	/**
	 * Constructor.
	 *
	 * @param CShield_Settings $settings Settings handler.
	 */
	public function __construct( CShield_Settings $settings ) {
		$this->settings = $settings;

		if ( $settings->get( 'locker_enabled' ) ) {
			add_shortcode( 'cshield_gate', array( $this, 'shortcode_gate' ) );
			add_shortcode( 'cshield_lock', array( $this, 'shortcode_lock' ) );
			add_action( 'wp_enqueue_scripts', array( $this, 'register_assets' ) );
			add_action( 'wp_ajax_cshield_unlock', array( $this, 'ajax_unlock' ) );
			add_action( 'wp_ajax_nopriv_cshield_unlock', array( $this, 'ajax_unlock' ) );
		}

		if ( is_admin() ) {
			add_action( 'admin_post_cshield_export_leads', array( $this, 'export_leads' ) );
			add_action( 'admin_post_cshield_clear_leads', array( $this, 'clear_leads' ) );
		}
	}

	/* --------------------------------------------------------------------- */
	/* Leads table                                                           */
	/* --------------------------------------------------------------------- */

	/**
	 * Leads table name.
	 *
	 * @return string
	 */
	public static function table() {
		global $wpdb;
		return $wpdb->prefix . 'cshield_leads';
	}

	/**
	 * Create the table when the stored DB version differs.
	 */
	public static function maybe_install() {
		if ( get_option( 'cshield_leads_db' ) === self::DB_VERSION ) {
			return;
		}
		self::install();
		update_option( 'cshield_leads_db', self::DB_VERSION, false );
	}

	/**
	 * Create the leads table via dbDelta.
	 */
	public static function install() {
		global $wpdb;
		$table   = self::table();
		$collate = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE {$table} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			email varchar(190) NOT NULL DEFAULT '',
			post_id bigint(20) unsigned NOT NULL DEFAULT 0,
			created_at datetime NOT NULL DEFAULT '1970-01-01 00:00:00',
			PRIMARY KEY  (id),
			KEY email (email)
		) {$collate};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );
	}

	/* --------------------------------------------------------------------- */
	/* Shortcodes                                                            */
	/* --------------------------------------------------------------------- */

	/**
	 * Email gate: shows content only after an email is submitted.
	 *
	 * @param array  $atts    Attributes (unused).
	 * @param string $content Protected content.
	 * @return string
	 */
	public function shortcode_gate( $atts, $content = '' ) {
		$post_id = (int) get_the_ID();

		if ( $this->is_gate_unlocked( $post_id ) ) {
			return wp_kses_post( wpautop( do_shortcode( (string) $content ) ) );
		}

		wp_enqueue_script( 'cshield-locker' );
		wp_enqueue_style( 'cshield-locker' );
		return $this->render_form( 'gate', $post_id );
	}

	/**
	 * Password lock: shows content only after the correct password.
	 *
	 * @param array  $atts    Attributes ( password ).
	 * @param string $content Protected content.
	 * @return string
	 */
	public function shortcode_lock( $atts, $content = '' ) {
		$atts    = shortcode_atts( array( 'password' => '' ), $atts, 'cshield_lock' );
		$post_id = (int) get_the_ID();

		if ( '' !== $atts['password'] && $this->is_lock_unlocked( $atts['password'] ) ) {
			return wp_kses_post( wpautop( do_shortcode( (string) $content ) ) );
		}

		wp_enqueue_script( 'cshield-locker' );
		wp_enqueue_style( 'cshield-locker' );
		return $this->render_form( 'lock', $post_id );
	}

	/**
	 * Render the gate/lock form (no protected content is emitted here).
	 *
	 * @param string $type    'gate' or 'lock'.
	 * @param int    $post_id Post ID.
	 * @return string
	 */
	private function render_form( $type, $post_id ) {
		$is_gate = ( 'gate' === $type );
		$title   = $is_gate ? $this->settings->get( 'locker_gate_title', '' ) : $this->settings->get( 'locker_lock_title', '' );
		$desc    = $is_gate ? $this->settings->get( 'locker_gate_desc', '' ) : $this->settings->get( 'locker_lock_desc', '' );
		$button  = $this->settings->get( 'locker_gate_button', '' );

		ob_start();
		?>
		<div class="cs-gate" data-type="<?php echo esc_attr( $type ); ?>" data-post="<?php echo (int) $post_id; ?>">
			<div class="cs-gate-box">
				<span class="cs-gate-lock" aria-hidden="true"><svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"></rect><path d="M8 11V7a4 4 0 0 1 8 0v4"></path></svg></span>
				<?php if ( $title ) : ?>
					<strong class="cs-gate-title"><?php echo esc_html( $title ); ?></strong>
				<?php endif; ?>
				<?php if ( $desc ) : ?>
					<p class="cs-gate-desc"><?php echo esc_html( $desc ); ?></p>
				<?php endif; ?>
				<?php if ( $is_gate ) : ?>
					<input type="email" class="cs-gate-input" inputmode="email" autocomplete="email" placeholder="you@example.com" />
				<?php else : ?>
					<input type="password" class="cs-gate-input" autocomplete="off" placeholder="<?php esc_attr_e( 'Password', 'copyfend' ); ?>" />
				<?php endif; ?>
				<button type="button" class="cs-gate-btn"><?php echo esc_html( $button ? $button : __( 'Unlock', 'copyfend' ) ); ?></button>
				<div class="cs-gate-msg" role="alert"></div>
			</div>
		</div>
		<?php
		return (string) ob_get_clean();
	}

	/* --------------------------------------------------------------------- */
	/* Unlock endpoint                                                       */
	/* --------------------------------------------------------------------- */

	/**
	 * AJAX: verify an unlock attempt, set the cookie, and tell the JS to reload.
	 */
	public function ajax_unlock() {
		check_ajax_referer( 'cshield_unlock', 'nonce' );

		$type    = isset( $_POST['type'] ) ? sanitize_key( $_POST['type'] ) : '';
		$post_id = isset( $_POST['post'] ) ? absint( $_POST['post'] ) : 0;

		if ( 'gate' === $type ) {
			$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
			if ( ! is_email( $email ) ) {
				wp_send_json_error( array( 'message' => __( 'Please enter a valid email address.', 'copyfend' ) ) );
			}
			$this->store_lead( $email, $post_id );
			$this->set_cookie( 'cshield_gate_' . $post_id, '1' );
			wp_send_json_success();
		}

		if ( 'lock' === $type ) {
			// Throttle password guessing: max 10 failed attempts per IP / 15 min.
			$throttle = 'cshield_fail_' . md5( (string) CShield_IP::client_ip() );
			if ( (int) get_transient( $throttle ) >= 10 ) {
				wp_send_json_error( array( 'message' => __( 'Too many attempts. Please try again later.', 'copyfend' ) ) );
			}

			// A password must not be altered by sanitization; it is only unslashed, then compared with hash_equals().
			$password = isset( $_POST['password'] ) ? (string) wp_unslash( $_POST['password'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			if ( '' !== $password && $this->post_has_lock_password( $post_id, $password ) ) {
				delete_transient( $throttle );
				$this->set_cookie( 'cshield_lock_' . substr( md5( $password ), 0, 10 ), wp_hash( 'cslock' . $password ) );
				wp_send_json_success();
			}

			set_transient( $throttle, (int) get_transient( $throttle ) + 1, 15 * MINUTE_IN_SECONDS );
			wp_send_json_error( array( 'message' => __( 'Incorrect password.', 'copyfend' ) ) );
		}

		wp_send_json_error();
	}

	/**
	 * Whether the email gate for a post has been unlocked (cookie present).
	 *
	 * @param int $post_id Post ID.
	 * @return bool
	 */
	private function is_gate_unlocked( $post_id ) {
		return ! empty( $_COOKIE[ 'cshield_gate_' . $post_id ] );
	}

	/**
	 * Whether a password lock has been unlocked (signed cookie matches).
	 *
	 * @param string $password Lock password.
	 * @return bool
	 */
	private function is_lock_unlocked( $password ) {
		$name = 'cshield_lock_' . substr( md5( $password ), 0, 10 );
		return isset( $_COOKIE[ $name ] ) && hash_equals( wp_hash( 'cslock' . $password ), sanitize_text_field( wp_unslash( $_COOKIE[ $name ] ) ) );
	}

	/**
	 * Whether the post contains a cshield_lock with the given password.
	 *
	 * @param int    $post_id  Post ID.
	 * @param string $password Submitted password.
	 * @return bool
	 */
	private function post_has_lock_password( $post_id, $password ) {
		$post = $post_id ? get_post( $post_id ) : null;
		if ( ! $post || false === strpos( $post->post_content, '[cshield_lock' ) ) {
			return false;
		}

		preg_match_all( '/\[cshield_lock\b([^\]]*)\]/', $post->post_content, $matches );
		foreach ( $matches[1] as $att_string ) {
			$atts = shortcode_parse_atts( $att_string );
			if ( isset( $atts['password'] ) && hash_equals( (string) $atts['password'], $password ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Set a 30-day unlock cookie.
	 *
	 * @param string $name  Cookie name.
	 * @param string $value Cookie value.
	 */
	private function set_cookie( $name, $value ) {
		$path = defined( 'COOKIEPATH' ) && COOKIEPATH ? COOKIEPATH : '/';
		setcookie( $name, $value, time() + 30 * DAY_IN_SECONDS, $path, defined( 'COOKIE_DOMAIN' ) ? (string) COOKIE_DOMAIN : '', is_ssl(), true );
	}

	/* --------------------------------------------------------------------- */
	/* Leads                                                                 */
	/* --------------------------------------------------------------------- */

	/**
	 * Store a captured lead (de-duplicated per email + post).
	 *
	 * @param string $email   Email.
	 * @param int    $post_id Post ID.
	 */
	private function store_lead( $email, $post_id ) {
		global $wpdb;
		$table = self::table();

		$exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix, not user input.
			$wpdb->prepare( "SELECT id FROM {$table} WHERE email = %s AND post_id = %d", $email, $post_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		);
		if ( $exists ) {
			return;
		}

		$wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$table,
			array(
				'email'      => $email,
				'post_id'    => (int) $post_id,
				'created_at' => current_time( 'mysql' ),
			),
			array( '%s', '%d', '%s' )
		);
	}

	/**
	 * Recent leads.
	 *
	 * @param int $limit Max rows.
	 * @return array
	 */
	public static function recent_leads( $limit = 100 ) {
		global $wpdb;
		$table = self::table();
		$rows  = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix, not user input.
			$wpdb->prepare( "SELECT email, post_id, created_at FROM {$table} ORDER BY id DESC LIMIT %d", $limit ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			ARRAY_A
		);
		return is_array( $rows ) ? $rows : array();
	}

	/**
	 * Total number of leads.
	 *
	 * @return int
	 */
	public static function lead_count() {
		global $wpdb;
		$table = self::table();
		return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore WordPress.DB,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix, not user input.
	}

	/**
	 * Admin: stream every lead as a CSV download.
	 */
	public function export_leads() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to do this.', 'copyfend' ), '', array( 'response' => 403 ) );
		}
		check_admin_referer( 'cshield_leads' );

		global $wpdb;
		$table = self::table();
		$rows  = $wpdb->get_results( "SELECT email, post_id, created_at FROM {$table} ORDER BY id DESC", ARRAY_A ); // phpcs:ignore WordPress.DB,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix, not user input.

		nocache_headers();
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="copyfend-leads.csv"' );

		$out = fopen( 'php://output', 'w' );
		fputcsv( $out, array( 'email', 'post_id', 'post_title', 'created_at' ) );
		foreach ( (array) $rows as $row ) {
			fputcsv(
				$out,
				array(
					self::csv_cell( $row['email'] ),
					$row['post_id'],
					self::csv_cell( $row['post_id'] ? get_the_title( (int) $row['post_id'] ) : '' ),
					$row['created_at'],
				)
			);
		}
		fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Writing to the php://output stream, not the filesystem.
		exit;
	}

	/**
	 * Neutralise CSV formula injection by prefixing risky leading characters.
	 *
	 * A cell beginning with =, +, -, @ (or tab/CR) can be executed as a formula
	 * by spreadsheet apps; prefixing a single quote forces it to be treated as text.
	 *
	 * @param mixed $value Cell value.
	 * @return string
	 */
	private static function csv_cell( $value ) {
		$value = (string) $value;
		if ( '' !== $value && false !== strpos( "=+-@\t\r", $value[0] ) ) {
			$value = "'" . $value;
		}
		return $value;
	}

	/**
	 * Admin: delete every lead.
	 */
	public function clear_leads() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to do this.', 'copyfend' ), '', array( 'response' => 403 ) );
		}
		check_admin_referer( 'cshield_leads' );

		global $wpdb;
		$wpdb->query( 'TRUNCATE TABLE ' . self::table() ); // phpcs:ignore WordPress.DB,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix, not user input.

		wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=copyfend' ) );
		exit;
	}

	/* --------------------------------------------------------------------- */
	/* Assets                                                                */
	/* --------------------------------------------------------------------- */

	/**
	 * Register (not enqueue) the front-end gate assets; shortcodes enqueue them.
	 */
	public function register_assets() {
		wp_register_style( 'cshield-locker', CSHIELD_URL . 'assets/css/locker.css', array(), CSHIELD_VERSION );
		wp_register_script( 'cshield-locker', CSHIELD_URL . 'assets/js/locker.js', array(), CSHIELD_VERSION, array( 'in_footer' => true ) );
		wp_add_inline_script(
			'cshield-locker',
			'window.cshieldLocker = ' . wp_json_encode(
				array(
					'ajax'  => admin_url( 'admin-ajax.php' ),
					'nonce' => wp_create_nonce( 'cshield_unlock' ),
					'i18n'  => array(
						'errGeneric' => __( 'Something went wrong.', 'copyfend' ),
						'errNetwork' => __( 'Network error. Please try again.', 'copyfend' ),
					),
				)
			) . ';',
			'before'
		);

		// Load the gate styles in the head when the current post has a gate.
		if ( is_singular() ) {
			$post = get_post();
			if ( $post && ( has_shortcode( $post->post_content, 'cshield_gate' ) || has_shortcode( $post->post_content, 'cshield_lock' ) ) ) {
				wp_enqueue_style( 'cshield-locker' );
				wp_enqueue_script( 'cshield-locker' );
			}
		}
	}
}
