<?php
/**
 * Moteur de Scan AVI — Orchestrateur centralisé DataForSEO + Google Business
 *
 * Alimente les tables "Cœur d'AVI" :
 *   - {prefix}avi_keyword_history  (Radar de Croissance)
 *   - {prefix}avi_net_worth_stats  (Richesse Numérique)
 *
 * Délègue les appels API bas-niveau aux méthodes existantes de Avision_Database
 * (fetch_dataforseo_maps_sector, fetch_google_business_metrics) pour éviter
 * toute duplication de code.
 *
 * @package Avision
 */

namespace Avision;

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

class Avision_Scan_Engine {

    /** @var Avision_Database */
    private $db;

    /** Délais de scan par fréquence (en secondes). */
    private const SCAN_DELAYS = [
        'daily'   => 86400,    // 24 h
        'weekly'  => 604800,   // 7 j
        'monthly' => 2592000,  // 30 j
    ];

    /** Limite d'appels API par exécution (rate-limit par batch). */
    private const MAX_API_CALLS_PER_RUN = 15;

    /** Pause entre chaque appel API (millisecondes). */
    private const API_THROTTLE_MS = 1200;

    /** Chemin du log dédié (relatif à wp-content/plugins/avision-dashboard/). */
    private const LOG_FILE = 'logs/scan-engine.log';

    // ──────────────────────────────────────────────
    // Initialisation
    // ──────────────────────────────────────────────

    public function __construct() {
        $this->ensure_log_dir();
    }

    /**
     * Initialise le moteur avec une connexion BDD active.
     * Doit être appelé avant toute opération.
     */
    public function init(): self {
        if ( ! class_exists( __NAMESPACE__ . '\Avision_Database' ) ) {
            $this->log( 'CRITICAL', 'Avision_Database introuvable' );
            return $this;
        }
        $this->db = new Avision_Database();
        $this->db->init();
        return $this;
    }

    // ──────────────────────────────────────────────
    // 1. AUTHENTIFICATION & SÉCURITÉ
    // ──────────────────────────────────────────────

    /**
     * Retourne les credentials DataForSEO (filtre WP, constantes AVISION_*, getenv / $_ENV).
     *
     * @return array{login: string, password: string}|null  null si indisponibles
     */
    public static function get_dataforseo_credentials(): ?array {
        if ( function_exists( 'avision_get_dataforseo_credentials_resolved' ) ) {
            $res = \avision_get_dataforseo_credentials_resolved();
            if ( is_array( $res ) && isset( $res['login'], $res['password'] )
                && $res['login'] !== '' && $res['password'] !== '' ) {
                return [ 'login' => $res['login'], 'password' => $res['password'] ];
            }

            return null;
        }

        $credentials = function_exists( 'apply_filters' )
            ? apply_filters( 'avision_dataforseo_credentials', [] )
            : [];

        $login    = isset( $credentials['login'] ) ? trim( (string) $credentials['login'] ) : '';
        $password = isset( $credentials['password'] ) ? trim( (string) $credentials['password'] ) : '';

        if ( ( $login === '' || $password === '' ) && defined( 'AVISION_DATAFORSEO_LOGIN' ) && defined( 'AVISION_DATAFORSEO_PASSWORD' ) ) {
            $login    = trim( (string) AVISION_DATAFORSEO_LOGIN );
            $password = trim( (string) AVISION_DATAFORSEO_PASSWORD );
        }

        if ( $login === '' || $password === '' ) {
            return null;
        }

        return [ 'login' => $login, 'password' => $password ];
    }

    /**
     * Retourne le Google Access Token du profil (stocké en BDD).
     *
     * @param int $profile_id
     * @return string|null  null si inexistant ou expiré
     */
    private function get_google_token( int $profile_id ): ?string {
        if ( ! $this->db ) {
            return null;
        }
        if ( method_exists( $this->db, 'refresh_google_access_token_if_needed' ) ) {
            $this->db->refresh_google_access_token_if_needed( $profile_id, false );
        }
        $profile = $this->db->get_saas_profile_for_sync( $profile_id );
        if ( ! $profile || empty( $profile['google_access_token'] ) ) {
            return null;
        }
        return $profile['google_access_token'];
    }

