<?php
/**
 * Auto-purge (data retention).
 *
 * Optionally deletes leads older than a configured number of days on a
 * daily WP-Cron schedule. Deletion re-uses LeadsRepository::delete(), so
 * the full cascade applies: submitted fields, notes, activity log, tag
 * relations and physical attachment files.
 *
 * Complements the GDPR tools: erasure handles individual data subjects,
 * auto-purge implements a site-wide retention policy.
 *
 * @package Leadite
 */

namespace Leadite\Support;

use Leadite\Config;
use Leadite\Storage\LeadsRepository;

defined( 'ABSPATH' ) || exit;

class AutoPurge {

	/**
	 * Cron hook name.
	 */
	const HOOK = 'leadite_autopurge_daily';

	/**
	 * Maximum leads deleted per run (a follow-up run is scheduled if more remain).
	 */
	const BATCH = 200;

	/**
	 * Safety floor for the retention window, in days.
	 */
	const MIN_DAYS = 7;

	/**
	 * Hook everything up. Called once from Plugin::boot().
	 *
	 * @return void
	 */
	public static function register() {
		add_action( self::HOOK, array( __CLASS__, 'run' ) );
		// Self-heal: keep the schedule in sync with the setting on every load.
		add_action( 'init', array( __CLASS__, 'sync_schedule' ) );
	}

	/**
	 * (Un)schedule the daily event according to the current setting.
	 *
	 * Cheap: one wp_next_scheduled() lookup per request.
	 *
	 * @return void
	 */
	public static function sync_schedule() {
		$enabled = ! empty( Config::get( 'autopurge_enabled', false ) );
		$next    = wp_next_scheduled( self::HOOK );

		if ( $enabled && ! $next ) {
			// First run in ~1 hour, then daily.
			wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::HOOK );
		} elseif ( ! $enabled && $next ) {
			wp_clear_scheduled_hook( self::HOOK );
		}
	}

	/**
	 * Cron callback: delete one batch of expired leads.
	 *
	 * @param int|null $override_days Optional explicit retention window (used by WP-CLI).
	 * @param bool|null $override_only_closed Optional explicit scope (used by WP-CLI).
	 * @return int Number of leads deleted in this run.
	 */
	public static function run( $override_days = null, $override_only_closed = null ) {
		if ( null === $override_days && empty( Config::get( 'autopurge_enabled', false ) ) ) {
			return 0;
		}

		$days = null !== $override_days
			? (int) $override_days
			: (int) Config::get( 'autopurge_days', 365 );
		$days = max( self::MIN_DAYS, $days );

		$only_closed = null !== $override_only_closed
			? (bool) $override_only_closed
			: ! empty( Config::get( 'autopurge_only_closed', true ) );

		$ids     = self::find_expired_ids( $days, $only_closed, self::BATCH );
		$deleted = 0;
		$start   = time();

		foreach ( $ids as $id ) {
			if ( LeadsRepository::delete( $id ) ) {
				$deleted++;
			}
			// Time guard: never run longer than ~20 seconds per invocation.
			if ( ( time() - $start ) > 20 ) {
				break;
			}
		}

		update_option(
			'leadite_autopurge_last',
			array(
				'time'    => time(),
				'deleted' => $deleted,
			),
			false
		);

		// Backlog larger than one batch: continue shortly instead of waiting a day.
		if ( count( $ids ) === self::BATCH && $deleted > 0 ) {
			wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, self::HOOK );
		}

		return $deleted;
	}

	/**
	 * IDs of leads older than the retention window.
	 *
	 * @param int  $days        Retention window in days.
	 * @param bool $only_closed Restrict to Closed & Spam statuses.
	 * @param int  $limit       Max IDs to return.
	 * @return int[]
	 */
	public static function find_expired_ids( $days, $only_closed, $limit ) {
		global $wpdb;

		// created_at is stored via current_time( 'mysql' ) — site-local time.
		$cutoff = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - ( max( self::MIN_DAYS, (int) $days ) * DAY_IN_SECONDS ) );

		if ( $only_closed ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$ids = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT id FROM %i WHERE created_at < %s AND status IN ('closed','spam') ORDER BY id ASC LIMIT %d",
					LeadsRepository::table(),
					$cutoff,
					(int) $limit
				)
			);
		} else {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$ids = $wpdb->get_col(
				$wpdb->prepare(
					'SELECT id FROM %i WHERE created_at < %s ORDER BY id ASC LIMIT %d',
					LeadsRepository::table(),
					$cutoff,
					(int) $limit
				)
			);
		}

		return array_map( 'intval', (array) $ids );
	}

	/**
	 * Count of leads that WOULD be purged right now (settings UI / CLI dry-run).
	 *
	 * @param int  $days        Retention window in days.
	 * @param bool $only_closed Restrict to Closed & Spam statuses.
	 * @return int
	 */
	public static function count_expired( $days, $only_closed ) {
		global $wpdb;

		$cutoff = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - ( max( self::MIN_DAYS, (int) $days ) * DAY_IN_SECONDS ) );

		if ( $only_closed ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			return (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM %i WHERE created_at < %s AND status IN ('closed','spam')",
					LeadsRepository::table(),
					$cutoff
				)
			);
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				'SELECT COUNT(*) FROM %i WHERE created_at < %s',
				LeadsRepository::table(),
				$cutoff
			)
		);
	}
}