<?php
/**
 * Admin: Payouts Overview
 *
 * @package     AffiliateWP
 * @subpackage  Admin/Payouts
 * @copyright   Copyright (c) 2021, Sandhills Development, LLC
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       1.9
 */

// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) exit;

include AFFILIATEWP_PLUGIN_DIR . 'includes/admin/payouts/screen-options.php';
require_once AFFILIATEWP_PLUGIN_DIR . 'includes/admin/payouts/class-list-table.php';
require_once AFFILIATEWP_PLUGIN_DIR . 'includes/admin/payouts/class-list-table-batches.php';

/**
 * Renders the Payouts / Batches tab nav above the list views.
 *
 * Uses the WP core `nav-tab-wrapper` treatment so it matches other tabbed
 * admin screens (Reports, Tools), selected via the `tab` URL param to
 * match those screens. Kept in payouts.php alongside the page entry point
 * so adding another tab later is a one-line edit.
 *
 * @since 2.33.0
 *
 * @param string $active Which tab to mark active. Accepts 'payouts' or 'batches'.
 */
function affwp_payouts_render_tabs( $active = 'payouts' ) {
	$tabs = [
		'payouts' => [
			'label' => __( 'Payouts', 'affiliate-wp' ),
			'url'   => affwp_admin_url( 'payouts' ),
		],
		'batches' => [
			'label' => __( 'Payout Batches', 'affiliate-wp' ),
			'url'   => affwp_admin_url( 'payouts', [ 'tab' => 'batches' ] ),
		],
	];
	?>
	<h2 class="nav-tab-wrapper affwp-tab-wrapper" style="margin-bottom: 1em;">
		<?php foreach ( $tabs as $key => $tab ) : ?>
			<a href="<?php echo esc_url( $tab['url'] ); ?>"
				class="nav-tab <?php echo $active === $key ? 'nav-tab-active' : ''; ?>">
				<?php echo esc_html( $tab['label'] ); ?>
			</a>
		<?php endforeach; ?>
	</h2>
	<?php
}

/**
 * Redirects the legacy `?action=view_payout&payout_id=X` URL to `?payout_id=X`.
 *
 * The classic full-page view was replaced by the drawer. The drawer auto-opens
 * from the `payout_id` URL param (see drawer.php Alpine store init()). This
 * preserves bookmarks, email links, and integrator URLs — they get one redirect.
 *
 * Fires on `admin_init` (before any output) so `wp_safe_redirect()` can send
 * headers successfully. Doing this inside the page callback is too late.
 *
 * @since 2.33.0
 */
function affwp_payouts_view_payout_redirect() {

	if ( ! is_admin() ) {
		return;
	}

	if ( empty( $_GET['page'] ) || 'affiliate-wp-payouts' !== $_GET['page'] ) {
		return;
	}

	if ( empty( $_GET['action'] ) || 'view_payout' !== $_GET['action'] ) {
		return;
	}

	$payout_id = isset( $_GET['payout_id'] ) ? absint( $_GET['payout_id'] ) : 0;

	$target = affwp_admin_url( 'payouts', $payout_id ? [ 'payout_id' => $payout_id ] : [] );

	wp_safe_redirect( $target );
	exit;
}
add_action( 'admin_init', 'affwp_payouts_view_payout_redirect' );

/**
 * Hide the Screen Options tab on the Pay Affiliates form. The page is a one-shot
 * configuration screen with no list table or columns to toggle.
 *
 * @since 2.33.0
 */
function affwp_payouts_hide_screen_options( $screen ) {

	if ( ! ( $screen instanceof WP_Screen ) ) {
		return;
	}

	if ( false === strpos( $screen->id, 'affiliate-wp-payouts' ) ) {
		return;
	}

	if ( empty( $_GET['action'] ) || ! in_array( $_GET['action'], array( 'new_payout', 'preview_payout', 'payout_submitted' ), true ) ) {
		return;
	}

	add_filter( 'screen_options_show_screen', '__return_false' );
	add_filter( 'admin_footer_text', '__return_empty_string', 99 );
	add_filter( 'update_footer', '__return_empty_string', 99 );
}
add_action( 'current_screen', 'affwp_payouts_hide_screen_options' );

