<?php
/**
 * Minimal CAPTCHA and rate-limit settings screen.
 *
 * Built on the Settings API and kept deliberately tiny: this is not a general
 * settings framework, only the few options the public visit form needs. The
 * page is gated by the manage-leads capability and registered under the Leads
 * menu so staff manage spam protection beside the worklist they protect.
 *
 * Everything is stored in a single option array (wrenchpress_settings), the
 * same array WrenchPress_Anti_Abuse::max_per_hour() already reads for the
 * rate-limit threshold. CAPTCHA defaults OFF; with it off the form makes zero
 * external calls and pulls no third-party dependency (D-09).
 *
 * @package WrenchPress
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class WrenchPress_Settings_Page
 */
class WrenchPress_Settings_Page {

	/**
	 * Option key holding the settings array.
	 */
	const OPTION = 'wrenchpress_settings';

	/**
	 * Settings group / page slug used by the Settings API and the submenu.
	 */
	const PAGE = 'wrenchpress-settings';

	/**
	 * Allowed CAPTCHA provider slugs.
	 *
	 * @var string[]
	 */
	const PROVIDERS = array( 'recaptcha_v3', 'turnstile' );

	/**
	 * Allowed mileage-unit slugs.
	 *
	 * @var string[]
	 */
	const MILEAGE_UNITS = array( 'mi', 'km' );

	/**
	 * Allowed currency-symbol placement slugs.
	 *
	 * @var string[]
	 */
	const CURRENCY_PLACEMENTS = array( 'before', 'after' );