    // ──────────────────────────────────────────────
    // 2. RATE LIMITING
    // ──────────────────────────────────────────────

    /** @var int Compteur d'appels API dans le batch courant. */
    private $api_call_count = 0;

    /**
     * Vérifie si on peut encore faire un appel API dans ce batch.
     * Applique un throttle (sleep) entre les appels.
     *
     * @return bool  false si la limite est atteinte
     */
    private function rate_limit_check(): bool {
        if ( $this->api_call_count >= self::MAX_API_CALLS_PER_RUN ) {
            $this->log( 'WARN', 'Rate limit atteint (' . self::MAX_API_CALLS_PER_RUN . ' appels)' );
            return false;
        }
        if ( $this->api_call_count > 0 ) {
            usleep( self::API_THROTTLE_MS * 1000 );
        }
        $this->api_call_count++;
        return true;
    }

    /**
     * Remet le compteur à zéro (début de batch).
     */
    public function reset_rate_limit(): void {
        $this->api_call_count = 0;
    }

    // ──────────────────────────────────────────────
    // 3. MOTEUR DATAFORSEO — Radar de Croissance
    // ──────────────────────────────────────────────

    /**
     * Synchronise les positions des mots-clés d'un utilisateur.
     *
     * - Récupère jusqu'à 15 mots-clés depuis avi_saas_keywords.
     * - Vérifie last_scan_date dans avi_keyword_history (respect du plan).
     * - Appelle DataForSEO via Avision_Database::run_single_keyword_scan().
     * - Écrit l'historique dans avi_keyword_history.
     *
     * @param int $user_id  ID utilisateur (avi_saas_users.id)
     * @return array{scanned: int, skipped: int, errors: int}
     */
    public function sync_keyword_rankings( int $user_id ): array {
        $stats = [ 'scanned' => 0, 'skipped' => 0, 'errors' => 0 ];


        if ( ! $this->db ) {
            $this->log( 'ERROR', "sync_keyword_rankings: DB non initialisée (user_id={$user_id})" );
            return $stats;
        }

        $profile = $this->get_profile_by_user_id( $user_id );
        if ( ! $profile ) {
            $this->log( 'WARN', "sync_keyword_rankings: profil introuvable pour user_id={$user_id}" );
            return $stats;
        }

        $profile_id = (int) $profile['id'];
        $keywords   = $this->get_keywords_for_radar( $profile_id, 15 );

        if ( empty( $keywords ) ) {
            $this->log( 'INFO', "sync_keyword_rankings: aucun mot-clé pour profile_id={$profile_id}" );
            return $stats;
        }

        $scan_frequency = $this->get_scan_frequency( $profile );

        $this->reset_rate_limit();

        foreach ( $keywords as $kw ) {
            $kw_id   = (int) ( $kw['id'] ?? 0 );
            $kw_text = trim( (string) ( $kw['keyword'] ?? '' ) );

            if ( $kw_id <= 0 || $kw_text === '' ) {
                $stats['skipped']++;
                continue;
            }

            if ( $this->is_scan_still_fresh( $user_id, $kw_text, $scan_frequency ) ) {
                $stats['skipped']++;
                continue;
            }

            if ( ! $this->rate_limit_check() ) {
                $this->log( 'WARN', "sync_keyword_rankings: rate limit, arrêt à {$stats['scanned']} scans" );
                break;
            }

            $old_rank = $kw['current_rank'] ?? $kw['previous_rank'] ?? null;


            $success = $this->db->run_single_keyword_scan( $kw, $profile );

            if ( ! $success ) {
                $stats['errors']++;
                $this->log( 'ERROR', "Scan échoué: kw_id={$kw_id} keyword=\"{$kw_text}\"" );
                continue;
            }

            $stats['scanned']++;

            $refreshed = $this->get_keyword_by_id( $kw_id );
            $new_rank  = $refreshed ? ( (int) ( $refreshed['current_rank'] ?? 101 ) ) : 101;
            $old_rank  = $old_rank !== null ? (int) $old_rank : null;
            $change    = ( $old_rank !== null && $old_rank > 0 ) ? ( $old_rank - $new_rank ) : 0;

            $this->write_keyword_history( $user_id, $kw_text, $old_rank, $new_rank, $change, $scan_frequency );

            $this->log( 'OK', "Keyword \"{$kw_text}\": {$old_rank} → {$new_rank} (Δ{$change})" );
        }

        $this->log( 'INFO', "sync_keyword_rankings terminé: user_id={$user_id} " . wp_json_encode( $stats ) );
        return $stats;
    }

