<?php
/**
 * Subscriber CRUD with secure unsubscribe tokens (RGPD-compliant).
 *
 * @package SuiteMkt3k
 */

namespace SuiteMkt3k\Modules\Subscribers;

use SuiteMkt3k\Installer;
use SuiteMkt3k\Security\Rate_Limiter;
use SuiteMkt3k\Utils\Logger;
use SuiteMkt3k\Utils\Sanitizer;
use SuiteMkt3k\Utils\Validator;

defined( 'ABSPATH' ) || exit;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching

final class Subscriber_Manager {

	public const STATUS_ACTIVE       = 'active';
	public const STATUS_UNSUBSCRIBED = 'unsubscribed';
	public const STATUS_BOUNCED      = 'bounced';

	public static function register(): void {
		add_action( 'admin_post_suitemkt3k_subscriber_save', array( __CLASS__, 'handle_save' ) );
		add_action( 'admin_post_suitemkt3k_subscriber_delete', array( __CLASS__, 'handle_delete' ) );
		add_action( 'admin_post_suitemkt3k_subscribers_export', array( __CLASS__, 'handle_export' ) );
	}

	public static function table(): string {
		return Installer::table( 'subscribers' );
	}

	/**
	 * Sources allowed for the source_type column.
	 */
	public static function sources(): array {
		return array(
			'manual'           => __( 'Manual', 'suitemkt3k' ),
			'coupon_popup'     => __( 'Coupon popup', 'suitemkt3k' ),
			'newsletter_form'  => __( 'Newsletter form', 'suitemkt3k' ),
			'cart_recovery'    => __( 'Cart recovery', 'suitemkt3k' ),
		);
	}

	public static function statuses(): array {
		return array(
			self::STATUS_ACTIVE       => __( 'Active', 'suitemkt3k' ),
			self::STATUS_UNSUBSCRIBED => __( 'Unsubscribed', 'suitemkt3k' ),
			self::STATUS_BOUNCED      => __( 'Bounced', 'suitemkt3k' ),
		);
	}

	/**
	 * Human-readable source label for subscribers table/export UI.
	 */
	public static function source_label( array $row ): string {
		$source_type = (string) ( $row['source_type'] ?? '' );
		$campaign    = (string) ( $row['source_campaign_name'] ?? '' );

		if ( 'cart_recovery' === $source_type ) {
			if ( '' !== $campaign ) {
				return $campaign;
			}
			return __( 'Abandoned cart', 'suitemkt3k' );
		}
		if ( 'coupon_popup' === $source_type ) {
			if ( '' !== $campaign ) {
				return sprintf(
					/* translators: %s coupon campaign title */
					__( 'Coupon: %s', 'suitemkt3k' ),
					$campaign
				);
			}
			return __( 'Coupon popup', 'suitemkt3k' );
		}
		if ( '' !== $campaign ) {
			return $campaign;
		}
		$sources = self::sources();
		return $sources[ $source_type ] ?? $source_type;
	}

	/**
	 * Insert or update subscriber by email. Returns subscriber row.
	 *
	 * @param array $args { name, email, birth_date, source_type, source_campaign_id, source_campaign_name, locale, ip }
	 * @return array|null
	 */
	public static function upsert( array $args ): ?array {
		global $wpdb;
		$email = Sanitizer::email( $args['email'] ?? '' );
		if ( ! Validator::is_valid_email( $email ) ) {
			return null;
		}

		$table = self::table();
		$existing = self::get_by_email( $email );

		$payload = array(
			'name'                 => Sanitizer::text( $args['name'] ?? '', 190 ),
			'email'                => $email,
			'birth_date'           => Sanitizer::date( $args['birth_date'] ?? '' ),
			'source_type'          => Sanitizer::slug( $args['source_type'] ?? 'manual', 40 ),
			'source_campaign_id'   => isset( $args['source_campaign_id'] ) ? Sanitizer::int( $args['source_campaign_id'], 0 ) : null,
			'source_campaign_name' => Sanitizer::text( $args['source_campaign_name'] ?? '', 190 ),
			'status'               => self::STATUS_ACTIVE,
			'locale'               => Sanitizer::text( $args['locale'] ?? '', 10 ),
			'ip_hash'              => isset( $args['ip'] ) ? hash( 'sha256', (string) $args['ip'] . '|' . get_option( 'suitemkt3k_secret_key' ) ) : null,
			'consent_at'           => current_time( 'mysql', true ),
		);

		if ( $existing ) {
			$update = array_filter(
				$payload,
				static fn( $v ) => null !== $v && '' !== $v
			);
			$update['unsubscribed_at'] = null;
			$wpdb->update(
				$table,
				$update,
				array( 'id' => (int) $existing['id'] ),
				null,
				array( '%d' )
			);
			return self::get( (int) $existing['id'] );
		}

		$payload['unsubscribe_token'] = self::generate_token();
		$payload['subscribed_at']     = current_time( 'mysql', true );

		$wpdb->insert( $table, $payload );
		$id = (int) $wpdb->insert_id;
		Logger::info( 'subscribers', 'Inserted', array( 'id' => $id, 'source' => $payload['source_type'] ) );
		return self::get( $id );
	}

