<?php
/**
 * 工作流异步 Job REST：代理到 siteForge POST/GET /api/workflow/jobs。
 * Step1/2/3、单页规划、Tailwind 整页生成等长耗时 AI 均经此路径，避免 WordPress 出站长连接超时。
 *
 * siteForge 需注册 job 类型 `tailwind-generate`：worker 使用与本插件 POST /api/ai/generate-block
 * 相同的请求体调用生成服务，completed 时 data 为 { html, css, usage? }（与同步接口一致）。
 *
 * @package PufferGo
 */

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

/**
 * Class PufferGo_Workflow_Jobs_REST
 */
class PufferGo_Workflow_Jobs_REST {

	/**
	 * 代理 siteForge 创建 Job 时 HTTP 超时（秒）。
	 */
	const UPSTREAM_CREATE_TIMEOUT_SECONDS = 20;

	/**
	 * 批量创建（jobs 数组）时上游超时（秒），略高于单条。
	 */
	const UPSTREAM_CREATE_BULK_TIMEOUT_SECONDS = 45;

	/**
	 * 单次批量创建 / 状态查询允许的 job 数量上限（与 siteForge 一致）。
	 */
	const WORKFLOW_JOBS_BULK_MAX = 8;

	/**
	 * 代理 siteForge 查询 Job 时 HTTP 超时（秒）。
	 */
	const UPSTREAM_GET_TIMEOUT_SECONDS = 15;

	const JOB_TYPE_STRUCTURE          = 'structure-data';
	const JOB_TYPE_SITE_ARCHITECTURE  = 'site-architecture';
	const JOB_TYPE_PAGE_BLOCK_PLANNING = 'page-block-planning';
	const JOB_TYPE_TAILWIND_GENERATE   = 'tailwind-generate';

	/**
	 * 与 siteForge WORKFLOW_JOB_TYPES 保持一致。
	 *
	 * @return string[]
	 */
	private function allowed_job_types() {
		return array(
			self::JOB_TYPE_STRUCTURE,
			self::JOB_TYPE_SITE_ARCHITECTURE,
			self::JOB_TYPE_PAGE_BLOCK_PLANNING,
			self::JOB_TYPE_TAILWIND_GENERATE,
		);
	}

	/**
	 * 前端轮询 workflow Job 的默认参数（插件内唯一来源，毫秒）。
	 *
	 * @return array<string, int|string>
	 */
	private static function default_client_poll_config() {
		return array(
			'slowPhaseDurationMs' => 30 * 1000,
			'slowIntervalMs'      => 10 * 1000,
			'fastIntervalMs'      => 3 * 1000,
			'maxWaitMs'           => 5 * 60 * 1000,
			'createJobPath'       => 'puffergo/v1/workflow/jobs',
			'getJobPathPrefix'    => 'puffergo/v1/workflow/jobs',
			'pollJobStatusPath'   => 'puffergo/v1/workflow/jobs/status',
		);
	}

	/**
	 * 将 `puffergoWorkflowJobPoll` 注入到已入队的脚本之前执行。
	 *
	 * 禁止使用 `wp_localize_script` 传数字：WordPress 核心 `WP_Scripts::localize()` 会把标量先转为字符串再
	 * `json_encode`，导致前端收到 `"300000"` 而非 `300000`，轮询校验会失败。
	 *
	 * @param string $handle 已通过 `wp_enqueue_script` 注册的句柄。
	 * @return void
	 */
	public static function add_poll_config_inline_script( $handle ) {
		if ( ! is_string( $handle ) || '' === $handle ) {
			return;
		}
		$config = apply_filters(
			'puffergo_workflow_job_poll_config',
			self::default_client_poll_config()
		);
		if ( ! is_array( $config ) ) {
			$config = array();
		}
		$json   = wp_json_encode(
			$config,
			JSON_HEX_TAG | JSON_UNESCAPED_SLASHES
		);
		if ( false === $json ) {
			return;
		}
		wp_add_inline_script(
			$handle,
			'var puffergoWorkflowJobPoll = ' . $json . ';',
			'before'
		);
	}

	/**
	 * PufferGo_Workflow_Jobs_REST constructor.
	 */
	public function __construct() {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Register REST routes.
	 *
	 * @return void
	 */
	public function register_routes() {
		register_rest_route(
			'puffergo/v1',
			'/workflow/jobs/status',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'post_jobs_status' ),
				'permission_callback' => array( $this, 'check_permission' ),
			)
		);

