<?php
/**
 * Delivery-log REST controller.
 *
 * @package RewindBrevoFormBridge
 */

namespace RewindBrevoFormBridge\Rest\Controllers;

use RewindBrevoFormBridge\Admin\Settings;
use RewindBrevoFormBridge\Api\Contacts;
use RewindBrevoFormBridge\Logs\LogRepository;
use RewindBrevoFormBridge\Rest\BaseController;
use RewindBrevoFormBridge\Support\Errors;

defined( 'ABSPATH' ) || exit;

/**
 * The Logs screen: filtered reads, one detail row, retry, and maintenance.
 *
 * A retry replays the payload stored on the log row. That payload is what
 * the Processor actually sent, minus nothing — the API key travels in a
 * header, never the body — so a replay is a faithful second attempt without
 * needing the original form submission back.
 *
 * @since 0.4.0
 */
class LogsController extends BaseController {

	/**
	 * Log repository.
	 *
	 * @var LogRepository
	 */
	private LogRepository $logs;

	/**
	 * Contacts resource, for replays.
	 *
	 * @var Contacts
	 */
	private Contacts $contacts;

	/**
	 * Settings storage service.
	 *
	 * @var Settings
	 */
	private Settings $settings;

	/**
	 * Constructor.
	 *
	 * @since 0.4.0
	 * @param LogRepository $logs     Log repository.
	 * @param Contacts      $contacts Contacts resource.
	 * @param Settings      $settings Settings storage service.
	 */
	public function __construct( LogRepository $logs, Contacts $contacts, Settings $settings ) {
		parent::__construct( 'logs' );

		$this->logs     = $logs;
		$this->contacts = $contacts;
		$this->settings = $settings;
	}

	/**
	 * Register routes.
	 *
	 * @since 0.4.0
	 * @return void
	 */
	public function register_routes(): void {
		register_rest_route(
			self::NAMESPACE_V1,
			'/' . $this->rest_base,
			array(
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_logs' ),
				'permission_callback' => array( $this, 'can_manage' ),
				'args'                => $this->filter_args(
					array(
						'page'     => array(
							'type'              => 'integer',
							'default'           => 1,
							'minimum'           => 1,
							'sanitize_callback' => 'absint',
						),
						'per_page' => array(
							'type'              => 'integer',
							'default'           => 20,
							'minimum'           => 1,
							'maximum'           => 100,
							'sanitize_callback' => 'absint',
						),
					)
				),
			)
		);

		register_rest_route(
			self::NAMESPACE_V1,
			'/' . $this->rest_base . '/stats',
			array(
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_stats' ),
				'permission_callback' => array( $this, 'can_manage' ),
				'args'                => array(
					'days' => array(
						'type'              => 'integer',
						'default'           => 30,
						'minimum'           => 1,
						'maximum'           => 365,
						'sanitize_callback' => 'absint',
					),
				),
			)
		);

		register_rest_route(
			self::NAMESPACE_V1,
			'/' . $this->rest_base . '/export',
			array(
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => array( $this, 'export' ),
				'permission_callback' => array( $this, 'can_manage' ),
				'args'                => $this->filter_args(),
			)
		);

		register_rest_route(
			self::NAMESPACE_V1,
			'/' . $this->rest_base . '/purge',
			array(
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'purge' ),
				'permission_callback' => array( $this, 'can_manage' ),
				'args'                => array(
					'days' => array(
						'type'              => 'integer',
						'required'          => true,
						'minimum'           => 1,
						'maximum'           => 3650,
						'sanitize_callback' => 'absint',
					),
				),
			)
		);

