<?php
/**
 * Internal Links Sync Contract
 *
 * Ported from the original SEOvault AI connector to preserve
 * backward compatibility with the web app's internal-links sync.
 *
 * @package SEOvaultSEO\Api
 */

namespace SEOvaultSEO\Api;

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

/**
 * Handles internal-links manifest and sync endpoints.
 */
class InternalLinks {

	const SCHEMA_VERSION = 1;

	public static function site_id() {
		$host = wp_parse_url( home_url(), PHP_URL_HOST );
		if ( ! is_string( $host ) || '' === $host ) {
			return 'unknown-site';
		}
		return strtolower( $host );
	}

	public static function supported_types() {
		$types  = array( 'post', 'page' );
		$custom = get_post_types(
			array(
				'public'       => true,
				'show_in_rest' => true,
				'_builtin'     => false,
			),
			'names'
		);

		// Exclude the plugin's own structured-data CPTs (e.g. locations),
		// which are synced via their own mechanism, not internal-links.
		$excluded = self::excluded_post_types();
		if ( is_array( $custom ) ) {
			$custom = array_diff( $custom, $excluded );
		}

		if ( is_array( $custom ) && ! empty( $custom ) ) {
			$types[] = 'cpt';
		}

		return array_values( array_unique( $types ) );
	}

	/**
	 * Post types owned by this plugin that should be excluded from
	 * internal-links sync enumeration.
	 *
	 * @return array
	 */
	public static function excluded_post_types() {
		$excluded = array();

		if ( class_exists( '\SEOvaultSEO\LocalSEO\PostType' ) ) {
			$excluded[] = \SEOvaultSEO\LocalSEO\PostType::get_post_type();
		}

		return apply_filters( 'seovaultseo_internal_links_excluded_post_types', $excluded );
	}

	public static function normalize_type( $post_type ) {
		if ( 'post' === $post_type ) {
			return 'post';
		}
		if ( 'page' === $post_type ) {
			return 'page';
		}
		return 'cpt';
	}

	public static function encode_cursor( array $state ) {
		return rtrim( strtr( base64_encode( wp_json_encode( $state ) ), '+/', '-_' ), '=' );
	}

	public static function decode_cursor( $cursor ) {
		if ( '' === $cursor ) {
			return array(
				'typeIndex' => 0,
				'page'      => 1,
			);
		}

		$decoded = base64_decode( strtr( $cursor, '-_', '+/' ), true );
		if ( ! is_string( $decoded ) || '' === $decoded ) {
			return array(
				'typeIndex' => 0,
				'page'      => 1,
			);
		}

		$json = json_decode( $decoded, true );
		if ( ! is_array( $json ) ) {
			return array(
				'typeIndex' => 0,
				'page'      => 1,
			);
		}

		$type_index = isset( $json['typeIndex'] ) ? max( 0, (int) $json['typeIndex'] ) : 0;
		$page       = isset( $json['page'] ) ? max( 1, (int) $json['page'] ) : 1;

		return array(
			'typeIndex' => $type_index,
			'page'      => $page,
		);
	}

	public static function normalize_types_param( $types_param ) {
		if ( '' === trim( $types_param ) ) {
			return array( 'post', 'page', 'cpt' );
		}

		$parts = array_filter(
			array_map(
				static function ( $item ) {
					return strtolower( trim( (string) $item ) );
				},
				explode( ',', $types_param )
			)
		);

		$normalized = array();
		foreach ( $parts as $part ) {
			if ( 'post' === $part || 'page' === $part || 'cpt' === $part ) {
				$normalized[] = $part;
			}
		}

		if ( empty( $normalized ) ) {
			return array( 'post', 'page', 'cpt' );
		}

		return array_values( array_unique( $normalized ) );
	}

	public static function include_body( $raw ) {
		$normalized = strtolower( trim( $raw ) );
		return in_array( $normalized, array( '1', 'true', 'yes', 'on' ), true );
	}

	public static function parse_since( $since ) {
		$since = trim( $since );
		if ( '' === $since ) {
			return null;
		}
		$ts = strtotime( $since );
		if ( false === $ts ) {
			return null;
		}
		return gmdate( 'Y-m-d H:i:s', $ts );
	}

