<?php
/**
 * Wrenio URL Rewriter Class
 *
 * Rewrites WordPress attachment URLs to serve from Cloudflare R2.
 * Only rewrites URLs for files confirmed on R2 (have _r2_url or _r2_url_{size}
 * post meta). Unsynced files continue serving from local.
 *
 * @package Wrenio_Media_Offload
 * @since 1.0.0
 */

namespace Wrenio_Media_Offload;

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

/**
 * Class Wrenio_URL_Rewriter
 */
class Wrenio_URL_Rewriter {

    /** @var array */
    private $settings;

    /** @var Wrenio_Logger */
    private $logger;

    /**
     * Constructor
     *
     * @param array $settings Plugin settings
     */
    public function __construct( $settings ) {
        $this->settings = $settings;
        $this->logger   = Wrenio_Logger::get_instance();
        $this->init_hooks();
    }

    /**
     * Initialize rewrite hooks
     */
    private function init_hooks() {
        add_filter( 'wp_get_attachment_url', array( $this, 'rewrite_attachment_url' ), 10, 2 );
        add_filter( 'wp_calculate_image_srcset', array( $this, 'rewrite_srcset' ), 10, 5 );
        add_filter( 'wp_get_attachment_image_src', array( $this, 'rewrite_image_src' ), 10, 3 );
        add_filter( 'wp_get_attachment_image_attributes', array( $this, 'rewrite_image_attributes' ), 10, 3 );
        add_filter( 'the_content', array( $this, 'rewrite_content_images' ), 10 );

        // Fallbacks for when local files are deleted
        add_filter( 'wp_prepare_attachment_for_js', array( $this, 'fix_media_library_thumbnails' ), 10, 3 );
        add_filter( 'wp_get_attachment_image_src', array( $this, 'fix_missing_thumbnail_src' ), 999, 3 );
    }

    /**
     * Get the R2 URL for a specific attachment size, if it exists on R2.
     *
     * @param int          $attachment_id Attachment ID
     * @param string|array $size          WordPress size name or [width, height] array
     * @return string R2 URL if the size is on R2, empty string otherwise
     */
    private function get_r2_url_for_size( $attachment_id, $size = 'full' ) {
        // Normalize array sizes to 'full' (arrays don't map to named meta keys)
        if ( is_array( $size ) ) {
            $size = 'full';
        }

        // Prime meta cache to avoid N+1 queries when rewriting many sizes.
        // Use identity check — wp_cache_get returns false on miss, but the cached
        // value for an attachment with no meta is array(), which is also falsy.
        if ( false === wp_cache_get( $attachment_id, 'post_meta' ) ) {
            get_post_meta( $attachment_id );
        }

        $meta_key = $size === 'full' ? '_r2_url' : '_r2_url_' . $size;
        $url      = get_post_meta( $attachment_id, $meta_key, true );

        return ! empty( $url ) ? (string) $url : '';
    }

    /**
     * Rewrite attachment URL to serve from R2 (full-size only).
     *
     * @param string $url           Original URL
     * @param int    $attachment_id Attachment ID
     * @return string
     */
    public function rewrite_attachment_url( $url, $attachment_id ) {
        if ( empty( $this->settings['enable_url_rewrite'] ) ) {
            return $url;
        }

        // Only rewrite full-size URLs here; size-specific filters handle the rest
        if ( preg_match( '/-[0-9]+x[0-9]+\.[a-z]+$/i', $url ) ) {
            return $url;
        }

        $r2_url = $this->get_r2_url_for_size( $attachment_id, 'full' );
        return ! empty( $r2_url ) ? $r2_url : $url;
    }

    /**
     * Rewrite srcset for responsive images.
     *
     * @param array  $sources
     * @param array  $size_array
     * @param string $image_src
     * @param array  $image_meta
     * @param int    $attachment_id
     * @return array
     */
    public function rewrite_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
        if ( empty( $this->settings['enable_url_rewrite'] ) ) {
            return $sources;
        }

        $r2_base_url = $this->get_r2_url_for_size( $attachment_id, 'full' );
        if ( empty( $r2_base_url ) ) {
            return $sources;
        }

        $r2_size_urls = $this->get_all_r2_size_urls( $attachment_id, $image_meta );

