<?php
/**
 * Wrenio Filename Cleanup Class — NEW in v4.1
 *
 * Detects and repairs cases where `_wp_attached_file` (local path) and
 * `_r2_key` (R2 object key) disagree on the filename portion — typically
 * double-dot or other corrupt characters that were normalized differently
 * between WordPress and R2.
 *
 * How it works:
 *  1. Scan query finds attachments whose filename portion differs between
 *     `_wp_attached_file` and `_r2_key` (the directory portion is allowed
 *     to differ — that's what R2 Reconcile handles).
 *  2. Preview: shows current vs proposed new value.
 *  3. Repair: updates `_wp_attached_file` to use a normalized form of the
 *     filename that matches what's on R2.
 *
 * Normalization uses regex to collapse runs of consecutive dots to a
 * single dot (single-pass — handles `..jpg`, `...jpg`, `....jpg` all in
 * one go, unlike SQL REPLACE which would need multiple passes).
 *
 * @package Wrenio_Media_Offload
 * @since 4.1.0
 */

namespace Wrenio_Media_Offload;

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

/**
 * Class Wrenio_Filename_Cleanup
 */
class Wrenio_Filename_Cleanup {

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

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

    private function init_hooks() {
        add_action( 'wp_ajax_wrenio_media_offload_filename_scan', array( $this, 'ajax_scan' ) );
        add_action( 'wp_ajax_wrenio_media_offload_filename_preview', array( $this, 'ajax_preview' ) );
        add_action( 'wp_ajax_wrenio_media_offload_filename_repair', array( $this, 'ajax_repair' ) );
    }