	public static function custom_anchors( $post_id ) {
		$candidates = array(
			get_post_meta( $post_id, 'seovaultai_custom_anchors', true ),
			get_post_meta( $post_id, '_seovaultai_custom_anchors', true ),
			get_post_meta( $post_id, 'custom_anchors', true ),
		);

		$anchors = array();
		foreach ( $candidates as $candidate ) {
			if ( is_array( $candidate ) ) {
				$anchors = array_merge( $anchors, $candidate );
				continue;
			}
			if ( is_string( $candidate ) && '' !== trim( $candidate ) ) {
				if ( '[' === substr( trim( $candidate ), 0, 1 ) ) {
					$decoded = json_decode( $candidate, true );
					if ( is_array( $decoded ) ) {
						$anchors = array_merge( $anchors, $decoded );
						continue;
					}
				}
				$anchors = array_merge( $anchors, preg_split( '/[\r\n,]+/', $candidate ) ?: array() );
			}
		}

		$anchors = array_values(
			array_unique(
				array_filter(
					array_map(
						static function ( $item ) {
							return trim( wp_strip_all_tags( (string) $item ) );
						},
						$anchors
					)
				)
			)
		);

		return $anchors;
	}

	public static function headings( $html ) {
		if ( '' === trim( $html ) ) {
			return array();
		}

		$headings = array();
		if ( preg_match_all( '/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/i', $html, $matches ) ) {
			foreach ( $matches[1] as $raw ) {
				$text = trim( wp_strip_all_tags( html_entity_decode( (string) $raw, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ) );
				if ( '' !== $text ) {
					$headings[] = $text;
				}
			}
		}

		return array_values( array_unique( $headings ) );
	}

	public static function is_noindex( $post_id ) {
		$yoast = get_post_meta( $post_id, '_yoast_wpseo_meta-robots-noindex', true );
		if ( '1' === (string) $yoast || 'noindex' === strtolower( (string) $yoast ) ) {
			return true;
		}

		$rank_math = get_post_meta( $post_id, 'rank_math_robots', true );
		if ( is_array( $rank_math ) && in_array( 'noindex', $rank_math, true ) ) {
			return true;
		}
		if ( is_string( $rank_math ) && false !== strpos( strtolower( $rank_math ), 'noindex' ) ) {
			return true;
		}

		$seovault = get_post_meta( $post_id, '_seovault_robots_noindex', true );
		if ( (bool) $seovault ) {
			return true;
		}

		return false;
	}

	public static function get_canonical( $post_id ) {
		$candidates = array(
			get_post_meta( $post_id, '_yoast_wpseo_canonical', true ),
			get_post_meta( $post_id, 'rank_math_canonical_url', true ),
			get_post_meta( $post_id, '_rank_math_canonical_url', true ),
			get_post_meta( $post_id, '_seovault_canonical_url', true ),
		);

		foreach ( $candidates as $candidate ) {
			if ( is_string( $candidate ) && '' !== trim( $candidate ) ) {
				return esc_url_raw( trim( $candidate ) );
			}
		}

		return null;
	}

	public static function get_language( $post_id ) {
		if ( function_exists( 'pll_get_post_language' ) ) {
			$lang = call_user_func( 'pll_get_post_language', $post_id, 'slug' );
			if ( is_string( $lang ) && '' !== trim( $lang ) ) {
				return strtolower( trim( $lang ) );
			}
		}

		$meta_lang = get_post_meta( $post_id, '_pll_language', true );
		if ( is_string( $meta_lang ) && '' !== trim( $meta_lang ) ) {
			return strtolower( trim( $meta_lang ) );
		}

		return null;
	}

	public static function is_cornerstone( $post_id ) {
		$yoast = get_post_meta( $post_id, '_yoast_wpseo_is_cornerstone', true );
		return '1' === (string) $yoast || 'yes' === strtolower( (string) $yoast );
	}

	public static function normalize_permalink( $url ) {
		$trimmed = trim( $url );
		if ( '' === $trimmed ) {
			return '';
		}
		return untrailingslashit( strtolower( $trimmed ) );
	}

	public static function hash_seed( array $record ) {
		return md5( wp_json_encode( $record ) ?: '' );
	}

	public static function format_record( \WP_Post $post, $include_body_phrases ) {
		$post_type          = self::normalize_type( $post->post_type );
		$site_id            = self::site_id();
		$permalink          = get_permalink( $post->ID ) ?: '';
		$canonical          = self::get_canonical( $post->ID );
		$noindex            = self::is_noindex( $post->ID );
		$password_protected = ! empty( $post->post_password );

		$html           = (string) $post->post_content;
		$content_text   = trim( wp_strip_all_tags( html_entity_decode( $html, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ) );
		$excerpt_text   = trim( wp_strip_all_tags( html_entity_decode( (string) $post->post_excerpt, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ) );
		$headings       = self::headings( $html );
		$custom_anchors = self::custom_anchors( $post->ID );

		// Do not expose protected body content over the sync API.
		if ( $password_protected ) {
			$content_text   = '';
			$excerpt_text   = '';
			$headings       = array();
			$custom_anchors = array();
		}

		$status         = (string) $post->post_status;
		$canonical_away = false;
		if ( is_string( $canonical ) && '' !== trim( $canonical ) ) {
			$canonical_away = self::normalize_permalink( $canonical ) !== self::normalize_permalink( $permalink );
		}

		$is_published = 'publish' === $status;
		$is_indexable = $is_published && ! $noindex && ! $password_protected && ! $canonical_away;

		$content_for_index = $include_body_phrases
			? $content_text
			: trim( implode( ' ', array_filter( array( $post->post_title, $excerpt_text, implode( ' ', $headings ) ) ) ) );

		if ( $password_protected ) {
			$content_for_index = (string) $post->post_title;
		}

		$categories = wp_get_post_terms( $post->ID, 'category', array( 'fields' => 'names' ) );
		$tags       = wp_get_post_terms( $post->ID, 'post_tag', array( 'fields' => 'names' ) );

		$record = array(
			'docId'             => $site_id . ':' . $post_type . ':' . (int) $post->ID,
			'wpId'              => (int) $post->ID,
			'type'              => $post_type,
			'status'            => $status,
			'title'             => (string) $post->post_title,
			'slug'              => (string) $post->post_name,
			'permalink'         => $permalink,
			'canonicalUrl'      => $canonical,
			'excerpt'           => $excerpt_text,
			'contentText'       => $content_for_index,
			'headings'          => $headings,
			'categories'        => is_array( $categories ) ? array_values( $categories ) : array(),
			'tags'              => is_array( $tags ) ? array_values( $tags ) : array(),
			'language'          => self::get_language( $post->ID ),
			'isCornerstone'     => self::is_cornerstone( $post->ID ),
			'isIndexable'       => $is_indexable,
			'noindex'           => $noindex,
			'passwordProtected' => $password_protected,
			'parentId'          => $post->post_parent ? (int) $post->post_parent : null,
			'modifiedGmt'       => $post->post_modified_gmt ? (string) $post->post_modified_gmt : null,
			'wordCount'         => str_word_count( $content_text ),
			'customAnchors'     => $custom_anchors,
		);

		$record['hash'] = self::hash_seed( $record );
		return $record;
	}

	public static function query_for_type( $type, $page, $per_page, $since_gmt ) {
		$post_types = array();
		if ( 'post' === $type ) {
			$post_types = array( 'post' );
		} elseif ( 'page' === $type ) {
			$post_types = array( 'page' );
		} else {
			$custom     = get_post_types(
				array(
					'public'       => true,
					'show_in_rest' => true,
					'_builtin'     => false,
				),
				'names'
			);
			$post_types = is_array( $custom ) ? array_values( $custom ) : array();
		}

		if ( empty( $post_types ) ) {
			$post_types = array( 'post' );
		}

		$args = array(
			'post_type'              => $post_types,
			'post_status'            => array( 'publish', 'draft', 'private', 'pending', 'future' ),
			'posts_per_page'         => $per_page,
			'paged'                  => $page,
			'orderby'                => 'modified',
			'order'                  => 'DESC',
			'ignore_sticky_posts'    => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
			'no_found_rows'          => false,
		);

		if ( null !== $since_gmt ) {
			$args['date_query'] = array(
				array(
					'column'    => 'post_modified_gmt',
					'after'     => $since_gmt,
					'inclusive' => true,
				),
			);
		}

		return new \WP_Query( $args );
	}

	public static function latest_modified_gmt() {
		$latest_posts = get_posts(
			array(
				'post_type'              => array( 'post', 'page' ),
				'post_status'            => array( 'publish', 'draft', 'private', 'pending', 'future' ),
				'posts_per_page'         => 1,
				'orderby'                => 'modified',
				'order'                  => 'DESC',
				'fields'                 => 'ids',
				'no_found_rows'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			)
		);
		if ( empty( $latest_posts ) ) {
			return null;
		}

		$value = get_post_field( 'post_modified_gmt', (int) $latest_posts[0] );
		if ( ! is_string( $value ) || '' === trim( $value ) ) {
			return null;
		}

		return $value;
	}

	public static function record_count() {
		$counts      = wp_count_posts( 'post' );
		$page_counts = wp_count_posts( 'page' );
		$total       = 0;

		if ( is_object( $counts ) ) {
			foreach ( array( 'publish', 'draft', 'private', 'pending', 'future' ) as $status ) {
				$total += isset( $counts->{$status} ) ? (int) $counts->{$status} : 0;
			}
		}
		if ( is_object( $page_counts ) ) {
			foreach ( array( 'publish', 'draft', 'private', 'pending', 'future' ) as $status ) {
				$total += isset( $page_counts->{$status} ) ? (int) $page_counts->{$status} : 0;
			}
		}

		return $total;
	}

	public static function manifest() {
		$latest_modified       = self::latest_modified_gmt();
		$record_count          = self::record_count();
		$snapshot_version_seed = (string) $record_count . '|' . (string) $latest_modified . '|' . (string) self::SCHEMA_VERSION;
		$snapshot_version      = 'plugin:' . md5( $snapshot_version_seed );

		return rest_ensure_response(
			array(
				'ok'                     => true,
				'schemaVersion'          => self::SCHEMA_VERSION,
				'snapshotVersion'        => $snapshot_version,
				'siteUrl'                => home_url(),
				'supportedTypes'         => self::supported_types(),
				'lastModifiedGmt'        => $latest_modified,
				'recordCount'            => $record_count,
				'fullReindexRecommended' => false,
				'capabilityFlags'        => array(
					'hasCornerstone'   => true,
					'hasLanguage'      => function_exists( 'pll_get_post_language' ),
					'hasCanonical'     => true,
					'hasCustomAnchors' => true,
				),
			)
		);
	}

	public static function sync( \WP_REST_Request $request ) {
		$limit = (int) $request->get_param( 'limit' );
		if ( $limit <= 0 ) {
			$limit = 100;
		}
		$limit = min( max( $limit, 1 ), 200 );

		$cursor       = (string) $request->get_param( 'cursor' );
		$cursor_state = self::decode_cursor( $cursor );
		$since_gmt    = self::parse_since( (string) $request->get_param( 'since' ) );
		$types        = self::normalize_types_param( (string) $request->get_param( 'types' ) );
		$include_body = self::include_body( (string) $request->get_param( 'includeBodyPhrases' ) );

		$type_index = min( (int) $cursor_state['typeIndex'], max( count( $types ) - 1, 0 ) );
		$page       = max( 1, (int) $cursor_state['page'] );
		$records    = array();

		while ( $type_index < count( $types ) && count( $records ) < $limit ) {
			$type  = $types[ $type_index ];
			$query = self::query_for_type( $type, $page, min( $limit, 100 ), $since_gmt );

			if ( ! empty( $query->posts ) ) {
				foreach ( $query->posts as $post ) {
					if ( count( $records ) >= $limit ) {
						break;
					}
					if ( ! ( $post instanceof \WP_Post ) ) {
						continue;
					}
					$records[] = self::format_record( $post, $include_body );
				}
			}

			if ( $page < (int) $query->max_num_pages ) {
				$page += 1;
				if ( count( $records ) >= $limit ) {
					break;
				}
			} else {
				$type_index += 1;
				$page        = 1;
			}
		}

		$done        = $type_index >= count( $types );
		$next_cursor = $done ? null : self::encode_cursor(
			array(
				'typeIndex' => $type_index,
				'page'      => $page,
			)
		);

		$latest_modified       = self::latest_modified_gmt();
		$record_count          = self::record_count();
		$snapshot_version_seed = (string) $record_count . '|' . (string) $latest_modified . '|' . (string) self::SCHEMA_VERSION;
		$snapshot_version      = 'plugin:' . md5( $snapshot_version_seed );

		return rest_ensure_response(
			array(
				'ok'              => true,
				'schemaVersion'   => self::SCHEMA_VERSION,
				'snapshotVersion' => $snapshot_version,
				'nextCursor'      => $next_cursor,
				'done'            => $done,
				'records'         => $records,
				'tombstones'      => array(),
			)
		);
	}
}