		register_rest_route(
			'puffergo/v1',
			'/workflow/jobs',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'create_job' ),
				'permission_callback' => array( $this, 'check_permission' ),
			)
		);

		register_rest_route(
			'puffergo/v1',
			'/workflow/jobs/(?P<job_id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})',
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'get_job' ),
				'permission_callback' => array( $this, 'check_permission' ),
			)
		);
	}

	/**
	 * Permission.
	 *
	 * @return bool
	 */
	public function check_permission() {
		return current_user_can( 'edit_posts' );
	}

	/**
	 * POST：创建 Job（短超时，仅等 jobId）。
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function create_job( $request ) {
		$params = $request->get_json_params();
		if ( ! is_array( $params ) ) {
			$params = array();
		}

		if ( isset( $params['jobs'] ) && is_array( $params['jobs'] ) ) {
			return $this->create_jobs_bulk( $params['jobs'] );
		}

		$type = isset( $params['type'] ) ? sanitize_key( $params['type'] ) : '';
		if ( ! in_array( $type, $this->allowed_job_types(), true ) ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => __( 'Invalid job type.', 'puffergo' ),
				),
				400
			);
		}

		$payload = isset( $params['payload'] ) && is_array( $params['payload'] ) ? $params['payload'] : array();

		try {
			$body = $this->build_upstream_body( $type, $payload );
		} catch ( Exception $e ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => $e->getMessage(),
				),
				400
			);
		}

		try {
			$api    = PufferGo_API_Client::get_instance();
			$result = $api->post(
				'/api/workflow/jobs',
				array(
					'type'    => $type,
					'payload' => $body,
				),
				array(
					'timeout'              => self::UPSTREAM_CREATE_TIMEOUT_SECONDS,
					'log_prefix'           => 'Workflow Job Create',
					'success_http_codes'   => array( 200, 202 ),
					'return_raw_result'    => true,
				)
			);

			$http_status = isset( $result['jobId'] ) ? 202 : 200;
			return new WP_REST_Response( $result, $http_status );
		} catch ( PufferGo_API_Exception $e ) {
			return new WP_REST_Response( $e->toArray(), $e->getCode() );
		} catch ( Exception $e ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => $e->getMessage(),
				),
				500
			);
		}
	}

	/**
	 * POST body: jobs[]，每项 { type, payload }，最多 8 条。
	 *
	 * @param array<int, mixed> $jobs_in Raw jobs from JSON.
	 * @return WP_REST_Response
	 */
	private function create_jobs_bulk( array $jobs_in ) {
		$n = count( $jobs_in );
		if ( $n < 1 || $n > self::WORKFLOW_JOBS_BULK_MAX ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => sprintf(
						/* translators: %d: max bulk jobs */
						__( 'Bulk jobs count must be between 1 and %d.', 'puffergo' ),
						self::WORKFLOW_JOBS_BULK_MAX
					),
				),
				400
			);
		}

		$upstream_jobs = array();
		foreach ( $jobs_in as $item ) {
			if ( ! is_array( $item ) ) {
				return new WP_REST_Response(
					array(
						'success' => false,
						'message' => __( 'Invalid job entry.', 'puffergo' ),
					),
					400
				);
			}
			$type = isset( $item['type'] ) ? sanitize_key( $item['type'] ) : '';
			if ( ! in_array( $type, $this->allowed_job_types(), true ) ) {
				return new WP_REST_Response(
					array(
						'success' => false,
						'message' => __( 'Invalid job type.', 'puffergo' ),
					),
					400
				);
			}
			$payload = isset( $item['payload'] ) && is_array( $item['payload'] ) ? $item['payload'] : array();
			try {
				$upstream_jobs[] = array(
					'type'    => $type,
					'payload' => $this->build_upstream_body( $type, $payload ),
				);
			} catch ( Exception $e ) {
				return new WP_REST_Response(
					array(
						'success' => false,
						'message' => $e->getMessage(),
					),
					400
				);
			}
		}

		try {
			$api    = PufferGo_API_Client::get_instance();
			$result = $api->post(
				'/api/workflow/jobs',
				array( 'jobs' => $upstream_jobs ),
				array(
					'timeout'              => self::UPSTREAM_CREATE_BULK_TIMEOUT_SECONDS,
					'log_prefix'           => 'Workflow Job Create Bulk',
					'success_http_codes'   => array( 200, 202 ),
					'return_raw_result'    => true,
				)
			);
			return new WP_REST_Response( $result, 202 );
		} catch ( PufferGo_API_Exception $e ) {
			return new WP_REST_Response( $e->toArray(), $e->getCode() );
		} catch ( Exception $e ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => $e->getMessage(),
				),
				500
			);
		}
	}

	/**
	 * POST body: { ids: string[] }，UUID，1～8 个。
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function post_jobs_status( $request ) {
		$params = $request->get_json_params();
		if ( ! is_array( $params ) ) {
			$params = array();
		}
		$ids = isset( $params['ids'] ) && is_array( $params['ids'] ) ? $params['ids'] : array();
		$n   = count( $ids );
		if ( $n < 1 || $n > self::WORKFLOW_JOBS_BULK_MAX ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => sprintf(
						/* translators: %d: max ids */
						__( 'ids must contain between 1 and %d UUIDs.', 'puffergo' ),
						self::WORKFLOW_JOBS_BULK_MAX
					),
				),
				400
			);
		}

		$uuid_pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i';
		$sanitized    = array();
		foreach ( $ids as $id ) {
			$s = sanitize_text_field( (string) $id );
			if ( ! preg_match( $uuid_pattern, $s ) ) {
				return new WP_REST_Response(
					array(
						'success' => false,
						'message' => __( 'Invalid job id format.', 'puffergo' ),
					),
					400
				);
			}
			$sanitized[] = strtolower( $s );
		}

		try {
			$api    = PufferGo_API_Client::get_instance();
			$result = $api->post(
				'/api/workflow/jobs/status',
				array( 'ids' => $sanitized ),
				array(
					'timeout'              => self::UPSTREAM_GET_TIMEOUT_SECONDS,
					'log_prefix'           => 'Workflow Job Status Bulk',
					'success_http_codes'   => array( 200 ),
					'return_raw_result'    => true,
				)
			);
			return new WP_REST_Response( $result, 200 );
		} catch ( PufferGo_API_Exception $e ) {
			return new WP_REST_Response( $e->toArray(), $e->getCode() );
		} catch ( Exception $e ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => $e->getMessage(),
				),
				500
			);
		}
	}

	/**
	 * GET：轮询 Job（短超时）。
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function get_job( $request ) {
		$job_id = isset( $request['job_id'] ) ? sanitize_text_field( $request['job_id'] ) : '';
		if ( '' === $job_id ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => __( 'Missing job id.', 'puffergo' ),
				),
				400
			);
		}

		try {
			$api    = PufferGo_API_Client::get_instance();
			$result = $api->get(
				'/api/workflow/jobs/' . rawurlencode( $job_id ),
				array(),
				array(
					'timeout'                    => self::UPSTREAM_GET_TIMEOUT_SECONDS,
					'log_prefix'                 => 'Workflow Job Poll',
					'return_raw_result'          => true,
					'return_body_on_client_error' => true,
				)
			);
			if ( isset( $result['_httpStatus'] ) ) {
				$code = (int) $result['_httpStatus'];
				unset( $result['_httpStatus'] );
				return new WP_REST_Response( $result, $code );
			}
			return new WP_REST_Response( $result, 200 );
		} catch ( PufferGo_API_Exception $e ) {
			return new WP_REST_Response( $e->toArray(), $e->getCode() );
		} catch ( Exception $e ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => $e->getMessage(),
				),
				500
			);
		}
	}

	/**
	 * 按 Job 类型组装发往 siteForge 的 payload（含 page-block-planning 的站点 profile 合并）。
	 *
	 * @param string               $type    Job type.
	 * @param array<string, mixed> $payload Client payload.
	 * @return array<string, mixed>
	 * @throws Exception When validation fails.
	 */
	private function build_upstream_body( $type, array $payload ) {
		switch ( $type ) {
			case self::JOB_TYPE_STRUCTURE:
				return $this->build_structure_payload( $payload );
			case self::JOB_TYPE_SITE_ARCHITECTURE:
				return $this->build_site_architecture_payload( $payload );
			case self::JOB_TYPE_PAGE_BLOCK_PLANNING:
				return $this->build_page_block_planning_payload( $payload );
			case self::JOB_TYPE_TAILWIND_GENERATE:
				return $this->build_tailwind_generate_payload( $payload );
			default:
				throw new Exception( __( 'Unsupported job type.', 'puffergo' ) );
		}
	}

	/**
	 * Tailwind 生成：请求体与同步 POST /api/ai/generate-block 一致。
	 *
	 * @param array<string, mixed> $payload Payload.
	 * @return array<string, mixed>
	 * @throws Exception When invalid.
	 */
	private function build_tailwind_generate_payload( array $payload ) {
		if ( ! class_exists( 'PufferGo_Tailwind_Container' ) ) {
			throw new Exception( __( 'Tailwind module is not available.', 'puffergo' ) );
		}
		return PufferGo_Tailwind_Container::instance()->build_generate_block_request_body( $payload );
	}

	/**
	 * @param array<string, mixed> $payload Payload.
	 * @return array<string, mixed>
	 * @throws Exception When invalid.
	 */
	private function build_structure_payload( array $payload ) {
		$raw = isset( $payload['rawInput'] ) ? sanitize_textarea_field( wp_unslash( (string) $payload['rawInput'] ) ) : '';
		if ( '' === trim( $raw ) ) {
			throw new Exception( __( 'Please provide user input information', 'puffergo' ) );
		}
		if ( mb_strlen( trim( $raw ) ) < 10 ) {
			throw new Exception( __( 'Input content must be at least 10 characters', 'puffergo' ) );
		}
		$language = isset( $payload['language'] ) ? sanitize_text_field( wp_unslash( (string) $payload['language'] ) ) : 'zh-CN';
		$model    = isset( $payload['model'] ) ? sanitize_text_field( wp_unslash( (string) $payload['model'] ) ) : 'gemini-3-pro-preview';
		$out      = array(
			'rawInput' => $raw,
			'language' => $language,
			'model'    => $model,
		);
		if ( isset( $payload['keywords'] ) && is_array( $payload['keywords'] ) ) {
			$kw = array_values(
				array_filter(
					array_map(
						static function ( $k ) {
							return sanitize_text_field( (string) $k );
						},
						$payload['keywords']
					)
				)
			);
			if ( ! empty( $kw ) ) {
				$out['keywords'] = array_slice( $kw, 0, 30 );
			}
		}
		return $out;
	}

	/**
	 * @param array<string, mixed> $payload Payload.
	 * @return array<string, mixed>
	 * @throws Exception When invalid.
	 */
	private function build_site_architecture_payload( array $payload ) {
		$structured = isset( $payload['structuredData'] ) ? $payload['structuredData'] : null;
		if ( empty( $structured ) || ! is_array( $structured ) ) {
			throw new Exception( __( 'Please provide structured data', 'puffergo' ) );
		}
		$language = isset( $payload['language'] ) ? sanitize_text_field( wp_unslash( (string) $payload['language'] ) ) : 'zh-CN';
		$model    = isset( $payload['model'] ) ? sanitize_text_field( wp_unslash( (string) $payload['model'] ) ) : 'gemini-3-pro-preview';
		return array(
			'structuredData' => $structured,
			'language'       => $language,
			'model'          => $model,
		);
	}

	/**
	 * 与 ai-page-builder::plan_page_structure 一致：合并已保存站点 profile。
	 *
	 * @param array<string, mixed> $payload Payload.
	 * @return array<string, mixed>
	 * @throws Exception When invalid.
	 */
	private function build_page_block_planning_payload( array $payload ) {
		$pages = isset( $payload['pages'] ) && is_array( $payload['pages'] ) ? $payload['pages'] : array();
		if ( empty( $pages ) ) {
			throw new Exception( __( '请提供有效的页面信息', 'puffergo' ) );
		}

		foreach ( $pages as $index => $page ) {
			if ( ! is_array( $page ) ) {
				throw new Exception( __( '请提供有效的页面信息', 'puffergo' ) );
			}
			if ( empty( $page['name'] ) ) {
				$pages[ $index ]['name'] = __( '首页', 'puffergo' );
			}
			if ( empty( $page['path'] ) ) {
				$pages[ $index ]['path'] = '/';
			}
			if ( empty( $page['purpose'] ) ) {
				throw new Exception( __( '页面需求描述不能为空', 'puffergo' ) );
			}
		}

		$language = isset( $payload['language'] ) ? sanitize_text_field( wp_unslash( (string) $payload['language'] ) ) : 'zh-CN';
		$model    = isset( $payload['model'] ) ? sanitize_text_field( wp_unslash( (string) $payload['model'] ) ) : 'gemini-3-pro-preview';

		$saved_profile = get_option( 'puffergo_site_profile', array() );
		if ( ! is_array( $saved_profile ) ) {
			$saved_profile = array();
		}
		if ( isset( $saved_profile['updated_at'] ) ) {
			unset( $saved_profile['updated_at'] );
		}

		$body = array(
			'structuredData' => $saved_profile,
			'pages'          => $pages,
			'language'       => $language,
			'model'          => $model,
		);

		if ( isset( $payload['designSystem'] ) ) {
			$body['designSystem'] = $payload['designSystem'];
		}
		if ( isset( $payload['globalElements'] ) ) {
			$body['globalElements'] = $payload['globalElements'];
		}

		return $body;
	}
}