	public static function get( int $id ): ?array {
		global $wpdb;
		$row = $wpdb->get_row(
			$wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', self::table(), $id ),
			ARRAY_A
		);
		return $row ?: null;
	}

	public static function get_by_email( string $email ): ?array {
		global $wpdb;
		$row = $wpdb->get_row(
			$wpdb->prepare( 'SELECT * FROM %i WHERE email = %s', self::table(), $email ),
			ARRAY_A
		);
		return $row ?: null;
	}

	public static function get_by_token( string $token ): ?array {
		global $wpdb;
		if ( strlen( $token ) !== 64 || ! ctype_xdigit( $token ) ) {
			return null;
		}
		$row = $wpdb->get_row(
			$wpdb->prepare( 'SELECT * FROM %i WHERE unsubscribe_token = %s', self::table(), $token ),
			ARRAY_A
		);
		return $row ?: null;
	}

	public static function update( int $id, array $fields ): bool {
		global $wpdb;
		$allowed = array( 'name', 'email', 'birth_date', 'status', 'source_campaign_name' );
		$update  = array();
		foreach ( $allowed as $key ) {
			if ( array_key_exists( $key, $fields ) ) {
				switch ( $key ) {
					case 'email':
						$value = Sanitizer::email( $fields[ $key ] );
						if ( ! Validator::is_valid_email( $value ) ) { return false; }
						$update[ $key ] = $value;
						break;
					case 'birth_date':
						$update[ $key ] = Sanitizer::date( $fields[ $key ] );
						break;
					case 'status':
						$status = Sanitizer::slug( $fields[ $key ] );
						if ( ! Validator::is_in( $status, array_keys( self::statuses() ) ) ) { return false; }
						$update[ $key ] = $status;
						break;
					default:
						$update[ $key ] = Sanitizer::text( $fields[ $key ], 190 );
				}
			}
		}
		if ( ! $update ) {
			return false;
		}
		return false !== $wpdb->update( self::table(), $update, array( 'id' => $id ), null, array( '%d' ) );
	}

	public static function delete( int $id ): bool {
		global $wpdb;
		return false !== $wpdb->delete( self::table(), array( 'id' => $id ), array( '%d' ) );
	}

	public static function unsubscribe_by_token( string $token ): bool {
		$subscriber = self::get_by_token( $token );
		if ( ! $subscriber ) {
			return false;
		}
		global $wpdb;
		$ok = false !== $wpdb->update(
			self::table(),
			array(
				'status'          => self::STATUS_UNSUBSCRIBED,
				'unsubscribed_at' => current_time( 'mysql', true ),
			),
			array( 'id' => (int) $subscriber['id'] ),
			array( '%s', '%s' ),
			array( '%d' )
		);
		if ( $ok ) {
			Logger::info( 'subscribers', 'Unsubscribed via token', array( 'id' => (int) $subscriber['id'] ) );
		}
		return $ok;
	}

	public static function generate_token(): string {
		try {
			return bin2hex( random_bytes( 32 ) );
		} catch ( \Throwable $e ) {
			return hash( 'sha256', wp_generate_password( 64, true, true ) . microtime( true ) );
		}
	}

	public static function unsubscribe_url( string $token ): string {
		$token = preg_replace( '/[^a-f0-9]/', '', strtolower( $token ) );
		if ( strlen( $token ) !== 64 ) {
			return home_url( '/' );
		}
		if ( get_option( 'permalink_structure' ) ) {
			return home_url( user_trailingslashit( 'b/' . $token ) );
		}
		return add_query_arg( 'wmk_baja', $token, home_url( '/' ) );
	}

