<?php
defined( 'ABSPATH' ) || exit;

class SITECL_Broken_Links {

    private const MAX_PAGES  = 100;
    private const MAX_LINKS  = 500;

    public static function scan(): array {
        $urls    = self::collect_urls();
        $checked = self::check_links( $urls );
        $broken  = array_filter( $checked, fn( $l ) => $l['http_status'] >= 400 || $l['redirect_hops'] > 2 );

        self::persist( array_values( $broken ) );

        $issues = [];
        foreach ( $broken as $link ) {
            $severity = $link['http_status'] >= 500 ? 'critical' : 'warning';
            $issues[] = [
                'type'     => 'broken_links',
                'severity' => $severity,
                'title'    => sprintf(
                    /* translators: %1$s: HTTP status code (e.g. 404), %2$s: broken URL */
                    __( 'Broken link (%1$s): %2$s', 'site-cliniq' ),
                    $link['http_status'],
                    esc_url( $link['target_url'] )
                ),
                'detail'   => sprintf(
                    /* translators: %s: URL of the page where the broken link was found */
                    __( 'Found on: %s', 'site-cliniq' ),
                    esc_url( $link['source_url'] )
                ),
                'link'     => $link,
            ];
        }

        return $issues;
    }

    // -------------------------------------------------------------------------

    /** Collect up to MAX_PAGES published URLs to crawl. */
    private static function collect_urls(): array {
        $posts = get_posts( [
            'post_type'      => [ 'post', 'page' ],
            'post_status'    => 'publish',
            'posts_per_page' => self::MAX_PAGES,
            'fields'         => 'ids',
        ] );

        return array_map( 'get_permalink', $posts );
    }

    /**
     * For each source URL, fetch its HTML and HEAD-check all hrefs.
     * Returns flat array of link result objects.
     */
    private static function check_links( array $source_urls ): array {
        $results = [];
        $checked = [];   // dedup target URLs

        foreach ( $source_urls as $source_url ) {
            $links = self::extract_links( $source_url );

            foreach ( $links as $target ) {
                if ( isset( $checked[ $target ] ) || count( $results ) >= self::MAX_LINKS ) {
                    continue;
                }

                $checked[ $target ] = true;
                $check              = self::head_check( $target );

                $results[] = [
                    'source_url'    => $source_url,
                    'target_url'    => $target,
                    'http_status'   => $check['status'],
                    'redirect_hops' => $check['hops'],
                ];
            }
        }

        return $results;
    }

    private static function extract_links( string $url ): array {
        $response = wp_remote_get( $url, [ 'timeout' => 15 ] );
        if ( is_wp_error( $response ) ) {
            return [];
        }

        $html = wp_remote_retrieve_body( $response );
        preg_match_all( '/<a\s[^>]*href=["\']([^"\'#?][^"\']*)["\'][^>]*>/i', $html, $matches );

        $links = array_unique( $matches[1] );

        return array_filter( $links, fn( $l ) => filter_var( $l, FILTER_VALIDATE_URL ) );
    }

    private static function head_check( string $url ): array {
        $hops   = 0;
        $status = 0;

        $response = wp_remote_head( $url, [ 'timeout' => 10, 'redirection' => 5 ] );

        if ( is_wp_error( $response ) ) {
            return [ 'status' => 0, 'hops' => 0 ];
        }

        $status = (int) wp_remote_retrieve_response_code( $response );

        // Count redirect hops via response headers.
        $location = wp_remote_retrieve_header( $response, 'location' );
        if ( $location && $status >= 300 && $status < 400 ) {
            $hops = 1;
        }

        return [ 'status' => $status, 'hops' => $hops ];
    }

    private static function persist( array $links ): void {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query( $wpdb->prepare( 'DELETE FROM `' . $wpdb->prefix . 'sitecl_broken_links` WHERE 1=%d', 1 ) );

        foreach ( $links as $link ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
            $wpdb->insert(
                $wpdb->prefix . 'sitecl_broken_links',
                [
                    'source_url'    => esc_url_raw( $link['source_url'] ),
                    'target_url'    => esc_url_raw( $link['target_url'] ),
                    'http_status'   => (int) $link['http_status'],
                    'redirect_hops' => (int) $link['redirect_hops'],
                    'last_checked'  => current_time( 'mysql' ),
                ],
                [ '%s', '%s', '%d', '%d', '%s' ]
            );
        }
    }
}