    // ──────────────────────────────────────────────
    // 4. MOTEUR GOOGLE — Visibilité & Notoriété
    // ──────────────────────────────────────────────

    /**
     * Interroge l'API Google Business Profile pour récupérer les métriques
     * et calcule le gain publicitaire quotidien (Net Worth).
     *
     * @param int $user_id  ID utilisateur
     * @return array{success: bool, daily_saving: float, cumulative: float}
     */
    public function sync_business_insights( int $user_id ): array {
        $result = [ 'success' => false, 'daily_saving' => 0.0, 'cumulative' => 0.0 ];

        if ( ! $this->db ) {
            $this->log( 'ERROR', "sync_business_insights: DB non initialisée (user_id={$user_id})" );
            return $result;
        }

        $profile = $this->get_profile_by_user_id( $user_id );
        if ( ! $profile ) {
            $this->log( 'WARN', "sync_business_insights: profil introuvable pour user_id={$user_id}" );
            return $result;
        }

        $profile_id  = (int) $profile['id'];
        $location_id = trim( (string) ( $profile['google_location_name'] ?? '' ) );
        $token       = $this->get_google_token( $profile_id );

        if ( ! $token || $location_id === '' ) {
            $this->log( 'WARN', "sync_business_insights: token/location manquant pour profile_id={$profile_id}" );
            return $result;
        }

        if ( ! $this->rate_limit_check() ) {
            return $result;
        }


        $metrics = $this->fetch_performance_metrics( $token, $location_id, $user_id, $profile_id );

        if ( $metrics === null ) {
            $this->log( 'ERROR', "sync_business_insights: échec Google API pour profile_id={$profile_id}" );
            return $result;
        }


        $cpc_ref      = $this->get_sector_cpc( $profile );
        $daily_saving = $this->calculate_daily_saving( $metrics, $cpc_ref, $profile_id );

        $cumulative = $this->get_last_cumulative( $user_id ) + $daily_saving;

        $this->write_net_worth_stats( $user_id, $daily_saving, $cumulative, $cpc_ref );

        $result['success']      = true;
        $result['daily_saving'] = $daily_saving;
        $result['cumulative']   = $cumulative;

        $this->log( 'OK', "sync_business_insights: user_id={$user_id} daily={$daily_saving}€ cumul={$cumulative}€" );
        return $result;
    }