		register_rest_route(
			self::NAMESPACE_V1,
			'/' . $this->rest_base . '/retry-failed',
			array(
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'retry_failed' ),
				'permission_callback' => array( $this, 'can_manage' ),
				'args'                => $this->filter_args(
					array(
						'limit' => array(
							'type'              => 'integer',
							'default'           => 25,
							'minimum'           => 1,
							'maximum'           => 100,
							'sanitize_callback' => 'absint',
						),
					)
				),
			)
		);

		register_rest_route(
			self::NAMESPACE_V1,
			'/' . $this->rest_base . '/(?P<id>\d+)',
			array(
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_log' ),
				'permission_callback' => array( $this, 'can_manage' ),
				'args'                => $this->id_arg(),
			)
		);

		register_rest_route(
			self::NAMESPACE_V1,
			'/' . $this->rest_base . '/(?P<id>\d+)/retry',
			array(
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'retry' ),
				'permission_callback' => array( $this, 'can_manage' ),
				'args'                => $this->id_arg(),
			)
		);
	}

	/**
	 * The shared id path argument.
	 *
	 * @since 0.4.0
	 * @return array
	 */
	private function id_arg(): array {
		return array(
			'id' => array(
				'type'              => 'integer',
				'required'          => true,
				'minimum'           => 1,
				'sanitize_callback' => 'absint',
			),
		);
	}

	/**
	 * The filter arguments every log read shares.
	 *
	 * @since 0.4.0
	 * @param array $extra Additional arguments to merge in.
	 * @return array
	 */
	private function filter_args( array $extra = array() ): array {
		return array_merge(
			array(
				'status'    => array(
					'type'    => 'string',
					'default' => '',
					'enum'    => array_merge( array( '' ), LogRepository::STATUSES ),
				),
				'feed_id'   => array(
					'type'              => 'integer',
					'default'           => 0,
					'sanitize_callback' => 'absint',
				),
				'search'    => array(
					'type'              => 'string',
					'default'           => '',
					'sanitize_callback' => array( $this, 'sanitize_text' ),
				),
				'date_from' => array(
					'type'              => 'string',
					'default'           => '',
					'sanitize_callback' => array( $this, 'sanitize_text' ),
					'validate_callback' => array( $this, 'validate_date' ),
				),
				'date_to'   => array(
					'type'              => 'string',
					'default'           => '',
					'sanitize_callback' => array( $this, 'sanitize_text' ),
					'validate_callback' => array( $this, 'validate_date' ),
				),
			),
			$extra
		);
	}

	/**
	 * Pull the filter values out of a request.
	 *
	 * @since 0.4.0
	 * @param \WP_REST_Request $request Request.
	 * @return array
	 */
	private function filters( \WP_REST_Request $request ): array {
		return array(
			'status'    => (string) $request->get_param( 'status' ),
			'feed_id'   => (int) $request->get_param( 'feed_id' ),
			'search'    => (string) $request->get_param( 'search' ),
			'date_from' => (string) $request->get_param( 'date_from' ),
			'date_to'   => (string) $request->get_param( 'date_to' ),
		);
	}

	/**
	 * One filtered page of log rows.
	 *
	 * @since 0.4.0
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response
	 */
	public function get_logs( \WP_REST_Request $request ): \WP_REST_Response {
		$result = $this->logs->paginate(
			$this->filters( $request ),
			(int) $request->get_param( 'page' ),
			(int) $request->get_param( 'per_page' )
		);

		return $this->respond( $result );
	}

	/**
	 * One log row.
	 *
	 * @since 0.4.0
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function get_log( \WP_REST_Request $request ) {
		$row = $this->logs->find( (int) $request->get_param( 'id' ) );

		if ( null === $row ) {
			return $this->error( 'rbfb_log_not_found', __( 'That log entry no longer exists.', 'rewind-form-bridge-for-brevo' ), self::HTTP_NOT_FOUND );
		}

		return $this->respond( $row );
	}

	/**
	 * Dashboard counters.
	 *
	 * @since 0.4.0
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response
	 */
	public function get_stats( \WP_REST_Request $request ): \WP_REST_Response {
		$days = (int) $request->get_param( 'days' );

		return $this->respond(
			array(
				'days'  => $days,
				'stats' => $this->logs->stats( $days ),
			)
		);
	}

	/**
	 * Export the filtered rows as CSV text.
	 *
	 * Returned as a JSON string rather than a file download so the SPA can
	 * build the Blob itself and stay inside the authenticated REST call.
	 *
	 * @since 0.4.0
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response
	 */
	public function export( \WP_REST_Request $request ): \WP_REST_Response {
		$rows = $this->logs->export( $this->filters( $request ) );

		$columns = array( 'id', 'created_at', 'feed_title', 'email', 'status', 'http_code', 'error_code', 'attempts', 'message' );

		/*
		 * php://temp is an in-memory buffer, not a file on disk, so
		 * WP_Filesystem has nothing to do here. fputcsv() below is what
		 * makes this worth a stream at all: it handles the quoting and
		 * escaping rules that hand-built CSV consistently gets wrong.
		 */
		$handle = fopen( 'php://temp', 'r+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen

		fputcsv( $handle, $columns );

		foreach ( $rows as $row ) {
			$line = array();
			foreach ( $columns as $column ) {
				$line[] = isset( $row[ $column ] ) ? (string) $row[ $column ] : '';
			}
			fputcsv( $handle, $line );
		}

		rewind( $handle );
		$csv = (string) stream_get_contents( $handle );
		fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose

		return $this->respond(
			array(
				'csv'      => $csv,
				'rows'     => count( $rows ),
				'filename' => 'rbfb-delivery-log-' . gmdate( 'Y-m-d' ) . '.csv',
			)
		);
	}

	/**
	 * Delete log rows older than N days.
	 *
	 * @since 0.4.0
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response
	 */
	public function purge( \WP_REST_Request $request ): \WP_REST_Response {
		$deleted = $this->logs->purge_older_than( (int) $request->get_param( 'days' ) );

		return $this->respond(
			array(
				'deleted'   => $deleted,
				'remaining' => $this->logs->count(),
			)
		);
	}

	/**
	 * Retry one failed row.
	 *
	 * @since 0.4.0
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function retry( \WP_REST_Request $request ) {
		$row = $this->logs->find( (int) $request->get_param( 'id' ) );

		if ( null === $row ) {
			return $this->error( 'rbfb_log_not_found', __( 'That log entry no longer exists.', 'rewind-form-bridge-for-brevo' ), self::HTTP_NOT_FOUND );
		}

		$outcome = $this->replay( $row );

		if ( is_wp_error( $outcome ) ) {
			return $outcome;
		}

		return $this->respond( $outcome );
	}

	/**
	 * Retry every failed row matching the current filters, up to a limit.
	 *
	 * @since 0.4.0
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response
	 */
	public function retry_failed( \WP_REST_Request $request ): \WP_REST_Response {
		$ids = $this->logs->failed_ids( $this->filters( $request ), (int) $request->get_param( 'limit' ) );

		$succeeded = 0;
		$failed    = 0;
		$skipped   = 0;

		foreach ( $ids as $id ) {
			$row = $this->logs->find( $id );

			if ( null === $row ) {
				continue;
			}

			$outcome = $this->replay( $row );

			if ( is_wp_error( $outcome ) ) {
				++$skipped;
				continue;
			}

			if ( 'success' === $outcome['status'] ) {
				++$succeeded;
			} else {
				++$failed;
			}
		}

		return $this->respond(
			array(
				'attempted' => count( $ids ),
				'succeeded' => $succeeded,
				'failed'    => $failed,
				'skipped'   => $skipped,
			)
		);
	}

	/**
	 * Replay one log row's stored payload and record the outcome.
	 *
	 * @since 0.4.0
	 * @param array $row Shaped log row.
	 * @return array|\WP_Error The refreshed row, or why it could not be replayed.
	 */
	private function replay( array $row ) {
		if ( '' === $this->settings->api_key() ) {
			return $this->error(
				'rbfb_no_key',
				__( 'Add your Brevo API key before retrying deliveries.', 'rewind-form-bridge-for-brevo' )
			);
		}

		$payload = json_decode( $row['request'], true );

		if ( ! is_array( $payload ) || empty( $payload['email'] ) ) {
			return $this->error(
				'rbfb_not_replayable',
				__( 'This entry has no stored payload to resend — it failed before a request was built.', 'rewind-form-bridge-for-brevo' )
			);
		}

		$response = $this->contacts->upsert( $payload );

		$status  = $response->is_success() ? 'success' : 'failed';
		$message = $response->is_success()
			? __( 'Resent from the delivery log.', 'rewind-form-bridge-for-brevo' )
			: Errors::translate( $response );

		$this->logs->record_attempt(
			$row['id'],
			$status,
			$response->status(),
			$response->error_code(),
			$message,
			$response->raw_body()
		);

		$refreshed = $this->logs->find( $row['id'] );

		return null !== $refreshed ? $refreshed : $row;
	}
}
