<?php
namespace Avision;

/**
 * Mainteneurs du Mode Gardien — Vague 3 (Maintenance & Fraîcheur)
 *
 * Trois boucliers de maintenance :
 *   4. Image Cleaner    — Détection photos clients non conformes / basse résolution
 *   5. Drip Feed        — Publication automatique des photos en file d'attente (1 / 7 jours)
 *   6. Infinite Posts   — Recyclage des posts (réécriture + republication si > 7 jours)
 *
 * Réutilise les classes existantes sans duplication :
 *   - Google_My_Business_API → get_customer_photos(), create_media_item(), get_local_posts(), create_local_post()
 *   - Guardian_Response_Generator → apply_temperature_variation() (réécritures de posts)
 *   - Table avi_saas_media  → file d'attente Drip Feed (status in_stock → published)
 *   - Table avi_saas_posts  → historique posts Google (type update/offer)
 *
 * @package Avision
 */

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

class Avision_Guardian_Maintainers {

    private const LOG_FILE        = 'logs/guardian-maintainers.log';
    private const CRON_TASK_LOG   = 'logs/cron-task.log';
    private const DRIP_INTERVAL   = 7;   // jours entre chaque publication photo
    private const POST_INTERVAL   = 7;   // jours entre chaque post Google
    private const MIN_WIDTH       = 720; // largeur min acceptable (pixels)
    private const MIN_HEIGHT      = 480; // hauteur min acceptable (pixels)

    // ──────────────────────────────────────────────
    // Point d'entrée
    // ──────────────────────────────────────────────

    /**
     * Exécute les 3 boucliers de maintenance pour un utilisateur.
     * Cold Start : si compte Premium (Mode Gardien) et aucun post publié, génère immédiatement
     * un post "Présentation" (sans attendre 7 jours). [cite: 2026-02-17]
     *
     * @param int   $user_id
     * @param array $profile  Ligne complète avi_saas_profiles
     * @return array{cleaned: int, drip: int, posts: int}
     */
    public function run_wave3( int $user_id, array $profile ): array {
        $results = [ 'cleaned' => 0, 'drip' => 0, 'posts' => 0 ];

        $token         = trim( (string) ( $profile['google_access_token'] ?? '' ) );
        $location_id   = trim( (string) ( $profile['google_location_name'] ?? '' ) );
        $has_google_api = $token !== '' && $location_id !== '';

        $profile_id   = (int) ( $profile['id'] ?? 0 );
        $is_premium   = ! empty( $profile['guardian_active'] ) && (int) $profile['guardian_active'] === 1;
        $posts_count  = $this->get_published_posts_count( $profile_id );

        // Limiter à un seul Cold Start par exécution cron si le filtre est activé (évite 3 posts quand 3 profils éligibles).
        static $cold_start_done_this_run = false;
        $limit_one = (bool) apply_filters( 'avision_cold_start_limit_one_per_run', true );
        if ( $limit_one && $cold_start_done_this_run ) {
            $is_premium = false; // Skip Cold Start pour les profils suivants dans ce run.
        }

        // Hiérarchie stricte Wave 3 (Silence Radio) : priorité Cold Start, puis Infinite Posts si dernier post > 7 jours. On ne recycle pas un post si on est en Cold Start.
        if ( $posts_count === 0 ) {
            // SI le nombre de posts publiés est égal à 0 → Cold Start uniquement, puis ARRÊTER LÀ (ne pas passer à Image Cleaner / Drip / Infinite Posts).
            if ( $is_premium && ! $this->has_cold_start_draft( $profile_id ) ) {
                $cold = $this->cold_start_presentation_post( $user_id, $profile );
                if ( $cold > 0 ) {
                    $results['posts'] += $cold;
                    $cold_start_done_this_run = true;
                    $this->log( 'OK', "Cold Start: post Présentation généré (user={$user_id}, profile={$profile_id})" );
                }
                if ( $has_google_api ) {
                    $drip_cold = $this->cold_start_drip_if_empty( $user_id, $profile );
                    if ( $drip_cold > 0 ) {
                        $results['drip'] += $drip_cold;
                    }
                }
            }
            $this->log( 'INFO', "Wave3 terminée (posts=0, Cold Start prioritaire): user={$user_id} cleaned={$results['cleaned']}, drip={$results['drip']}, posts={$results['posts']}" );
            return $results;
        }

        // Boucliers suivants uniquement si token/location disponibles (appels API Google).
        if ( ! $has_google_api ) {
            $this->log( 'DEBUG', "Wave3: pas de token/location pour user={$user_id}, skip Image Cleaner / Drip / Infinite Posts" );
            $this->log( 'INFO', "Wave3 terminée: user={$user_id} cleaned={$results['cleaned']}, drip={$results['drip']}, posts={$results['posts']}" );
            return $results;
        }

        // Bouclier 4 — Image Cleaner
        $results['cleaned'] = $this->process_image_cleaner( $user_id, $profile );

        // Bouclier 5 — Drip Feed
        $results['drip'] += $this->manage_drip_feed( $user_id, $profile );

        // Bouclier 6 — Infinite Posts : SINON SI le dernier post date de plus de 7 jours → recycler (handle_infinite_posts vérifie is_post_due).
        $results['posts'] += $this->handle_infinite_posts( $user_id, $profile );

        $this->log( 'INFO', "Wave3 terminée: user={$user_id} cleaned={$results['cleaned']}, drip={$results['drip']}, posts={$results['posts']}" );
        return $results;
    }

