<?php
/**
 * Populate API — REST endpoints for automated AI field population.
 *
 * Provides temporary API key authentication and bulk read/write endpoints
 * so an external AI service can populate all SEO fields automatically.
 *
 * @package Dennis_SEO
 * @license GPL-2.0-or-later
 */

defined( 'ABSPATH' ) || exit;

class Dennis_SEO_Populate_API {

	const KEY_TRANSIENT    = 'dennis_seo_api_key';
	const STATUS_TRANSIENT = 'dennis_seo_populate_status';
	const KEY_TTL          = DAY_IN_SECONDS;

	/** Meta keys the AI should NOT populate (managed by site owner). */
	const EXCLUDED_META = array(
		'_dennis_seo_canonical',
		'_dennis_seo_robots_noindex',
		'_dennis_seo_robots_nofollow',
		'_dennis_seo_og_image',
	);

	public function __construct() {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
		add_action( 'wp_ajax_dennis_seo_generate_api_key', array( $this, 'ajax_generate_key' ) );
		add_action( 'wp_ajax_dennis_seo_check_status', array( $this, 'ajax_check_status' ) );
	}

	/**
	 * AJAX: Return current populate status for admin pages to poll.
	 */
	public function ajax_check_status(): void {
		check_ajax_referer( 'dennis_seo_check_status', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( __( 'Unauthorized', 'dennis-geo' ) );
		}
		$status = get_transient( self::STATUS_TRANSIENT ) ?: 'idle';
		wp_send_json_success( array( 'status' => $status ) );
	}

	/**
	 * AJAX: Generate a temporary API key.
	 * Stores SHA-256 hash as transient. Returns raw key once.
	 */
	public function ajax_generate_key(): void {
		check_ajax_referer( 'dennis_seo_generate_key', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Unauthorized' );
		}

		$raw_key = wp_generate_password( 32, false );
		$hashed  = hash( 'sha256', $raw_key );

		set_transient( self::KEY_TRANSIENT, $hashed, self::KEY_TTL );
		set_transient( self::STATUS_TRANSIENT, 'idle', self::KEY_TTL );

		wp_send_json_success( array( 'key' => $raw_key ) );
	}

	/**
	 * Verify the API key from the X-Dennis-API-Key header.
	 */
	public function check_api_key( \WP_REST_Request $request ): bool {
		$key = $request->get_header( 'X-Dennis-API-Key' );

		if ( empty( $key ) ) {
			return false;
		}

		$stored_hash = get_transient( self::KEY_TRANSIENT );

		if ( false === $stored_hash ) {
			return false;
		}

		return hash_equals( $stored_hash, hash( 'sha256', $key ) );
	}