/**
 * Routes the Payouts-overview counts through the DevTools-only data seam.
 *
 * The empty state, its variant, and the first-run CTA branch are all
 * conclusions drawn from these counts — so the data is the only override
 * point and there is no filter on the rendered decision. The seam is gated
 * behind AFFWP_DEVTOOLS (the Empty States preview tool short-circuits the
 * counts to zero so the production renderer draws the real state); it never
 * fires on a production install.
 *
 * @since 2.33.0
 *
 * @param array $counts Real counts. Keys: payouts, unpaid, affiliates (or batches).
 * @return array Counts, possibly overridden in a DevTools environment.
 */
function affwp_payouts_overview_counts( array $counts ) {

	if ( ! ( defined( 'AFFWP_DEVTOOLS' ) && AFFWP_DEVTOOLS ) ) {
		return $counts;
	}

	return (array) apply_filters( 'affwp_devtools_payouts_overview_counts', $counts );
}

/**
 * Loads the Payouts admin screen.
 *
 * @since 1.9
 */
function affwp_payouts_admin() {

	$action = null;

	if ( isset( $_GET['action2'] ) && '-1' !== $_GET['action2'] ) {
		$action = $_GET['action2'];
	} elseif ( isset( $_GET['action'] ) && '-1' !== $_GET['action'] ) {
		$action = $_GET['action'];
	}

	/*
	 * NOTE: `view_payout` is redirected to `?payout_id=X` on the `admin_init` hook
	 * (see affwp_payouts_view_payout_redirect below). By the time this callback
	 * runs, admin output has already started and `wp_safe_redirect()` can't send
	 * headers — the redirect has to happen earlier.
	 */

	if ( 'new_payout' === $action ) {
		include AFFILIATEWP_PLUGIN_DIR . 'includes/admin/payouts/new.php';
		return;
	}

	if ( 'preview_payout' === $action ) {
		include AFFILIATEWP_PLUGIN_DIR . 'includes/admin/payouts/preview.php';
		return;
	}

	if ( 'payout_submitted' === $action ) {
		include AFFILIATEWP_PLUGIN_DIR . 'includes/admin/payouts/submitted.php';
		return;
	}

	/*
	 * Tab router for the main list views. Out-of-flow workflows above
	 * (new/preview/submitted) don't render tabs — they're modal-ish screens.
	 */
	$tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'payouts';

	if ( 'batches' === $tab ) {
		include AFFILIATEWP_PLUGIN_DIR . 'includes/admin/payouts/batches.php';
		return;
	}

	/*
	 * The payout drawer mirrors the open payout into the URL as ?payout_id=X
	 * (deep-link / refresh-persistence). WP_List_Table builds its pagination and
	 * sortable-column links from $_SERVER['REQUEST_URI'], so after a reload with
	 * that param present every page/sort link inherits it — paging then re-opens
	 * the same payout on each navigation. Strip it from the request URI before the
	 * table renders; the drawer reads payout_id client-side from the address bar,
	 * so deep-linking is unaffected.
	 */
	if ( isset( $_GET['payout_id'] ) && ! empty( $_SERVER['REQUEST_URI'] ) ) {
		$_SERVER['REQUEST_URI'] = remove_query_arg( 'payout_id', $_SERVER['REQUEST_URI'] );
	}

	$payouts_table = new AffWP_Payouts_Table();
	$payouts_table->prepare_items();

	$is_filtered_view = ! empty( $_GET['status'] ) || ! empty( $_GET['s'] ) || ! empty( $_GET['filter_from'] ) || ! empty( $_GET['filter_to'] );

	/*
	 * Whether to show the empty state, which variant, and which CTA branch are
	 * all conclusions drawn from these three counts. They run through one
	 * DevTools-only data seam (see affwp_payouts_overview_counts) so a preview
	 * short-circuits the underlying data and the production renderer draws the
	 * real state. There is deliberately no filter on the rendered decision.
	 */
	$counts = affwp_payouts_overview_counts(
		array(
			'payouts'    => (int) $payouts_table->total_count,
			'unpaid'     => (int) affiliate_wp()->referrals->unpaid_count(),
			'affiliates' => (int) affiliate_wp()->affiliates->count(),
		)
	);

	$show_empty_state    = (int) $counts['payouts'] < 1 && ! $is_filtered_view;
	$has_any_affiliates  = (int) $counts['affiliates'] > 0;
	$empty_state_variant = '';

	if ( $show_empty_state ) {
		$empty_state_variant = (int) $counts['unpaid'] > 0 ? 'ready-to-pay' : 'first-run';
	}
?>
	<div class="wrap">
		<h1>
			<?php _e( 'Payouts', 'affiliate-wp' ); ?>
			<?php if ( ! $show_empty_state ) : ?>
				<?php
				echo affwp_admin_link( 'reports', _x( 'Reports', 'payouts', 'affiliate-wp' ), array( 'tab' => 'payouts' ), array( 'class' => 'button action button-small' ) );
				?>
				<?php
				echo affwp_admin_link( 'referrals', __( 'Manage Referrals', 'affiliate-wp' ), array(), array( 'class' => 'button action button-small' ) );
				?>
				<?php if ( current_user_can( 'manage_payouts' ) ) : ?>
					<a href="<?php echo esc_url( affwp_admin_url( 'payouts', array( 'action' => 'new_payout' ) ) ); ?>" class="button action button-primary button-small"><?php _e( 'Pay Affiliates', 'affiliate-wp' ); ?></a>
				<?php endif; // manage_payouts ?>
			<?php endif; ?>
		</h1>

		<?php if ( ! $show_empty_state ) : ?>
			<?php affwp_payouts_render_tabs( 'payouts' ); ?>
		<?php endif; ?>

		<?php
		/**
		 * Fires at the top of the Payouts page (outside the form element).
		 *
		 * @since 1.9
		 */
		do_action( 'affwp_payouts_page_top' );
		?>

		<?php if ( $show_empty_state ) : ?>

			<?php affwp_payouts_render_empty_state( $empty_state_variant, $has_any_affiliates ); ?>

		<?php else : ?>

			<form id="affwp-payouts-filter" method="get" action="<?php echo esc_url( affwp_admin_url( 'payouts' ) ); ?>">
				<?php $payouts_table->search_box( __( 'Search', 'affiliate-wp' ), 'affwp-payouts' ); ?>

				<input type="hidden" name="page" value="affiliate-wp-payouts" />

				<?php $payouts_table->views(); ?>
				<?php $payouts_table->display(); ?>
			</form>

		<?php endif; ?>
		<?php
		/**
		 * Fires at the bottom of the Payouts page (outside the form element).
		 *
		 * @since 1.9
		 */
		do_action( 'affwp_payouts_page_bottom' );
		?>
	</div>

	<?php
	/*
	 * Render the payout detail drawer. Auto-opens when the URL carries
	 * ?payout_id=X (drawer JS reads the URL and opens on Alpine init).
	 * Row-click intercept also lives in the drawer JS, not here.
	 */
	affwp_render_payout_drawer();

	/*
	 * Also render the batch drawer so the payout drawer's "#N" Batch link
	 * (in the Details panel) can open-as-drawer instead of navigating. The
	 * batch drawer's auto-open logic is scoped to the `batches` tab,
	 * so adding it here is zero-cost — it only activates on an explicit
	 * `openFor()` call, not on URL params.
	 */
	affwp_render_batch_drawer();
}