    /**
     * Appelle l'API Google Business Profile Performance pour récupérer
     * les métriques de la journée (appels, clics, vues, directions).
     *
     * @param string $access_token
     * @param string $location_id  ex. "locations/123456789"
     * @param int    $user_id      Contexte log uniquement
     * @param int    $profile_id   Contexte log uniquement
     * @return array|null  null en cas d'erreur
     */
    private function fetch_performance_metrics( string $access_token, string $location_id, int $user_id = 0, int $profile_id = 0 ): ?array {
        if ( ! function_exists( 'wp_remote_get' ) ) {
            return null;
        }

        $location_name = ltrim( $location_id, '/' );
        $end_date      = gmdate( 'Y-m-d' );
        $start_date    = gmdate( 'Y-m-d', strtotime( '-1 day' ) );


        $url = sprintf(
            'https://businessprofileperformance.googleapis.com/v1/%s:getDailyMetricsTimeSeries?dailyRange.startDate.year=%s&dailyRange.startDate.month=%s&dailyRange.startDate.day=%s&dailyRange.endDate.year=%s&dailyRange.endDate.month=%s&dailyRange.endDate.day=%s&dailyMetrics=WEBSITE_CLICKS&dailyMetrics=CALL_CLICKS&dailyMetrics=BUSINESS_DIRECTION_REQUESTS&dailyMetrics=BUSINESS_IMPRESSIONS_MOBILE_MAPS',
            $location_name,
            date( 'Y', strtotime( $start_date ) ), date( 'n', strtotime( $start_date ) ), date( 'j', strtotime( $start_date ) ),
            date( 'Y', strtotime( $end_date ) ),   date( 'n', strtotime( $end_date ) ),   date( 'j', strtotime( $end_date ) )
        );

        for ( $attempt = 1; $attempt <= 2; $attempt++ ) {
            if ( $profile_id > 0 && $this->db && method_exists( $this->db, 'refresh_google_access_token_if_needed' ) ) {
                if ( $attempt === 1 ) {
                    $this->db->refresh_google_access_token_if_needed( $profile_id, false );
                }
                $prow = $this->db->get_saas_profile_for_sync( $profile_id );
                if ( is_array( $prow ) && ! empty( $prow['google_access_token'] ) ) {
                    $access_token = (string) $prow['google_access_token'];
                }
            }

            $response = wp_remote_get( $url, [
                'headers' => [
                    'Authorization' => 'Bearer ' . $access_token,
                    'Content-Type'  => 'application/json',
                ],
                'timeout' => 30,
            ] );

            if ( is_wp_error( $response ) ) {
                $this->log( 'ERROR', 'Google Performance API: ' . $response->get_error_message() );
                return null;
            }

            $code = wp_remote_retrieve_response_code( $response );
            if ( $code === 401 && $profile_id > 0 && $attempt < 2 && $this->db && method_exists( $this->db, 'refresh_google_access_token_if_needed' ) ) {
                $refreshed = false;
                if ( class_exists( __NAMESPACE__ . '\\Avision_API' ) ) {
                    $refreshed = Avision_API::refresh_google_token( $profile_id );
                } else {
                    if ( function_exists( 'error_log' ) ) {
                        error_log( '🔄 Token expiré, tentative de rafraîchissement pour le profil ' . $profile_id . '...' );
                    }
                    $refreshed = $this->db->refresh_google_access_token_if_needed( $profile_id, true );
                }
                if ( $refreshed ) {
                    continue;
                }
            }
            if ( $code !== 200 ) {
                $this->log( 'ERROR', "Google Performance API: HTTP {$code}" );
                $err_body = wp_remote_retrieve_body( $response );
                return null;
            }

            $body_raw = wp_remote_retrieve_body( $response );

            $body = json_decode( $body_raw, true );
            if ( ! is_array( $body ) ) {
                return null;
            }

            $out = [
                'website_clicks'      => 0,
                'call_clicks'         => 0,
                'direction_requests'  => 0,
                'impressions_maps'    => 0,
            ];

            if ( ! empty( $body['timeSeries'] ) ) {
                foreach ( $body['timeSeries'] as $series ) {
                    $metric = $series['dailyMetric'] ?? '';
                    $value  = 0;
                    if ( ! empty( $series['timeSeries']['datedValues'] ) ) {
                        foreach ( $series['timeSeries']['datedValues'] as $dv ) {
                            $value += (int) ( $dv['value'] ?? 0 );
                        }
                    }
                    switch ( $metric ) {
                        case 'WEBSITE_CLICKS':
                            $out['website_clicks'] = $value;
                            break;
                        case 'CALL_CLICKS':
                            $out['call_clicks'] = $value;
                            break;
                        case 'BUSINESS_DIRECTION_REQUESTS':
                            $out['direction_requests'] = $value;
                            break;
                        case 'BUSINESS_IMPRESSIONS_MOBILE_MAPS':
                            $out['impressions_maps'] = $value;
                            break;
                    }
                }
            }

            return $out;
        }

        return null;
    }

