<?php
/**
 * REST API exposure for DataStream ingested data.
 *
 * @package DataStream
 */

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

/**
 * Class DataStream_API
 */
class DataStream_API {

	/**
	 * REST namespace.
	 */
	private const NAMESPACE = 'datastream/v1';

	/**
	 * DAO instance.
	 *
	 * @var DataStream_DAO
	 */
	private $dao;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->dao = new DataStream_DAO();
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Register REST routes.
	 */
	public function register_routes() {
		register_rest_route(
			self::NAMESPACE,
			'/ingested-data',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
				'args'                => array(
					'limit' => array(
						'default'           => 50,
						'sanitize_callback' => 'absint',
						'validate_callback' => function ( $value ) {
							return $value >= 1 && $value <= 100;
						},
					),
					'stream_type' => array(
						'default'           => '',
						'sanitize_callback' => 'sanitize_key',
					),
				),
			)
		);

		register_rest_route(
			self::NAMESPACE,
			'/ingested-data/(?P<external_id>[a-zA-Z0-9_.-]+)',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
				'args'                => array(
					'external_id' => array(
						'required'          => true,
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			)
		);

		register_rest_route(
			self::NAMESPACE,
			'/status',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_status' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
			)
		);

		register_rest_route(
			self::NAMESPACE,
			'/health',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_health' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
			)
		);

		register_rest_route(
			self::NAMESPACE,
			'/sync',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'trigger_sync' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
				'args'                => array(
					'stream' => array(
						'default'           => '',
						'sanitize_callback' => 'sanitize_key',
					),
				),
			)
		);

		register_rest_route(
			self::NAMESPACE,
			'/streams',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_streams' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
			)
		);

		register_rest_route(
			self::NAMESPACE,
			'/preview',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'preview_stream' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
				'args'                => array(
					'stream' => array(
						'default'           => '',
						'sanitize_callback' => 'sanitize_key',
					),
					'url'    => array(
						'default'           => '',
						'sanitize_callback' => 'esc_url_raw',
					),
				),
			)
		);

		register_rest_route(
			self::NAMESPACE,
			'/detect',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'detect' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
				'args'                => array(
					'url' => array(
						'required'          => true,
						'sanitize_callback' => 'esc_url_raw',
					),
					'key' => array(
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			)
		);

		register_rest_route(
			self::NAMESPACE,
			'/wizard-create',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'wizard_create' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
			)
		);

		register_rest_route(
			self::NAMESPACE,
			'/fields',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_fields' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
				'args'                => array(
					'stream' => array(
						'default'           => '',
						'sanitize_callback' => 'sanitize_key',
					),
				),
			)
		);

		register_rest_route(
			self::NAMESPACE,
			'/test-connection',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'test_connection' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
				'args'                => array(
					'url' => array(
						'default'           => '',
						'sanitize_callback' => 'esc_url_raw',
					),
				),
			)
		);

		register_rest_route(
			self::NAMESPACE,
			'/tools/(?P<action>[a-z-]+)',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'run_tool' ),
				'permission_callback' => array( $this, 'can_manage_data' ),
				'args'                => array(
					'action' => array(
						'required'          => true,
						'sanitize_callback' => 'sanitize_key',
					),
					'confirm' => array(
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'stream_type' => array(
						'default'           => '',
						'sanitize_callback' => 'sanitize_key',
					),
				),
			)
		);
	}

	/**
	 * Permission check: administrators only.
	 *
	 * @return bool
	 */
	public function can_manage_data() {
		return current_user_can( 'manage_options' );
	}

	/**
	 * Retrieve and serialize recent records.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return WP_REST_Response
	 */
	public function get_items( WP_REST_Request $request ) {
		$limit       = (int) $request->get_param( 'limit' );
		$stream_type = (string) $request->get_param( 'stream_type' );
		$results     = $this->dao->get_recent_records( $limit, $stream_type );
		$serialized  = array_map( array( $this, 'serialize_record' ), $results );

		return rest_ensure_response(
			array(
				'records' => $serialized,
				'total'   => count( $serialized ),
			)
		);
	}

	/**
	 * Retrieve a single record by external ID.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_item( WP_REST_Request $request ) {
		$external_id = $request->get_param( 'external_id' );
		$data        = DataStream_Provider::get_data( $external_id );

		if ( null === $data ) {
			return new WP_Error(
				'not_found',
				__( 'Record not found.', 'datastream-api-sync' ),
				array( 'status' => 404 )
			);
		}

		return rest_ensure_response(
			array(
				'external_id' => sanitize_text_field( $external_id ),
				'data'        => $data,
			)
		);
	}

	/**
	 * Return engine status for the admin dashboard.
	 *
	 * @return WP_REST_Response
	 */
	public function get_status() {
		$health = DataStream_Health::get_report();

		return rest_ensure_response(
			array(
				'record_count'     => $health['record_count'],
				'stream_counts'    => $health['stream_counts'],
				'last_sync'          => $health['last_sync'],
				'last_sync_result'   => $health['last_sync_result'],
				'next_sync'          => $health['next_sync'],
				'next_sync_unix'     => $health['next_sync_unix'],
				'api_url'            => esc_url_raw( (string) DataStream_Config::get( 'api_url' ) ),
				'cron_schedule'      => $health['cron_schedule'],
				'cron_enabled'       => $health['cron_enabled'],
				'logging_enabled'    => (bool) DataStream_Config::get( 'enable_logging' ),
				'healthy'            => $health['healthy'],
				'warnings'           => $health['warnings'],
			)
		);
	}

	/**
	 * Full health diagnostics.
	 *
	 * @return WP_REST_Response
	 */
	public function get_health() {
		return rest_ensure_response( DataStream_Health::get_report() );
	}

	/**
	 * Trigger a manual ingestion sync.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return WP_REST_Response
	 */
	public function trigger_sync( WP_REST_Request $request ) {
		$stream = (string) $request->get_param( 'stream' );
		$result = DataStream_Engine::get_instance()->run_ingestion_sync( true, $stream );
		return rest_ensure_response( $result );
	}

	/**
	 * List streams with secrets masked.
	 *
	 * @return WP_REST_Response
	 */
	public function get_streams() {
		$streams = array();

		foreach ( DataStream_Streams::get_streams() as $stream ) {
			$stream['api_bearer_token'] = '' !== (string) $stream['api_bearer_token'] ? '********' : '';
			$stream['api_basic_pass']   = '' !== (string) $stream['api_basic_pass'] ? '********' : '';
			$stream['api_key_value']    = '' !== (string) $stream['api_key_value'] ? '********' : '';
			$streams[]                  = $stream;
		}

		return rest_ensure_response(
			array(
				'streams'     => $streams,
				'max_enabled' => DataStream_Feature_Gate::max_enabled_streams(),
				'premium'     => DataStream_Feature_Gate::is_premium(),
			)
		);
	}

	/**
	 * Fetch one page of a stream (or ad-hoc URL) without persisting, and
	 * report what was found: item count, detected fields, a sample record.
	 * This is the "does my config work?" tool for non-technical setup.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return WP_REST_Response
	 */
	public function preview_stream( WP_REST_Request $request ) {
		$stream_id = (string) $request->get_param( 'stream' );
		$url       = (string) $request->get_param( 'url' );
		$result    = DataStream_Engine::get_instance()->test_connection( $url, $stream_id );

		$sample = isset( $result['sample'] ) && is_array( $result['sample'] ) ? $result['sample'] : null;

		$result['fields'] = null !== $sample ? $this->detect_fields( $sample ) : array();

		return rest_ensure_response( $result );
	}

	/**
	 * Wizard step 1: fetch a URL, run every detector, optionally probe where
	 * a pasted key belongs. Nothing is persisted.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return WP_REST_Response
	 */
	public function detect( WP_REST_Request $request ) {
		$url     = (string) $request->get_param( 'url' );
		$key     = (string) $request->get_param( 'key' );
		$started = microtime( true );

		$requester = static function ( $request_url, $headers = array() ) {
			return wp_remote_get(
				$request_url,
				array(
					'timeout'             => DataStream_Config::sanitize_timeout( DataStream_Config::get( 'api_timeout' ) ),
					'redirection'         => 3,
					'sslverify'           => (bool) DataStream_Config::get( 'api_ssl_verify' ),
					'headers'             => $headers,
					'reject_unsafe_urls'  => DataStream_Config::reject_unsafe_urls(),
					'limit_response_size' => DataStream_Config::max_response_bytes(),
				)
			);
		};

		$auth_patch    = array();
		$auth_evidence = '';
		$response      = $requester( $url );

		if ( is_wp_error( $response ) ) {
			return rest_ensure_response(
				array(
					'success' => false,
					'message' => $response->get_error_message(),
				)
			);
		}

		$status = (int) wp_remote_retrieve_response_code( $response );

		if ( in_array( $status, array( 401, 403 ), true ) ) {
			if ( '' === $key ) {
				return rest_ensure_response(
					array(
						'success'    => false,
						'needs_auth' => true,
						'status'     => $status,
						'message'    => __( 'This API wants a key. Paste it below and DataStream will figure out where it goes.', 'datastream-api-sync' ),
					)
				);
			}

			$probe = DataStream_Detector::probe_auth( $url, $key, $requester );

			if ( null === $probe ) {
				return rest_ensure_response(
					array(
						'success'    => false,
						'needs_auth' => true,
						'status'     => $status,
						'message'    => __( 'The key did not work in any of the usual places (Bearer, X-Api-Key, api_key/apikey/key parameter). Check the key, or use the Advanced form for unusual auth schemes.', 'datastream-api-sync' ),
					)
				);
			}

			$auth_patch    = $probe['patch'];
			$auth_evidence = $probe['evidence'];

			if ( isset( $auth_patch['api_key_param'] ) ) {
				$probe_url = $url . ( false === strpos( $url, '?' ) ? '?' : '&' ) . $auth_patch['api_key_param'] . '=' . rawurlencode( $key );
				$response  = $requester( $probe_url );
			} elseif ( isset( $auth_patch['api_bearer_token'] ) ) {
				$response = $requester( $url, array( 'Authorization' => 'Bearer ' . $key ) );
			} else {
				$response = $requester( $url, array( 'X-Api-Key' => $key ) );
			}

			$status = (int) wp_remote_retrieve_response_code( $response );
		}

		if ( $status < 200 || $status >= 300 ) {
			$body_hint = trim( (string) substr( (string) wp_remote_retrieve_body( $response ), 0, 200 ) );

			return rest_ensure_response(
				array(
					'success' => false,
					'status'  => $status,
					'message' => sprintf(
						/* translators: %d: HTTP status code */
						__( 'The site answered with HTTP %d, not data.', 'datastream-api-sync' ),
						$status
					),
					'body_hint' => $body_hint,
				)
			);
		}

		$body         = trim( (string) wp_remote_retrieve_body( $response ) );
		$content_type = wp_remote_retrieve_header( $response, 'content-type' );
		$content_type = is_array( $content_type ) ? (string) reset( $content_type ) : (string) $content_type;
		$parsed       = DataStream_Parser::parse( $body, $content_type );

		if ( null === $parsed ) {
			return rest_ensure_response(
				array(
					'success' => false,
					'status'  => $status,
					'message' => __( 'The site answered, but DataStream could not read it as JSON, XML, RSS, or CSV.', 'datastream-api-sync' ),
				)
			);
		}

		$data        = $parsed['data'];
		$headers_obj = wp_remote_retrieve_headers( $response );
		$headers     = is_object( $headers_obj ) && method_exists( $headers_obj, 'getAll' ) ? $headers_obj->getAll() : (array) $headers_obj;
		$detection   = DataStream_Detector::detect( $data, $headers );

		$detection['format'] = $parsed['format'];

		if ( 'json' !== $parsed['format'] ) {
			array_unshift( $detection['evidence'], sprintf(
				/* translators: %s: detected format name */
				__( 'Read the response as %s.', 'datastream-api-sync' ),
				strtoupper( $parsed['format'] )
			) );
		}

		if ( '' !== $auth_evidence ) {
			array_unshift( $detection['evidence'], $auth_evidence );
		}

		$detection['success']     = true;
		$detection['status']      = $status;
		$detection['auth_patch']  = $auth_patch;
		$detection['duration_ms'] = (int) round( ( microtime( true ) - $started ) * 1000 );

		return rest_ensure_response( $detection );
	}

	/**
	 * Wizard step 3: persist the stream and optionally create a draft page
	 * carrying the configured block.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function wizard_create( WP_REST_Request $request ) {
		$stream_input = $request->get_param( 'stream' );

		if ( ! is_array( $stream_input ) ) {
			return new WP_Error( 'invalid_stream', __( 'Stream configuration missing.', 'datastream-api-sync' ), array( 'status' => 400 ) );
		}

		$saved = DataStream_Streams::save_stream( $stream_input );

		if ( is_wp_error( $saved ) ) {
			$saved->add_data( array( 'status' => 400 ) );
			return $saved;
		}

		// First sync right away so the page has data the moment it exists.
		$sync = DataStream_Engine::get_instance()->run_ingestion_sync( true, $saved['id'] );

		$page_id  = 0;
		$fields   = sanitize_text_field( (string) $request->get_param( 'fields' ) );
		$template = sanitize_key( (string) $request->get_param( 'template' ) ) ?: 'list';

		if ( $request->get_param( 'create_page' ) ) {
			$block = sprintf(
				'<!-- wp:datastream/list {"stream":"%s","template":"%s","limit":10,"fields":"%s"} /-->',
				esc_attr( $saved['id'] ),
				esc_attr( $template ),
				esc_attr( $fields )
			);

			$page_id = wp_insert_post(
				array(
					'post_type'    => 'page',
					'post_status'  => 'draft',
					'post_title'   => '' !== $saved['label'] ? $saved['label'] : $saved['id'],
					'post_content' => $block,
				)
			);
		}

		return rest_ensure_response(
			array(
				'success'   => true,
				'stream'    => $saved['id'],
				'processed' => (int) ( $sync['processed'] ?? 0 ),
				'shortcode' => sprintf(
					'[datastream_list stream="%s" template="%s" fields="%s"]',
					$saved['id'],
					$template,
					$fields
				),
				'page_id'   => (int) $page_id,
				'page_edit' => $page_id ? admin_url( 'post.php?post=' . (int) $page_id . '&action=edit' ) : '',
			)
		);
	}

	/**
	 * Typed field paths for a stream, computed from records already synced —
	 * feeds the block editor's visual field picker without any external call.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return WP_REST_Response
	 */
	public function get_fields( WP_REST_Request $request ) {
		$stream  = (string) $request->get_param( 'stream' );
		$records = DataStream_Renderer::get_records(
			array(
				'stream' => $stream,
				'limit'  => 20,
			)
		);

		$items = array();

		foreach ( $records as $record ) {
			$items[] = $record['data'];
		}

		$typed = DataStream_Detector::type_fields( $items );
		$out   = array();

		foreach ( $typed as $path => $info ) {
			$out[] = array(
				'path' => $path,
				'type' => $info['type'],
			);
		}

		return rest_ensure_response( array( 'fields' => $out ) );
	}

	/**
	 * Detect display-usable field paths from a sample record: top-level
	 * scalars plus one level of nested scalars as dot paths.
	 *
	 * @param array<string, mixed> $sample Sample item.
	 * @return array<int, string>
	 */
	private function detect_fields( $sample ) {
		$fields = array();

		foreach ( $sample as $key => $value ) {
			if ( is_scalar( $value ) || null === $value ) {
				$fields[] = (string) $key;
			} elseif ( is_array( $value ) ) {
				foreach ( $value as $sub_key => $sub_value ) {
					if ( ( is_scalar( $sub_value ) || null === $sub_value ) && ! is_int( $sub_key ) ) {
						$fields[] = $key . '.' . $sub_key;
					}

					if ( count( $fields ) >= 40 ) {
						break;
					}
				}
			}

			if ( count( $fields ) >= 40 ) {
				break;
			}
		}

		return $fields;
	}

	/**
	 * Test remote API connectivity.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return WP_REST_Response
	 */
	public function test_connection( WP_REST_Request $request ) {
		$url    = (string) $request->get_param( 'url' );
		$result = DataStream_Engine::get_instance()->test_connection( $url );
		return rest_ensure_response( $result );
	}

	/**
	 * Run maintenance tools.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function run_tool( WP_REST_Request $request ) {
		$action = (string) $request->get_param( 'action' );

		switch ( $action ) {
			case 'repair-cron':
				$ok = DataStream_Health::repair_cron();
				return rest_ensure_response(
					array(
						'success' => $ok,
						'message' => $ok
							? __( 'Cron schedule repaired.', 'datastream-api-sync' )
							: __( 'Failed to repair cron schedule.', 'datastream-api-sync' ),
					)
				);

			case 'repair-schema':
				DataStream_Health::repair_schema();
				return rest_ensure_response(
					array(
						'success' => true,
						'message' => __( 'Database schema repaired.', 'datastream-api-sync' ),
					)
				);

			case 'flush-cache':
				$count = DataStream_Provider::flush_all_caches();
				return rest_ensure_response(
					array(
						'success' => true,
						'message' => sprintf(
							/* translators: %d: cache entries cleared */
							__( 'Cleared %d cache entries.', 'datastream-api-sync' ),
							$count
						),
						'cleared' => $count,
					)
				);

			case 'clear-lock':
				DataStream_Sync_Lock::force_release();
				return rest_ensure_response(
					array(
						'success' => true,
						'message' => __( 'Sync lock cleared.', 'datastream-api-sync' ),
					)
				);

			case 'purge-all':
				if ( 'DELETE' !== strtoupper( (string) $request->get_param( 'confirm' ) ) ) {
					return new WP_Error( 'confirm_required', __( 'Confirmation token required.', 'datastream-api-sync' ), array( 'status' => 400 ) );
				}
				$deleted = $this->dao->purge_all();
				DataStream_Provider::flush_all_caches();
				return rest_ensure_response(
					array(
						'success' => true,
						'message' => __( 'All ingested records purged.', 'datastream-api-sync' ),
						'deleted' => $deleted,
					)
				);

			case 'purge-stale':
				$days = absint( DataStream_Config::get( 'stale_record_days' ) );
				if ( $days < 1 ) {
					return new WP_Error( 'invalid_config', __( 'Stale record days must be configured first.', 'datastream-api-sync' ), array( 'status' => 400 ) );
				}
				$deleted = $this->dao->purge_stale_records( $days );
				return rest_ensure_response(
					array(
						'success' => true,
						'message' => sprintf(
							/* translators: %d: number of records deleted */
							__( 'Purged %d stale records.', 'datastream-api-sync' ),
							$deleted
						),
						'deleted' => $deleted,
					)
				);

			case 'purge-stream':
				$stream = sanitize_key( (string) $request->get_param( 'stream_type' ) );
				if ( '' === $stream ) {
					return new WP_Error( 'invalid_stream', __( 'Stream type is required.', 'datastream-api-sync' ), array( 'status' => 400 ) );
				}
				$deleted = $this->dao->purge_by_stream_type( $stream );
				return rest_ensure_response(
					array(
						'success' => true,
						'message' => sprintf(
							/* translators: %d: number of records deleted */
							__( 'Purged %d records for stream type.', 'datastream-api-sync' ),
							$deleted
						),
						'deleted' => $deleted,
					)
				);
		}

		return new WP_Error( 'unknown_action', __( 'Unknown tool action.', 'datastream-api-sync' ), array( 'status' => 400 ) );
	}

	/**
	 * Decode payload JSON for REST consumers.
	 *
	 * @param array<string, mixed> $record Database row.
	 * @return array<string, mixed>
	 */
	private function serialize_record( $record ) {
		$payload = json_decode( $record['data_payload'], true );

		return array(
			'id'          => (int) $record['id'],
			'external_id' => $record['external_id'],
			'stream_type' => $record['stream_type'] ?? 'generic',
			'source'      => $record['source'] ?? '',
			'data'        => is_array( $payload ) ? $payload : array(),
			'updated_at'  => $record['updated_at'],
		);
	}
}