	public static function query( array $args ): array {
		global $wpdb;
		$paged    = max( 1, (int) ( $args['paged'] ?? 1 ) );
		$per_page = max( 1, min( 200, (int) ( $args['per_page'] ?? 20 ) ) );
		$offset   = ( $paged - 1 ) * $per_page;
		$search_like = '';
		if ( ! empty( $args['search'] ) ) {
			$search_like = '%' . $wpdb->esc_like( (string) $args['search'] ) . '%';
		}
		$source = ! empty( $args['source'] ) ? (string) $args['source'] : '';
		$status = ! empty( $args['status'] ) ? (string) $args['status'] : '';

		$total = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM %i
				WHERE (%s = '' OR name LIKE %s OR email LIKE %s)
				AND (%s = '' OR source_type = %s)
				AND (%s = '' OR status = %s)",
				self::table(),
				$search_like,
				$search_like,
				$search_like,
				$source,
				$source,
				$status,
				$status
			)
		);
		$rows  = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM %i
				WHERE (%s = '' OR name LIKE %s OR email LIKE %s)
				AND (%s = '' OR source_type = %s)
				AND (%s = '' OR status = %s)
				ORDER BY subscribed_at DESC LIMIT %d OFFSET %d",
				self::table(),
				$search_like,
				$search_like,
				$search_like,
				$source,
				$source,
				$status,
				$status,
				$per_page,
				$offset
			),
			ARRAY_A
		);
		return array(
			'rows'     => is_array( $rows ) ? $rows : array(),
			'total'    => $total,
			'paged'    => $paged,
			'per_page' => $per_page,
			'pages'    => (int) ceil( $total / $per_page ),
		);
	}

	public static function handle_save(): void {
		\SuiteMkt3k\Security\Capabilities::ensure( \SuiteMkt3k\Security\Capabilities::EDIT_CAMPAIGN );
		check_admin_referer( \SuiteMkt3k\Security\Nonces::ADMIN_ACTION, '_wpnonce' );

		$id     = max( 0, (int) \SuiteMkt3k\Utils\Sanitizer::post( 'id', 'int' ) );
		$name   = \SuiteMkt3k\Utils\Sanitizer::text( \SuiteMkt3k\Utils\Sanitizer::post( 'name', 'text' ), 190 );
		$email  = \SuiteMkt3k\Utils\Sanitizer::email( \SuiteMkt3k\Utils\Sanitizer::post( 'email', 'email' ) );
		$birth  = \SuiteMkt3k\Utils\Sanitizer::date( \SuiteMkt3k\Utils\Sanitizer::post( 'birth_date', 'text' ) ) ?? '';
		$status = \SuiteMkt3k\Utils\Sanitizer::slug( \SuiteMkt3k\Utils\Sanitizer::post( 'status', 'key' ) );
		if ( '' === $status ) {
			$status = self::STATUS_ACTIVE;
		}
		if ( $id > 0 ) {
			self::update(
				$id,
				array(
					'name'       => $name,
					'email'      => $email,
					'birth_date' => $birth,
					'status'     => $status,
				)
			);
		} else {
			self::upsert(
				array(
					'name'        => $name,
					'email'       => $email,
					'birth_date'  => $birth,
					'source_type' => 'manual',
				)
			);
		}

		wp_safe_redirect( admin_url( 'admin.php?page=suitemkt3k-subscribers&updated=1' ) );
		exit;
	}

	public static function handle_delete(): void {
		\SuiteMkt3k\Security\Capabilities::ensure( \SuiteMkt3k\Security\Capabilities::EDIT_CAMPAIGN );
		check_admin_referer( \SuiteMkt3k\Security\Nonces::ADMIN_ACTION, '_wpnonce' );
		$id_raw = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT );
		$id     = max( 0, (int) $id_raw );
		if ( $id ) {
			self::delete( $id );
		}
		wp_safe_redirect( admin_url( 'admin.php?page=suitemkt3k-subscribers&deleted=1' ) );
		exit;
	}

	public static function handle_export(): void {
		\SuiteMkt3k\Security\Capabilities::ensure( \SuiteMkt3k\Security\Capabilities::EDIT_CAMPAIGN );
		check_admin_referer( \SuiteMkt3k\Security\Nonces::ADMIN_ACTION, '_wpnonce' );

		nocache_headers();
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="suitemkt3k-subscribers-' . gmdate( 'Y-m-d' ) . '.csv"' );

		$out = fopen( 'php://output', 'w' );
		fputcsv( $out, array( 'id', 'name', 'email', 'birth_date', 'source_type', 'source_campaign_name', 'status', 'subscribed_at' ) );

		global $wpdb;
		$rows = $wpdb->get_results( $wpdb->prepare( 'SELECT id,name,email,birth_date,source_type,source_campaign_name,status,subscribed_at FROM %i ORDER BY id DESC', self::table() ), ARRAY_A );
		foreach ( (array) $rows as $row ) {
			fputcsv( $out, $row );
		}
		if ( is_resource( $out ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
			fclose( $out );
		}
		exit;
	}
}