	/**
	 * Register all /populate/ REST routes.
	 */
	public function register_routes(): void {
		$ns = 'dennis-seo/v1';

		register_rest_route( $ns, '/populate/pages', array(
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'get_pages' ),
				'permission_callback' => array( $this, 'check_api_key' ),
			),
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'update_pages' ),
				'permission_callback' => array( $this, 'check_api_key' ),
			),
		) );

		register_rest_route( $ns, '/populate/settings', array(
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'update_settings' ),
				'permission_callback' => array( $this, 'check_api_key' ),
			),
		) );

		register_rest_route( $ns, '/populate/status', array(
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'get_status' ),
				'permission_callback' => array( $this, 'check_api_key' ),
			),
		) );
	}

	/**
	 * GET /populate/pages — return all pages with content and current meta.
	 */
	public function get_pages(): \WP_REST_Response {
		set_transient( self::STATUS_TRANSIENT, 'in_progress', self::KEY_TTL );

		$post_types = get_post_types( array( 'public' => true ), 'names' );
		unset( $post_types['attachment'] );

		$posts = get_posts( array(
			'post_type'      => array_values( $post_types ),
			'post_status'    => 'publish',
			'posts_per_page' => 500,
		) );

		// Exclude noindexed pages.
		$posts = array_filter( $posts, function ( $post ) {
			return '1' !== get_post_meta( $post->ID, '_dennis_seo_robots_noindex', true );
		} );

		$meta_keys = array_diff( array_keys( DENNIS_SEO_META_KEYS ), self::EXCLUDED_META );
		$pages     = array();

		foreach ( $posts as $post ) {
			$content_html = apply_filters( 'the_content', $post->post_content );
			$content_text = wp_strip_all_tags( $content_html );

			// ACF: append text from custom fields.
			if ( function_exists( 'get_fields' ) ) {
				$acf = get_fields( $post->ID );
				if ( is_array( $acf ) ) {
					$acf_text = self::extract_acf_text( $acf );
					if ( $acf_text ) {
						$content_text .= "\n" . $acf_text;
					}
				}
			}

			// Elementor: extract text from stored JSON data.
			$elementor_data = get_post_meta( $post->ID, '_elementor_data', true );
			if ( $elementor_data && is_string( $elementor_data ) ) {
				$el_parsed = json_decode( $elementor_data, true );
				if ( is_array( $el_parsed ) ) {
					$el_text = self::extract_elementor_text( $el_parsed );
					if ( $el_text ) {
						$content_text .= "\n" . $el_text;
					}
				}
			}

			$content_text = trim( $content_text );
			if ( strlen( $content_text ) > 5000 ) {
				$content_text = mb_substr( $content_text, 0, 5000 ) . '...';
			}

			$current_meta = array();
			foreach ( $meta_keys as $key ) {
				$current_meta[ $key ] = get_post_meta( $post->ID, $key, true );
			}

			$featured = get_the_post_thumbnail_url( $post->ID, 'full' );

			$pages[] = array(
				'id'             => $post->ID,
				'title'          => $post->post_title,
				'slug'           => $post->post_name,
				'post_type'      => $post->post_type,
				'url'            => get_permalink( $post->ID ),
				'content_text'   => $content_text,
				'featured_image' => $featured ?: '',
				'current_meta'   => $current_meta,
			);
		}

		$settings = Dennis_SEO_Settings::get_all();

		// Gather extra discoverable info the AI can use.
		$extra = array();

		// Logo URL.
		$custom_logo_id = get_theme_mod( 'custom_logo' );
		if ( $custom_logo_id ) {
			$extra['logo_url'] = wp_get_attachment_image_url( $custom_logo_id, 'full' ) ?: '';
		}

		// Combine multiple sources for extra_data extraction:
		// 1. Rendered homepage HTML (best — covers custom blocks like Emrahus)
		// 2. Block theme template parts (parts/footer.html, parts/header.html)
		// 3. Pattern files
		// The rendered homepage is primary; template files supplement for edge cases.
		$template_html = '';

		// 1. Fetch rendered homepage — try multiple URL variants for dev/prod compatibility.
		$template_html .= self::fetch_rendered_homepage();

		// 2. Read block theme template parts (supplement).
		$footer_file = get_stylesheet_directory() . '/parts/footer.html';
		if ( file_exists( $footer_file ) ) {
			$template_html .= "\n" . file_get_contents( $footer_file );
		}
		$header_file = get_stylesheet_directory() . '/parts/header.html';
		if ( file_exists( $header_file ) ) {
			$template_html .= "\n" . file_get_contents( $header_file );
		}

		// 3. Read patterns (often contain phone/email CTAs).
		$patterns_dir = get_stylesheet_directory() . '/patterns/';
		if ( is_dir( $patterns_dir ) ) {
			foreach ( glob( $patterns_dir . '*.php' ) as $pattern_file ) {
				ob_start();
				include $pattern_file;
				$template_html .= "\n" . ob_get_clean();
			}
			foreach ( glob( $patterns_dir . '*.html' ) as $pattern_file ) {
				$template_html .= "\n" . file_get_contents( $pattern_file );
			}
		}

		if ( $template_html ) {
			// Extract social links.
			preg_match_all( '/href="(https?:\/\/(www\.)?(facebook|instagram|linkedin|youtube|twitter|x\.com|google\.com\/maps|g\.page)[^"]*)"/', $template_html, $social_matches );
			if ( ! empty( $social_matches[1] ) ) {
				$extra['social_links'] = array_values( array_unique( $social_matches[1] ) );
			}
			// Extract phone tel: links.
			preg_match_all( '/href="tel:([^"]*)"/', $template_html, $phone_matches );
			if ( ! empty( $phone_matches[1] ) ) {
				$extra['phone_numbers'] = array_values( array_unique( $phone_matches[1] ) );
			}
			// Extract visible phone numbers from stripped text (any format: "Tele: 0418 48 60 40", ">0418..<", etc.).
			$stripped_for_phones = wp_strip_all_tags( $template_html );
			preg_match_all( '/(?:\+46|0)[\s\-]?\d[\s\-\d]{7,17}/', $stripped_for_phones, $phone_text_matches );
			if ( ! empty( $phone_text_matches[0] ) ) {
				$phones = array_filter( array_map( 'trim', $phone_text_matches[0] ), function ( $p ) {
					$digits = preg_replace( '/[^0-9]/', '', $p );
					// Valid Swedish-like phone: 7-12 digits, doesn't start with too many zeros.
					return strlen( $digits ) >= 7 && strlen( $digits ) <= 13 && ! preg_match( '/^0{3,}/', $digits );
				} );
				if ( $phones ) {
					$extra['phone_display'] = array_values( array_unique( $phones ) );
				}
			}
			// Extract logo URL from rendered HTML (images in header/logo containers).
			if ( empty( $extra['logo_url'] ) ) {
				preg_match( '/<img[^>]+src="([^"]*(?:logo|logotyp)[^"]*\.(?:png|jpg|jpeg|svg|webp))[^"]*"/i', $template_html, $logo_match );
				if ( ! empty( $logo_match[1] ) ) {
					$extra['logo_url'] = self::absolute_url( $logo_match[1] );
				}
			}
			// Footer text — strip scripts/styles first, then HTML.
			$clean = preg_replace( '/<script\b[^>]*>.*?<\/script>/is', ' ', $template_html );
			$clean = preg_replace( '/<style\b[^>]*>.*?<\/style>/is', ' ', (string) $clean );
			$footer_text = wp_strip_all_tags( (string) $clean );
			$footer_text = preg_replace( '/\s+/', ' ', $footer_text );
			$extra['footer_text'] = mb_substr( trim( $footer_text ), 0, 3000 );
		}

		// Default OG image candidates (first few content images from homepage).
		$front_id = (int) get_option( 'page_on_front' );
		if ( $front_id ) {
			$front_content = get_post_field( 'post_content', $front_id );
			preg_match_all( '/<img[^>]+src="([^"]*wp-content\/uploads[^"]*)"/', $front_content, $img_matches );
			if ( ! empty( $img_matches[1] ) ) {
				$extra['homepage_images'] = array_slice( array_values( array_unique( $img_matches[1] ) ), 0, 5 );
			}
		}

		return new \WP_REST_Response( array(
			'site_info' => array(
				'site_name'   => $settings['site_name'] ?: get_bloginfo( 'name' ),
				'site_url'    => home_url(),
				'locale'      => get_locale(),
				'content_language' => self::detect_content_language(),
				'total_pages' => count( $pages ),
			),
			'settings'   => $settings,
			'extra_data' => $extra,
			'pages'      => $pages,
		), 200 );
	}

	/**
	 * Recursively extract text from Elementor JSON data.
	 * Elementor stores widgets in a nested elements[] structure with settings.
	 */
	private static function extract_elementor_text( array $elements, int $depth = 0 ): string {
		if ( $depth > 10 ) {
			return '';
		}
		$parts = array();
		foreach ( $elements as $el ) {
			// Extract text from widget settings.
			if ( isset( $el['settings'] ) && is_array( $el['settings'] ) ) {
				foreach ( array( 'title', 'editor', 'text', 'description', 'heading_title', 'testimonial_content', 'tab_title', 'tab_content', 'item_description', 'alert_message', 'blockquote_content' ) as $field ) {
					if ( ! empty( $el['settings'][ $field ] ) && is_string( $el['settings'][ $field ] ) ) {
						$stripped = wp_strip_all_tags( $el['settings'][ $field ] );
						$stripped = preg_replace( '/\s+/', ' ', $stripped );
						$stripped = trim( $stripped );
						if ( strlen( $stripped ) > 10 ) {
							$parts[] = $stripped;
						}
					}
				}
				// Repeater/list items.
				foreach ( $el['settings'] as $val ) {
					if ( is_array( $val ) && isset( $val[0] ) && is_array( $val[0] ) ) {
						foreach ( $val as $item ) {
							if ( is_array( $item ) ) {
								foreach ( $item as $v ) {
									if ( is_string( $v ) && strlen( $v ) > 10 ) {
										$stripped = wp_strip_all_tags( $v );
										$stripped = trim( preg_replace( '/\s+/', ' ', $stripped ) );
										if ( strlen( $stripped ) > 10 && ! preg_match( '/^https?:\/\//', $stripped ) ) {
											$parts[] = $stripped;
										}
									}
								}
							}
						}
					}
				}
			}
			// Recurse into child elements.
			if ( ! empty( $el['elements'] ) && is_array( $el['elements'] ) ) {
				$nested = self::extract_elementor_text( $el['elements'], $depth + 1 );
				if ( $nested ) {
					$parts[] = $nested;
				}
			}
		}
		return implode( "\n", $parts );
	}

	/**
	 * Recursively extract text from ACF fields.
	 * Handles strings, arrays, nested groups/repeaters, and flexible content.
	 */
	private static function extract_acf_text( $fields, int $depth = 0 ): string {
		if ( $depth > 5 ) {
			return '';
		}
		$parts = array();
		foreach ( $fields as $key => $value ) {
			if ( is_string( $value ) && strlen( $value ) > 10 ) {
				$stripped = wp_strip_all_tags( $value );
				$stripped = preg_replace( '/\s+/', ' ', $stripped );
				$stripped = trim( $stripped );
				if ( strlen( $stripped ) > 10 && ! preg_match( '/^https?:\/\//', $stripped ) && ! preg_match( '/^\d+$/', $stripped ) ) {
					$parts[] = $stripped;
				}
			} elseif ( is_array( $value ) ) {
				$nested = self::extract_acf_text( $value, $depth + 1 );
				if ( $nested ) {
					$parts[] = $nested;
				}
			}
		}
		return implode( "\n", $parts );
	}

	/**
	 * Detect the content language of the site.
	 * Checks the <html lang=""> attribute on the rendered homepage,
	 * falls back to get_bloginfo('language'), then get_locale().
	 */
	private static function detect_content_language(): string {
		$html = self::fetch_rendered_homepage();
		if ( $html && preg_match( '/<html[^>]*\slang=["\']([a-zA-Z-]+)["\']/', $html, $m ) ) {
			return $m[1];
		}
		$lang = get_bloginfo( 'language' );
		if ( $lang ) {
			return $lang;
		}
		return get_locale();
	}

	/**
	 * Fetch the rendered homepage HTML. Tries multiple URL variants
	 * so it works in production and in Docker dev environments.
	 * Returns the HTML body or empty string.
	 */
	private static function fetch_rendered_homepage(): string {
		$home = home_url( '/' );

		// phpcs:ignore PluginCheck.CodeAnalysis.Localhost.Found -- dev-only fallbacks, not used in production.
		$candidates = array( $home );
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			$parsed = wp_parse_url( $home );
			$scheme = $parsed['scheme'] ?? 'http';
			$port   = isset( $parsed['port'] ) ? ':' . $parsed['port'] : '';
			$candidates[] = $scheme . '://127.0.0.1' . $port . '/'; // phpcs:ignore PluginCheck.CodeAnalysis.Localhost.Found
			$candidates[] = $scheme . '://localhost' . $port . '/'; // phpcs:ignore PluginCheck.CodeAnalysis.Localhost.Found
		}

		$tried = array();
		foreach ( $candidates as $url ) {
			if ( in_array( $url, $tried, true ) ) continue;
			$tried[] = $url;
			$resp = wp_remote_get( $url, array(
				'timeout'     => 10,
				'sslverify'   => false,
				'redirection' => 3,
			) );
			if ( is_wp_error( $resp ) ) continue;
			$code = wp_remote_retrieve_response_code( $resp );
			// Accept 200 or redirects that still include the body.
			if ( $code >= 200 && $code < 400 ) {
				$body = wp_remote_retrieve_body( $resp );
				if ( strlen( $body ) > 500 ) {
					return $body;
				}
			}
		}
		return '';
	}

	/**
	 * Resolve a relative URL to absolute against home_url.
	 */
	private static function absolute_url( string $url ): string {
		if ( preg_match( '/^https?:\/\//i', $url ) ) {
			return $url;
		}
		if ( strpos( $url, '//' ) === 0 ) {
			$parsed = wp_parse_url( home_url() );
			return ( $parsed['scheme'] ?? 'https' ) . ':' . $url;
		}
		if ( strpos( $url, '/' ) === 0 ) {
			return rtrim( home_url(), '/' ) . $url;
		}
		return rtrim( home_url(), '/' ) . '/' . ltrim( $url, '/' );
	}

	/**
	 * POST /populate/pages — write SEO fields for multiple pages.
	 */
	public function update_pages( \WP_REST_Request $request ): \WP_REST_Response {
		$data    = $request->get_json_params();
		$pages   = isset( $data['pages'] ) ? $data['pages'] : array();
		$updated = 0;
		$errors  = array();

		$valid_keys = array_keys( DENNIS_SEO_META_KEYS );

		foreach ( $pages as $page ) {
			$post_id = isset( $page['id'] ) ? (int) $page['id'] : 0;
			$meta    = isset( $page['meta'] ) ? $page['meta'] : array();

			if ( ! $post_id || ! get_post( $post_id ) ) {
				$errors[] = "Post ID {$post_id} not found";
				continue;
			}

			foreach ( $meta as $key => $value ) {
				if ( ! in_array( $key, $valid_keys, true ) ) {
					continue;
				}
				$existing = get_post_meta( $post_id, $key, true );
				if ( '' !== $existing && false !== $existing ) {
					continue;
				}
				$sanitize_cb = DENNIS_SEO_META_KEYS[ $key ];
				$clean_value = call_user_func( $sanitize_cb, $value );
				update_post_meta( $post_id, $key, $clean_value );
			}

			$updated++;
		}

		// Bust caches.
		delete_transient( 'dennis_seo_sitemap_post' );
		delete_transient( 'dennis_seo_sitemap_page' );
		delete_transient( 'dennis_seo_llms_txt' );

		if ( empty( $errors ) ) {
			set_transient( self::STATUS_TRANSIENT, 'completed', self::KEY_TTL );
		}

		return new \WP_REST_Response( array(
			'updated' => $updated,
			'failed'  => count( $errors ),
			'errors'  => $errors,
		), 200 );
	}

	/**
	 * POST /populate/settings — write global settings.
	 */
	public function update_settings( \WP_REST_Request $request ): \WP_REST_Response {
		$data     = $request->get_json_params();
		$defaults = Dennis_SEO_Settings::get_all();
		$filtered = array_intersect_key( $data, $defaults );
		$merged   = wp_parse_args( $filtered, $defaults );

		foreach ( $merged as $key => $value ) {
			if ( is_bool( $value ) ) {
				$merged[ $key ] = (bool) $value;
			} elseif ( is_string( $value ) ) {
				if ( in_array( $key, array( 'llms_full_content', 'custom_robots_rules' ), true ) ) {
					$merged[ $key ] = sanitize_textarea_field( $value );
				} else {
					$merged[ $key ] = sanitize_text_field( $value );
				}
			}
		}

		update_option( Dennis_SEO_Settings::OPTION_KEY, $merged );

		// Bust caches.
		delete_transient( 'dennis_seo_llms_txt' );
		delete_transient( 'dennis_seo_llms_full_txt' );
		delete_transient( 'dennis_seo_sitemap_post' );
		delete_transient( 'dennis_seo_sitemap_page' );

		return new \WP_REST_Response( array( 'updated' => true ), 200 );
	}

	/**
	 * GET /populate/status — return current populate status.
	 */
	public function get_status(): \WP_REST_Response {
		$status   = get_transient( self::STATUS_TRANSIENT ) ?: 'idle';
		$key_hash = get_transient( self::KEY_TRANSIENT );

		return new \WP_REST_Response( array(
			'status'         => $status,
			'api_key_active' => false !== $key_hash,
		), 200 );
	}
}
