<?php
/**
 * Handles admin form submissions (admin-post.php) with nonce verification.
 *
 * @package Lucrativeit_Subscription_Recurring_Billing
 */

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

/**
 * Processes all create/update/delete and settings actions.
 */
class LUCRSURE_Admin_Handler {

	/**
	 * Settings instance.
	 *
	 * @var LUCRSURE_Settings
	 */
	protected $settings;

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

	/**
	 * Register admin-post handlers.
	 */
	public function init() {
		$actions = array(
			'lucrsure_save_branch'         => 'save_branch',
			'lucrsure_save_plan_category'  => 'save_plan_category',
			'lucrsure_save_customer'       => 'save_customer',
			'lucrsure_save_plan'         => 'save_plan',
			'lucrsure_save_subscription' => 'save_subscription',
			'lucrsure_save_invoice'      => 'save_invoice',
			'lucrsure_update_invoice'    => 'update_invoice',
			'lucrsure_invoice_action'    => 'invoice_action',
			'lucrsure_delete_record'     => 'delete_record',
			'lucrsure_save_settings'     => 'save_settings',
			'lucrsure_send_test_email'        => 'send_test_email',
			'lucrsure_import_demo_data'       => 'import_demo_data',
			'lucrsure_dismiss_onboarding'     => 'dismiss_onboarding',
			'lucrsure_dismiss_pro_activate_notice' => 'dismiss_pro_activate_notice',
			'lucrsure_reopen_setup'           => 'reopen_setup',
			'lucrsure_sync_plan_pages'        => 'sync_plan_pages',
		);

		foreach ( $actions as $action => $method ) {
			add_action( 'admin_post_' . $action, array( $this, $method ) );
		}

		add_action( 'wp_ajax_lucrsure_quick_create_plan', array( $this, 'ajax_create_plan' ) );
		add_action( 'wp_ajax_lucrsure_quick_create_customer', array( $this, 'ajax_create_customer' ) );
		add_action( 'wp_ajax_lucrsure_import_demo_step', array( $this, 'ajax_import_demo_step' ) );
		add_action( 'wp_ajax_lucrsure_dismiss_onboarding_ajax', array( $this, 'ajax_dismiss_onboarding' ) );
	}

	/**
	 * Verify the nonce + capability for a given action.
	 *
	 * @param string $action Nonce action.
	 */
	protected function guard( $action ) {
		if ( ! current_user_can( lucrsure_admin_capability() ) ) {
			wp_die( esc_html__( 'You are not allowed to do this.', 'lucrativeit-subscription-recurring-billing' ) );
		}
		check_admin_referer( $action );
	}

	/**
	 * Verify capability + AJAX nonce for inline wizard modals.
	 */
	protected function ajax_guard() {
		if ( ! current_user_can( lucrsure_admin_capability() ) ) {
			wp_send_json_error(
				array( 'message' => __( 'You are not allowed to do this.', 'lucrativeit-subscription-recurring-billing' ) ),
				403
			);
		}
		check_ajax_referer( 'lucrsure_admin', 'nonce' );
	}

	/**
	 * Redirect back to a module page with a notice code.
	 *
	 * @param string $page   Module slug (e.g. branches).
	 * @param string $notice Notice code.
	 * @param array  $args   Extra query args.
	 */
	protected function redirect( $page, $notice = 'saved', $args = array() ) {
		if ( $notice ) {
			set_transient(
				'lucrsure_admin_flash_' . get_current_user_id(),
				sanitize_key( $notice ),
				60
			);
		}

		$url_args = $args;
		$action   = isset( $args['action'] ) ? (string) $args['action'] : '';

		if ( in_array( $action, array( 'new', 'edit', 'view' ), true ) ) {
			$url = lucrsure_admin_nonce_url( $page, $url_args, 'lucrsure_admin_form' );
		} elseif ( isset( $args['tab'] ) ) {
			$url = lucrsure_admin_nonce_url( $page, $url_args, 'lucrsure_settings_tab' );
		} else {
			$url = lucrsure_admin_url( $page, $url_args );
		}

		wp_safe_redirect( $url );
		exit;
	}

	/**
	 * Read a posted, sanitized text field.
	 *
	 * @param string $key     Field key.
	 * @param string $default Default.
	 * @return string
	 */
	protected function text( $key, $default = '' ) {
		return isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : $default; // phpcs:ignore WordPress.Security.NonceVerification
	}

	/**
	 * Read a posted integer.
	 *
	 * @param string $key     Field key.
	 * @param int    $default Default.
	 * @return int
	 */
	protected function int( $key, $default = 0 ) {
		return isset( $_POST[ $key ] ) ? (int) $_POST[ $key ] : $default; // phpcs:ignore WordPress.Security.NonceVerification
	}

	/**
	 * Read a posted time value (HH:MM).
	 *
	 * @param string $key Field key.
	 * @return string
	 */
	protected function time( $key ) {
		$value = isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
		if ( '' === $value ) {
			return '';
		}
		return preg_match( '/^([01]?\d|2[0-3]):[0-5]\d$/', $value ) ? $value : '';
	}

	/* ----------------------------------------------------------------- *
	 * Branches
	 * ----------------------------------------------------------------- */