    /**
     * Count attachments with filename mismatches.
     *
     * Uses MySQL's SUBSTRING_INDEX to compare only the filename portion
     * (the last segment after the final slash), not the full path.
     *
     * @return int
     */
    public function get_mismatch_count() {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return (int) $wpdb->get_var(
            "SELECT COUNT(DISTINCT p.ID)
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} pm_file ON p.ID = pm_file.post_id AND pm_file.meta_key = '_wp_attached_file'
             INNER JOIN {$wpdb->postmeta} pm_key  ON p.ID = pm_key.post_id  AND pm_key.meta_key  = '_r2_key'
             WHERE p.post_type = 'attachment'
             AND p.post_status = 'inherit'
             AND pm_file.meta_value != ''
             AND pm_key.meta_value != ''
             AND SUBSTRING_INDEX(pm_file.meta_value, '/', -1) != SUBSTRING_INDEX(pm_key.meta_value, '/', -1)"
        );
    }

    /**
     * Get a preview of attachments with filename mismatches.
     *
     * Shows current file path, current R2 key, and proposed new file path
     * (after normalization).
     *
     * @param int $offset
     * @param int $limit
     * @return array
     */
    public function scan_batch( $offset, $limit ) {
        global $wpdb;

        $offset = max( 0, (int) $offset );
        $limit  = max( 1, min( 200, (int) $limit ) );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $rows = $wpdb->get_results( $wpdb->prepare(
            "SELECT p.ID AS id,
                    p.post_title AS title,
                    pm_file.meta_value AS current_file,
                    pm_key.meta_value AS current_key
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} pm_file ON p.ID = pm_file.post_id AND pm_file.meta_key = '_wp_attached_file'
             INNER JOIN {$wpdb->postmeta} pm_key  ON p.ID = pm_key.post_id  AND pm_key.meta_key  = '_r2_key'
             WHERE p.post_type = 'attachment'
             AND p.post_status = 'inherit'
             AND pm_file.meta_value != ''
             AND pm_key.meta_value != ''
             AND SUBSTRING_INDEX(pm_file.meta_value, '/', -1) != SUBSTRING_INDEX(pm_key.meta_value, '/', -1)
             ORDER BY p.ID ASC
             LIMIT %d OFFSET %d",
            $limit,
            $offset
        ) );

        $results = array();
        foreach ( $rows as $row ) {
            $results[] = array(
                'id'            => (int) $row->id,
                'title'         => $row->title,
                'current_file'  => $row->current_file,
                'current_key'   => $row->current_key,
                'proposed_file' => $this->normalize_path( $row->current_file ),
            );
        }

        return $results;
    }

    /**
     * Normalize a file path: collapse consecutive dots, remove leading/trailing
     * whitespace in each segment.
     *
     * Done in PHP so we can use regex for multi-dot collapse in one pass.
     * SQL's REPLACE would need multiple iterations.
     *
     * @param string $path
     * @return string
     */
    public function normalize_path( $path ) {
        // Collapse 2+ consecutive dots to a single dot.
        // This matches `..jpg`, `...jpg`, `....jpg` etc. in one pass.
        $normalized = preg_replace( '/\.{2,}/', '.', (string) $path );
        return $normalized;
    }

    /**
     * Repair a single attachment's _wp_attached_file to match its R2 key.
     *
     * This doesn't touch the R2 object — we change the WordPress record to
     * match what's actually on R2. If the attachment has already been
     * reconciled on R2 successfully, its `_r2_key` is authoritative and
     * `_wp_attached_file` should align to it.
     *
     * @param int $attachment_id
     * @return array { type, message }
     */
    public function repair_one( $attachment_id ) {
        $attachment_id = (int) $attachment_id;
        $current_file  = get_post_meta( $attachment_id, '_wp_attached_file', true );
        $current_key   = get_post_meta( $attachment_id, '_r2_key', true );
        $title         = get_the_title( $attachment_id ) ?: "Attachment {$attachment_id}";

        if ( empty( $current_file ) || empty( $current_key ) ) {
            return array(
                'type'    => 'info',
                /* translators: %s: attachment title */
                'message' => sprintf( __( 'Skipped %s: missing meta', 'wrenio-media-offload' ), $title ),
            );
        }

        // Compare filename portions only
        $current_filename  = basename( $current_file );
        $target_filename   = basename( $current_key );

        if ( $current_filename === $target_filename ) {
            return array(
                'type'    => 'info',
                /* translators: %s: attachment title */
                'message' => sprintf( __( 'Skipped %s: filenames already match', 'wrenio-media-offload' ), $title ),
            );
        }

        // Build new _wp_attached_file using current directory + normalized filename
        $directory = dirname( $current_file );
        $new_file  = ( $directory === '.' || $directory === '' )
            ? $this->normalize_path( $current_filename )
            : $directory . '/' . $this->normalize_path( $current_filename );

        // Sanity check: normalized filename should match R2 key filename
        $normalized_filename = basename( $new_file );
        if ( $normalized_filename !== $target_filename ) {
            // Fallback: use exactly the R2 key filename (not normalization result).
            // This handles cases where the difference isn't just double-dots.
            $new_file = ( $directory === '.' || $directory === '' )
                ? $target_filename
                : $directory . '/' . $target_filename;
        }

        update_post_meta( $attachment_id, '_wp_attached_file', $new_file );

        $this->logger->info( sprintf(
            '[Filename Cleanup] Attachment %d: _wp_attached_file changed from "%s" to "%s"',
            $attachment_id,
            $current_file,
            $new_file
        ) );

        return array(
            'type'    => 'success',
            /* translators: 1: attachment title, 2: old filename, 3: new filename */
            'message' => sprintf( __( 'Repaired %1$s: %2$s → %3$s', 'wrenio-media-offload' ), $title, $current_filename, basename( $new_file ) ),
        );
    }

    /**
     * AJAX: scan count.
     */
    public function ajax_scan() {
        $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
        if ( ! wp_verify_nonce( $nonce, 'wrenio_media_offload_filename' ) ) {
            wp_send_json_error( __( 'Security check failed', 'wrenio-media-offload' ) );
        }
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( __( 'Permission denied', 'wrenio-media-offload' ) );
        }

        wp_send_json_success( array(
            'count' => $this->get_mismatch_count(),
        ) );
    }

    /**
     * AJAX: preview batch.
     */
    public function ajax_preview() {
        $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
        if ( ! wp_verify_nonce( $nonce, 'wrenio_media_offload_filename' ) ) {
            wp_send_json_error( __( 'Security check failed', 'wrenio-media-offload' ) );
        }
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( __( 'Permission denied', 'wrenio-media-offload' ) );
        }

        $offset = isset( $_POST['offset'] ) ? max( 0, (int) $_POST['offset'] ) : 0;
        $limit  = isset( $_POST['limit'] ) ? (int) $_POST['limit'] : 20;
        $limit  = max( 1, min( 50, $limit ) );

        wp_send_json_success( array(
            'rows' => $this->scan_batch( $offset, $limit ),
        ) );
    }

    /**
     * AJAX: repair batch.
     */
    public function ajax_repair() {
        $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
        if ( ! wp_verify_nonce( $nonce, 'wrenio_media_offload_filename' ) ) {
            wp_send_json_error( __( 'Security check failed', 'wrenio-media-offload' ) );
        }
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( __( 'Permission denied', 'wrenio-media-offload' ) );
        }

        $batch_size = isset( $_POST['batch_size'] ) ? (int) $_POST['batch_size'] : 25;
        $batch_size = max( 1, min( 100, $batch_size ) );

        // Execute mode: always offset=0, items drop out of query as they're fixed
        $batch = $this->scan_batch( 0, $batch_size );

        $messages  = array();
        $processed = 0;
        $succeeded = 0;
        $failed    = 0;

        foreach ( $batch as $row ) {
            $result     = $this->repair_one( $row['id'] );
            $messages[] = $result;
            $processed++;
            if ( $result['type'] === 'success' ) {
                $succeeded++;
            } elseif ( $result['type'] === 'error' ) {
                $failed++;
            }
        }

        wp_send_json_success( array(
            'processed' => $processed,
            'succeeded' => $succeeded,
            'failed'    => $failed,
            'messages'  => $messages,
            'has_more'  => count( $batch ) === $batch_size,
        ) );
    }
}
