<?php

namespace SerpIQ\Links;

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

/**
 * Internal Links indexer (M1 - link graph).
 *
 * Scans published posts/pages and builds a link graph of internal links:
 * from_post_id -> to_post_id, with URLs and optional anchor text.
 * Data is stored in the serpiq_links table and will be used by
 * Internal Linking Opportunities (later milestones).
 */
final class Internal_Links {
	private static ?Internal_Links $instance = null;

	public static function instance(): Internal_Links {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	private function __construct() {}

	/**
	 * Rebuild the internal links index from scratch.
	 *
	 * @param array $args Optional:
	 *                    - post_types: array of post type names to include.
	 *                    - batch_size: int, how many posts per query (default 200).
	 * @return array{ scanned:int, links:int }
	 */
	public function rebuild_index( array $args = array() ): array {
		global $wpdb;

		$table = $this->get_table_name();

		// Determine which post types to index.
		$post_types = array();
		if ( isset( $args['post_types'] ) && is_array( $args['post_types'] ) ) {
			$post_types = array_map( 'sanitize_key', $args['post_types'] );
		} else {
			$post_types = get_post_types(
				array(
					'public' => true,
				),
				'names'
			);
			unset( $post_types['attachment'] );
		}

		if ( empty( $post_types ) ) {
			return array(
				'scanned' => 0,
				'links'   => 0,
			);
		}

		$batch_size = isset( $args['batch_size'] ) && (int) $args['batch_size'] > 0 ? (int) $args['batch_size'] : 200;

		// Clear existing data.
		$expected_table = $wpdb->prefix . 'serpiq_links';
		if ( $table === $expected_table && preg_match( '/^[a-zA-Z0-9_]+$/', $table ) ) {
			$wpdb->query( $wpdb->prepare( 'TRUNCATE TABLE %i', $table ) );
		}

		$scanned = 0;
		$links   = 0;

		$home_url = home_url();
		$home_host = wp_parse_url( $home_url, PHP_URL_HOST );

		$paged = 1;
		do {
			$q = new \WP_Query(
				array(
					'post_type'      => $post_types,
					'post_status'    => 'publish',
					'posts_per_page' => $batch_size,
					'paged'          => $paged,
					'fields'         => 'all',
					'no_found_rows'  => false,
				)
			);

			if ( ! $q->have_posts() ) {
				break;
			}

			foreach ( $q->posts as $post ) {
				if ( ! $post instanceof \WP_Post ) {
					continue;
				}

				$scanned++;
				$from_post_id = (int) $post->ID;
				$from_url     = get_permalink( $from_post_id );

				$rows = $this->extract_internal_links_from_content( $post->post_content, $from_post_id, $from_url, $home_host );
				if ( empty( $rows ) ) {
					continue;
				}

				foreach ( $rows as $row ) {
					$inserted = $wpdb->insert(
						$table,
						array(
							'from_post_id' => $row['from_post_id'],
							'to_post_id'   => $row['to_post_id'],
							'from_url'     => $row['from_url'],
							'to_url'       => $row['to_url'],
							'anchor_text'  => $row['anchor_text'],
						),
						array( '%d', '%d', '%s', '%s', '%s' )
					);
					if ( $inserted ) {
						$links++;
					}
				}
			}

			$paged++;
		} while ( $paged <= (int) $q->max_num_pages );

		return array(
			'scanned' => $scanned,
			'links'   => $links,
		);
	}

	/**
	 * Extract internal links (to other posts) from HTML content.
	 *
	 * @param string $content
	 * @param int    $from_post_id
	 * @param string $from_url
	 * @param string $home_host
	 * @return array<int,array{from_post_id:int,to_post_id:int,from_url:string,to_url:string,anchor_text:string}>
	 */
	private function extract_internal_links_from_content( string $content, int $from_post_id, string $from_url, string $home_host ): array {
		$results = array();

		if ( '' === $content ) {
			return $results;
		}

		// Find all anchor tags with href; simple regex is sufficient here.
		if ( ! preg_match_all( '#<a\s[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>#is', $content, $matches, PREG_SET_ORDER ) ) {
			return $results;
		}

		foreach ( $matches as $m ) {
			$href   = isset( $m[1] ) ? trim( (string) $m[1] ) : '';
			$anchor = isset( $m[2] ) ? wp_strip_all_tags( (string) $m[2], true ) : '';

			if ( '' === $href ) {
				continue;
			}

			// Skip mailto:, tel:, javascript: and pure fragments.
			if ( preg_match( '#^(mailto:|tel:|javascript:)#i', $href ) ) {
				continue;
			}
			if ( 0 === strpos( $href, '#' ) ) {
				continue;
			}

			$absolute = $this->normalize_internal_url( $href, $home_host );
			if ( '' === $absolute ) {
				continue;
			}

			$target_id = url_to_postid( $absolute );
			if ( $target_id <= 0 || $target_id === $from_post_id ) {
				continue;
			}

			$to_url = get_permalink( $target_id );
			if ( ! $to_url ) {
				continue;
			}

			$anchor_clean = trim( wp_html_excerpt( $anchor, 200, '…' ) );

			$results[] = array(
				'from_post_id' => $from_post_id,
				'to_post_id'   => (int) $target_id,
				'from_url'     => $from_url,
				'to_url'       => $to_url,
				'anchor_text'  => $anchor_clean,
			);
		}

		return $results;
	}

	/**
	 * Normalize a href into an absolute URL on the same host, or return empty string if external.
	 *
	 * @param string $href
	 * @param string $home_host
	 * @return string
	 */
	private function normalize_internal_url( string $href, string $home_host ): string {
		$href = trim( $href );

		// Relative paths starting with / or without scheme.
		if ( 0 === strpos( $href, '//' ) ) {
			// Protocol-relative URL.
			$href = ( is_ssl() ? 'https:' : 'http:' ) . $href;
		} elseif ( 0 === strpos( $href, '/' ) ) {
			$href = home_url( $href );
		} elseif ( ! preg_match( '#^[a-z][a-z0-9+\-.]*://#i', $href ) ) {
			// No scheme, treat as relative to home.
			$href = home_url( '/' . ltrim( $href, '/' ) );
		}

		$parts = wp_parse_url( $href );
		if ( ! is_array( $parts ) || empty( $parts['host'] ) ) {
			return '';
		}

		if ( strtolower( $parts['host'] ) !== strtolower( (string) $home_host ) ) {
			return '';
		}

		// Rebuild URL (keep path + query, ignore fragments).
		$scheme = ! empty( $parts['scheme'] ) ? $parts['scheme'] : ( is_ssl() ? 'https' : 'http' );
		$path   = isset( $parts['path'] ) ? $parts['path'] : '/';
		$query  = isset( $parts['query'] ) ? '?' . $parts['query'] : '';

		return $scheme . '://' . $parts['host'] . $path . $query;
	}

	/**
	 * Get the fully qualified table name for serpiq_links.
	 */
	private function get_table_name(): string {
		global $wpdb;
		return $wpdb->prefix . 'serpiq_links';
	}
}