/**
 * Renders the variant-specific empty state on the Payouts list page.
 *
 * Two variants:
 *  - `first-run`    No payouts and no unpaid referrals. Quiet onboarding text.
 *  - `ready-to-pay` No payouts but unpaid referrals exist. Action prompt with
 *                   counts, totals, and a top-3 affiliate illustration.
 *
 * @since 2.33.0
 *
 * @param string $variant        Variant slug.
 * @param bool   $has_affiliates Whether the site has any affiliates (drives the first-run CTA).
 */
function affwp_payouts_render_empty_state( $variant, $has_affiliates = false ) {

	if ( 'ready-to-pay' === $variant ) {
		affwp_payouts_render_ready_to_pay_empty_state();
		return;
	}

	affwp_payouts_render_first_run_empty_state( $has_affiliates );
}

/**
 * Renders the first-run empty state: no payouts, no unpaid referrals.
 *
 * Text-first by design. No icon, no CTA — there is nothing to pay yet, so
 * any action button would lead to another empty state.
 *
 * @since 2.33.0
 *
 * @param bool $has_affiliates Whether the site has any affiliates. Resolved by
 *                             the caller from the overview counts.
 */
function affwp_payouts_render_first_run_empty_state( $has_affiliates = false ) {
	$can_manage_affiliates = current_user_can( 'manage_affiliates' );

	if ( $has_affiliates ) {
		$cta_text = __( 'Manage affiliates', 'affiliate-wp' );
		$cta_url  = affwp_admin_url( 'affiliates' );
	} else {
		$cta_text = __( 'Add your first affiliate', 'affiliate-wp' );
		$cta_url  = affwp_admin_url( 'affiliates', array( 'action' => 'add_affiliate' ) );
	}

	$cta_subtitle = '';
	?>
	<div class="min-h-[60vh] flex items-center justify-center">
		<div class="w-full">
			<?php
			affwp_list_page_empty_state(
				array(
					'title'           => __( 'No payouts yet', 'affiliate-wp' ),
					'title_class'     => 'text-4xl font-semibold text-gray-900 m-0 tracking-tight',
					'description'     => __( "Once your affiliates earn referrals, you'll pay them from here.", 'affiliate-wp' ),
					'content'         => 'affwp_payouts_render_ghost_rows_card',
					'button_text'     => $can_manage_affiliates ? $cta_text : '',
					'button_url'      => $can_manage_affiliates ? $cta_url : '',
					'button_subtitle' => $can_manage_affiliates ? $cta_subtitle : '',
				)
			);
			?>
		</div>
	</div>
	<?php
}

