<?php
/**
 * Team Survey — public form handler.
 *
 * Intercepts the survey URL at template_redirect, validates token + window,
 * renders the form (GET) or accepts the submission (POST), then exits
 * before WP's theme runs. Robots-blocked at HTML + header level.
 *
 * See docs/SPEC-team-survey.md § Public survey.
 *
 * @package BW_AI_Schema_Pro
 * @since 2.2.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class BW_Schema_Survey_Public {

	const NONCE_ACTION    = 'bw_schema_survey_submit';
	const NONCE_FIELD     = '_bw_schema_survey_nonce';
	const HONEYPOT_FIELD  = 'bw_schema_survey_url';
	const TIMING_FIELD    = '_bw_schema_survey_t';
	const RATE_LIMIT_KEY  = 'bw_schema_survey_rate_';
	const RATE_MAX        = 5;       // submissions per IP per hour
	const TIME_TRAP_MIN   = 3;       // seconds — minimum render→submit gap
	const TIME_TRAP_MAX   = 86400;   // seconds — token max age

	public static function init() {
		add_action( 'template_redirect', array( __CLASS__, 'maybe_handle' ) );
	}

	/**
	 * Detect survey URL via query var. If matched, take over the request.
	 */
	public static function maybe_handle() {
		$token = get_query_var( BW_Schema_Survey::QUERY_VAR );
		if ( ! $token || ! preg_match( '/^[a-f0-9]{8,64}$/', $token ) ) {
			return;
		}

		// Token must match the configured token (constant-time compare).
		if ( ! hash_equals( BW_Schema_Survey::get_token(), $token ) ) {
			self::render_closed( __( 'This survey link is no longer valid.', 'bw-ai-schema-pro' ) );
		}

		if ( ! BW_Schema_Survey::is_open() ) {
			self::render_closed( __( 'This survey is currently closed.', 'bw-ai-schema-pro' ) );
		}

		if ( 'POST' === ( isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( (string) $_SERVER['REQUEST_METHOD'] ) : 'GET' ) ) {
			self::handle_submit();
		} else {
			self::render_form();
		}
	}

	/**
	 * Send the robots-blocking response header. Called from every code path
	 * that produces a survey-URL response.
	 */
	private static function send_robots_header() {
		if ( ! headers_sent() ) {
			header( 'X-Robots-Tag: noindex, nofollow, noarchive', true );
		}
	}

	/**
	 * Render the "closed" page and exit. 200 OK so we don't info-leak why.
	 */
	private static function render_closed( $message ) {
		self::send_robots_header();
		status_header( 200 );
		nocache_headers();

		$title   = esc_html__( 'Survey closed', 'bw-ai-schema-pro' );
		$message = (string) $message;

		self::render_shell(
			$title,
			'<p>' . esc_html( $message ) . '</p>'
		);
		exit;
	}

	/**
	 * Render the form (GET).
	 */
	private static function render_form() {
		self::send_robots_header();
		status_header( 200 );
		nocache_headers();

		$team_post_type = BW_Schema_Team_Member::get_team_post_type();
		$team_members   = self::get_team_member_choices( $team_post_type );
		$questions      = BW_Schema_Survey::survey_questions();
		$intro_html     = wp_kses_post( (string) get_option( BW_Schema_Survey::OPT_INTRO_HTML, BW_Schema_Survey::default_intro_html() ) );
		$timing_token   = self::create_timing_token();
		$nonce          = wp_create_nonce( self::NONCE_ACTION );
		$action_url     = esc_url( BW_Schema_Survey::public_url() );

		ob_start();
		include BW_SCHEMA_PLUGIN_DIR . 'admin/views/survey-public-form.php';
		$body = ob_get_clean();

		self::render_shell( esc_html__( 'Team profile survey', 'bw-ai-schema-pro' ), $body );
		exit;
	}

	/**
	 * Render the "thanks" page after a successful (or honeypot-trapped) submit.
	 */
	private static function render_thanks() {
		self::send_robots_header();
		status_header( 200 );
		nocache_headers();

		self::render_shell(
			esc_html__( 'Thanks!', 'bw-ai-schema-pro' ),
			'<p>' . esc_html__( "Thanks for sharing your info. A moderator will review your submission before anything is published.", 'bw-ai-schema-pro' ) . '</p>'
		);
		exit;
	}

	/**
	 * Render an error page and exit (used for invalid submissions, etc.).
	 */
	private static function render_error( $message, $http_code = 400 ) {
		self::send_robots_header();
		status_header( $http_code );
		nocache_headers();
		self::render_shell(
			esc_html__( 'Submission rejected', 'bw-ai-schema-pro' ),
			'<p>' . esc_html( $message ) . '</p>'
		);
		exit;
	}

	/**
	 * Minimal HTML shell — does not load the active theme. Robots-blocked.
	 */
	private static function render_shell( $title, $body_html ) {
		$site_name = esc_html( get_bloginfo( 'name' ) );
		$css_url   = esc_url( BW_SCHEMA_PLUGIN_URL . 'assets/survey-public.css?ver=' . rawurlencode( BW_SCHEMA_VERSION ) );

		header( 'Content-Type: text/html; charset=' . get_bloginfo( 'charset' ) );
		?><!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, nofollow, noarchive" />
<title><?php echo $title; // already escaped at call site ?> &mdash; <?php echo $site_name; ?></title>
<link rel="stylesheet" href="<?php echo $css_url; ?>" />
</head>
<body class="bw-schema-survey-public">
<div class="bw-schema-survey-wrap">
<h1 class="bw-schema-survey-title"><?php echo $title; // escaped at call site ?></h1>
<?php echo $body_html; // builders already escape; form view escapes per-field ?>
</div>
</body>
</html><?php
	}

	/**
	 * Validate + store a submission, then redirect to the thanks page.
	 */
	private static function handle_submit() {
		// Rate-limit early.
		$ip = self::client_ip();
		if ( ! self::within_rate_limit( $ip ) ) {
			self::render_error( __( 'Too many submissions from this address. Please try again later.', 'bw-ai-schema-pro' ), 429 );
		}

		// Nonce.
		$nonce = isset( $_POST[ self::NONCE_FIELD ] ) ? (string) wp_unslash( $_POST[ self::NONCE_FIELD ] ) : '';
		if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
			self::render_error( __( 'Your session expired before you submitted. Please reload and try again.', 'bw-ai-schema-pro' ) );
		}

		// Honeypot — silently accept (don't expose the trap) and redirect to thanks.
		$honeypot = isset( $_POST[ self::HONEYPOT_FIELD ] ) ? (string) $_POST[ self::HONEYPOT_FIELD ] : '';
		if ( '' !== $honeypot ) {
			self::record_rate_hit( $ip );
			self::render_thanks();
		}

		// Time-trap.
		$timing = isset( $_POST[ self::TIMING_FIELD ] ) ? (string) wp_unslash( $_POST[ self::TIMING_FIELD ] ) : '';
		if ( ! self::verify_timing_token( $timing ) ) {
			self::render_error( __( 'This form has expired. Please reload and try again.', 'bw-ai-schema-pro' ) );
		}

		// Target selection.
		$target_post_id   = isset( $_POST['target_post_id'] ) ? absint( $_POST['target_post_id'] ) : 0;
		$is_holding_queue = isset( $_POST['target_post_id'] ) && '__new__' === (string) wp_unslash( $_POST['target_post_id'] );
		if ( $target_post_id && ! $is_holding_queue ) {
			$post = get_post( $target_post_id );
			$tpt  = BW_Schema_Team_Member::get_team_post_type();
			if ( ! $post || $post->post_type !== $tpt ) {
				self::render_error( __( "We couldn't find the team member you selected.", 'bw-ai-schema-pro' ) );
			}
		} else {
			$target_post_id = 0; // holding queue
		}

		// Validate + collect answers.
		$questions  = BW_Schema_Survey::survey_questions();
		$payload    = array();
		$errors     = array();

		foreach ( $questions as $q ) {
			$raw = isset( $_POST[ $q['key'] ] ) ? wp_unslash( $_POST[ $q['key'] ] ) : '';
			$payload[ $q['key'] ] = self::sanitize_answer( $q, $raw );

			if ( ! empty( $q['required'] ) ) {
				$is_empty = is_array( $payload[ $q['key'] ] )
					? empty( $payload[ $q['key'] ] )
					: ( '' === trim( (string) $payload[ $q['key'] ] ) );
				// "full_name" is only required for holding-queue submissions; for
				// linked submissions, name is taken from the existing team CPT title.
				if ( 'full_name' === $q['key'] && ! $is_holding_queue ) {
					continue;
				}
				if ( $is_empty ) {
					$errors[] = sprintf(
						/* translators: %s: field label */
						__( '"%s" is required.', 'bw-ai-schema-pro' ),
						$q['label']
					);
				}
			}
		}

		if ( ! empty( $errors ) ) {
			self::render_error( implode( ' ', $errors ) );
		}

		// Derive submitter name from the chosen team CPT title if linked.
		if ( ! $is_holding_queue && $target_post_id ) {
			$submitter_name = get_the_title( $target_post_id );
		} else {
			$submitter_name = isset( $payload['full_name'] ) ? (string) $payload['full_name'] : '';
		}

		// Submitter email (separate from "public email" — used for audit / contact only).
		$submitter_email = isset( $_POST['submitter_email'] ) ? sanitize_email( wp_unslash( $_POST['submitter_email'] ) ) : '';

		// Store.
		$id = BW_Schema_Survey_Store::insert( array(
			'target_post_id'  => $target_post_id ?: null,
			'submitter_name'  => $submitter_name,
			'submitter_email' => $submitter_email ?: null,
			'submitter_ip'    => $ip,
			'raw_payload'     => $payload,
		) );

		if ( ! $id ) {
			self::render_error( __( "We couldn't save your submission. Please try again, and contact the site owner if it keeps failing.", 'bw-ai-schema-pro' ), 500 );
		}

		self::record_rate_hit( $ip );
		self::maybe_notify_moderator( $id, $submitter_name, $target_post_id );
		self::render_thanks();
	}

	/* ---------------- Sanitization ---------------- */

	private static function sanitize_answer( array $question, $raw ) {
		switch ( $question['type'] ) {
			case 'textarea':
				return sanitize_textarea_field( (string) $raw );

			case 'number':
				if ( '' === $raw || null === $raw ) {
					return '';
				}
				return max( 0, intval( $raw ) );

			case 'email':
				return sanitize_email( (string) $raw );

			case 'tel':
				// Allow digits, +, spaces, parens, dashes.
				return preg_replace( '/[^0-9\+\-\(\)\s]/', '', (string) $raw );

			case 'url':
				return esc_url_raw( (string) $raw );

			case 'url-list':
				$lines = is_array( $raw ) ? $raw : preg_split( '/[\r\n]+/', (string) $raw );
				$clean = array();
				foreach ( $lines as $line ) {
					$line = trim( (string) $line );
					if ( '' === $line ) {
						continue;
					}
					$url = esc_url_raw( $line );
					if ( $url ) {
						$clean[] = $url;
					}
				}
				return $clean;

			case 'text':
			default:
				return sanitize_text_field( (string) $raw );
		}
	}

	/* ---------------- Team CPT picker data ---------------- */

	private static function get_team_member_choices( $post_type ) {
		if ( ! $post_type ) {
			return array();
		}
		$posts = get_posts( array(
			'post_type'      => $post_type,
			'post_status'    => array( 'publish', 'draft', 'private' ),
			'numberposts'    => 200,
			'orderby'        => 'title',
			'order'          => 'ASC',
			'fields'         => 'ids',
			'suppress_filters' => false,
		) );
		$out = array();
		foreach ( $posts as $pid ) {
			$out[] = array(
				'id'    => (int) $pid,
				'title' => get_the_title( $pid ),
			);
		}
		return $out;
	}

	/* ---------------- Rate limit ---------------- */

	private static function client_ip() {
		if ( ! empty( $_SERVER['REMOTE_ADDR'] ) && filter_var( $_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP ) ) {
			return (string) $_SERVER['REMOTE_ADDR'];
		}
		return '0.0.0.0';
	}

	private static function rate_key( $ip ) {
		return self::RATE_LIMIT_KEY . md5( $ip );
	}

	private static function within_rate_limit( $ip ) {
		$count = (int) get_transient( self::rate_key( $ip ) );
		return $count < self::RATE_MAX;
	}

	private static function record_rate_hit( $ip ) {
		$key   = self::rate_key( $ip );
		$count = (int) get_transient( $key );
		$count++;
		set_transient( $key, $count, HOUR_IN_SECONDS );
	}

	/* ---------------- Time-trap token ---------------- */

	private static function create_timing_token() {
		$ts   = time();
		$hash = self::sign_timing( $ts );
		return $ts . '.' . $hash;
	}

	private static function verify_timing_token( $token ) {
		if ( ! is_string( $token ) || strpos( $token, '.' ) === false ) {
			return false;
		}
		list( $ts, $hash ) = array_pad( explode( '.', $token, 2 ), 2, '' );
		$ts = absint( $ts );
		if ( ! $ts || ! is_string( $hash ) ) {
			return false;
		}
		if ( ! hash_equals( self::sign_timing( $ts ), $hash ) ) {
			return false;
		}
		$elapsed = time() - $ts;
		if ( $elapsed < self::TIME_TRAP_MIN ) {
			return false;
		}
		if ( $elapsed > self::TIME_TRAP_MAX ) {
			return false;
		}
		return true;
	}

	private static function sign_timing( $ts ) {
		$key = wp_salt( 'auth' ) . BW_Schema_Survey::get_token();
		return hash_hmac( 'sha256', (string) $ts, $key );
	}

	/* ---------------- Moderator notification ---------------- */

	private static function maybe_notify_moderator( $response_id, $submitter_name, $target_post_id ) {
		if ( ! get_option( BW_Schema_Survey::OPT_NOTIFY_ENABLED, 0 ) ) {
			return;
		}
		$to = sanitize_email( (string) get_option( BW_Schema_Survey::OPT_NOTIFY_EMAIL, get_option( 'admin_email' ) ) );
		if ( ! $to ) {
			return;
		}

		$site    = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
		$subject = sprintf(
			/* translators: %s: site name */
			__( '[%s] New team-survey submission', 'bw-ai-schema-pro' ),
			$site
		);

		$target_label = $target_post_id
			? sprintf(
				/* translators: %s: team member name */
				__( 'Linked to: %s', 'bw-ai-schema-pro' ),
				get_the_title( $target_post_id )
			)
			: __( 'In holding queue (new team member).', 'bw-ai-schema-pro' );

		$detail_url = admin_url( 'options-general.php?page=bw-ai-schema-survey-detail&id=' . absint( $response_id ) );

		$lines = array(
			sprintf( __( 'A new team-survey response was submitted by %s.', 'bw-ai-schema-pro' ), $submitter_name ),
			$target_label,
			'',
			__( 'Review:', 'bw-ai-schema-pro' ) . ' ' . $detail_url,
		);

		wp_mail( $to, $subject, implode( "\n", $lines ) );
	}
}