    /**
     * Indique si un brouillon Cold Start "Présentation" existe déjà pour ce profil (évite doublons à chaque cron).
     */
    private function has_cold_start_draft( int $profile_id ): bool {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_posts';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return false;
        }
        $count = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE profile_id = %d AND status = 'draft' AND title = %s LIMIT 1",
            $profile_id,
            'Présentation — Bienvenue'
        ) );
        return $count > 0;
    }

    /**
     * Nombre de posts publiés pour un profil (avi_saas_posts, status = published).
     */
    private function get_published_posts_count( int $profile_id ): int {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_posts';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return 0;
        }
        return (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE profile_id = %d AND status = 'published'",
            $profile_id
        ) );
    }

    /**
     * Cold Start 100 % VIP : génère et publie un post "Présentation" via templates 3 paragraphes (Guardian_Response_Generator).
     * Aucun texte par défaut : rewrite_post_text avec contexte (city, business_name, category). [cite: 2026-01-27]
     * Image : get_best_available_image appelé en premier ; URL injectée dans le post / média avant toute insertion en BDD.
     * Le simulateur (AVI_Tester) n'appelle pas cette méthode ni n'insère dans avi_saas_posts. [cite: 2026-02-23]
     *
     * @return int 1 si publié, 0 sinon
     */
    private function cold_start_presentation_post( int $user_id, array $profile ): int {
        global $wpdb;
        $profile_id  = (int) ( $profile['id'] ?? 0 );
        if ( self::is_hotel_lodging_standard_posts_restricted( $profile ) ) {
            $this->log( 'DEBUG', "Cold Start: skip hôtel/hébergement (posts standard restreints) profile={$profile_id}" );
            return 0;
        }
        $token       = trim( (string) ( $profile['google_access_token'] ?? '' ) );
        $location_id = trim( (string) ( $profile['google_location_name'] ?? '' ) );
        $city        = trim( (string) ( $profile['city'] ?? '' ) );
        $business    = trim( (string) ( $profile['business_name'] ?? '' ) );
        $category    = trim( (string) ( $profile['category'] ?? '' ) );

        $lock_key = 'avision_cold_start_lock_' . $profile_id;
        if ( get_transient( $lock_key ) ) {
            $this->log( 'DEBUG', "Cold Start: verrou actif pour profile_id={$profile_id}, abandon." );
            return 0;
        }
        set_transient( $lock_key, 1, 45 );

        // Blocage doublons : vérification SQL ultra-stricte — si un seul enregistrement "Présentation" existe, on arrête TOUT. [cite: 2026-02-23]
        $table = $wpdb->prefix . 'avi_saas_posts';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table ) {
            $existing_id = $wpdb->get_var( $wpdb->prepare(
                "SELECT id FROM {$table} WHERE profile_id = %d AND title LIKE %s LIMIT 1",
                $profile_id,
                '%Présentation%'
            ) );
            if ( $existing_id !== null && $existing_id !== false && (int) $existing_id > 0 ) {
                delete_transient( $lock_key );
                $this->log( 'DEBUG', "Cold Start: doublon bloqué (id={$existing_id} pour profile_id={$profile_id})" );
                return 0;
            }
        }

        // Image en premier : force get_best_available_image, URL injectée dans le post avant toute insertion en BDD. [cite: 2026-01-27]
        $img_result = $this->get_best_available_image( $profile_id, $category, $city );
        $image_url  = isset( $img_result['url'] ) && $img_result['url'] !== '' ? $img_result['url'] : null;
        $img_source = $img_result['source'] ?? 'external';
        if ( $image_url === null || $image_url === '' ) {
            // Pas d’image stock / placeholder externe (règle Google 2026 — aligné avec get_best_available_image).
            $img_source = 'none';
            $image_url  = null;
        }

        // Contenu 100 % VIP : aucun texte fantôme (Bonjour {city}, Nous sommes ravis). Templates 3 paragraphes uniquement. [cite: 2026-01-27]
        $ctx = [
            'city'          => trim( $city ) !== '' ? $city : 'votre ville',
            'business_name' => trim( $business ) !== '' ? $business : 'notre établissement',
            'category'      => trim( $category ) !== '' ? $category : 'nos services',
        ];
        $content = '';
        if ( class_exists( __NAMESPACE__ . '\Guardian_Response_Generator' ) ) {
            $generator = new Guardian_Response_Generator();
            $content   = $generator->rewrite_post_text( '', $ctx );
        }
        $content = is_string( $content ) ? trim( $content ) : '';
        // Force VIP : le post n'existe QUE si format 3 paragraphes (au moins 2 séparateurs \n\n, longueur minimale). Interdiction d'insertion sinon.
        if ( $content === '' || substr_count( $content, "\n\n" ) < 2 || strlen( $content ) < 80 ) {
            delete_transient( $lock_key );
            $this->log( 'DEBUG', "Cold Start: contenu non-VIP ou générateur absent (paragraphes=" . substr_count( $content, "\n\n" ) . ", len=" . strlen( $content ) . "), insertion interdite." );
            return 0;
        }
        // Éradication fantôme : aucun post sans contenu Premium (> 100 car. + au moins un emoji).
        if ( strlen( $content ) < 100 ) {
            delete_transient( $lock_key );
            $this->log( 'DEBUG', 'Cold Start: contenu trop court (< 100 car.), insertion interdite.' );
            return 0;
        }
        if ( ! preg_match( '/[\x{1F300}-\x{1F9FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}]/u', $content ) ) {
            delete_transient( $lock_key );
            $this->log( 'DEBUG', 'Cold Start: contenu sans emoji (non Premium), insertion interdite.' );
            return 0;
        }
        // Extermination fantôme (Ghost Kill) : "Bonjour London", "ravis de vous accueillir" → annule l'insertion
        if ( stripos( $content, 'Bonjour London' ) !== false || stripos( $content, 'ravis de vous accueillir' ) !== false ) {
            delete_transient( $lock_key );
            $this->log( 'DEBUG', 'Ghost Text detected, generation retried.' );
            error_log( 'AVI Cold Start: Ghost Text detected, generation retried.' );
            return 0;
        }
        // Autres motifs fantôme (Bonjour+ville, Nous sommes ravis)
        if ( stripos( $content, 'Bonjour ' ) !== false && ( stripos( $content, 'ville' ) !== false || stripos( $content, $city ) !== false || stripos( $content, '{city}' ) !== false ) ) {
            delete_transient( $lock_key );
            $this->log( 'DEBUG', 'Ghost Text detected, generation retried.' );
            error_log( 'AVI Cold Start: Ghost Text detected, generation retried.' );
            return 0;
        }
        if ( stripos( $content, 'Nous sommes ravis' ) !== false ) {
            delete_transient( $lock_key );
            $this->log( 'DEBUG', 'Ghost Text detected, generation retried.' );
            error_log( 'AVI Cold Start: Ghost Text detected, generation retried.' );
            return 0;
        }

        $this->log_cron_task( 'Cold Start', sprintf(
            'profile_id=%d category=%s image=%s source=%s',
            $profile_id,
            $category ?: '(vide)',
            $image_url ? 'trouvée' : 'échec',
            $img_source
        ) );
        if ( $image_url !== null && $image_url !== '' ) {
            error_log( 'AVI Post Creation: Image used is ' . $image_url );
        }

        // Sans token/location (ex. profil WordPress sans sync Google) : créer uniquement le brouillon
        // (texte + image Unsplash enregistrée en in_stock) pour affichage dans le Centre d'intervention.
        if ( $token === '' || $location_id === '' ) {
            $this->save_cold_start_post_record( $user_id, $profile_id, $content, 'draft' );
            if ( $image_url !== null && $image_url !== '' && ( $img_result['source'] ?? '' ) === 'external' ) {
                $this->save_cold_start_media_in_stock( $profile_id, $image_url, $category );
            }
            delete_transient( $lock_key );
            $this->log( 'OK', "Cold Start: brouillon Présentation créé (sans API Google) user={$user_id}, profile={$profile_id}" );
            return 0;
        }

        if ( ! class_exists( __NAMESPACE__ . '\Google_My_Business_API' ) ) {
            $this->save_cold_start_post_record( $user_id, $profile_id, $content, 'draft' );
            if ( $image_url !== null && $image_url !== '' && ( $img_result['source'] ?? '' ) === 'external' ) {
                $this->save_cold_start_media_in_stock( $profile_id, $image_url, $category );
            }
            delete_transient( $lock_key );
            return 0;
        }

        $api    = new Google_My_Business_API( $token, $location_id );
        $media  = [];
        if ( $image_url !== null && $image_url !== '' ) {
            $media = [ 'sourceUrl' => $image_url, 'mediaFormat' => 'PHOTO' ];
        }
        $result = $api->create_local_post( $content, 'STANDARD', $media );

        if ( $result && ! isset( $result['error'] ) ) {
            $this->save_cold_start_post_record( $user_id, $profile_id, $content, 'published' );
            if ( $image_url !== null && $image_url !== '' && ( $img_result['source'] ?? '' ) === 'external' ) {
                $this->save_cold_start_media_published( $profile_id, $image_url, $category );
            }
            delete_transient( $lock_key );
            return 1;
        }

        $this->save_cold_start_post_record( $user_id, $profile_id, $content, 'draft' );
        if ( $image_url !== null && $image_url !== '' && ( $img_result['source'] ?? '' ) === 'external' ) {
            $this->save_cold_start_media_in_stock( $profile_id, $image_url, $category );
        }
        delete_transient( $lock_key );
        return 0;
    }

    /**
     * Enregistre le post Cold Start dans avi_saas_posts.
     */
    private function save_cold_start_post_record( int $user_id, int $profile_id, string $content, string $status ): void {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_posts';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return;
        }
        $published_at = ( $status === 'published' ) ? current_time( 'mysql', true ) : null;
        $wpdb->insert( $table, [
            'user_id'      => $user_id,
            'profile_id'   => $profile_id,
            'post_type'    => 'update',
            'title'        => 'Présentation — Bienvenue',
            'content'      => $content,
            'status'       => $status,
            'published_at' => $published_at,
            'created_at'   => current_time( 'mysql', true ),
        ], [ '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s' ] );
    }

    /**
     * Enregistre l'image Unsplash proposée (brouillon Cold Start) dans avi_saas_media avec status 'in_stock'
     * pour affichage dans le Centre d'intervention (proposition post + image).
     *
     * @param int    $profile_id
     * @param string $image_url URL de l'image (ex. Unsplash)
     * @param string $category  Catégorie métier (optionnel)
     */
    private function save_cold_start_media_in_stock( int $profile_id, string $image_url, string $category = '' ): void {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_media';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return;
        }
        $now = current_time( 'mysql', true );
        $wpdb->insert( $table, [
            'profile_id'   => $profile_id,
            'file_path'    => $image_url,
            'media_type'   => 'photo',
            'category'     => $category !== '' ? $category : null,
            'status'       => 'in_stock',
            'published_at' => null,
            'created_at'   => $now,
        ], [ '%d', '%s', '%s', '%s', '%s', '%s', '%s' ] );
    }

    /**
     * Enregistre l'image Unsplash utilisée (post ou Drip) dans avi_saas_media avec status 'published'
     * pour ne pas être réutilisée immédiatement par le Drip-Feed. [cite: 2026-02-17]
     *
     * @param int    $profile_id
     * @param string $image_url URL de l'image (ex. Unsplash)
     * @param string $category  Catégorie métier (optionnel)
     */
    private function save_cold_start_media_published( int $profile_id, string $image_url, string $category = '' ): void {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_media';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return;
        }
        $now = current_time( 'mysql', true );
        $wpdb->insert( $table, [
            'profile_id'   => $profile_id,
            'file_path'    => $image_url,
            'media_type'   => 'photo',
            'category'     => $category !== '' ? $category : null,
            'status'       => 'published',
            'published_at' => $now,
            'created_at'   => $now,
        ], [ '%d', '%s', '%s', '%s', '%s', '%s', '%s' ] );
    }

    // ══════════════════════════════════════════════
    // BOUCLIER 4 — Image Cleaner
    // ══════════════════════════════════════════════

    /**
     * Récupère les photos clients et génère des alertes pour celles
     * de faible résolution ou signalées non conformes par l'API.
     *
     * @param int   $user_id
     * @param array $profile
     * @return int  Nombre d'alertes générées
     */
    public function process_image_cleaner( int $user_id, array $profile ): int {
        $token       = trim( (string) ( $profile['google_access_token'] ?? '' ) );
        $location_id = trim( (string) ( $profile['google_location_name'] ?? '' ) );

        if ( ! class_exists( __NAMESPACE__ . '\Google_My_Business_API' ) ) {
            $this->log( 'ERROR', 'Image Cleaner: Google_My_Business_API introuvable' );
            return 0;
        }

        $api    = new Google_My_Business_API( $token, $location_id );
        $photos = $api->get_customer_photos( 30 );

        if ( empty( $photos ) ) {
            $this->log( 'DEBUG', "Image Cleaner: aucune photo client pour user={$user_id}" );
            return 0;
        }

        $alerts     = 0;
        $cache_key  = 'avi_image_cleaner_' . (int) ( $profile['id'] ?? 0 );
        $known_ids  = get_option( $cache_key, [] );
        $new_ids    = [];

        foreach ( $photos as $photo ) {
            $media_name = $photo['name'] ?? '';
            $media_key  = md5( $media_name );

            $new_ids[] = $media_key;

            if ( in_array( $media_key, $known_ids, true ) ) {
                continue;
            }

            $flagged = false;
            $reason  = '';

            // Vérification de la résolution
            $width  = (int) ( $photo['dimensions']['widthPixels']  ?? 0 );
            $height = (int) ( $photo['dimensions']['heightPixels'] ?? 0 );

            if ( $width > 0 && $height > 0 && ( $width < self::MIN_WIDTH || $height < self::MIN_HEIGHT ) ) {
                $flagged = true;
                $reason  = "Résolution faible ({$width}x{$height}px, min " . self::MIN_WIDTH . 'x' . self::MIN_HEIGHT . ')';
            }

            // Vérification du statut Google (rejeté, en cours de modération)
            $state = $photo['googleUpdatedTime'] ?? '';
            $view  = $photo['viewCount'] ?? null;

            if ( isset( $photo['insights']['viewCount'] ) && (int) $photo['insights']['viewCount'] === 0 && ! empty( $state ) ) {
                $flagged = true;
                $reason  = $reason !== '' ? $reason . ' + possible rejet Google' : 'Photo possiblement rejetée par Google (0 vues)';
            }

            if ( ! $flagged ) {
                continue;
            }

            $reviewer   = $photo['attribution']['profileName'] ?? 'Client';
            $source_url = $photo['thumbnailUrl'] ?? $photo['sourceUrl'] ?? '';

            $msg_fr = "Image Cleaner : photo de « {$reviewer} » signalée. {$reason}.";
            $msg_en = "Image Cleaner: photo from \"{$reviewer}\" flagged. {$reason}.";

            if ( $source_url !== '' ) {
                $msg_fr .= " URL : {$source_url}";
                $msg_en .= " URL: {$source_url}";
            }

            $this->write_security_log( $user_id, 'real', 12, 'alert', $msg_fr, $msg_en );
            $alerts++;
            $this->log( 'ALERT', "Image Cleaner: {$reason} — photo par « {$reviewer} » (user={$user_id})" );
        }

        update_option( $cache_key, $new_ids, false );

        return $alerts;
    }

    // ══════════════════════════════════════════════
    // BOUCLIER 5 — Drip Feed (Photos)
    // ══════════════════════════════════════════════

    /**
     * Vérifie la dernière photo publiée. Si > DRIP_INTERVAL jours,
     * publie la photo suivante depuis la file d'attente avi_saas_media.
     *
     * @param int   $user_id
     * @param array $profile
     * @return int  1 si une photo a été publiée, 0 sinon
     */
    public function manage_drip_feed( int $user_id, array $profile ): int {
        $profile_id  = (int) ( $profile['id'] ?? 0 );
        $token       = trim( (string) ( $profile['google_access_token'] ?? '' ) );
        $location_id = trim( (string) ( $profile['google_location_name'] ?? '' ) );
        $pilot_mode  = strtolower( trim( (string) ( $profile['pilot_posts_mode'] ?? 'manual' ) ) );

        if ( $pilot_mode !== 'auto' ) {
            $this->log( 'DEBUG', "Drip Feed: mode manuel, skip user={$user_id}" );
            return 0;
        }

        // Règle Google 2026 : bloquer Auto-pilot Drip si aucune photo réelle en file
        $queue_count = $this->count_media_in_queue( $profile_id );
        if ( $queue_count === 0 ) {
            $this->log( 'INFO', "Drip Feed Auto-pilot: galerie vide — mode auto suspendu jusqu'à ajout de photos réelles. Profile={$profile_id}" );
            update_user_meta( $user_id, 'avi_drip_gallery_empty_warning', 1 );
            return 0;
        }
        delete_user_meta( $user_id, 'avi_drip_gallery_empty_warning' );

        if ( ! $this->is_drip_due( $profile_id ) ) {
            return 0;
        }

        $next_media = $this->get_next_media_in_queue( $profile_id );
        if ( ! $next_media ) {
            $this->log( 'DEBUG', "Drip Feed: file d'attente vide pour profile={$profile_id}" );
            return 0;
        }

        $file_path   = $next_media['file_path'] ?? '';
        $category    = $next_media['category'] ?? '';
        $description = $category !== '' ? $category : 'Photo ' . ( $next_media['media_type'] ?? 'photo' );

        if ( ! class_exists( __NAMESPACE__ . '\Google_My_Business_API' ) ) {
            $this->log( 'ERROR', 'Drip Feed: Google_My_Business_API introuvable' );
            return 0;
        }

        $api    = new Google_My_Business_API( $token, $location_id );
        $result = $api->create_media_item( $file_path, 'ADDITIONAL', $description );

        if ( $result && ! isset( $result['error'] ) ) {
            $this->mark_media_published( (int) $next_media['id'] );
            $this->log( 'OK', "Drip Feed: photo publiée (media_id={$next_media['id']}, profile={$profile_id})" );
            return 1;
        }

        $this->log( 'WARN', "Drip Feed: publication échouée (media_id={$next_media['id']}, profile={$profile_id})" );
        return 0;
    }

    /**
     * Compte les médias en file (in_stock) pour le profil.
     */
    private function count_media_in_queue( int $profile_id ): int {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_media';
        $count = $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE profile_id = %d AND status = 'in_stock'",
            $profile_id
        ) );
        return (int) $count;
    }

    /**
     * Cold Start Drip : désactivé (règle Google 2026 — pas d’images stock/IA sur GBP).
     *
     * @return int toujours 0
     */
    private function cold_start_drip_if_empty( int $user_id, array $profile ): int {
        $profile_id = (int) ( $profile['id'] ?? 0 );
        $this->log( 'INFO', "Cold Start Drip: désactivé — règle Google 2026 (images stock/IA interdites). Profile={$profile_id}. Utilisateur invité à remplir la Smart Galerie." );
        return 0;
    }

    /**
     * Vérifie si une publication drip-feed est due (dernière publication > DRIP_INTERVAL jours).
     */
    private function is_drip_due( int $profile_id ): bool {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_media';

        $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
        if ( $table_exists !== $table ) {
            $wpdb_alt = $wpdb->prefix . 'avi_saas_media';
            $last = $wpdb->get_var( $wpdb->prepare(
                "SELECT MAX(published_at) FROM {$wpdb_alt} WHERE profile_id = %d AND status = 'published'",
                $profile_id
            ) );

            if ( ! $last ) {
                return true;
            }

            $diff = ( time() - strtotime( $last ) ) / 86400;
            return $diff >= self::DRIP_INTERVAL;
        }

        $last = $wpdb->get_var( $wpdb->prepare(
            "SELECT MAX(published_at) FROM {$table} WHERE profile_id = %d AND status = 'published'",
            $profile_id
        ) );

        if ( ! $last ) {
            return true;
        }

        $diff = ( time() - strtotime( $last ) ) / 86400;
        return $diff >= self::DRIP_INTERVAL;
    }

    /**
     * Récupère le prochain média en file d'attente (status = 'in_stock', le plus ancien).
     */
    private function get_next_media_in_queue( int $profile_id ): ?array {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_media';

        $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
        if ( $table_exists !== $table ) {
            return null;
        }

        $row = $wpdb->get_row( $wpdb->prepare(
            "SELECT * FROM {$table} WHERE profile_id = %d AND status = 'in_stock' ORDER BY created_at ASC LIMIT 1",
            $profile_id
        ), ARRAY_A );

        return is_array( $row ) ? $row : null;
    }

    /**
     * Stock interne prioritaire : retourne la meilleure image disponible (d'abord in_stock, sinon API).
     * Utilisé par Cold Start, Drip-Feed et le bouton "Changer la Photo". [cite: 2026-03-13]
     *
     * @param int    $profile_id     ID du profil (avi_saas_profiles.id).
     * @param string $category       Catégorie métier pour fallback API.
     * @param string $city           Ville optionnelle (ex. London → recherche EN : marketing agency, seo expert, consultant).
     * @param bool   $force_refresh  Si true, ignore le stock BDD et appelle l'API (bouton "Changer la Photo"). [cite: 2026-02-23]
     * @return array{url: string|null, source: 'internal'|'external'|'none', media_id: int|null}
     */
    public function get_best_available_image( int $profile_id, string $category = '', string $city = '', bool $force_refresh = false ): array {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_media';
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            // Google 2026 : images stock et IA interdites sur GBP
            // External image API (Unsplash/Pexels) désactivée
            $this->log( 'INFO', 'Image Source: External API désactivée (règle Google 2026 — stock photos interdites sur GBP)' );
            return [ 'url' => '', 'source' => 'none', 'media_id' => null ];
        }
        if ( $force_refresh ) {
            // Google 2026 : images stock et IA interdites sur GBP
            // External image API (Unsplash/Pexels) désactivée
            $this->log( 'INFO', 'Image Source: External API désactivée (règle Google 2026 — stock photos interdites sur GBP)' );
            return [ 'url' => '', 'source' => 'none', 'media_id' => null ];
        }
        $category_trim = trim( $category );
        $row = null;
        if ( $category_trim !== '' ) {
            $row = $wpdb->get_row( $wpdb->prepare(
                "SELECT id, file_path FROM {$table} WHERE profile_id = %d AND status = 'in_stock' AND category = %s ORDER BY created_at ASC LIMIT 1",
                $profile_id,
                $category_trim
            ), ARRAY_A );
        }
        if ( ! ( is_array( $row ) && ! empty( $row['file_path'] ) ) ) {
            $row = $wpdb->get_row( $wpdb->prepare(
                "SELECT id, file_path FROM {$table} WHERE profile_id = %d AND status = 'in_stock' ORDER BY created_at ASC LIMIT 1",
                $profile_id
            ), ARRAY_A );
        }
        if ( is_array( $row ) && ! empty( $row['file_path'] ) ) {
            $media_id = (int) $row['id'];
            $wpdb->update(
                $table,
                [
                    'status'       => 'published',
                    'published_at' => current_time( 'mysql', true ),
                ],
                [ 'id' => $media_id ],
                [ '%s', '%s' ],
                [ '%d' ]
            );
            $this->log_cron_task( 'Image', sprintf( 'Image Source: Internal Stock (ID: %d)', $media_id ) );
            return [
                'url'      => trim( (string) $row['file_path'] ),
                'source'   => 'internal',
                'media_id' => $media_id,
            ];
        }
        // Google 2026 : images stock et IA interdites sur GBP
        // External image API (Unsplash/Pexels) désactivée
        $this->log( 'INFO', 'Image Source: External API désactivée (règle Google 2026 — stock photos interdites sur GBP)' );
        return [ 'url' => '', 'source' => 'none', 'media_id' => null ];
    }

    /**
     * Marque un média comme publié dans avi_saas_media.
     */
    private function mark_media_published( int $media_id ): void {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_media';

        $wpdb->update(
            $table,
            [
                'status'       => 'published',
                'published_at' => current_time( 'mysql', true ),
            ],
            [ 'id' => $media_id ],
            [ '%s', '%s' ],
            [ '%d' ]
        );
    }

    // ══════════════════════════════════════════════
    // BOUCLIER 6 — Infinite Posts
    // ══════════════════════════════════════════════

    /**
     * Si aucun post n'a été publié depuis POST_INTERVAL jours,
     * récupère le dernier post "Offre" ou "Actualité",
     * le réécrit via Guardian_Response_Generator et le republie.
     *
     * @param int   $user_id
     * @param array $profile
     * @return int  1 si un post a été recyclé, 0 sinon
     */
    public function handle_infinite_posts( int $user_id, array $profile ): int {
        $profile_id  = (int) ( $profile['id'] ?? 0 );
        if ( self::is_hotel_lodging_standard_posts_restricted( $profile ) ) {
            $this->log( 'DEBUG', "Infinite Posts: skip hôtel/hébergement profile={$profile_id}" );
            return 0;
        }
        $token       = trim( (string) ( $profile['google_access_token'] ?? '' ) );
        $location_id = trim( (string) ( $profile['google_location_name'] ?? '' ) );
        $pilot_mode  = strtolower( trim( (string) ( $profile['pilot_posts_mode'] ?? 'manual' ) ) );

        if ( ! $this->is_post_due( $profile_id ) ) {
            return 0;
        }

        $source_post = $this->get_last_recyclable_post( $profile_id );
        if ( ! $source_post ) {
            $this->log( 'DEBUG', "Infinite Posts: aucun post recyclable pour profile={$profile_id}" );
            return 0;
        }

        $original_content = $source_post['content'] ?? '';
        if ( trim( $original_content ) === '' ) {
            return 0;
        }

        // Réécriture via Guardian_Response_Generator (variation de température)
        $rewritten = $this->rewrite_post_content( $original_content );

        if ( $pilot_mode === 'auto' ) {
            $published = $this->publish_post_to_google( $token, $location_id, $rewritten, $source_post );
            if ( $published ) {
                $this->save_recycled_post( $user_id, $profile_id, $source_post, $rewritten, 'published' );
                $this->log( 'OK', "Infinite Posts: post recyclé et publié (source_id={$source_post['id']}, profile={$profile_id})" );
                return 1;
            }
            $this->log( 'WARN', "Infinite Posts: publication échouée, sauvegarde en brouillon" );
        }

        // Co-pilote ou échec publication → brouillon
        $this->save_recycled_post( $user_id, $profile_id, $source_post, $rewritten, 'draft' );
        $this->log( 'OK', "Infinite Posts: brouillon créé (source_id={$source_post['id']}, profile={$profile_id})" );
        return 1;
    }

    /**
     * Vérifie si un post est dû (aucun post publié depuis POST_INTERVAL jours).
     */
    private function is_post_due( int $profile_id ): bool {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_posts';

        $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
        if ( $table_exists !== $table ) {
            return true;
        }

        $last = $wpdb->get_var( $wpdb->prepare(
            "SELECT MAX(published_at) FROM {$table} WHERE profile_id = %d AND status = 'published'",
            $profile_id
        ) );

        if ( ! $last ) {
            return true;
        }

        $diff = ( time() - strtotime( $last ) ) / 86400;
        return $diff >= self::POST_INTERVAL;
    }

    /**
     * Récupère le dernier post recyclable (type 'update' ou 'offer', tout statut sauf 'rejected').
     */
    private function get_last_recyclable_post( int $profile_id ): ?array {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_posts';

        $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
        if ( $table_exists !== $table ) {
            return null;
        }

        $row = $wpdb->get_row( $wpdb->prepare(
            "SELECT * FROM {$table}
             WHERE profile_id = %d
               AND post_type IN ('update','offer')
               AND status != 'rejected'
             ORDER BY created_at DESC
             LIMIT 1",
            $profile_id
        ), ARRAY_A );

        return is_array( $row ) ? $row : null;
    }

    /**
     * Réécrit le contenu d'un post en utilisant Guardian_Response_Generator
     * pour varier la formulation (anti-duplicate Google).
     */
    private function rewrite_post_content( string $original ): string {
        if ( class_exists( __NAMESPACE__ . '\Guardian_Response_Generator' ) ) {
            $generator = new Guardian_Response_Generator();
            if ( method_exists( $generator, 'rewrite_variation' ) ) {
                return $generator->rewrite_variation( $original );
            }
        }

        // Fallback : variation manuelle légère
        return $this->manual_rewrite( $original );
    }

    /**
     * Réécriture de secours si Guardian_Response_Generator ne fournit pas
     * de méthode rewrite_variation. Applique des substitutions synonymiques
     * simples pour éviter le duplicata.
     */
    private function manual_rewrite( string $text ): string {
        $replacements = [
            'Découvrez'           => 'Profitez de',
            'profitez de'         => 'ne manquez pas',
            'Venez'               => 'Rejoignez-nous pour',
            'N\'hésitez pas'      => 'N\'attendez plus',
            'Contactez-nous'      => 'Appelez-nous dès maintenant',
            'notre équipe'        => 'nos experts',
            'nos services'        => 'nos prestations',
            'Nouveau'             => 'Nouveauté',
            'Offre spéciale'      => 'Offre exclusive',
            'offre spéciale'      => 'offre exclusive',
            'Bonne nouvelle'      => 'Excellente nouvelle',
            'bonne nouvelle'      => 'excellente nouvelle',
            'Nous sommes ravis'   => 'Nous avons le plaisir',
            'nous sommes ravis'   => 'nous avons le plaisir',
            'de vous annoncer'    => 'de vous présenter',
            'Rendez-vous'         => 'On vous attend',
        ];

        $rewritten = $text;
        $applied   = 0;

        foreach ( $replacements as $search => $replace ) {
            if ( mb_stripos( $rewritten, $search ) !== false && $applied < 3 ) {
                $rewritten = preg_replace( '/' . preg_quote( $search, '/' ) . '/ui', $replace, $rewritten, 1 );
                $applied++;
            }
        }

        $suffixes = [
            "\n\n📍 Retrouvez-nous sur Google Maps !",
            "\n\n⭐ Votre satisfaction, notre priorité.",
            "\n\n🔔 Restez connecté pour plus d'actualités !",
            "\n\n💪 Toujours à votre service.",
        ];
        $rewritten .= $suffixes[ array_rand( $suffixes ) ];

        return $rewritten;
    }

    /**
     * Publie un post recyclé via Google_My_Business_API::create_local_post().
     */
    private function publish_post_to_google( string $token, string $location_id, string $content, array $source_post ): bool {
        if ( ! class_exists( __NAMESPACE__ . '\Google_My_Business_API' ) ) {
            return false;
        }

        $api   = new Google_My_Business_API( $token, $location_id );
        $topic = ( ( $source_post['post_type'] ?? '' ) === 'offer' ) ? 'OFFER' : 'STANDARD';

        $result = $api->create_local_post( $content, $topic );

        return $result && ! isset( $result['error'] );
    }

    /**
     * Enregistre le post recyclé dans avi_saas_posts.
     */
    private function save_recycled_post( int $user_id, int $profile_id, array $source, string $content, string $status ): void {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_posts';

        $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
        if ( $table_exists !== $table ) {
            return;
        }

        $post_type    = $source['post_type'] ?? 'update';
        $title        = 'Recyclé — ' . mb_substr( (string) ( $source['title'] ?? $post_type ), 0, 450 );
        $published_at = ( $status === 'published' ) ? current_time( 'mysql', true ) : null;

        $wpdb->insert( $table, [
            'user_id'      => $user_id,
            'profile_id'   => $profile_id,
            'post_type'    => $post_type,
            'title'        => $title,
            'content'      => $content,
            'status'       => $status,
            'published_at' => $published_at,
            'created_at'   => current_time( 'mysql', true ),
        ], [ '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s' ] );
    }

    // ══════════════════════════════════════════════
    // Écriture avi_security_logs
    // ══════════════════════════════════════════════

    private function write_security_log( int $user_id, string $event_type, int $signal_id, string $status, string $msg_fr, string $msg_en ): void {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_security_logs';

        $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
        if ( $table_exists !== $table ) {
            return;
        }

        $wpdb->insert( $table, [
            'user_id'      => $user_id,
            'event_type'   => $event_type,
            'signal_id'    => $signal_id,
            'status'       => $status,
            'message_json' => wp_json_encode( [ 'fr' => $msg_fr, 'en' => $msg_en ], JSON_UNESCAPED_UNICODE ),
            'created_at'   => current_time( 'mysql', true ),
        ], [ '%d', '%s', '%d', '%s', '%s', '%s' ] );
    }

    // ══════════════════════════════════════════════
    // Logging
    // ══════════════════════════════════════════════

    private function log( string $level, string $message ): void {
        $line = sprintf( "[%s] [%s] %s\n", gmdate( 'Y-m-d H:i:s' ), $level, $message );
        $log_path = $this->get_log_path();
        if ( $log_path ) {
            $dir = dirname( $log_path );
            if ( ! is_dir( $dir ) ) {
                @wp_mkdir_p( $dir );
            }
            @file_put_contents( $log_path, $line, FILE_APPEND | LOCK_EX );
        }
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            error_log( '[AVI Maintainers] [' . $level . '] ' . $message );
        }
    }

    private function get_log_path(): string {
        $plugin_dir = defined( 'AVISION_PLUGIN_DIR' ) ? AVISION_PLUGIN_DIR : ( dirname( __DIR__ ) . '/' );
        return $plugin_dir . self::LOG_FILE;
    }

    /**
     * Écrit une entrée de diagnostic dans logs/cron-task.log (Cold Start image, etc.).
     */
    private function log_cron_task( string $context, string $message ): void {
        $plugin_dir = defined( 'AVISION_PLUGIN_DIR' ) ? AVISION_PLUGIN_DIR : ( dirname( __DIR__ ) . '/' );
        $log_path   = $plugin_dir . self::CRON_TASK_LOG;
        $dir        = dirname( $log_path );
        if ( ! is_dir( $dir ) ) {
            @wp_mkdir_p( $dir );
        }
        $line = sprintf( "[%s] [%s] %s\n", gmdate( 'Y-m-d H:i:s' ), $context, $message );
        @file_put_contents( $log_path, $line, FILE_APPEND | LOCK_EX );
    }

    /**
     * Fiches Hôtel / hébergement : posts GBP « standard » (offres, actus…) restreints par Google.
     * Aligné sur le front (isAviHotelGooglePostBlocked) — les réponses aux avis ne sont pas concernées.
     */
    public static function is_hotel_lodging_standard_posts_restricted( array $profile ): bool {
        $cat = trim( (string) ( $profile['google_primary_category'] ?? $profile['category'] ?? '' ) );
        if ( $cat === '' ) {
            return false;
        }
        if ( function_exists( 'avi_detect_sector_for_lexicon_category' ) && avi_detect_sector_for_lexicon_category( $cat ) === 'HOSPITALITY' ) {
            return true;
        }
        $l = strtolower( $cat );
        if ( preg_match( '/\b(hotel|hôtel|motel|hostel|lodging|gîte|gite|resort|auberge|hébergement|hebergement)\b/u', $l ) ) {
            return true;
        }
        if ( preg_match( "/chambre[s]?\s+d['\x{2019}]h[ôo]tes?/iu", $cat ) ) {
            return true;
        }
        return false;
    }
}