    /**
     * CPC de référence du secteur (€). Extensible via filtre WP.
     *
     * @param array $profile  Données du profil (category, country)
     * @return float
     */
    private function get_sector_cpc( array $profile ): float {
        $category = strtolower( trim( (string) ( $profile['category'] ?? '' ) ) );

        $cpc_map = [
            'restaurant'  => 0.85,
            'plombier'    => 2.50,
            'plumber'     => 2.50,
            'dentiste'    => 3.20,
            'dentist'     => 3.20,
            'avocat'      => 4.80,
            'lawyer'      => 4.80,
            'coiffeur'    => 0.65,
            'hairdresser' => 0.65,
            'electricien' => 2.10,
            'electrician' => 2.10,
            'boulangerie' => 0.45,
            'bakery'      => 0.45,
            'garage'      => 1.80,
            'médecin'     => 2.90,
            'doctor'      => 2.90,
        ];

        $cpc = 1.25; // Défaut tous secteurs

        foreach ( $cpc_map as $key => $value ) {
            if ( strpos( $category, $key ) !== false ) {
                $cpc = $value;
                break;
            }
        }

        return (float) apply_filters( 'avision_sector_cpc', $cpc, $profile );
    }

    /**
     * Calcule l'économie publicitaire quotidienne basée sur les interactions Google.
     *
     * Formule : (clics_site + clics_appel + demandes_itinéraire) × CPC_secteur
     *
     * @param array $metrics  Métriques Google Performance
     * @param float $cpc      CPC du secteur
     * @param int   $profile_id
     * @return float
     */
    private function calculate_daily_saving( array $metrics, float $cpc, int $profile_id ): float {
        $interactions = ( $metrics['website_clicks'] ?? 0 )
                      + ( $metrics['call_clicks'] ?? 0 )
                      + ( $metrics['direction_requests'] ?? 0 );

        $saving = round( $interactions * $cpc, 2 );

        $this->log( 'DEBUG', "daily_saving: profile={$profile_id} interactions={$interactions} × cpc={$cpc} = {$saving}€" );

        return $saving;
    }

    // ──────────────────────────────────────────────
    // 5. ÉCRITURE DANS LES TABLES "CŒUR D'AVI"
    // ──────────────────────────────────────────────

    /**
     * Insère une ligne dans avi_keyword_history.
     */
    private function write_keyword_history( int $user_id, string $keyword, ?int $pos_j0, int $pos_current, int $pos_change, string $frequency ): void {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_keyword_history';

        $wpdb->insert( $table, [
            'user_id'        => $user_id,
            'keyword_text'   => $keyword,
            'pos_j0'         => $pos_j0,
            'pos_current'    => $pos_current,
            'pos_change'     => $pos_change,
            'scan_frequency' => $frequency,
            'last_scan_date' => current_time( 'mysql', true ),
        ], [ '%d', '%s', '%d', '%d', '%d', '%s', '%s' ] );

        if ( $wpdb->last_error ) {
            $this->log( 'ERROR', "write_keyword_history INSERT failed: {$wpdb->last_error}" );
        }
    }

    /**
     * Insère une ligne dans avi_net_worth_stats.
     */
    private function write_net_worth_stats( int $user_id, float $daily_saving, float $cumulative, float $cpc_ref ): void {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_net_worth_stats';

        $wpdb->insert( $table, [
            'user_id'          => $user_id,
            'daily_saving'     => $daily_saving,
            'cumulative_total' => $cumulative,
            'cpc_ref'          => $cpc_ref,
            'recorded_at'      => current_time( 'mysql', true ),
        ], [ '%d', '%f', '%f', '%f', '%s' ] );

        if ( $wpdb->last_error ) {
            $this->log( 'ERROR', "write_net_worth_stats INSERT failed: {$wpdb->last_error}" );
        }
    }

    // ──────────────────────────────────────────────
    // 6. HELPERS BDD (lectures)
    // ──────────────────────────────────────────────

    /**
     * Récupère le profil SaaS à partir du user_id.
     */
    private function get_profile_by_user_id( int $user_id ): ?array {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_profiles';
        $row   = $wpdb->get_row( $wpdb->prepare(
            "SELECT * FROM {$table} WHERE user_id = %d ORDER BY id DESC LIMIT 1",
            $user_id
        ), ARRAY_A );
        return is_array( $row ) ? $row : null;
    }

