<?php
/**
 * PK Core - the first-run Site Profile wizard.
 *
 * Nine questions, one screen. It renders the SAME question set as the full page
 * (PKCORE_Questions) filtered to the `wizard` tier, through the same renderer
 * (PKCORE_Fields), and saves through the same collector (PKCORE_Profile_Page).
 * There is one implementation and two views; a second implementation would drift
 * from the first, and a drifted save handler writes wrong data behind a screen
 * that looks right.
 *
 * WHAT IT DOES NOT DO
 *
 * It never re-asks an answered question. On a site where another PublisherKits
 * plugin already populated the profile, the wizard either does not run at all or
 * runs showing only the gaps - because the operator's reasonable reaction to
 * being asked twice is to stop trusting that anything is shared.
 *
 * It never writes silently. Values carried from the pre-core options are shown
 * as prefill for the operator to review and save, exactly as the migration shim
 * has always done.
 *
 * @package PublisherKits\Core
 * @since   0.10.0
 */

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

if ( ! class_exists( 'PKCORE_Setup' ) ) {

	/**
	 * Class PKCORE_Setup
	 *
	 * @since 0.10.0
	 */
	class PKCORE_Setup {

		const OPTION = 'pkcore_setup_state';
		const NONCE  = 'pkcore_setup_save';
		const SLUG   = 'publisherkits-setup';

		/**
		 * Wizard questions that have no answer yet.
		 *
		 * Derived from the profile itself rather than a "step reached" counter.
		 * A counter goes stale the moment anything else writes the profile - and
		 * something else always does, which is the entire premise of this module.
		 *
		 * @return array<string,array> key => spec.
		 */
		public static function unanswered() {
			$profile = PKCORE_Profile::all();
			$out     = array();
			foreach ( PKCORE_Questions::wizard() as $key => $spec ) {
				if ( self::is_empty_value( isset( $profile[ $key ] ) ? $profile[ $key ] : '' ) ) {
					$out[ $key ] = $spec;
				}
			}
			return $out;
		}

		/**
		 * Whether the wizard should offer itself.
		 *
		 * False once it has been completed OR dismissed - an operator who has
		 * said no should not be asked again on every page load, and a nag is how
		 * a setup screen teaches people to ignore it.
		 *
		 * @return bool
		 */
		public static function needs_run() {
			$state = get_option( self::OPTION, array() );
			$state = is_array( $state ) ? $state : array();

			if ( ! empty( $state['completed_at'] ) || ! empty( $state['dismissed'] ) ) {
				return false;
			}
			return ! empty( self::unanswered() );
		}

		/**
		 * Recursive emptiness - a nested default has KEYS and so is never
		 * `empty()`. Getting this wrong once already made a migration shim
		 * silently decline to carry anything.
		 *
		 * @param mixed $value Value.
		 * @return bool
		 */
		private static function is_empty_value( $value ) {
			if ( is_array( $value ) ) {
				foreach ( $value as $leaf ) {
					if ( ! self::is_empty_value( $leaf ) ) {
						return false;
					}
				}
				return true;
			}
			return ( '' === trim( (string) $value ) );
		}

		/**
		 * Render the wizard.
		 *
		 * @return void
		 */
		public static function render() {
			if ( ! current_user_can( 'manage_options' ) ) {
				return;
			}

			$unanswered = self::unanswered();
			$answered   = count( PKCORE_Questions::wizard() ) - count( $unanswered );

			// Prefill still-empty fields from the pre-core options, so the operator
			// reviews what the estate already knows rather than retyping it.
			$profile = PKCORE_Profile::all();
			if ( class_exists( 'PKCORE_Legacy_Profile' ) ) {
				$profile = PKCORE_Legacy_Profile::with_defaults( $profile );
			}

			echo '<div class="wrap pkcore-wrap pkcore-setup">';
			echo '<h1>' . esc_html__( 'Set up your Site Profile', 'publisherkits-socialpromoter' ) . '</h1>';

			if ( $answered > 0 ) {
				echo '<p class="pkcore-intro">' . esc_html(
					sprintf(
						/* translators: %d: number of questions already answered. */
						_n(
							'%d question is already answered from your existing settings, so it is not asked again. Review it below and fill in the rest.',
							'%d questions are already answered from your existing settings, so they are not asked again. Review them below and fill in the rest.',
							$answered,
							'publisherkits-socialpromoter'
						),
						$answered
					)
				) . '</p>';
			} else {
				echo '<p class="pkcore-intro">' . esc_html__( 'Nine questions. Every PublisherKits plugin you install reads these answers, so this is the only place you need to describe your publication.', 'publisherkits-socialpromoter' ) . '</p>';
			}

			echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
			echo '<input type="hidden" name="action" value="pkcore_save_setup">';
			echo '<input type="hidden" name="pkcore_tier" value="wizard">';
			wp_nonce_field( self::NONCE );

			foreach ( PKCORE_Questions::wizard() as $key => $spec ) {
				$value = isset( $profile[ $key ] ) ? $profile[ $key ] : '';
				PKCORE_Fields::render( $key, $spec, $value, 'wizard' );

				// The one box that captures two keys at first run.
				if ( ! empty( $spec['wizard_captures'] ) ) {
					foreach ( (array) $spec['wizard_captures'] as $extra ) {
						$extra_spec = PKCORE_Questions::core();
						$extra_spec = isset( $extra_spec[ $extra ] ) ? $extra_spec[ $extra ] : array(
							'type'  => 'textarea',
							'label' => $extra,
						);
						PKCORE_Fields::render( $extra, $extra_spec, isset( $profile[ $extra ] ) ? $profile[ $extra ] : '', 'wizard' );
					}
				}
			}

			// Plugin-contributed wizard questions, grouped by the plugin that
			// asked, so it is obvious who wants the answer and why.
			$contributed = PKCORE_Questions::contributed();
			$by_plugin   = array();
			foreach ( $contributed as $key => $spec ) {
				if ( 'wizard' === $spec['tier'] ) {
					$by_plugin[ $spec['plugin'] ][ $key ] = $spec;
				}
			}
			foreach ( $by_plugin as $plugin => $specs ) {
				echo '<div class="pkcore-section pkcore-section--contributed">';
				echo '<h2>' . esc_html( sprintf( /* translators: %s: plugin name. */ __( 'For %s', 'publisherkits-socialpromoter' ), ucfirst( (string) $plugin ) ) ) . '</h2>';
				foreach ( $specs as $key => $spec ) {
					PKCORE_Fields::render( $key, $spec, call_user_func( $spec['storage']['get'] ), 'wizard' );
				}
				echo '</div>';
			}

			// DISCLOSE WHAT WILL BE CARRIED OVER WITHOUT BEING ASKED.
			//
			// handle() calls PKCORE_Legacy_Profile::apply_pending() after its
			// own save, which writes legacy values for fields this form never
			// renders. Writing them without showing them would break the one
			// rule the whole shim is built on - the operator reviews and
			// confirms, nothing is written silently - so they are listed here,
			// attributed, immediately above the button that writes them.
			//
			// The old three-step wizard did this on its final step. That step
			// went when class-pkcore-wizard.php was deleted at 0.9.3, and the
			// disclosure went with it.
			self::render_carry_over( $profile );

			submit_button( __( 'Save and finish', 'publisherkits-socialpromoter' ) );
			echo ' <button type="submit" name="pkcore_skip" value="1" class="button-link pkcore-skip">' . esc_html__( 'Skip for now', 'publisherkits-socialpromoter' ) . '</button>';
			echo '</form>';
			echo '</div>';
		}

		/**
		 * List the legacy values that will be carried over unasked, and any
		 * that could NOT be carried.
		 *
		 * Two lists, because they need opposite responses from the operator:
		 * the first is "we already know this, we are about to keep it"; the
		 * second is "we found this and cannot use it, please answer it
		 * yourself". Collapsing them would hide the second, which is the one
		 * that loses data.
		 *
		 * @since 0.9.11
		 * @param array $profile Profile with legacy defaults merged in.
		 * @return void
		 */
		private static function render_carry_over( $profile ) {
			unset( $profile );

			if ( ! class_exists( 'PKCORE_Legacy_Profile' ) ) {
				return;
			}

			// Only the fields this form does NOT render: everything else is
			// already on screen in a box the operator can edit.
			$asked = PKCORE_Questions::wizard();
			foreach ( $asked as $spec ) {
				if ( ! empty( $spec['wizard_captures'] ) ) {
					foreach ( (array) $spec['wizard_captures'] as $extra ) {
						$asked[ $extra ] = true;
					}
				}
			}

			$questions = PKCORE_Questions::core();
			$unasked   = array();
			foreach ( PKCORE_Legacy_Profile::sources_labelled() as $key => $entry ) {
				if ( isset( $asked[ $key ] ) ) {
					continue;
				}
				$label           = isset( $questions[ $key ]['label'] ) ? $questions[ $key ]['label'] : $key;
				$unasked[ $key ] = array(
					'label'  => $label,
					'value'  => $entry['value'],
					'source' => $entry['source'],
				);
			}

			$unmapped = PKCORE_Legacy_Profile::unmapped();

			if ( empty( $unasked ) && empty( $unmapped ) ) {
				return;
			}

			echo '<div class="pkcore-section pkcore-section--carry-over">';

			if ( ! empty( $unasked ) ) {
				echo '<h2>' . esc_html__( 'Also carried over from your existing settings', 'publisherkits-socialpromoter' ) . '</h2>';
				echo '<p class="description">' . esc_html__( 'These are not asked above because they are optional, but you have already answered them elsewhere. They will be saved with your profile. You can change any of them on the Site Profile page afterwards.', 'publisherkits-socialpromoter' ) . '</p>';
				echo '<ul class="pkcore-carry-over-list">';
				foreach ( $unasked as $entry ) {
					echo '<li><strong>' . esc_html( $entry['label'] ) . '</strong>: ' . esc_html( self::summarise( $entry['value'] ) );
					echo ' <span class="description">' . esc_html( sprintf( /* translators: %s: where the value came from. */ __( '(from %s)', 'publisherkits-socialpromoter' ), $entry['source'] ) ) . '</span></li>';
				}
				echo '</ul>';
			}

			// The values we found and CANNOT use. Silence here is what turns a
			// migration into data loss the user never learns about.
			if ( ! empty( $unmapped ) ) {
				echo '<h2>' . esc_html__( 'Found, but needs your answer', 'publisherkits-socialpromoter' ) . '</h2>';
				echo '<ul class="pkcore-carry-over-list pkcore-carry-over-list--unmapped">';
				foreach ( $unmapped as $key => $entry ) {
					$label = isset( $questions[ $key ]['label'] ) ? $questions[ $key ]['label'] : $key;
					echo '<li><strong>' . esc_html( $label ) . '</strong>: ';
					echo esc_html( sprintf( /* translators: 1: the stored value, 2: source plugin. */ __( '%1$s (from %2$s)', 'publisherkits-socialpromoter' ), self::summarise( $entry['value'] ), $entry['source'] ) );
					echo '<br><span class="description">' . esc_html( $entry['reason'] ) . '</span></li>';
				}
				echo '</ul>';
			}

			echo '</div>';
		}

		/**
		 * A short, readable rendering of a profile value for the disclosure.
		 *
		 * @since 0.9.11
		 * @param mixed $value Value.
		 * @return string
		 */
		private static function summarise( $value ) {
			if ( is_array( $value ) ) {
				$parts = array();
				foreach ( $value as $leaf ) {
					if ( is_array( $leaf ) ) {
						$leaf = implode( ', ', array_map( 'strval', $leaf ) );
					}
					$leaf = trim( (string) $leaf );
					if ( '' !== $leaf ) {
						$parts[] = $leaf;
					}
				}
				$value = implode( '; ', $parts );
			}

			$value = trim( preg_replace( '/\s+/', ' ', (string) $value ) );
			if ( function_exists( 'mb_strlen' ) && mb_strlen( $value ) > 120 ) {
				return mb_substr( $value, 0, 117 ) . '...';
			}
			if ( strlen( $value ) > 120 ) {
				return substr( $value, 0, 117 ) . '...';
			}
			return $value;
		}

		/**
		 * Persist the wizard and send the operator on to the full page.
		 *
		 * @return void
		 */
		public static function handle() {
			if ( ! current_user_can( 'manage_options' ) ) {
				wp_die( esc_html__( 'You do not have permission to do that.', 'publisherkits-socialpromoter' ) );
			}
			check_admin_referer( self::NONCE );

			$state = get_option( self::OPTION, array() );
			$state = is_array( $state ) ? $state : array();

			if ( ! empty( $_POST['pkcore_skip'] ) ) {
				$state['dismissed'] = true;
				update_option( self::OPTION, $state, false );
				wp_safe_redirect( admin_url( 'admin.php?page=publisherkits-profile' ) );
				exit;
			}

			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Each value is sanitised per key in PKCORE_Profile::set_many().
			$raw    = wp_unslash( $_POST );
			$values = PKCORE_Profile_Page::collect_all( $raw, 'wizard' );

			if ( ! empty( $values ) ) {
				PKCORE_Profile::set_many( $values );
			}

			// CARRY OVER THE LEGACY VALUES THE WIZARD NEVER ASKED ABOUT.
			//
			// The wizard renders PKCORE_Questions::wizard() only, so legacy
			// values for full-tier fields - keywords, capacity,
			// secondary_goals - are prefilled into nothing and lost the moment
			// the operator finishes. apply_pending() writes them.
			//
			// THIS CALL WAS MISSING FROM 0.9.3 TO 0.9.11. It lived in
			// class-pkcore-wizard.php, which was deleted at 0.9.3 when this
			// class replaced it, and was not carried across - so the migration
			// the shim's docblock describes has not actually run on the wizard
			// path for eight core releases. It mattered little on the estate,
			// where an operator eventually opens the full Site Profile page
			// (which prefills and saves everything). It matters on
			// WordPress.org, where a user may complete the wizard and never
			// open the page.
			//
			// Runs AFTER the wizard's own save, so an answer the operator just
			// typed always outranks a legacy value: pending() re-reads the
			// profile and only offers fields that are still empty.
			if ( class_exists( 'PKCORE_Legacy_Profile' ) ) {
				PKCORE_Legacy_Profile::apply_pending();
			}

			$state['completed_at'] = current_time( 'mysql' );
			update_option( self::OPTION, $state, false );

			// Straight to the full page with a thank-you, rather than a dead-end
			// "all done" screen: the wizard is the short version, and the moment
			// someone has just answered nine questions is the moment they are
			// most willing to answer the optional ones.
			$redirect = add_query_arg(
				array(
					'page'          => 'publisherkits-profile',
					'pkcore-notice' => 'setup-complete',
				),
				admin_url( 'admin.php' )
			);
			wp_safe_redirect( $redirect );
			exit;
		}
	}
}