/**
 * Renders a preview card of three sample payout rows.
 *
 * Visual mass for the first-run empty state without resorting to a literal
 * coins/wallet icon. The card frame matches the Ready-to-pay state's
 * affiliate-rows card so the two empty states feel like one design. Sample
 * data (plausible affiliate names + amounts + status badges) is hard-coded
 * so this works on a fresh install with zero records in the database.
 *
 * A linear-gradient mask fades the right side of the card into the page
 * background, signalling "preview" rather than "loading."
 *
 * @since 2.33.0
 */
function affwp_payouts_render_ghost_rows_card() {

	$demo_affiliates = affwp_get_demo_affiliates( 3 );

	$amounts  = array(
		affwp_currency_filter( affwp_format_amount( 284.50 ) ),
		affwp_currency_filter( affwp_format_amount( 156.23 ) ),
		affwp_currency_filter( affwp_format_amount( 98.75 ) ),
	);
	$statuses = array( 'paid', 'processing', 'paid' );

	$rows = array();
	foreach ( array_values( $demo_affiliates ) as $i => $affiliate ) {
		$rows[] = array(
			'name'   => $affiliate['name'],
			'amount' => $amounts[ $i ],
			'status' => $statuses[ $i ],
		);
	}

	$last = count( $rows ) - 1;
	?>
	<div class="mx-auto max-w-xl mt-8 rounded-2xl bg-white shadow-sm text-left overflow-hidden mask-[linear-gradient(to_right,black_70%,transparent_100%)]" aria-hidden="true">
		<table class="w-full border-collapse">
			<tbody>
			<?php foreach ( $rows as $i => $row ) : ?>
				<tr class="<?php echo $i === $last ? '' : 'border-b border-gray-100'; ?>">
					<td class="pl-5 pr-3 py-4 align-middle font-semibold text-gray-900 tabular-nums text-right whitespace-nowrap"><?php echo esc_html( $row['amount'] ); ?></td>
					<td class="pr-3 py-4 align-middle whitespace-nowrap">
						<?php echo affwp_get_payout_status_badge( $row['status'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper returns trusted HTML. ?>
					</td>
					<td class="w-full p-0"></td>
					<td class="pl-3 pr-5 py-4 align-middle text-right text-gray-700 max-w-48 truncate"><?php echo esc_html( $row['name'] ); ?></td>
				</tr>
			<?php endforeach; ?>
			</tbody>
		</table>
	</div>
	<?php
}

/**
 * Renders the ready-to-pay empty state: unpaid referrals exist, no payouts yet.
 *
 * The illustration is a small list card of the top 3 affiliates by unpaid
 * total, mirroring the layout the user will see once they run a payout.
 *
 * @since 2.33.0
 */
function affwp_payouts_render_ready_to_pay_empty_state() {

	$unpaid_count     = (int) affiliate_wp()->referrals->unpaid_count();
	$unpaid_total     = affiliate_wp()->referrals->unpaid_earnings();
	$top_unpaid       = affwp_payouts_get_top_unpaid_affiliates( 3 );
	$affiliate_count  = affwp_payouts_get_unpaid_affiliate_count();

	$data = array(
		'unpaid_count'    => $unpaid_count,
		'unpaid_total'    => $unpaid_total,
		'affiliate_count' => $affiliate_count,
		'top_affiliates'  => $top_unpaid,
	);

	/*
	 * DevTools-only seam: lets the Empty States preview tool inject sample
	 * values so the ready-to-pay illustration renders on a fresh install,
	 * without bypassing the production renderer. Inert outside DevTools.
	 */
	if ( defined( 'AFFWP_DEVTOOLS' ) && AFFWP_DEVTOOLS ) {
		$data = (array) apply_filters( 'affwp_devtools_payouts_ready_to_pay_data', $data );
	}

	$count           = isset( $data['unpaid_count'] ) ? (int) $data['unpaid_count'] : 0;
	$total           = isset( $data['unpaid_total'] ) ? (string) $data['unpaid_total'] : '';
	$top             = isset( $data['top_affiliates'] ) ? (array) $data['top_affiliates'] : array();
	$affiliate_count = isset( $data['affiliate_count'] ) ? (int) $data['affiliate_count'] : count( $top );

	$title = sprintf(
		/* translators: %d: number of unpaid referrals */
		_n( 'Pay %d referral', 'Pay %d referrals', $count, 'affiliate-wp' ),
		$count
	);

	$description = $total
		? sprintf(
			/* translators: 1: formatted currency total, 2: number of affiliates */
			_n( '%1$s owed to %2$d affiliate.', '%1$s owed across %2$d affiliates.', $affiliate_count, 'affiliate-wp' ),
			$total,
			$affiliate_count
		)
		: '';

	?>
	<div class="min-h-[60vh] flex items-center justify-center">
		<div class="w-full">
			<?php
			affwp_list_page_empty_state(
				array(
					'title'           => $title,
					'title_class'     => 'text-4xl font-semibold text-gray-900 m-0 tracking-tight',
					'description'     => $description,
					'content'         => function () use ( $top ) {
						affwp_payouts_render_unpaid_affiliates_card( $top );
					},
					'button_text'     => current_user_can( 'manage_payouts' ) ? __( 'Pay Affiliates', 'affiliate-wp' ) : '',
					'button_url'      => current_user_can( 'manage_payouts' ) ? affwp_admin_url( 'payouts', array( 'action' => 'new_payout' ) ) : '',
					'button_subtitle' => current_user_can( 'manage_payouts' ) ? __( 'Sends payouts via your configured methods', 'affiliate-wp' ) : '',
					'footer'          => sprintf(
						'<a href="%s" class="text-gray-600 hover:text-gray-900 underline">%s</a>',
						esc_url( affwp_admin_url( 'referrals', array( 'status' => 'unpaid' ) ) ),
						esc_html__( 'View unpaid referrals', 'affiliate-wp' )
					),
				)
			);
			?>
		</div>
	</div>
	<?php
}

/**
 * Renders the affiliate-rows card used inside the ready-to-pay empty state.
 *
 * @since 2.33.0
 *
 * @param array $rows Top affiliate rows (see affwp_payouts_get_top_unpaid_affiliates()).
 */
function affwp_payouts_render_unpaid_affiliates_card( array $rows ) {
	if ( empty( $rows ) ) {
		return;
	}

	$rows      = array_values( $rows );
	$row_total = count( $rows );
	?>
	<div class="mx-auto max-w-xl mt-6 rounded-2xl bg-white shadow-sm text-left overflow-hidden">
		<table class="w-full border-collapse">
			<tbody>
			<?php foreach ( $rows as $i => $row ) :
				$name           = isset( $row['name'] ) ? $row['name'] : '';
				$referral_count = isset( $row['referral_count'] ) ? (int) $row['referral_count'] : 0;
				$total          = isset( $row['total'] ) ? (string) $row['total'] : '';
				$is_last        = ( $i === $row_total - 1 );

				$count_label = sprintf(
					/* translators: %d: number of unpaid referrals for this affiliate */
					_n( '%d unpaid referral', '%d unpaid referrals', $referral_count, 'affiliate-wp' ),
					$referral_count
				);
			?>
				<tr class="<?php echo $is_last ? '' : 'border-b border-gray-100'; ?>">
					<td class="pl-5 pr-3 py-4 align-middle font-semibold text-gray-900 tabular-nums text-right whitespace-nowrap"><?php echo esc_html( $total ); ?></td>
					<td class="pr-3 py-4 align-middle whitespace-nowrap">
						<?php echo affwp_get_referral_status_badge( 'unpaid' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper returns trusted HTML. ?>
					</td>
					<td class="w-full p-0"></td>
					<td class="pl-3 pr-5 py-4 align-middle text-right text-gray-900 max-w-48 truncate" title="<?php echo esc_attr( $name . ' — ' . $count_label ); ?>">
						<?php echo esc_html( $name ); ?>
					</td>
				</tr>
			<?php endforeach; ?>
			</tbody>
		</table>
	</div>
	<?php
}

/**
 * Returns the number of distinct affiliates with at least one unpaid referral.
 *
 * Naive count: no date-range, minimum-payout, holding-period, or payout-method
 * filtering. The Preview Payout flow applies all those filters, so the Pay
 * Affiliates run can include fewer affiliates than this count suggests. Used
 * by the ready-to-pay empty state CTA to surface the unfiltered total; the
 * empty state copy intentionally says "owed across N affiliates" without
 * claiming all of them will be paid this run.
 *
 * If a caller needs the count that Preview would actually settle on, route
 * through `affwp_compute_eligible_payouts()` with the same filter set.
 *
 * @since 2.33.0
 *
 * @return int
 */
function affwp_payouts_get_unpaid_affiliate_count() {

	global $wpdb;

	$table = affiliate_wp()->referrals->table_name;

	$count = $wpdb->get_var(
		"SELECT COUNT(DISTINCT affiliate_id)
		 FROM {$table}
		 WHERE status = 'unpaid'"
	);

	return (int) $count;
}

/**
 * Returns the top affiliates by unpaid-referral total.
 *
 * @since 2.33.0
 *
 * @param int $limit Max number of affiliates to return.
 * @return array<int, array{affiliate_id: int, name: string, referral_count: int, total: string}>
 */
function affwp_payouts_get_top_unpaid_affiliates( $limit = 3 ) {

	global $wpdb;

	$limit = max( 1, (int) $limit );
	$table = affiliate_wp()->referrals->table_name;

	$results = $wpdb->get_results(
		$wpdb->prepare(
			"SELECT affiliate_id, COUNT(*) AS referral_count, SUM(amount) AS total_amount
			 FROM {$table}
			 WHERE status = 'unpaid'
			 GROUP BY affiliate_id
			 ORDER BY total_amount DESC
			 LIMIT %d",
			$limit
		)
	);

	if ( empty( $results ) ) {
		return array();
	}

	$rows = array();

	foreach ( $results as $row ) {
		$affiliate_id = (int) $row->affiliate_id;
		$name         = affwp_get_affiliate_name( $affiliate_id );

		if ( '' === $name ) {
			$name = affwp_get_affiliate_username( $affiliate_id );
		}

		if ( '' === $name ) {
			$name = sprintf( __( 'Affiliate #%d', 'affiliate-wp' ), $affiliate_id );
		}

		$rows[] = array(
			'affiliate_id'   => $affiliate_id,
			'name'           => $name,
			'referral_count' => (int) $row->referral_count,
			'total'          => affwp_currency_filter( affwp_format_amount( (float) $row->total_amount ) ),
		);
	}

	return $rows;
}