    /**
     * Récupère les N mots-clés du Radar (avi_saas_keywords).
     */
    private function get_keywords_for_radar( int $profile_id, int $limit = 15 ): array {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_keywords';
        $rows  = $wpdb->get_results( $wpdb->prepare(
            "SELECT * FROM {$table} WHERE profile_id = %d ORDER BY is_strategic DESC, id ASC LIMIT %d",
            $profile_id, $limit
        ), ARRAY_A );
        return is_array( $rows ) ? $rows : [];
    }

    /**
     * Récupère un mot-clé par son ID (après scan, pour lire le rank mis à jour).
     */
    private function get_keyword_by_id( int $kw_id ): ?array {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_saas_keywords';
        $row   = $wpdb->get_row( $wpdb->prepare(
            "SELECT * FROM {$table} WHERE id = %d LIMIT 1",
            $kw_id
        ), ARRAY_A );
        return is_array( $row ) ? $row : null;
    }

    /**
     * Vérifie si le dernier scan d'un mot-clé est encore "frais" (pas expiré).
     */
    private function is_scan_still_fresh( int $user_id, string $keyword, string $frequency ): bool {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_keyword_history';

        $last_scan = $wpdb->get_var( $wpdb->prepare(
            "SELECT last_scan_date FROM {$table} WHERE user_id = %d AND keyword_text = %s ORDER BY last_scan_date DESC LIMIT 1",
            $user_id, $keyword
        ) );

        if ( ! $last_scan ) {
            return false;
        }

        $delay   = self::SCAN_DELAYS[ $frequency ] ?? self::SCAN_DELAYS['weekly'];
        $elapsed = time() - strtotime( $last_scan . ' UTC' );

        return $elapsed < $delay;
    }

    /**
     * Retourne la fréquence de scan selon le plan du profil (radar_plan).
     */
    private function get_scan_frequency( array $profile ): string {
        $plan = strtolower( trim( (string) ( $profile['radar_plan'] ?? 'offert' ) ) );
        $map  = [
            'offert'  => 'monthly',
            'hebdo'   => 'weekly',
            'weekly'  => 'weekly',
            'elite'   => 'daily',
            'daily'   => 'daily',
        ];
        return $map[ $plan ] ?? 'monthly';
    }

    /**
     * Récupère le dernier cumulative_total pour un utilisateur.
     */
    private function get_last_cumulative( int $user_id ): float {
        global $wpdb;
        $table = $wpdb->prefix . 'avi_net_worth_stats';
        $val   = $wpdb->get_var( $wpdb->prepare(
            "SELECT cumulative_total FROM {$table} WHERE user_id = %d ORDER BY recorded_at DESC LIMIT 1",
            $user_id
        ) );
        return $val !== null ? (float) $val : 0.0;
    }

    // ──────────────────────────────────────────────
    // 7. LOGGING DÉDIÉ
    // ──────────────────────────────────────────────

    /**
     * Écrit un message dans logs/scan-engine.log ET error_log.
     *
     * @param string $level  OK|INFO|WARN|ERROR|CRITICAL|DEBUG
     * @param string $message
     */
    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 ) {
            @file_put_contents( $log_path, $line, FILE_APPEND | LOCK_EX );
        }

        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            error_log( '[AVI ScanEngine] [' . $level . '] ' . $message );
        }
    }

    /**
     * Retourne le chemin absolu du fichier de log.
     */
    private function get_log_path(): ?string {
        $plugin_dir = defined( 'AVISION_PLUGIN_DIR' ) ? AVISION_PLUGIN_DIR : ( dirname( __DIR__ ) . '/' );
        $path       = $plugin_dir . self::LOG_FILE;
        return $path;
    }

    /**
     * Crée le dossier logs/ s'il n'existe pas.
     */
    private function ensure_log_dir(): void {
        $log_path = $this->get_log_path();
        if ( ! $log_path ) {
            return;
        }
        $dir = dirname( $log_path );
        if ( ! is_dir( $dir ) ) {
            @wp_mkdir_p( $dir );
        }
    }
}
