<?php

namespace WPSN\SEO;

defined('ABSPATH') || exit;

/**
 * Best-effort internal link estimators.
 *
 * Purpose: provide lightweight counts for features like “orphan pages”
 * without building a full link graph.
 */
final class InternalLinkEstimator
{
    /**
     * Estimate inbound internal links to a target URL.
     *
     * This is intentionally heuristic (LIKE query on content + Elementor JSON).
     *
     * @param int $targetPostId
     * @param string $targetUrl
     * @param string[]|null $sourcePostTypes Public post types to scan (default: all public except attachment).
     */
    public function estimateInbound(int $targetPostId, string $targetUrl, ?array $sourcePostTypes = null): int
    {
        global $wpdb;

        $targetPostId = max(1, (int) $targetPostId);
        $targetUrl = trim((string) $targetUrl);
        if ($targetPostId <= 0 || $targetUrl === '') {
            return 0;
        }

        $path = (string) wp_parse_url($targetUrl, PHP_URL_PATH);
        if ($path === '' || $path === '/') {
            // Fallback (best-effort) if URL parsing fails.
            $path = $targetUrl;
        }

        $like = '%' . $wpdb->esc_like($path) . '%';

        if ($sourcePostTypes === null) {
            $sourcePostTypes = get_post_types(['public' => true], 'names');
        }

        $sourcePostTypes = array_values(array_filter(array_map('sanitize_key', (array) $sourcePostTypes)));
        $sourcePostTypes = array_values(array_diff($sourcePostTypes, ['attachment']));

        if (empty($sourcePostTypes)) {
            return 0;
        }

        $placeholders = implode(',', array_fill(0, count($sourcePostTypes), '%s'));

        // Scan post_content and Elementor JSON storage (if present).
        // Counting DISTINCT posts prevents double counting where both match.
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $sql = "SELECT COUNT(DISTINCT p.ID)
            FROM {$wpdb->posts} p
            LEFT JOIN {$wpdb->postmeta} pm
              ON pm.post_id = p.ID AND pm.meta_key = %s
            WHERE p.post_status = %s
              AND p.post_type IN ({$placeholders})
              AND p.ID <> %d
              AND (p.post_content LIKE %s OR pm.meta_value LIKE %s)";

        $args = array_merge(
            ['_elementor_data', 'publish'],
            $sourcePostTypes,
            [$targetPostId, $like, $like]
        );

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        $prepared = $wpdb->prepare($sql, $args);
        $v = $wpdb->get_var($prepared);

        return is_numeric($v) ? (int) $v : 0;
    }

    /**
     * Estimate inbound internal links to a target URL when there is no WP post ID.
     *
     * Useful for “URL items” (custom endpoints, parameter pages, archives) that
     * are tracked outside of the WP object model.
     *
     * This is intentionally heuristic (LIKE query on content + Elementor JSON).
     *
     * @param string $targetUrl
     * @param string[]|null $sourcePostTypes Public post types to scan (default: all public except attachment).
     */
    public function estimateInboundToUrl(string $targetUrl, ?array $sourcePostTypes = null): int
    {
        global $wpdb;

        $targetUrl = trim((string) $targetUrl);
        if ($targetUrl === '') {
            return 0;
        }

        $path = (string) wp_parse_url($targetUrl, PHP_URL_PATH);
        if ($path === '' || $path === '/') {
            // Fallback (best-effort) if URL parsing fails.
            $path = $targetUrl;
        }

        $like = '%' . $wpdb->esc_like($path) . '%';

        if ($sourcePostTypes === null) {
            $sourcePostTypes = get_post_types(['public' => true], 'names');
        }

        $sourcePostTypes = array_values(array_filter(array_map('sanitize_key', (array) $sourcePostTypes)));
        $sourcePostTypes = array_values(array_diff($sourcePostTypes, ['attachment']));

        if (empty($sourcePostTypes)) {
            return 0;
        }

        $placeholders = implode(',', array_fill(0, count($sourcePostTypes), '%s'));

        // Scan post_content and Elementor JSON storage (if present).
        // Counting DISTINCT posts prevents double counting where both match.
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $sql = "SELECT COUNT(DISTINCT p.ID)
            FROM {$wpdb->posts} p
            LEFT JOIN {$wpdb->postmeta} pm
              ON pm.post_id = p.ID AND pm.meta_key = %s
            WHERE p.post_status = %s
              AND p.post_type IN ({$placeholders})
              AND (p.post_content LIKE %s OR pm.meta_value LIKE %s)";

        $args = array_merge(
            ['_elementor_data', 'publish'],
            $sourcePostTypes,
            [$like, $like]
        );

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        $prepared = $wpdb->prepare($sql, $args);
        $v = $wpdb->get_var($prepared);

        return is_numeric($v) ? (int) $v : 0;
    }
}