	/**
	 * Create/update a branch.
	 */
	public function save_branch() {
		$this->guard( 'lucrsure_save_branch' );

		$repo = new LUCRSURE_Branch_Repository();
		$id   = $this->int( 'id' );

		$data = array(
			'name'         => $this->text( 'name' ),
			'code'         => sanitize_title( $this->text( 'code' ) ),
			'region'       => $this->text( 'region' ),
			'address'      => $this->text( 'address' ),
			'phone'        => $this->text( 'phone' ),
			'opening_time' => $this->time( 'opening_time' ),
			'closing_time' => $this->time( 'closing_time' ),
			'image_id'     => $this->int( 'image_id' ),
			'amenities'    => isset( $_POST['amenities'] ) ? sanitize_textarea_field( wp_unslash( $_POST['amenities'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification
			'description'  => isset( $_POST['description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['description'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification
			'is_main'      => isset( $_POST['is_main'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
			'is_active'    => isset( $_POST['is_active'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
		);

		if ( $id ) {
			$repo->update( $id, $data );
			$this->redirect( 'branches', 'updated' );
		}

		$repo->insert( $data );
		$this->redirect( 'branches', 'branch_created' );
	}

	/* ----------------------------------------------------------------- *
	 * Plan categories
	 * ----------------------------------------------------------------- */

	/**
	 * Create/update a plan category.
	 */
	public function save_plan_category() {
		$this->guard( 'lucrsure_save_plan_category' );

		$repo = new LUCRSURE_Plan_Category_Repository();
		$id   = $this->int( 'id' );

		$name = $this->text( 'name' );

		$data = array(
			'name'        => $name,
			'slug'        => sanitize_title( $this->text( 'slug', $name ) ),
			'description' => isset( $_POST['description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['description'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification
			'is_active'   => isset( $_POST['is_active'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
		);

		if ( $id ) {
			$repo->update( $id, $data );
			$this->redirect( 'plan-categories', 'updated' );
		}

		$existing = $repo->find_by_slug( $data['slug'] );
		if ( $existing ) {
			$this->redirect( 'plan-categories', 'error' );
		}

		$repo->insert( $data );
		$this->redirect( 'plan-categories', 'category_created' );
	}

	/* ----------------------------------------------------------------- *
	 * Customers
	 * ----------------------------------------------------------------- */

	/**
	 * Create/update a customer.
	 */
	public function save_customer() {
		$this->guard( 'lucrsure_save_customer' );

		$repo = new LUCRSURE_Customer_Repository();
		$id   = $this->int( 'id' );

		$data = array_merge(
			array(
				'name'      => $this->text( 'name' ),
				'email'     => sanitize_email( $this->text( 'email' ) ),
				'phone'     => $this->text( 'phone' ),
				'company'   => $this->text( 'company' ),
				'branch_id' => $this->int( 'branch_id' ),
				'status'    => $this->text( 'status', 'active' ),
				'notes'     => isset( $_POST['notes'] ) ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification
			),
			lucrsure_sanitize_billing_address( wp_unslash( $_POST ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified via guard().
		);

		if ( $id ) {
			$repo->update( $id, $data );
			$this->redirect( 'customers', 'updated' );
		}

		$repo->insert( $data );
		$this->redirect( 'customers', 'created' );
	}

	/**
	 * AJAX: quick-create a customer from the subscription wizard.
	 */
	public function ajax_create_customer() {
		$this->ajax_guard();

		$name  = $this->text( 'name' );
		$email = sanitize_email( $this->text( 'email' ) );

		if ( '' === $name || '' === $email ) {
			wp_send_json_error(
				array( 'message' => __( 'Name and email are required.', 'lucrativeit-subscription-recurring-billing' ) )
			);
		}

		$repo = new LUCRSURE_Customer_Repository();
		$id   = $repo->insert(
			array(
				'name'      => $name,
				'email'     => $email,
				'phone'     => $this->text( 'phone' ),
				'company'   => $this->text( 'company' ),
				'branch_id' => $this->int( 'branch_id' ),
				'status'    => 'active',
				'notes'     => '',
			)
		);

		if ( ! $id ) {
			wp_send_json_error(
				array( 'message' => __( 'Could not create customer. Please try again.', 'lucrativeit-subscription-recurring-billing' ) )
			);
		}

		$customer = $repo->find( $id );

		wp_send_json_success(
			array(
				'id'       => (int) $customer['id'],
				'name'     => $customer['name'],
				'email'    => $customer['email'],
				'company'  => $customer['company'],
				'status'   => $customer['status'],
				'initials' => strtoupper( substr( $customer['name'], 0, 2 ) ),
				'search'   => strtolower( $customer['name'] . ' ' . $customer['email'] . ' ' . $customer['company'] ),
			)
		);
	}

	/**
	 * Save email templates.
	 */
	protected function save_templates() {
		$templates = get_option( 'lucrsure_email_templates', lucrsure_default_email_templates() );
		$keys      = array_keys( $templates );

		foreach ( $keys as $key ) {
			if ( isset( $_POST[ $key . '_subject' ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
				$templates[ $key ]['subject'] = sanitize_text_field( wp_unslash( $_POST[ $key . '_subject' ] ) ); // phpcs:ignore WordPress.Security.NonceVerification
			}
			if ( isset( $_POST[ $key . '_body' ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
				$templates[ $key ]['body'] = lucrsure_email_ensure_greeting(
					sanitize_textarea_field( wp_unslash( $_POST[ $key . '_body' ] ) ) // phpcs:ignore WordPress.Security.NonceVerification
				);
			}
		}

		update_option( 'lucrsure_email_templates', $templates );
	}

	/**
	 * Create/update a plan.
	 */
	public function save_plan() {
		$this->guard( 'lucrsure_save_plan' );

		$repo = new LUCRSURE_Plan_Repository();
		$id   = $this->int( 'id' );

		$features_raw = isset( $_POST['features'] ) ? sanitize_textarea_field( wp_unslash( $_POST['features'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
		$features     = array_values( array_filter( array_map( 'trim', explode( "\n", $features_raw ) ) ) );

		$name = $this->text( 'name' );

		$data = array(
			'name'             => $name,
			'slug'             => sanitize_title( $this->text( 'slug', $name ) ),
			'description'      => isset( $_POST['description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['description'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification
			'price'                  => isset( $_POST['price'] ) ? (float) $_POST['price'] : 0, // phpcs:ignore WordPress.Security.NonceVerification
			'special_price'          => isset( $_POST['special_price'] ) ? (float) $_POST['special_price'] : 0, // phpcs:ignore WordPress.Security.NonceVerification
			'special_price_enabled'  => isset( $_POST['special_price_enabled'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
			'currency'         => $this->text( 'currency', 'USD' ),
			'billing_interval' => $this->text( 'billing_interval', 'month' ),
			'interval_count'   => max( 1, $this->int( 'interval_count', 1 ) ),
			'trial_days'       => $this->int( 'trial_days' ),
			'features'         => wp_json_encode( $features ),
			'category_id'      => $this->int( 'category_id' ),
			'thumbnail_id'     => $this->int( 'thumbnail_id' ),
			'is_active'        => isset( $_POST['is_active'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
			'is_popular'       => isset( $_POST['is_popular'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
		);

		if ( $id ) {
			$repo->update( $id, $data );
			$plan_id = $id;
			$notice  = 'updated';
		} else {
			$plan_id = $repo->insert( $data );
			$notice  = 'plan_created';
		}

		if ( ! empty( $data['is_popular'] ) && ! empty( $data['category_id'] ) ) {
			$repo->clear_popular_in_category( (int) $data['category_id'], $data['billing_interval'], $plan_id );
			$repo->update( $plan_id, array( 'is_popular' => 1 ) );
		}

		$data['id']            = $plan_id;
		$data['wc_product_id'] = (int) ( $repo->find( $plan_id )['wc_product_id'] ?? 0 );

		/**
		 * Fires after a plan is saved (used by the WooCommerce integration).
		 *
		 * @param int   $plan_id Plan ID.
		 * @param array $data    Plan data.
		 */
		do_action( 'lucrsure_plan_saved', $plan_id, $data );

		$this->redirect( 'plans', $notice );
	}

	/**
	 * AJAX: quick-create a plan from the subscription wizard.
	 */
	public function ajax_create_plan() {
		$this->ajax_guard();

		$name = $this->text( 'name' );
		if ( '' === $name ) {
			wp_send_json_error(
				array( 'message' => __( 'Plan name is required.', 'lucrativeit-subscription-recurring-billing' ) )
			);
		}

		$repo = new LUCRSURE_Plan_Repository();
		$data = array(
			'name'             => $name,
			'slug'             => sanitize_title( $name ),
			'description'      => isset( $_POST['description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['description'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification
			'price'                  => isset( $_POST['price'] ) ? (float) $_POST['price'] : 0, // phpcs:ignore WordPress.Security.NonceVerification
			'special_price'          => isset( $_POST['special_price'] ) ? (float) $_POST['special_price'] : 0, // phpcs:ignore WordPress.Security.NonceVerification
			'special_price_enabled'  => isset( $_POST['special_price_enabled'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
			'currency'         => $this->text( 'currency', $this->settings->get( 'default_currency', 'USD' ) ),
			'billing_interval' => $this->text( 'billing_interval', 'month' ),
			'interval_count'   => 1,
			'trial_days'       => $this->int( 'trial_days' ),
			'features'         => wp_json_encode( array() ),
			'category_id'      => $this->int( 'category_id' ),
			'thumbnail_id'     => $this->int( 'thumbnail_id' ),
			'is_active'        => 1,
		);

		$plan_id = $repo->insert( $data );
		if ( ! $plan_id ) {
			wp_send_json_error(
				array( 'message' => __( 'Could not create plan. Please try again.', 'lucrativeit-subscription-recurring-billing' ) )
			);
		}

		$data['id']            = $plan_id;
		$data['wc_product_id'] = (int) ( $repo->find( $plan_id )['wc_product_id'] ?? 0 );

		/** This action is documented in save_plan(). */
		do_action( 'lucrsure_plan_saved', $plan_id, $data );

		$plan     = $repo->find( $plan_id );
		$interval = isset( lucrsure_billing_intervals()[ $plan['billing_interval'] ] )
			? strtolower( $plan['billing_interval'] )
			: $plan['billing_interval'];

		wp_send_json_success(
			array(
				'id'          => (int) $plan['id'],
				'name'        => $plan['name'],
				'price'       => lucrsure_plan_format_price_display( $plan )['effective'],
				'interval'    => $interval,
				'description' => $plan['description'],
				'trial_days'  => (int) $plan['trial_days'],
				'trial_label' => (int) $plan['trial_days'] > 0
					? sprintf(
						/* translators: %d days */
						__( '%d-day free trial', 'lucrativeit-subscription-recurring-billing' ),
						(int) $plan['trial_days']
					)
					: '',
			)
		);
	}

	/* ----------------------------------------------------------------- *
	 * Subscriptions
	 * ----------------------------------------------------------------- */

	/**
	 * Create/update a subscription.
	 */
	public function save_subscription() {
		$this->guard( 'lucrsure_save_subscription' );

		$repo  = new LUCRSURE_Subscription_Repository();
		$plans = new LUCRSURE_Plan_Repository();
		$id    = $this->int( 'id' );

		$plan_id  = $this->int( 'plan_id' );
		$plan     = $plans->find( $plan_id );
		$quantity = max( 1, $this->int( 'quantity', 1 ) );
		$mrr      = $plan ? lucrsure_plan_effective_price( $plan ) * $quantity : 0;
		$interval = $plan ? $plan['billing_interval'] : 'month';
		$icount   = $plan ? (int) $plan['interval_count'] : 1;

		$start = $this->text( 'start_date', gmdate( 'Y-m-d' ) );
		$start = $start ? gmdate( 'Y-m-d H:i:s', strtotime( $start ) ) : current_time( 'mysql' );

		$trial_ends = $this->text( 'trial_ends' );
		$trial_ends = $trial_ends ? gmdate( 'Y-m-d H:i:s', strtotime( $trial_ends ) ) : null;

		$status = $this->text( 'status', 'active' );

		$data = array(
			'customer_id'          => $this->int( 'customer_id' ),
			'plan_id'              => $plan_id,
			'status'               => $status,
			'quantity'             => $quantity,
			'mrr'                  => $mrr,
			'start_date'           => $start,
			'trial_ends'           => $trial_ends,
			'current_period_start' => $start,
			'current_period_end'   => lucrsure_next_period_end( $interval, $icount, $start ),
			'gateway'              => $this->sanitize_gateway_id( $this->text( 'gateway', 'manual' ) ),
			'auto_renew'           => isset( $_POST['auto_renew'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
		);

		if ( $id ) {
			$repo->update( $id, $data );
			$this->redirect( 'subscriptions', 'updated' );
		}

		$sub_id = $repo->insert( $data );

		// Begin billing: Stripe creates a recurring checkout (card saved &
		// auto-charged); invoice-based gateways generate an invoice + link.
		if ( $sub_id ) {
			$sub = $repo->find( $sub_id );
			LUCRSURE_Billing_Engine::instance()->start_subscription( $sub );
		}

		$this->redirect( 'subscriptions', 'created' );
	}

	/* ----------------------------------------------------------------- *
	 * Invoices / Billing
	 * ----------------------------------------------------------------- */

	/**
	 * Create a manual invoice.
	 */
	public function save_invoice() {
		$this->guard( 'lucrsure_save_invoice' );

		$repo = new LUCRSURE_Invoice_Repository();

		$customer_id = $this->int( 'customer_id' );
		$amount      = isset( $_POST['amount'] ) ? (float) $_POST['amount'] : 0; // phpcs:ignore WordPress.Security.NonceVerification
		$due_days    = (int) $this->settings->get( 'invoice_due_days', 7 );
		$customer    = ( new LUCRSURE_Customer_Repository() )->find( $customer_id );

		$invoice_id = $repo->insert(
			array_merge(
				array(
					'invoice_number'  => $repo->next_number(),
					'customer_id'     => $customer_id,
					'subscription_id' => $this->int( 'subscription_id' ),
					'amount'          => $amount,
					'currency'        => $this->text( 'currency', $this->settings->get( 'default_currency', 'USD' ) ),
					'status'          => 'unpaid',
					'due_date'        => gmdate( 'Y-m-d H:i:s', strtotime( '+' . $due_days . ' days' ) ),
					'gateway'         => $this->sanitize_gateway_id( $this->text( 'gateway', $this->settings->get( 'default_gateway', 'manual' ) ) ),
				),
				lucrsure_sanitize_billing_address( is_array( $customer ) ? $customer : array() )
			)
		);

		if ( $invoice_id ) {
			LUCRSURE_Billing_Engine::instance()->dispatch_payment( $invoice_id );
		}

		$this->redirect( 'billing', 'created' );
	}

	/**
	 * Update an existing invoice from the admin detail view.
	 */
	public function update_invoice() {
		$this->guard( 'lucrsure_update_invoice' );

		$repo    = new LUCRSURE_Invoice_Repository();
		$id      = $this->int( 'id' );
		$invoice = $repo->find( $id );

		if ( ! $invoice ) {
			$this->redirect( 'billing', 'error' );
		}

		$statuses = array_keys( lucrsure_invoice_statuses() );
		$status   = $this->text( 'status', $invoice['status'] );
		if ( ! in_array( $status, $statuses, true ) ) {
			$status = $invoice['status'];
		}

		$due_input = $this->text( 'due_date' );
		$due_date  = $due_input ? gmdate( 'Y-m-d H:i:s', strtotime( $due_input . ' 23:59:59' ) ) : null;

		$paid_input = $this->text( 'paid_at' );
		$paid_at    = $paid_input ? gmdate( 'Y-m-d H:i:s', strtotime( $paid_input ) ) : null;

		$data = array_merge(
			array(
				'amount'             => isset( $_POST['amount'] ) ? (float) $_POST['amount'] : (float) $invoice['amount'], // phpcs:ignore WordPress.Security.NonceVerification
				'currency'           => $this->text( 'currency', $invoice['currency'] ),
				'gateway'            => $this->sanitize_gateway_id( $this->text( 'gateway', $invoice['gateway'] ) ),
				'gateway_payment_id' => $this->text( 'gateway_payment_id', $invoice['gateway_payment_id'] ),
				'payment_url'        => isset( $_POST['payment_url'] ) ? esc_url_raw( wp_unslash( $_POST['payment_url'] ) ) : $invoice['payment_url'], // phpcs:ignore WordPress.Security.NonceVerification
				'due_date'           => $due_date,
			),
			lucrsure_sanitize_billing_address( wp_unslash( $_POST ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified via guard().
		);

		$was_paid = 'paid' === $invoice['status'];
		$now_paid = 'paid' === $status;
		$notice   = 'updated';

		if ( ! $was_paid && $now_paid ) {
			$repo->update( $id, $data );
			$result = LUCRSURE_Billing_Engine::instance()->mark_invoice_paid( $id );
			if ( $paid_at ) {
				$repo->update( $id, array( 'paid_at' => $paid_at ) );
			}
			if ( ! empty( $result['notified'] ) ) {
				$notice = 'invoice_paid_notified';
			} elseif ( ! empty( $result['marked'] ) ) {
				$notice = 'invoice_paid';
			}
		} else {
			$data['status'] = $status;
			if ( $now_paid ) {
				$data['paid_at'] = $paid_at ? $paid_at : ( $invoice['paid_at'] ?: current_time( 'mysql' ) );
			} else {
				$data['paid_at'] = null;
			}
			$repo->update( $id, $data );
		}

		$this->redirect( 'billing', $notice, array( 'action' => 'view', 'id' => $id ) );
	}

	/**
	 * Invoice row actions: mark paid, resend, delete.
	 */
	public function invoice_action() {
		$this->guard( 'lucrsure_invoice_action' );

		$id          = $this->int( 'id' );
		$do          = $this->text( 'do' );
		$return_view = isset( $_POST['return_view'] ); // phpcs:ignore WordPress.Security.NonceVerification
		$eng         = LUCRSURE_Billing_Engine::instance();
		$notice      = 'updated';

		if ( 'paid' === $do ) {
			$result = $eng->mark_invoice_paid( $id );
			if ( ! empty( $result['notified'] ) ) {
				$notice = 'invoice_paid_notified';
			} elseif ( ! empty( $result['marked'] ) ) {
				$notice = 'invoice_paid';
			}
		} elseif ( 'resend' === $do ) {
			$eng->dispatch_payment( $id );
		} elseif ( 'delete' === $do ) {
			( new LUCRSURE_Invoice_Repository() )->delete( $id );
			$this->redirect( 'billing', 'deleted' );
		}

		if ( $return_view ) {
			$this->redirect( 'billing', $notice, array( 'action' => 'view', 'id' => $id ) );
		}

		$this->redirect( 'billing', $notice );
	}

	/* ----------------------------------------------------------------- *
	 * Generic delete
	 * ----------------------------------------------------------------- */

	/**
	 * Delete a record from any repository.
	 */
	public function delete_record() {
		$this->guard( 'lucrsure_delete_record' );

		$type = $this->text( 'type' );
		$id   = $this->int( 'id' );

		$map = array(
			'branch'        => array( 'LUCRSURE_Branch_Repository', 'branches' ),
			'plan_category' => array( 'LUCRSURE_Plan_Category_Repository', 'plan-categories' ),
			'customer'      => array( 'LUCRSURE_Customer_Repository', 'customers' ),
			'plan'         => array( 'LUCRSURE_Plan_Repository', 'plans' ),
			'subscription' => array( 'LUCRSURE_Subscription_Repository', 'subscriptions' ),
		);

		/**
		 * Filter deletable record types (Pro adds staff/role mappings).
		 *
		 * @param array $map Type => array( class, redirect_module ).
		 */
		$map = apply_filters( 'lucrsure_delete_record_map', $map );

		if ( isset( $map[ $type ] ) ) {
			$class = $map[ $type ][0];
			$repo  = new $class();
			$repo->delete( $id );
			$this->redirect( $map[ $type ][1], 'deleted' );
		}

		$this->redirect( 'dashboard', 'error' );
	}

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

	/**
	 * Save a settings tab.
	 */
	public function save_settings() {
		$this->guard( 'lucrsure_save_settings' );

		$tab      = $this->text( 'tab', 'general' );
		$settings = $this->settings->all();

		switch ( $tab ) {
			case 'general':
				$settings['subscription_engine'] = in_array( $this->text( 'subscription_engine', 'custom' ), array( 'custom', 'woocommerce' ), true ) ? $this->text( 'subscription_engine', 'custom' ) : 'custom';
				$settings['company_name']        = $this->text( 'company_name' );
				$settings['support_email']       = sanitize_email( $this->text( 'support_email' ) );
				$settings['default_currency']    = $this->text( 'default_currency', 'USD' );
				$settings['timezone']            = $this->text( 'timezone', 'UTC' );
				$settings['footer_text']         = $this->text( 'footer_text', lucrsure_default_footer_text() );
				$notice                          = 'saved';
				break;

			case 'frontend':
				$branch_ids   = isset( $_POST['locations_branch_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['locations_branch_ids'] ) ) : array(); // phpcs:ignore WordPress.Security.NonceVerification
				$faq_questions = isset( $_POST['faq_question'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['faq_question'] ) ) : array(); // phpcs:ignore WordPress.Security.NonceVerification
				$faq_answers   = isset( $_POST['faq_answer'] ) ? array_map( 'sanitize_textarea_field', (array) wp_unslash( $_POST['faq_answer'] ) ) : array(); // phpcs:ignore WordPress.Security.NonceVerification
				$faq_items     = array();
				$faq_count     = max( count( $faq_questions ), count( $faq_answers ) );

				for ( $i = 0; $i < $faq_count; $i++ ) {
					$question = isset( $faq_questions[ $i ] ) ? sanitize_text_field( $faq_questions[ $i ] ) : '';
					$answer   = isset( $faq_answers[ $i ] ) ? sanitize_textarea_field( $faq_answers[ $i ] ) : '';
					if ( '' === $question || '' === $answer ) {
						continue;
					}
					$faq_items[] = array(
						'question' => $question,
						'answer'   => $answer,
					);
				}

				$appearance_defaults = lucrsure_default_frontend_appearance();
				$settings['frontend'] = array(
					'hero_enabled'         => isset( $_POST['hero_enabled'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
					'hero_heading'         => $this->text( 'hero_heading', __( 'Professional Services You Can Trust', 'lucrativeit-subscription-recurring-billing' ) ),
					'hero_subtitle'        => isset( $_POST['hero_subtitle'] ) ? sanitize_textarea_field( wp_unslash( $_POST['hero_subtitle'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification
					'hero_button_text'     => $this->text( 'hero_button_text', __( 'View Plans', 'lucrativeit-subscription-recurring-billing' ) ),
					'hero_button_url'      => esc_url_raw( $this->text( 'hero_button_url' ) ),
					'hero_image_id'        => $this->int( 'hero_image_id' ),
					'locations_enabled'    => isset( $_POST['locations_enabled'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
					'locations_heading'    => $this->text( 'locations_heading', __( 'Our Locations', 'lucrativeit-subscription-recurring-billing' ) ),
					'locations_subtitle'   => $this->text( 'locations_subtitle' ),
					'locations_branch_ids' => array_values( array_filter( $branch_ids ) ),
					'plans_heading'        => $this->text( 'plans_heading', __( 'Membership Plans', 'lucrativeit-subscription-recurring-billing' ) ),
					'plans_subtitle'       => isset( $_POST['plans_subtitle'] ) ? sanitize_textarea_field( wp_unslash( $_POST['plans_subtitle'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification
					'faq_enabled'          => isset( $_POST['faq_enabled'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
					'faq_heading'          => $this->text( 'faq_heading', __( 'Frequently Asked Questions', 'lucrativeit-subscription-recurring-billing' ) ),
					'faq_subtitle'         => $this->text( 'faq_subtitle' ),
					'faq_items'            => $faq_items,
					'appearance'           => array(
						'font_family'         => lucrsure_esc_css_font_family( $this->text( 'appearance_font_family' ) ),
						'text_color'          => lucrsure_sanitize_color_setting( $this->text( 'appearance_text_color' ), $appearance_defaults['text_color'] ),
						'muted_color'         => lucrsure_sanitize_color_setting( $this->text( 'appearance_muted_color' ), $appearance_defaults['muted_color'] ),
						'border_color'        => lucrsure_sanitize_color_setting( $this->text( 'appearance_border_color' ), $appearance_defaults['border_color'] ),
						'accent_color'        => lucrsure_sanitize_color_setting( $this->text( 'appearance_accent_color' ), $appearance_defaults['accent_color'] ),
						'accent_text_color'   => lucrsure_sanitize_color_setting( $this->text( 'appearance_accent_text_color' ), $appearance_defaults['accent_text_color'] ),
						'popular_badge_bg'    => lucrsure_sanitize_color_setting( $this->text( 'appearance_popular_badge_bg' ), $appearance_defaults['popular_badge_bg'] ),
						'popular_badge_text'  => lucrsure_sanitize_color_setting( $this->text( 'appearance_popular_badge_text' ), $appearance_defaults['popular_badge_text'] ),
						'hero_button_bg'      => lucrsure_sanitize_color_setting( $this->text( 'appearance_hero_button_bg' ), $appearance_defaults['hero_button_bg'] ),
						'hero_button_text'    => lucrsure_sanitize_color_setting( $this->text( 'appearance_hero_button_text' ), $appearance_defaults['hero_button_text'] ),
						'hero_title_color'    => lucrsure_sanitize_color_setting( $this->text( 'appearance_hero_title_color' ), $appearance_defaults['hero_title_color'] ),
						'hero_subtitle_color' => lucrsure_sanitize_color_setting( $this->text( 'appearance_hero_subtitle_color' ), $appearance_defaults['hero_subtitle_color'] ),
						'hero_gradient_start' => lucrsure_sanitize_color_setting( $this->text( 'appearance_hero_gradient_start' ), $appearance_defaults['hero_gradient_start'] ),
						'hero_gradient_end'   => lucrsure_sanitize_color_setting( $this->text( 'appearance_hero_gradient_end' ), $appearance_defaults['hero_gradient_end'] ),
						'check_color'         => lucrsure_sanitize_color_setting( $this->text( 'appearance_check_color' ), $appearance_defaults['check_color'] ),
					),
				);
				$notice               = 'saved';
				break;

			case 'gateways':
				$settings['default_gateway']  = $this->sanitize_gateway_id( $this->text( 'default_gateway', 'manual' ) );
				$settings['invoice_due_days'] = max( 0, $this->int( 'invoice_due_days', 7 ) );
				$settings['gateways']         = $this->collect_gateways( $settings );
				$notice                       = 'gateways_saved';
				break;

			case 'email':
				$settings['email'] = array(
					'mail_driver'   => $this->text( 'mail_driver', 'wp_mail' ),
					'encryption'    => $this->text( 'encryption', 'none' ),
					'smtp_host'     => $this->text( 'smtp_host' ),
					'smtp_port'     => $this->text( 'smtp_port' ),
					'smtp_username' => $this->text( 'smtp_username' ),
					'smtp_password' => $this->preserve_secret( 'smtp_password', $settings['email']['smtp_password'] ?? '' ),
					'from_address'  => sanitize_email( $this->text( 'from_address' ) ),
					'from_name'     => $this->text( 'from_name' ),
				);
				$notice = 'saved';
				break;

			case 'notifications':
				$prev_notif = isset( $settings['notifications'] ) && is_array( $settings['notifications'] )
					? $settings['notifications']
					: array();

				$settings['notifications'] = array(
					'invoice_sent'        => isset( $_POST['invoice_sent'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
					'payment_received'    => isset( $_POST['payment_received'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
					'subscription_active' => isset( $_POST['subscription_active'] ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification
					'attach_pdf_invoice'  => ( lucrsure_is_pro() && isset( $_POST['attach_pdf_invoice'] ) ) // phpcs:ignore WordPress.Security.NonceVerification
						? 1
						: ( ! empty( $prev_notif['attach_pdf_invoice'] ) ? 1 : 0 ),
				);
				$notice = 'saved';
				break;

			case 'templates':
				$this->save_templates();
				$notice = 'saved';
				break;

			case 'email_branding':
			case 'pdf_invoice':
				// Persisted by the Pro add-on via `lucrsure_after_save_settings`.
				$notice = 'saved';
				break;

			default:
				$notice = 'saved';
		}

		$this->settings->save( $settings );

		/**
		 * Fires after a settings tab is saved.
		 *
		 * Runs inside the verified `lucrsure_save_settings` request, so add-ons may
		 * read $_POST to persist their own fields rendered into the tab.
		 *
		 * @param string $tab      The settings tab that was saved.
		 * @param array  $settings The full settings array after saving.
		 */
		do_action( 'lucrsure_after_save_settings', $tab, $settings );

		$redirect_tab = 'email_branding' === $tab ? 'templates' : $tab;

		$this->redirect( 'settings', $notice, array( 'tab' => $redirect_tab ) );
	}

	/**
	 * Collect and sanitize gateway config from POST.
	 *
	 * Called only after `guard( 'lucrsure_save_settings' )` has verified the nonce
	 * and capability.
	 *
	 * @param array $settings Current settings.
	 * @return array
	 */
	protected function collect_gateways( $settings ) {
		$existing = isset( $settings['gateways'] ) && is_array( $settings['gateways'] ) ? $settings['gateways'] : array();

		$gateways = $existing;

		$gateways['stripe'] = array(
			'enabled'         => $this->bool_post( 'stripe_enabled' ),
			'secret_key'      => $this->preserve_secret( 'stripe_secret_key', $existing['stripe']['secret_key'] ?? '' ),
			'publishable_key' => $this->text( 'stripe_publishable_key' ),
			'webhook_secret'  => $this->preserve_secret( 'stripe_webhook_secret', $existing['stripe']['webhook_secret'] ?? '' ),
		);

		$gateways['paypal'] = array(
			'enabled'       => $this->bool_post( 'paypal_enabled' ),
			'client_id'     => $this->text( 'paypal_client_id' ),
			'client_secret' => $this->preserve_secret( 'paypal_client_secret', $existing['paypal']['client_secret'] ?? '' ),
			'webhook_id'    => $this->text( 'paypal_webhook_id' ),
			'sandbox'       => $this->bool_post( 'paypal_sandbox' ),
		);

		$gateways['sslcommerz'] = array(
			'enabled'        => $this->bool_post( 'sslcommerz_enabled' ),
			'store_id'       => $this->text( 'sslcommerz_store_id' ),
			'store_password' => $this->preserve_secret( 'sslcommerz_store_password', $existing['sslcommerz']['store_password'] ?? '' ),
			'sandbox'        => $this->bool_post( 'sslcommerz_sandbox' ),
		);

		$gateways['razorpay'] = array(
			'enabled'        => $this->bool_post( 'razorpay_enabled' ),
			'key_id'         => $this->text( 'razorpay_key_id' ),
			'key_secret'     => $this->preserve_secret( 'razorpay_key_secret', $existing['razorpay']['key_secret'] ?? '' ),
			'webhook_secret' => $this->preserve_secret( 'razorpay_webhook_secret', $existing['razorpay']['webhook_secret'] ?? '' ),
		);

		$gateways['manual'] = array(
			'enabled' => $this->bool_post( 'manual_enabled' ),
		);

		return $gateways;
	}

	/**
	 * Read a checkbox-style POST flag as 1 or 0.
	 *
	 * @param string $key POST key.
	 * @return int
	 */
	protected function bool_post( $key ) {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Caller verifies nonce via guard().
		return isset( $_POST[ $key ] ) ? 1 : 0;
	}

	/**
	 * Allowlist a gateway id against registered gateways.
	 *
	 * @param string $gateway_id Submitted gateway id.
	 * @return string
	 */
	protected function sanitize_gateway_id( $gateway_id ) {
		$gateway_id = sanitize_key( $gateway_id );
		$allowed    = array_keys( LUCRSURE_Gateway_Manager::instance()->all() );

		if ( in_array( $gateway_id, $allowed, true ) ) {
			return $gateway_id;
		}

		return 'manual';
	}

	/**
	 * Keep the previous secret if the field was submitted empty.
	 *
	 * @param string $key      POST key.
	 * @param string $previous Previous value.
	 * @return string
	 */
	protected function preserve_secret( $key, $previous ) {
		$value = $this->text( $key );
		return '' === $value ? sanitize_text_field( (string) $previous ) : $value;
	}

	/**
	 * Send a test email.
	 */
	public function send_test_email() {
		$this->guard( 'lucrsure_send_test_email' );
		$to = sanitize_email( $this->text( 'test_email' ) );

		if ( ! $to ) {
			$this->redirect( 'settings', 'email_invalid', array( 'tab' => 'email' ) );
		}

		$sent = LUCRSURE_Emails::send_test( $to );
		$this->redirect( 'settings', $sent ? 'email_sent' : 'email_failed', array( 'tab' => 'email' ) );
	}

	/**
	 * AJAX: run one step of the staged demo data import.
	 */
	public function ajax_import_demo_step() {
		$this->ajax_guard();

		$step  = isset( $_POST['step'] ) ? sanitize_key( wp_unslash( $_POST['step'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
		$start = isset( $_POST['start'] ) && '1' === $_POST['start']; // phpcs:ignore WordPress.Security.NonceVerification

		if ( $start ) {
			$categories = array();
			if ( isset( $_POST['categories'] ) && is_array( $_POST['categories'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
				$categories = array_map( 'sanitize_key', wp_unslash( $_POST['categories'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
			}

			if ( ! LUCRSURE_Demo_Data::start_import( $categories ) ) {
				wp_send_json_error(
					array( 'message' => __( 'Sample data could not be imported. Select at least one category, or records may already exist.', 'lucrativeit-subscription-recurring-billing' ) )
				);
			}

			$steps = LUCRSURE_Demo_Data::get_import_steps( $categories );
			$step  = $steps[0]['key'];
		}

		if ( '' === $step ) {
			wp_send_json_error( array( 'message' => __( 'Invalid import step.', 'lucrativeit-subscription-recurring-billing' ) ) );
		}

		$result = LUCRSURE_Demo_Data::run_step( $step );

		if ( empty( $result['success'] ) ) {
			delete_option( LUCRSURE_Demo_Data::STATE_OPTION );
			wp_send_json_error( array( 'message' => $result['message'] ) );
		}

		if ( ! empty( $result['done'] ) ) {
			set_transient(
				'lucrsure_admin_flash_' . get_current_user_id(),
				'demo_imported',
				60
			);
		}

		$next_step = '';
		if ( empty( $result['done'] ) ) {
			$steps  = LUCRSURE_Demo_Data::get_import_steps();
			$found  = false;
			foreach ( $steps as $row ) {
				if ( $found ) {
					$next_step = $row['key'];
					break;
				}
				if ( $row['key'] === $step ) {
					$found = true;
				}
			}
		}

		wp_send_json_success(
			array(
				'message'   => $result['message'],
				'percent'   => (int) $result['percent'],
				'done'      => ! empty( $result['done'] ),
				'next_step' => $next_step,
				'hub_url'   => isset( $result['hub_url'] ) ? $result['hub_url'] : '',
				'redirect'  => ! empty( $result['done'] ) ? lucrsure_admin_url( 'dashboard' ) : '',
			)
		);
	}

	/**
	 * AJAX: dismiss setup modal without importing data.
	 */
	public function ajax_dismiss_onboarding() {
		$this->ajax_guard();

		update_option( 'lucrsure_onboarding_dismissed', true );
		set_transient(
			'lucrsure_admin_flash_' . get_current_user_id(),
			'onboarding_dismissed',
			60
		);
		wp_send_json_success(
			array(
				'message'  => __( 'Setup skipped. You can add your own data anytime.', 'lucrativeit-subscription-recurring-billing' ),
				'redirect' => lucrsure_admin_url( 'dashboard' ),
			)
		);
	}

	/**
	 * Import sample categories, plans, customers, subscriptions, and invoices.
	 */
	public function import_demo_data() {
		$this->guard( 'lucrsure_import_demo_data' );

		if ( ! LUCRSURE_Demo_Data::can_import() ) {
			$this->redirect( 'dashboard', 'demo_import_blocked' );
		}

		$categories = array();
		if ( isset( $_POST['categories'] ) && is_array( $_POST['categories'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			$categories = array_map( 'sanitize_key', wp_unslash( $_POST['categories'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
		}
		if ( empty( $categories ) ) {
			$categories = LUCRSURE_Demo_Data::get_default_setup_categories();
		}

		$ok = LUCRSURE_Demo_Data::import( $categories );
		$this->redirect( 'dashboard', $ok ? 'demo_imported' : 'error' );
	}

	/**
	 * Dismiss the post-install onboarding card without importing data.
	 */
	public function dismiss_onboarding() {
		$this->guard( 'lucrsure_dismiss_onboarding' );

		update_option( 'lucrsure_onboarding_dismissed', true );
		$this->redirect( 'dashboard', 'onboarding_dismissed' );
	}

	/**
	 * Dismiss the “activate Pro” admin notice for the current user.
	 */
	public function dismiss_pro_activate_notice() {
		$this->guard( 'lucrsure_dismiss_pro_activate_notice' );

		update_user_meta( get_current_user_id(), 'lucrsure_dismiss_pro_activate_notice', 1 );

		$redirect = wp_get_referer() ? wp_get_referer() : lucrsure_admin_url( 'dashboard' );
		wp_safe_redirect( $redirect );
		exit;
	}

	/**
	 * Reopen the setup wizard on an empty site (clears onboarding flags).
	 *
	 * Prefer linking via lucrsure_reopen_setup_url() so the request carries a nonce.
	 */
	public function reopen_setup() {
		$this->guard( 'lucrsure_reopen_setup' );

		$plans = new LUCRSURE_Plan_Repository();
		$cust  = new LUCRSURE_Customer_Repository();
		if ( 0 !== $plans->count() || 0 !== $cust->count() ) {
			$this->redirect( 'dashboard', 'error' );
		}

		delete_option( 'lucrsure_onboarding_dismissed' );
		delete_option( 'lucrsure_demo_data_imported' );
		update_option( 'lucrsure_activation_redirect', true );

		wp_safe_redirect( lucrsure_admin_url( 'setup' ) );
		exit;
	}

	/**
	 * Create or refresh public WordPress pages with [lucrsure_plans] shortcodes per category.
	 */
	public function sync_plan_pages() {
		$this->guard( 'lucrsure_sync_plan_pages' );

		$created = LUCRSURE_Plan_Pages::sync_all_categories();
		$this->redirect( 'plan-categories', empty( $created ) ? 'error' : 'plan_pages_synced' );
	}
}