        foreach ( $sources as $width => &$source ) {
            $matched = false;
            foreach ( $r2_size_urls as $size_name => $r2_url ) {
                if ( empty( $r2_url ) || ! isset( $image_meta['sizes'][ $size_name ] ) ) {
                    continue;
                }
                $size_info = $image_meta['sizes'][ $size_name ];
                if ( (int) $size_info['width'] === (int) $width ) {
                    $source['url'] = $r2_url;
                    $matched       = true;
                    break;
                }
            }

            // Full-size source has no size suffix
            if ( ! $matched && ! preg_match( '/-[0-9]+x[0-9]+\.[a-z]+$/i', $source['url'] ) ) {
                $source['url'] = $r2_base_url;
            }
        }

        return $sources;
    }

    /**
     * Rewrite image src array.
     *
     * @param array|false  $image
     * @param int          $attachment_id
     * @param string|array $size
     * @return array|false
     */
    public function rewrite_image_src( $image, $attachment_id, $size ) {
        if ( empty( $this->settings['enable_url_rewrite'] ) || empty( $image ) ) {
            return $image;
        }

        $r2_url = $this->get_r2_url_for_size( $attachment_id, $size );
        if ( ! empty( $r2_url ) ) {
            $image[0] = $r2_url;
        }

        return $image;
    }

    /**
     * Rewrite image attributes (src and srcset).
     *
     * @param array         $attr
     * @param \WP_Post      $attachment
     * @param string|array  $size
     * @return array
     */
    public function rewrite_image_attributes( $attr, $attachment, $size ) {
        if ( empty( $this->settings['enable_url_rewrite'] ) ) {
            return $attr;
        }

        $attachment_id = $attachment->ID;

        if ( isset( $attr['src'] ) ) {
            $r2_url = $this->get_r2_url_for_size( $attachment_id, $size );
            if ( ! empty( $r2_url ) ) {
                $attr['src'] = $r2_url;
            }
        }

        if ( isset( $attr['srcset'] ) ) {
            $metadata = wp_get_attachment_metadata( $attachment_id );
            if ( ! empty( $metadata ) ) {
                $attr['srcset'] = $this->rewrite_srcset_string( $attr['srcset'], $attachment_id, $metadata );
            }
        }

        return $attr;
    }

    /**
     * Rewrite images in post content.
     *
     * @param string $content
     * @return string
     */
    public function rewrite_content_images( $content ) {
        if ( empty( $this->settings['enable_url_rewrite'] ) || empty( $content ) ) {
            return $content;
        }

        $upload_dir = wp_upload_dir();
        $upload_url = $upload_dir['baseurl'];

        if ( ! preg_match_all( '/<img[^>]+>/i', $content, $matches ) ) {
            return $content;
        }

        foreach ( $matches[0] as $img_tag ) {
            if ( ! preg_match( '/src=["\']([^"\']+)["\']/i', $img_tag, $src_match ) ) {
                continue;
            }
            $src = $src_match[1];

            if ( strpos( $src, $upload_url ) === false ) {
                continue;
            }

            $attachment_id = $this->get_attachment_id_from_url( $src );
            if ( ! $attachment_id ) {
                continue;
            }

            $r2_full_url = $this->get_r2_url_for_size( $attachment_id, 'full' );
            if ( empty( $r2_full_url ) ) {
                continue;
            }

            $new_src = $this->rewrite_attachment_url( $src, $attachment_id );
            if ( $new_src === $src ) {
                continue;
            }

            // Escape the rewritten URL before inserting it back into post HTML.
            $new_img_tag = str_replace( $src, esc_url( $new_src ), $img_tag );

            if ( preg_match( '/srcset=["\']([^"\']+)["\']/i', $new_img_tag, $srcset_match ) ) {
                $srcset     = $srcset_match[1];
                $metadata   = wp_get_attachment_metadata( $attachment_id );
                $new_srcset = $this->rewrite_srcset_string( $srcset, $attachment_id, $metadata );
                // Escape each rewritten srcset URL before inserting into post HTML.
                $new_img_tag = str_replace( $srcset, $this->escape_srcset_urls( $new_srcset ), $new_img_tag );
            }

            $content = str_replace( $img_tag, $new_img_tag, $content );
        }

        return $content;
    }

    /**
     * Escape every URL in a srcset string for safe output in post content.
     *
     * A srcset is a comma-separated list of "URL [descriptor]" candidates.
     * esc_url() is applied to the URL portion only; the width/density
     * descriptor (e.g. "640w", "2x") is a fixed token preserved verbatim.
     *
     * @param string $srcset
     * @return string
     */
    private function escape_srcset_urls( $srcset ) {
        $candidates = array();

        foreach ( explode( ',', $srcset ) as $candidate ) {
            $candidate = trim( $candidate );
            if ( '' === $candidate ) {
                continue;
            }
            if ( preg_match( '/^(.*\S)\s+(\d+[wx])$/', $candidate, $m ) ) {
                $candidates[] = esc_url( $m[1] ) . ' ' . $m[2];
            } else {
                $candidates[] = esc_url( $candidate );
            }
        }

        return implode( ', ', $candidates );
    }

    /**
     * Resolve attachment ID from a media URL, with per-request caching.
     *
     * @param string $url
     * @return int|null
     */
    private function get_attachment_id_from_url( $url ) {
        static $cache = array();

        $normalized = preg_replace( '/-\d+x\d+(?=\.[a-z]+$)/i', '', $url );

        if ( isset( $cache[ $normalized ] ) ) {
            return $cache[ $normalized ];
        }

        $upload_dir = wp_upload_dir();
        $path       = str_replace( $upload_dir['baseurl'] . '/', '', $normalized );

        // Check object cache next (persistent across requests)
        $cache_key     = 'wrenio_url_to_id_' . md5( $path );
        $attachment_id = wp_cache_get( $cache_key, 'wrenio_media_offload' );

        if ( false === $attachment_id ) {
            global $wpdb;
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $attachment_id = (int) $wpdb->get_var( $wpdb->prepare(
                "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s LIMIT 1",
                '_wp_attached_file',
                $path
            ) );

            wp_cache_set( $cache_key, $attachment_id, 'wrenio_media_offload', HOUR_IN_SECONDS );
        }

        $cache[ $normalized ] = $attachment_id > 0 ? $attachment_id : null;
        return $cache[ $normalized ];
    }

    /**
     * Build lookup of R2 URLs for all sizes of an attachment.
     *
     * @param int   $attachment_id
     * @param array $metadata
     * @return array
     */
    private function get_all_r2_size_urls( $attachment_id, $metadata ) {
        if ( false === wp_cache_get( $attachment_id, 'post_meta' ) ) {
            get_post_meta( $attachment_id );
        }

        $urls = array();
        if ( ! empty( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
            foreach ( $metadata['sizes'] as $size_name => $size_data ) {
                $urls[ $size_name ] = (string) get_post_meta( $attachment_id, '_r2_url_' . $size_name, true );
            }
        }
        return $urls;
    }

    /**
     * Rewrite srcset string, only replacing URLs for sizes confirmed on R2.
     *
     * @param string     $srcset
     * @param int        $attachment_id
     * @param array|null $metadata
     * @return string
     */
    private function rewrite_srcset_string( $srcset, $attachment_id, $metadata = null ) {
        $r2_base_url = $this->get_r2_url_for_size( $attachment_id, 'full' );
        if ( empty( $r2_base_url ) ) {
            return $srcset;
        }

        if ( null === $metadata ) {
            $metadata = wp_get_attachment_metadata( $attachment_id );
        }

        $r2_size_urls = $this->get_all_r2_size_urls( $attachment_id, $metadata ?: array() );

        $parts     = explode( ',', $srcset );
        $new_parts = array();

        foreach ( $parts as $part ) {
            $part = trim( $part );
            if ( ! preg_match( '/^(.+?)\s+(\d+w|\d+x)$/', $part, $matches ) ) {
                $new_parts[] = $part;
                continue;
            }

            $url        = $matches[1];
            $descriptor = $matches[2];

            if ( preg_match( '/-(\d+)x(\d+)\.[a-z]+$/i', $url, $dim_match ) ) {
                $w          = (int) $dim_match[1];
                $rewritten  = false;
                foreach ( $r2_size_urls as $size_name => $r2_url ) {
                    if ( empty( $r2_url ) || ! isset( $metadata['sizes'][ $size_name ] ) ) {
                        continue;
                    }
                    if ( (int) $metadata['sizes'][ $size_name ]['width'] === $w ) {
                        $new_parts[] = $r2_url . ' ' . $descriptor;
                        $rewritten   = true;
                        break;
                    }
                }
                if ( ! $rewritten ) {
                    $new_parts[] = $url . ' ' . $descriptor;
                }
            } else {
                $new_parts[] = $r2_base_url . ' ' . $descriptor;
            }
        }

        return implode( ', ', $new_parts );
    }

    /**
     * Fix missing thumbnail src when local file is deleted.
     *
     * @param array|false  $image
     * @param int          $attachment_id
     * @param string|array $size
     * @return array|false
     */
    public function fix_missing_thumbnail_src( $image, $attachment_id, $size ) {
        if ( ! empty( $image ) || empty( $this->settings['enable_url_rewrite'] ) ) {
            return $image;
        }

        $size_key = is_array( $size ) ? 'full' : $size;
        $r2_url   = $this->get_r2_url_for_size( $attachment_id, $size_key );

        if ( empty( $r2_url ) && $size_key !== 'full' ) {
            $base_url = $this->get_r2_url_for_size( $attachment_id, 'full' );
            if ( ! empty( $base_url ) ) {
                $metadata = wp_get_attachment_metadata( $attachment_id );
                if ( isset( $metadata['sizes'][ $size_key ]['file'] ) ) {
                    $sized_file = $metadata['sizes'][ $size_key ]['file'];
                    $r2_url     = preg_replace( '/[^\/]+$/', rawurlencode( $sized_file ), $base_url );
                }
            }
        }

        if ( empty( $r2_url ) ) {
            return $image;
        }

        $metadata = wp_get_attachment_metadata( $attachment_id );
        $width    = 0;
        $height   = 0;

        if ( $size_key === 'full' ) {
            $width  = $metadata['width'] ?? 0;
            $height = $metadata['height'] ?? 0;
        } elseif ( isset( $metadata['sizes'][ $size_key ] ) ) {
            $width  = $metadata['sizes'][ $size_key ]['width'] ?? 0;
            $height = $metadata['sizes'][ $size_key ]['height'] ?? 0;
        }

        return array( $r2_url, $width, $height, true );
    }

    /**
     * Fix thumbnails in Media Library grid view.
     *
     * Checks post meta first to determine if local is deleted, avoiding
     * an expensive file_exists() call on every grid item.
     *
     * @param array    $response
     * @param \WP_Post $attachment
     * @param array    $meta
     * @return array
     */
    public function fix_media_library_thumbnails( $response, $attachment, $meta ) {
        if ( empty( $this->settings['enable_url_rewrite'] ) ) {
            return $response;
        }

        $attachment_id = $attachment->ID;
        $r2_base_url   = $this->get_r2_url_for_size( $attachment_id, 'full' );

        if ( empty( $r2_base_url ) ) {
            return $response;
        }

        // Check meta first (cheap) before stat-ing the filesystem
        $local_deleted = (bool) get_post_meta( $attachment_id, '_r2_local_deleted', true );
        if ( ! $local_deleted ) {
            $file_path     = get_attached_file( $attachment_id );
            $local_deleted = ! $file_path || ! file_exists( $file_path );
        }

        $response['url'] = $r2_base_url;

        if ( isset( $response['sizes'] ) ) {
            foreach ( $response['sizes'] as $size => &$size_data ) {
                $size_r2_url = $this->get_r2_url_for_size( $attachment_id, $size );

                if ( ! empty( $size_r2_url ) ) {
                    $size_data['url'] = $size_r2_url;
                } elseif ( $local_deleted ) {
                    $sized_filename   = basename( $size_data['url'] );
                    $size_data['url'] = preg_replace( '/[^\/]+$/', rawurlencode( $sized_filename ), $r2_base_url );
                }
            }
        }

        $response['cloudflare_r2_synced'] = true;
        $response['local_deleted']        = $local_deleted;

        return $response;
    }
}