	/**
	 * Register the single settings option (with a sanitize callback) plus the
	 * section and fields that drive the form.
	 *
	 * @return void
	 */
	public static function register_settings() {
		register_setting(
			self::PAGE,
			self::OPTION,
			array(
				'type'              => 'array',
				'sanitize_callback' => array( __CLASS__, 'sanitize' ),
				'default'           => self::defaults(),
			)
		);

		add_settings_section(
			'wrenchpress_spam_section',
			__( 'Spam protection', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'render_section_intro' ),
			self::PAGE
		);

		/*
		 * Deliberately registered WITHOUT a 'label_for' argument, unlike every
		 * other single-control field on this screen. Its callback already wraps
		 * the checkbox in its own <label>, so adding label_for here would give
		 * one control two labels, which is the form-field-multiple-labels
		 * violation. Do not "complete" the pattern by adding it.
		 */
		add_settings_field(
			'captcha_enabled',
			__( 'Spam protection (CAPTCHA)', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_captcha_enabled' ),
			self::PAGE,
			'wrenchpress_spam_section'
		);

		add_settings_field(
			'captcha_provider',
			__( 'CAPTCHA provider', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_captcha_provider' ),
			self::PAGE,
			'wrenchpress_spam_section',
			array( 'label_for' => 'wrenchpress_captcha_provider' )
		);

		add_settings_field(
			'captcha_site_key',
			__( 'Site key', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_captcha_site_key' ),
			self::PAGE,
			'wrenchpress_spam_section',
			array( 'label_for' => 'wrenchpress_captcha_site_key' )
		);

		add_settings_field(
			'captcha_secret_key',
			__( 'Secret key', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_captcha_secret_key' ),
			self::PAGE,
			'wrenchpress_spam_section',
			array( 'label_for' => 'wrenchpress_captcha_secret_key' )
		);

		add_settings_field(
			'rate_limit_per_hour',
			__( 'Rate limit per hour', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_rate_limit' ),
			self::PAGE,
			'wrenchpress_spam_section',
			array( 'label_for' => 'wrenchpress_rate_limit_per_hour' )
		);

		add_settings_section(
			'wrenchpress_workorders_section',
			__( 'Work orders', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'render_workorders_section_intro' ),
			self::PAGE
		);

		add_settings_field(
			'default_tax_rate',
			__( 'Default tax rate', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_default_tax_rate' ),
			self::PAGE,
			'wrenchpress_workorders_section',
			array( 'label_for' => 'wrenchpress_default_tax_rate' )
		);

		add_settings_section(
			'wrenchpress_reminders_section',
			__( 'Service reminders', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'render_reminders_section_intro' ),
			self::PAGE
		);

		/*
		 * Also registered without a 'label_for', and for the opposite reason to
		 * the checkbox above: this row renders TWO number inputs (months and
		 * distance) and label_for can only point at one of them. The callback
		 * gives each input its own explicit <label for>.
		 */
		add_settings_field(
			'reminder_default_interval',
			__( 'Default interval', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_reminder_default_interval' ),
			self::PAGE,
			'wrenchpress_reminders_section'
		);

		add_settings_field(
			'mileage_unit',
			__( 'Mileage unit', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_mileage_unit' ),
			self::PAGE,
			'wrenchpress_reminders_section',
			array( 'label_for' => 'wrenchpress_mileage_unit' )
		);

		add_settings_section(
			'wrenchpress_shop_identity_section',
			__( 'Shop identity', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'render_shop_identity_section_intro' ),
			self::PAGE
		);

		add_settings_field(
			'shop_name',
			__( 'Shop name', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_shop_name' ),
			self::PAGE,
			'wrenchpress_shop_identity_section',
			array( 'label_for' => 'wrenchpress_shop_name' )
		);

		add_settings_field(
			'shop_address',
			__( 'Address', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_shop_address' ),
			self::PAGE,
			'wrenchpress_shop_identity_section',
			array( 'label_for' => 'wrenchpress_shop_address' )
		);

		add_settings_field(
			'shop_phone',
			__( 'Phone', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_shop_phone' ),
			self::PAGE,
			'wrenchpress_shop_identity_section',
			array( 'label_for' => 'wrenchpress_shop_phone' )
		);

		add_settings_field(
			'shop_email',
			__( 'Email', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_shop_email' ),
			self::PAGE,
			'wrenchpress_shop_identity_section',
			array( 'label_for' => 'wrenchpress_shop_email' )
		);

		add_settings_field(
			'shop_tax_number',
			__( 'Tax / license number', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_shop_tax_number' ),
			self::PAGE,
			'wrenchpress_shop_identity_section',
			array( 'label_for' => 'wrenchpress_shop_tax_number' )
		);

		/*
		 * The logo row renders a hidden id input, a Select-logo button and a
		 * preview image, so a single label_for could only name one of them. The
		 * button carries its own visible text; no label_for is registered.
		 */
		add_settings_field(
			'shop_logo_id',
			__( 'Logo', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_shop_logo' ),
			self::PAGE,
			'wrenchpress_shop_identity_section'
		);

		add_settings_section(
			'wrenchpress_currency_section',
			__( 'Currency', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'render_currency_section_intro' ),
			self::PAGE
		);

		add_settings_field(
			'currency_symbol',
			__( 'Currency symbol', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_currency_symbol' ),
			self::PAGE,
			'wrenchpress_currency_section',
			array( 'label_for' => 'wrenchpress_currency_symbol' )
		);

		add_settings_field(
			'currency_code',
			__( 'Currency code', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_currency_code' ),
			self::PAGE,
			'wrenchpress_currency_section',
			array( 'label_for' => 'wrenchpress_currency_code' )
		);

		add_settings_field(
			'currency_placement',
			__( 'Symbol placement', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_currency_placement' ),
			self::PAGE,
			'wrenchpress_currency_section',
			array( 'label_for' => 'wrenchpress_currency_placement' )
		);

		add_settings_field(
			'document_notes_default',
			__( 'Default notes / terms', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_document_notes_default' ),
			self::PAGE,
			'wrenchpress_currency_section',
			array( 'label_for' => 'wrenchpress_document_notes_default' )
		);

		add_settings_section(
			'wrenchpress_retention_section',
			__( 'Data retention', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'render_retention_section_intro' ),
			self::PAGE
		);

		add_settings_field(
			'retention_lead_days',
			__( 'Delete unpromoted leads after', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_retention_lead_days' ),
			self::PAGE,
			'wrenchpress_retention_section',
			array( 'label_for' => 'wrenchpress_retention_lead_days' )
		);

		add_settings_field(
			'retention_ip_days',
			__( 'Remove submitter IP after', 'pitdesk-repair-crm' ),
			array( __CLASS__, 'field_retention_ip_days' ),
			self::PAGE,
			'wrenchpress_retention_section',
			array( 'label_for' => 'wrenchpress_retention_ip_days' )
		);

		// Load the media picker assets on the Settings screen only, mirroring the
		// hook-suffix enqueue guard used by the work-orders island. Registered here
		// (on admin_init) so it is in place before admin_enqueue_scripts fires, and
		// gated inside the callback so nothing loads elsewhere in wp-admin.
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_logo_picker' ) );
	}

	/**
	 * Enqueue the wp.media logo picker on the Settings screen only.
	 *
	 * Gated on the Settings page hook suffix exactly like the work-orders island
	 * enqueue, so the media library assets never load across the rest of wp-admin.
	 *
	 * @param string $hook_suffix Current admin page hook suffix.
	 * @return void
	 */
	public static function enqueue_logo_picker( $hook_suffix ) {
		if ( false === strpos( (string) $hook_suffix, self::PAGE ) ) {
			return;
		}

		wp_enqueue_media();

		$select_title  = esc_js( __( 'Select shop logo', 'pitdesk-repair-crm' ) );
		$select_button = esc_js( __( 'Use this logo', 'pitdesk-repair-crm' ) );

		$script = '( function( $ ) {'
			. 'var frame;'
			. '$( document ).on( "click", "#wrenchpress_shop_logo_select", function( e ) {'
			. 'e.preventDefault();'
			. 'if ( frame ) { frame.open(); return; }'
			. 'frame = wp.media( { title: "' . $select_title . '", button: { text: "' . $select_button . '" }, library: { type: "image" }, multiple: false } );'
			. 'frame.on( "select", function() {'
			. 'var att = frame.state().get( "selection" ).first().toJSON();'
			. 'var url = ( att.sizes && att.sizes.thumbnail ) ? att.sizes.thumbnail.url : att.url;'
			. '$( "#wrenchpress_shop_logo_id" ).val( att.id );'
			. '$( "#wrenchpress_shop_logo_preview" ).attr( "src", url ).show();'
			. '$( "#wrenchpress_shop_logo_remove" ).show();'
			. '} );'
			. 'frame.open();'
			. '} );'
			. '$( document ).on( "click", "#wrenchpress_shop_logo_remove", function( e ) {'
			. 'e.preventDefault();'
			. '$( "#wrenchpress_shop_logo_id" ).val( "0" );'
			. '$( "#wrenchpress_shop_logo_preview" ).attr( "src", "" ).hide();'
			. '$( this ).hide();'
			. '} );'
			. '} )( jQuery );';

		wp_add_inline_script( 'jquery-core', $script );
	}

	/**
	 * Default settings. CAPTCHA is OFF by default so the form makes no external
	 * calls out of the box (D-09); the rate limit mirrors the anti-abuse default.
	 *
	 * @return array<string,mixed>
	 */
	public static function defaults() {
		return array(
			'captcha_enabled'           => 0,
			'captcha_provider'          => 'recaptcha_v3',
			'captcha_site_key'          => '',
			'captcha_secret_key'        => '',
			'rate_limit_per_hour'       => WrenchPress_Anti_Abuse::MAX_PER_HOUR,
			'default_tax_rate'          => '0',
			'mileage_unit'              => 'mi',
			'reminder_default_months'   => 6,
			'reminder_default_distance' => 5000,
			'shop_name'                 => '',
			'shop_address'              => '',
			'shop_phone'                => '',
			'shop_email'                => '',
			'shop_logo_id'              => 0,
			'shop_tax_number'           => '',
			'currency_symbol'           => '$',
			'currency_code'             => 'USD',
			'currency_placement'        => 'before',
			'document_notes_default'    => '',
			'retention_lead_days'       => 180,
			'retention_ip_days'         => 30,
		);
	}

	/**
	 * Read the shop-wide default tax rate as a decimal fraction (D-15).
	 *
	 * The value is stored as the fraction the work-order totals() helper expects
	 * (for example 0.0825 for 8.25 percent), so the Work Orders slice can pre-fill
	 * a new work order's tax_rate directly from this getter.
	 *
	 * @return float Default tax rate as a decimal fraction (0 or greater).
	 */
	public static function default_tax_rate() {
		$settings = self::get();

		return max( 0.0, (float) $settings['default_tax_rate'] );
	}

	/**
	 * Read the shop-wide mileage unit (D-03).
	 *
	 * Controls how mileage is entered and displayed across the plugin. Stored
	 * odometer values are never rewritten when the shop switches units; only the
	 * label and entry unit change.
	 *
	 * @return string Either 'mi' (default) or 'km'.
	 */
	public static function mileage_unit() {
		$settings = self::get();
		$unit     = (string) $settings['mileage_unit'];

		return in_array( $unit, self::MILEAGE_UNITS, true ) ? $unit : 'mi';
	}

	/**
	 * Read the shop name for documents (D-07).
	 *
	 * Prefilled lazily at read time: a blank stored value falls back to the site
	 * title (blogname) rather than being baked into defaults(), so the fallback
	 * tracks the site even before the shop saves an explicit name.
	 *
	 * @return string Stored shop name, or the site title when blank.
	 */
	public static function shop_name() {
		$value = trim( (string) self::get()['shop_name'] );

		return '' !== $value ? $value : (string) get_option( 'blogname' );
	}

	/**
	 * Read the shop email for documents (D-07).
	 *
	 * Prefilled lazily at read time exactly like shop_name(): a blank stored
	 * value falls back to the site admin email rather than being baked into
	 * defaults().
	 *
	 * @return string Stored shop email, or the site admin email when blank.
	 */
	public static function shop_email() {
		$value = trim( (string) self::get()['shop_email'] );

		return '' !== $value ? $value : (string) get_option( 'admin_email' );
	}

	/**
	 * Read the shop-wide currency-symbol placement (D-08).
	 *
	 * @return string Either 'before' (default) or 'after'.
	 */
	public static function currency_placement() {
		$placement = (string) self::get()['currency_placement'];

		return in_array( $placement, self::CURRENCY_PLACEMENTS, true ) ? $placement : 'before';
	}

	/**
	 * Read the default reminder interval in months (D-02).
	 *
	 * Prefills the reminder date when a work order is closed; each reminder can
	 * still be edited before it is saved.
	 *
	 * @return int Default interval in months (0 or greater).
	 */
	public static function reminder_default_months() {
		$settings = self::get();

		return absint( $settings['reminder_default_months'] );
	}

	/**
	 * Read the default reminder interval distance in the active mileage unit (D-02).
	 *
	 * Prefills the reminder mileage when a work order is closed; the value is
	 * entered and shown in whatever unit mileage_unit() reports.
	 *
	 * @return int Default interval distance (0 or greater).
	 */
	public static function reminder_default_distance() {
		$settings = self::get();

		return absint( $settings['reminder_default_distance'] );
	}

	/**
	 * Read the current settings merged over the defaults.
	 *
	 * @return array<string,mixed>
	 */
	public static function get() {
		$stored = get_option( self::OPTION, array() );

		if ( ! is_array( $stored ) ) {
			$stored = array();
		}

		return wp_parse_args( $stored, self::defaults() );
	}

	/**
	 * Sanitize the submitted settings on save.
	 *
	 * The toggle and threshold pass through absint, the provider is whitelisted
	 * against PROVIDERS, and the keys are sanitized as plain text (assumption A7:
	 * the secret is stored as a plain option, never echoed back to the page).
	 *
	 * @param mixed $input Raw submitted value.
	 * @return array<string,mixed>
	 */
	public static function sanitize( $input ) {
		$input = is_array( $input ) ? $input : array();

		$provider = isset( $input['captcha_provider'] ) ? sanitize_key( $input['captcha_provider'] ) : 'recaptcha_v3';

		if ( ! in_array( $provider, self::PROVIDERS, true ) ) {
			$provider = 'recaptcha_v3';
		}

		$rate_limit = isset( $input['rate_limit_per_hour'] ) ? absint( $input['rate_limit_per_hour'] ) : WrenchPress_Anti_Abuse::MAX_PER_HOUR;

		if ( $rate_limit < 1 ) {
			$rate_limit = WrenchPress_Anti_Abuse::MAX_PER_HOUR;
		}

		// Tax rate is entered as a percentage (0 to 100) and stored as the decimal
		// fraction the work-order totals() helper expects (for example 8.25 percent
		// is stored as 0.0825). Negative input is clamped to 0 and anything above
		// 100 percent is clamped to 100.
		$tax_percent = isset( $input['default_tax_rate'] ) ? (float) $input['default_tax_rate'] : 0.0;
		$tax_percent = max( 0.0, min( 100.0, $tax_percent ) );
		$tax_rate    = $tax_percent / 100;

		$mileage_unit = isset( $input['mileage_unit'] ) ? sanitize_key( $input['mileage_unit'] ) : 'mi';

		if ( ! in_array( $mileage_unit, self::MILEAGE_UNITS, true ) ) {
			$mileage_unit = 'mi';
		}

		// Currency-symbol placement is whitelisted the same way mileage_unit is:
		// anything outside the known slugs falls back to 'before'.
		$currency_placement = isset( $input['currency_placement'] ) ? sanitize_key( $input['currency_placement'] ) : 'before';

		if ( ! in_array( $currency_placement, self::CURRENCY_PLACEMENTS, true ) ) {
			$currency_placement = 'before';
		}

		return array(
			'captcha_enabled'           => empty( $input['captcha_enabled'] ) ? 0 : 1,
			'captcha_provider'          => $provider,
			'captcha_site_key'          => isset( $input['captcha_site_key'] ) ? sanitize_text_field( $input['captcha_site_key'] ) : '',
			'captcha_secret_key'        => isset( $input['captcha_secret_key'] ) ? sanitize_text_field( $input['captcha_secret_key'] ) : '',
			'rate_limit_per_hour'       => $rate_limit,
			'default_tax_rate'          => (string) round( $tax_rate, 4 ),
			'mileage_unit'              => $mileage_unit,
			'reminder_default_months'   => isset( $input['reminder_default_months'] ) ? absint( $input['reminder_default_months'] ) : 6,
			'reminder_default_distance' => isset( $input['reminder_default_distance'] ) ? absint( $input['reminder_default_distance'] ) : 5000,
			'shop_name'                 => isset( $input['shop_name'] ) ? sanitize_text_field( $input['shop_name'] ) : '',
			'shop_address'              => isset( $input['shop_address'] ) ? sanitize_textarea_field( $input['shop_address'] ) : '',
			'shop_phone'                => isset( $input['shop_phone'] ) ? sanitize_text_field( $input['shop_phone'] ) : '',
			'shop_email'                => isset( $input['shop_email'] ) ? sanitize_email( $input['shop_email'] ) : '',
			'shop_logo_id'              => isset( $input['shop_logo_id'] ) ? absint( $input['shop_logo_id'] ) : 0,
			'shop_tax_number'           => isset( $input['shop_tax_number'] ) ? sanitize_text_field( $input['shop_tax_number'] ) : '',
			'currency_symbol'           => isset( $input['currency_symbol'] ) ? sanitize_text_field( $input['currency_symbol'] ) : '$',
			'currency_code'             => isset( $input['currency_code'] ) ? sanitize_text_field( $input['currency_code'] ) : 'USD',
			'currency_placement'        => $currency_placement,
			'document_notes_default'    => isset( $input['document_notes_default'] ) ? sanitize_textarea_field( $input['document_notes_default'] ) : '',
			'retention_lead_days'       => isset( $input['retention_lead_days'] ) ? absint( $input['retention_lead_days'] ) : 180,
			'retention_ip_days'         => isset( $input['retention_ip_days'] ) ? absint( $input['retention_ip_days'] ) : 30,
		);
	}

	/**
	 * Render the section intro copy.
	 *
	 * @return void
	 */
	public static function render_section_intro() {
		echo '<p>' . esc_html__( 'Off by default. When off, the form makes no external calls. Turn on to require reCAPTCHA v3 or Cloudflare Turnstile.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the CAPTCHA on/off checkbox.
	 *
	 * The checkbox is wrapped in its own <label>, which is what gives it an
	 * accessible name. That is why register_settings() does not hand this field
	 * a label_for argument the way it does for every other single-control field
	 * on this screen: a wrapping label plus a generated one would leave the
	 * control with two labels.
	 *
	 * @return void
	 */
	public static function field_captcha_enabled() {
		$settings = self::get();
		$name     = self::OPTION . '[captcha_enabled]';

		echo '<label><input type="checkbox" aria-describedby="wrenchpress_captcha_enabled_description" name="' . esc_attr( $name ) . '" value="1"' . checked( 1, (int) $settings['captcha_enabled'], false ) . ' /> ';
		echo esc_html__( 'Require a CAPTCHA on the public visit form.', 'pitdesk-repair-crm' ) . '</label>';
		echo '<p class="description" id="wrenchpress_captcha_enabled_description">' . esc_html__( 'Off by default. When off, the form makes no external calls. Turn on to require reCAPTCHA v3 or Cloudflare Turnstile.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the provider select.
	 *
	 * @return void
	 */
	public static function field_captcha_provider() {
		$settings = self::get();
		$name     = self::OPTION . '[captcha_provider]';
		$current  = (string) $settings['captcha_provider'];

		$choices = array(
			'recaptcha_v3' => __( 'Google reCAPTCHA v3', 'pitdesk-repair-crm' ),
			'turnstile'    => __( 'Cloudflare Turnstile', 'pitdesk-repair-crm' ),
		);

		echo '<select id="wrenchpress_captcha_provider" name="' . esc_attr( $name ) . '">';
		foreach ( $choices as $value => $label ) {
			echo '<option value="' . esc_attr( $value ) . '"' . selected( $current, $value, false ) . '>' . esc_html( $label ) . '</option>';
		}
		echo '</select>';
	}

	/**
	 * Render the site-key text input.
	 *
	 * @return void
	 */
	public static function field_captcha_site_key() {
		$settings = self::get();
		$name     = self::OPTION . '[captcha_site_key]';

		echo '<input type="text" id="wrenchpress_captcha_site_key" aria-describedby="wrenchpress_captcha_site_key_description" class="regular-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $settings['captcha_site_key'] ) . '" autocomplete="off" />';
		echo '<p class="description" id="wrenchpress_captcha_site_key_description">' . esc_html__( 'The public site key from your CAPTCHA provider.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the secret-key text input.
	 *
	 * @return void
	 */
	public static function field_captcha_secret_key() {
		$settings = self::get();
		$name     = self::OPTION . '[captcha_secret_key]';

		echo '<input type="text" id="wrenchpress_captcha_secret_key" aria-describedby="wrenchpress_captcha_secret_key_description" class="regular-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $settings['captcha_secret_key'] ) . '" autocomplete="off" />';
		echo '<p class="description" id="wrenchpress_captcha_secret_key_description">' . esc_html__( 'The secret key used for server-side verification. Kept on your server only.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the rate-limit number input.
	 *
	 * @return void
	 */
	public static function field_rate_limit() {
		$settings = self::get();
		$name     = self::OPTION . '[rate_limit_per_hour]';

		echo '<input type="number" id="wrenchpress_rate_limit_per_hour" aria-describedby="wrenchpress_rate_limit_per_hour_description" min="1" step="1" class="small-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) absint( $settings['rate_limit_per_hour'] ) ) . '" />';
		echo '<p class="description" id="wrenchpress_rate_limit_per_hour_description">' . esc_html__( 'Maximum visit requests accepted per hour from one visitor (by IP and by email).', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the work-orders section intro copy.
	 *
	 * @return void
	 */
	public static function render_workorders_section_intro() {
		echo '<p>' . esc_html__( 'Defaults applied to new work orders.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the default-tax-rate number input.
	 *
	 * The stored value is a decimal fraction; it is shown to staff as a percentage
	 * (the fraction multiplied by 100) and saved back as a fraction on submit.
	 *
	 * @return void
	 */
	public static function field_default_tax_rate() {
		$settings = self::get();
		$name     = self::OPTION . '[default_tax_rate]';
		$percent  = round( (float) $settings['default_tax_rate'] * 100, 4 );

		echo '<input type="number" id="wrenchpress_default_tax_rate" aria-describedby="wrenchpress_default_tax_rate_description" min="0" max="100" step="0.0001" class="small-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $percent ) . '" /> %';
		echo '<p class="description" id="wrenchpress_default_tax_rate_description">' . esc_html__( 'Shop-wide default tax rate as a percentage (for example 8.25). Applied to new work orders; each work order can override it.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the service-reminders section intro copy.
	 *
	 * @return void
	 */
	public static function render_reminders_section_intro() {
		echo '<p>' . esc_html__( 'Defaults applied when you set a reminder at work-order close.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the default reminder interval: a months input and a distance input
	 * on the same row, the distance labelled in the active mileage unit.
	 *
	 * Two controls share this row, so a single label_for could only ever name
	 * one of them. Each input therefore carries its own explicit <label for>,
	 * built around the unit word already shown beside it, so the visible text is
	 * the label rather than a second, hidden name competing with it. The
	 * screen-reader-text prefix supplies the row context that the presentational
	 * form-table <th> cannot convey.
	 *
	 * @return void
	 */
	public static function field_reminder_default_interval() {
		$settings      = self::get();
		$months_name   = self::OPTION . '[reminder_default_months]';
		$distance_name = self::OPTION . '[reminder_default_distance]';
		$months        = absint( $settings['reminder_default_months'] );
		$distance      = absint( $settings['reminder_default_distance'] );
		$unit_label    = 'km' === self::mileage_unit()
			? __( 'km', 'pitdesk-repair-crm' )
			: __( 'mi', 'pitdesk-repair-crm' );

		/*
		 * One description covers both inputs, so both point at the same id.
		 * That is valid and is the correct reading: the sentence describes the
		 * interval, not either half of it.
		 */
		echo '<input type="number" id="wrenchpress_reminder_default_months" aria-describedby="wrenchpress_reminder_default_interval_description" min="0" step="1" class="small-text" name="' . esc_attr( $months_name ) . '" value="' . esc_attr( (string) $months ) . '" /> ';
		echo '<label for="wrenchpress_reminder_default_months"><span class="screen-reader-text">' . esc_html__( 'Default interval: ', 'pitdesk-repair-crm' ) . '</span>' . esc_html__( 'months', 'pitdesk-repair-crm' ) . '</label> ';
		echo esc_html__( 'and', 'pitdesk-repair-crm' ) . ' ';
		echo '<input type="number" id="wrenchpress_reminder_default_distance" aria-describedby="wrenchpress_reminder_default_interval_description" min="0" step="1" class="small-text" name="' . esc_attr( $distance_name ) . '" value="' . esc_attr( (string) $distance ) . '" /> ';
		echo '<label for="wrenchpress_reminder_default_distance"><span class="screen-reader-text">' . esc_html__( 'Default interval distance: ', 'pitdesk-repair-crm' ) . '</span>' . esc_html( $unit_label ) . '</label>';
		echo '<p class="description" id="wrenchpress_reminder_default_interval_description">' . esc_html__( 'Prefills the reminder date and mileage when you close a work order. Each reminder can be edited.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the mileage-unit select.
	 *
	 * @return void
	 */
	public static function field_mileage_unit() {
		$name    = self::OPTION . '[mileage_unit]';
		$current = self::mileage_unit();

		$choices = array(
			'mi' => __( 'Miles (mi)', 'pitdesk-repair-crm' ),
			'km' => __( 'Kilometers (km)', 'pitdesk-repair-crm' ),
		);

		echo '<select id="wrenchpress_mileage_unit" aria-describedby="wrenchpress_mileage_unit_description" name="' . esc_attr( $name ) . '">';
		foreach ( $choices as $value => $label ) {
			echo '<option value="' . esc_attr( $value ) . '"' . selected( $current, $value, false ) . '>' . esc_html( $label ) . '</option>';
		}
		echo '</select>';
		echo '<p class="description" id="wrenchpress_mileage_unit_description">' . esc_html__( 'Controls how mileage is entered and shown across the plugin. Stored values do not change when you switch.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the shop-identity section intro copy.
	 *
	 * @return void
	 */
	public static function render_shop_identity_section_intro() {
		echo '<p>' . esc_html__( 'Appears on generated invoices and estimates. Name and email fall back to your site title and admin email when left blank.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the shop-name text input.
	 *
	 * @return void
	 */
	public static function field_shop_name() {
		$settings = self::get();
		$name     = self::OPTION . '[shop_name]';

		echo '<input type="text" id="wrenchpress_shop_name" aria-describedby="wrenchpress_shop_name_description" class="regular-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $settings['shop_name'] ) . '" />';
		echo '<p class="description" id="wrenchpress_shop_name_description">' . esc_html__( 'Leave blank to use the site title.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the shop-address textarea.
	 *
	 * @return void
	 */
	public static function field_shop_address() {
		$settings = self::get();
		$name     = self::OPTION . '[shop_address]';

		echo '<textarea id="wrenchpress_shop_address" class="large-text" rows="3" name="' . esc_attr( $name ) . '">' . esc_textarea( (string) $settings['shop_address'] ) . '</textarea>';
	}

	/**
	 * Render the shop-phone text input.
	 *
	 * @return void
	 */
	public static function field_shop_phone() {
		$settings = self::get();
		$name     = self::OPTION . '[shop_phone]';

		echo '<input type="text" id="wrenchpress_shop_phone" class="regular-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $settings['shop_phone'] ) . '" />';
	}

	/**
	 * Render the shop-email input.
	 *
	 * @return void
	 */
	public static function field_shop_email() {
		$settings = self::get();
		$name     = self::OPTION . '[shop_email]';

		echo '<input type="email" id="wrenchpress_shop_email" aria-describedby="wrenchpress_shop_email_description" class="regular-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $settings['shop_email'] ) . '" />';
		echo '<p class="description" id="wrenchpress_shop_email_description">' . esc_html__( 'Leave blank to use the site admin email.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the tax / license number text input.
	 *
	 * @return void
	 */
	public static function field_shop_tax_number() {
		$settings = self::get();
		$name     = self::OPTION . '[shop_tax_number]';

		echo '<input type="text" id="wrenchpress_shop_tax_number" class="regular-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $settings['shop_tax_number'] ) . '" />';
	}

	/**
	 * Render the logo picker: a hidden attachment id, a Select-logo button and a
	 * thumbnail preview. The chosen value is stored as an attachment id (absint on
	 * save) and resolved to a local file path later at document generation, never a
	 * raw URL.
	 *
	 * @return void
	 */
	public static function field_shop_logo() {
		$settings = self::get();
		$name     = self::OPTION . '[shop_logo_id]';
		$logo_id  = absint( $settings['shop_logo_id'] );
		$preview  = $logo_id > 0 ? wp_get_attachment_image_url( $logo_id, 'thumbnail' ) : '';
		$has_logo = '' !== (string) $preview;

		echo '<input type="hidden" id="wrenchpress_shop_logo_id" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $logo_id ) . '" />';
		echo '<img id="wrenchpress_shop_logo_preview" src="' . esc_url( (string) $preview ) . '" alt="" style="max-width:150px;height:auto;display:' . ( $has_logo ? 'block' : 'none' ) . ';margin-bottom:8px;" />';
		echo '<button type="button" class="button" id="wrenchpress_shop_logo_select">' . esc_html__( 'Select logo', 'pitdesk-repair-crm' ) . '</button> ';
		echo '<button type="button" class="button-link" id="wrenchpress_shop_logo_remove" style="display:' . ( $has_logo ? 'inline' : 'none' ) . ';">' . esc_html__( 'Remove logo', 'pitdesk-repair-crm' ) . '</button>';
		echo '<p class="description">' . esc_html__( 'Shown on generated invoices and estimates.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the currency section intro copy.
	 *
	 * @return void
	 */
	public static function render_currency_section_intro() {
		echo '<p>' . esc_html__( 'Controls how money is formatted on generated documents.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the currency-symbol text input.
	 *
	 * @return void
	 */
	public static function field_currency_symbol() {
		$settings = self::get();
		$name     = self::OPTION . '[currency_symbol]';

		echo '<input type="text" id="wrenchpress_currency_symbol" class="small-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $settings['currency_symbol'] ) . '" />';
	}

	/**
	 * Render the currency-code text input.
	 *
	 * @return void
	 */
	public static function field_currency_code() {
		$settings = self::get();
		$name     = self::OPTION . '[currency_code]';

		echo '<input type="text" id="wrenchpress_currency_code" aria-describedby="wrenchpress_currency_code_description" class="small-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $settings['currency_code'] ) . '" />';
		echo '<p class="description" id="wrenchpress_currency_code_description">' . esc_html__( 'For example USD, EUR, or SEK.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the currency-symbol placement select (before / after).
	 *
	 * @return void
	 */
	public static function field_currency_placement() {
		$name    = self::OPTION . '[currency_placement]';
		$current = self::currency_placement();

		$choices = array(
			'before' => __( 'Before the amount', 'pitdesk-repair-crm' ),
			'after'  => __( 'After the amount', 'pitdesk-repair-crm' ),
		);

		echo '<select id="wrenchpress_currency_placement" name="' . esc_attr( $name ) . '">';
		foreach ( $choices as $value => $label ) {
			echo '<option value="' . esc_attr( $value ) . '"' . selected( $current, $value, false ) . '>' . esc_html( $label ) . '</option>';
		}
		echo '</select>';
	}

	/**
	 * Render the default document notes / terms textarea.
	 *
	 * @return void
	 */
	public static function field_document_notes_default() {
		$settings = self::get();
		$name     = self::OPTION . '[document_notes_default]';

		echo '<textarea id="wrenchpress_document_notes_default" aria-describedby="wrenchpress_document_notes_default_description" class="large-text" rows="3" name="' . esc_attr( $name ) . '">' . esc_textarea( (string) $settings['document_notes_default'] ) . '</textarea>';
		echo '<p class="description" id="wrenchpress_document_notes_default_description">' . esc_html__( 'Prefilled onto new documents; still editable at generation.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the data-retention section intro copy.
	 *
	 * @return void
	 */
	public static function render_retention_section_intro() {
		echo '<p>' . esc_html__( 'A daily cleanup removes stale visit-request leads and strips the submitter IP and user-agent from older leads. Customers and job history are never auto-deleted. Set a window to 0 to turn that cleanup off.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the unpromoted-lead retention window (in days).
	 *
	 * @return void
	 */
	public static function field_retention_lead_days() {
		$settings = self::get();
		$name     = self::OPTION . '[retention_lead_days]';

		echo '<input type="number" id="wrenchpress_retention_lead_days" aria-describedby="wrenchpress_retention_lead_days_description" min="0" step="1" class="small-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) absint( $settings['retention_lead_days'] ) ) . '" /> ' . esc_html__( 'days', 'pitdesk-repair-crm' );
		echo '<p class="description" id="wrenchpress_retention_lead_days_description">' . esc_html__( 'Unpromoted visit-request leads older than this are deleted by the daily cleanup. Promoted leads and every customer and work order are always kept. Default 180 days; 0 turns this off.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the submitter-IP retention window (in days).
	 *
	 * @return void
	 */
	public static function field_retention_ip_days() {
		$settings = self::get();
		$name     = self::OPTION . '[retention_ip_days]';

		echo '<input type="number" id="wrenchpress_retention_ip_days" aria-describedby="wrenchpress_retention_ip_days_description" min="0" step="1" class="small-text" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) absint( $settings['retention_ip_days'] ) ) . '" /> ' . esc_html__( 'days', 'pitdesk-repair-crm' );
		echo '<p class="description" id="wrenchpress_retention_ip_days_description">' . esc_html__( 'The submitter IP and user-agent are removed from leads older than this. Default 30 days; 0 turns this off.', 'pitdesk-repair-crm' ) . '</p>';
	}

	/**
	 * Render the settings page wrapper and form.
	 *
	 * @return void
	 */
	public static function render() {
		if ( ! current_user_can( WrenchPress_Capabilities::MANAGE_LEADS ) ) {
			wp_die( esc_html__( 'You do not have permission to do that.', 'pitdesk-repair-crm' ) );
		}

		echo '<div class="wrap">';
		echo '<h1>' . esc_html__( 'PitDesk Settings', 'pitdesk-repair-crm' ) . '</h1>';
		echo '<form method="post" action="options.php">';

		settings_fields( self::PAGE );
		do_settings_sections( self::PAGE );
		submit_button();

		echo '</form>';
		echo '</div>';
	}
}
