<?php
/**
 * Avision Heatmap Scheduler — Sprint 43
 * Gestion des scans heatmap programmés (quotidien/hebdo/mensuel)
 * Anti-régression : ne jamais modifier handle_checkout_completed ici
 * Ne jamais hardcoder de clés API — utiliser wp-config.php constants
 */

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

class Avision_Heatmap_Scheduler {

    const TABLE          = 'wor1515_avi_heatmap_schedules';
    const CRON_HOOK      = 'avision_run_scheduled_scans';
    const CRON_INTERVAL  = 'avision_hourly';

    const FREQUENCIES = array(
        'daily'      => DAY_IN_SECONDS,
        'every3days' => 3 * DAY_IN_SECONDS,
        'weekly'     => WEEK_IN_SECONDS,
        'every2weeks'=> 2 * WEEK_IN_SECONDS,
        'monthly'    => 30 * DAY_IN_SECONDS,
    );

    const MAX_NOTIFY_ATTEMPTS = 3;

    public static function init() : void {
        add_filter( 'cron_schedules', array( __CLASS__, 'add_cron_interval' ) );
        add_action( self::CRON_HOOK, array( __CLASS__, 'run_due_scans' ) );
        if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
            wp_schedule_event( time(), self::CRON_INTERVAL, self::CRON_HOOK );
        }
        // REST endpoints
        add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
    }

    public static function add_cron_interval( array $schedules ) : array {
        $schedules[ self::CRON_INTERVAL ] = array(
            'interval' => HOUR_IN_SECONDS,
            'display'  => 'Avision — toutes les heures',
        );
        return $schedules;
    }

    // ── CRUD ──────────────────────────────────────────────────────────────

    public static function create( int $profile_id, array $keywords, int $grid_size, float $radius_km, string $frequency, string $notify_email = '' ) : int|false {
        global $wpdb;
        if ( ! array_key_exists( $frequency, self::FREQUENCIES ) ) return false;
        if ( empty( $keywords ) || $profile_id <= 0 ) return false;
        $next_run = self::compute_next_run( $frequency );
        // #Sprint43-scheduler — 1 schedule actif par profil + par grille
        $existing_id = $wpdb->get_var( $wpdb->prepare(
            "SELECT id FROM `{$wpdb->prefix}avi_heatmap_schedules`
             WHERE profile_id = %d
               AND grid_size  = %d
               AND status     = 'active'
             LIMIT 1",
            $profile_id,
            $grid_size
        ) );
        if ( $existing_id ) {
            $wpdb->update(
                $wpdb->prefix . 'avi_heatmap_schedules',
                array(
                    'keywords'     => wp_json_encode( $keywords ),
                    'radius_km'    => $radius_km,
                    'frequency'    => $frequency,
                    'notify_email' => $notify_email,
                    'next_run_at'  => self::compute_next_run( $frequency ),
                    'status'       => 'active',
                ),
                array( 'id' => $existing_id ),
                array( '%s','%f','%s','%s','%s','%s' ),
                array( '%d' )
            );
            return (int) $existing_id;
        }
        $result = $wpdb->insert(
            $wpdb->prefix . 'avi_heatmap_schedules',
            array(
                'profile_id'      => $profile_id,
                'keywords'        => wp_json_encode( array_values( $keywords ) ),
                'grid_size'       => $grid_size,
                'radius_km'       => $radius_km,
                'frequency'       => $frequency,
                'status'          => 'active',
                'next_run_at'     => $next_run,
                'notify_email'    => sanitize_email( $notify_email ),
                'notify_attempts' => 0,
            ),
            array( '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%d' )
        );
        return $result ? (int) $wpdb->insert_id : false;
    }

    public static function update( int $schedule_id, array $data ) : bool {
        global $wpdb;
        $allowed = array( 'keywords', 'grid_size', 'radius_km', 'frequency', 'status', 'next_run_at', 'notify_email' );
        $update  = array();
        $formats = array();
        foreach ( $allowed as $key ) {
            if ( ! isset( $data[ $key ] ) ) continue;
            $update[ $key ] = $key === 'keywords' ? wp_json_encode( $data[ $key ] ) : $data[ $key ];
            $formats[] = is_int( $data[ $key ] ) ? '%d' : ( is_float( $data[ $key ] ) ? '%f' : '%s' );
        }
        if ( empty( $update ) ) return false;
        return (bool) $wpdb->update(
            $wpdb->prefix . 'avi_heatmap_schedules',
            $update,
            array( 'id' => $schedule_id ),
            $formats,
            array( '%d' )
        );
    }

    public static function delete( int $schedule_id ) : bool {
        global $wpdb;
        return (bool) $wpdb->delete(
            $wpdb->prefix . 'avi_heatmap_schedules',
            array( 'id' => $schedule_id ),
            array( '%d' )
        );
    }

    public static function get_by_profile( int $profile_id ) : array {
        global $wpdb;
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM `{$wpdb->prefix}avi_heatmap_schedules` WHERE profile_id = %d ORDER BY created_at DESC",
                $profile_id
            ),
            ARRAY_A
        );
        if ( ! $rows ) return array();
        return array_map( function( $row ) {
            $row['keywords'] = json_decode( $row['keywords'] ?? '[]', true ) ?: array();
            return $row;
        }, $rows );
    }

    // ── EXÉCUTION ─────────────────────────────────────────────────────────

    public static function run_due_scans() : void {
        global $wpdb;
        $now  = current_time( 'mysql', true ); // UTC
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM `{$wpdb->prefix}avi_heatmap_schedules`
                 WHERE status = 'active'
                   AND next_run_at <= %s
                 LIMIT 20",
                $now
            ),
            ARRAY_A
        );
        foreach ( $rows as $schedule ) {
            self::execute_schedule( $schedule );
        }
    }

    private static function execute_schedule( array $schedule ) : void {
        global $wpdb;
        $schedule_id = (int) $schedule['id'];
        $profile_id  = (int) $schedule['profile_id'];
        $keywords    = json_decode( $schedule['keywords'] ?? '[]', true ) ?: array();
        $grid_size   = (int) $schedule['grid_size'];
        $radius_km   = (float) $schedule['radius_km'];
        $frequency   = $schedule['frequency'];

        // Vérifier crédits disponibles
        $credits_needed = $grid_size * $grid_size * count( $keywords );
        $balance        = self::get_credit_balance( $profile_id );

        if ( $balance < $credits_needed ) {
            self::handle_insufficient_credits( $schedule_id, $schedule, $credits_needed, $balance );
            return;
        }

        // Récupérer centre du profil
        $center = self::get_profile_center( $profile_id );
        if ( ! $center ) {
            self::mark_error( $schedule_id, 'Centre géographique introuvable' );
            return;
        }

        // Lancer les scans via backend FastAPI
        $scan_ids    = array();
        $backend_url = 'http://127.0.0.1:8001';
        $admin_key   = defined( 'AVISION_ADMIN_API_KEY' ) ? AVISION_ADMIN_API_KEY : '';

        foreach ( $keywords as $keyword ) {
            $response = wp_remote_post(
                $backend_url . '/heatmap/scan',
                array(
                    'timeout' => 15,
                    'headers' => array(
                        'Content-Type'    => 'application/json',
                        'X-Profile-ID'    => (string) $profile_id,
                        'X-AVI-Admin-Key' => $admin_key,
                    ),
                    'body' => wp_json_encode( array(
                        'profile_id' => $profile_id,
                        'keyword'    => $keyword,
                        'center'     => $center,
                        'grid_size'  => $grid_size,
                        'radius_km'  => $radius_km,
                        'scan_mode'  => 'standard',
                    ) ),
                )
            );
            if ( ! is_wp_error( $response ) ) {
                $body = json_decode( wp_remote_retrieve_body( $response ), true );
                if ( ! empty( $body['scan_id'] ) ) {
                    $scan_ids[] = (int) $body['scan_id'];
                }
            }
        }

        // Mettre à jour le schedule
        $next_run = self::compute_next_run( $frequency );
        $wpdb->update(
            $wpdb->prefix . 'avi_heatmap_schedules',
            array(
                'last_run_at'     => current_time( 'mysql', true ),
                'next_run_at'     => $next_run,
                'last_scan_ids'   => wp_json_encode( $scan_ids ),
                'notify_attempts' => 0,
                'status'          => 'active',
            ),
            array( 'id' => $schedule_id ),
            array( '%s', '%s', '%s', '%d', '%s' ),
            array( '%d' )
        );
    }

    // ── CRÉDITS ───────────────────────────────────────────────────────────

    private static function get_credit_balance( int $profile_id ) : int {
        global $wpdb;
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT credits_total + credits_bonus - credits_used + credits_refunded as balance
                 FROM `{$wpdb->prefix}avi_heatmap_credits`
                 WHERE profile_id = %d OR agency_id = (
                     SELECT agency_id FROM `{$wpdb->prefix}avi_saas_profiles` WHERE id = %d LIMIT 1
                 )
                 LIMIT 1",
                $profile_id, $profile_id
            ),
            ARRAY_A
        );
        return $row ? max( 0, (int) $row['balance'] ) : 0;
    }

    private static function handle_insufficient_credits( int $schedule_id, array $schedule, int $needed, int $balance ) : void {
        global $wpdb;
        $attempts = (int) $schedule['notify_attempts'] + 1;
        $new_status = $attempts >= self::MAX_NOTIFY_ATTEMPTS ? 'insufficient_credits' : 'active';

        $wpdb->update(
            $wpdb->prefix . 'avi_heatmap_schedules',
            array(
                'notify_attempts' => $attempts,
                'status'          => $new_status,
            ),
            array( 'id' => $schedule_id ),
            array( '%d', '%s' ),
            array( '%d' )
        );

        // Email notification
        $email = sanitize_email( $schedule['notify_email'] ?? '' );
        if ( $email && $attempts <= self::MAX_NOTIFY_ATTEMPTS ) {
            wp_mail(
                $email,
                '[Avision] Scan programmé — crédits insuffisants',
                sprintf(
                    "Votre scan programmé n'a pas pu s'exécuter.\n\nCrédits nécessaires : %d\nCrédits disponibles : %d\n\nRechargeez vos crédits sur https://avision.org pour reprendre le monitoring automatique.\n\nL'équipe Avision",
                    $needed,
                    $balance
                )
            );
        }
    }

    // #Sprint43-scheduler — purge doublons (à appeler manuellement si besoin)
    public static function cleanup_duplicate_schedules() : void {
        global $wpdb;
        // Garder uniquement le schedule actif le plus récent par profil + grille
        $wpdb->query(
            "UPDATE `{$wpdb->prefix}avi_heatmap_schedules` s1
             INNER JOIN `{$wpdb->prefix}avi_heatmap_schedules` s2
             ON s1.profile_id = s2.profile_id
             AND s1.grid_size = s2.grid_size
             AND s1.status = 'active'
             AND s2.status = 'active'
             AND s1.id < s2.id
             SET s1.status = 'cancelled'"
        );
    }

    private static function mark_error( int $schedule_id, string $reason ) : void {
        global $wpdb;
        $wpdb->update(
            $wpdb->prefix . 'avi_heatmap_schedules',
            array( 'status' => 'error' ),
            array( 'id' => $schedule_id ),
            array( '%s' ),
            array( '%d' )
        );
    }

    // ── HELPERS ───────────────────────────────────────────────────────────

    private static function compute_next_run( string $frequency ) : string {
        $interval = self::FREQUENCIES[ $frequency ] ?? WEEK_IN_SECONDS;
        return gmdate( 'Y-m-d H:i:s', time() + $interval );
    }

    private static function get_profile_center( int $profile_id ) : array|null {
        global $wpdb;
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT center_lat, center_lng FROM `{$wpdb->prefix}avi_heatmap_results`
                 WHERE profile_id = %d AND center_lat IS NOT NULL
                 ORDER BY id DESC LIMIT 1",
                $profile_id
            ),
            ARRAY_A
        );
        if ( ! $row ) return null;
        return array(
            'lat' => (float) $row['center_lat'],
            'lng' => (float) $row['center_lng'],
        );
    }

    // ── REST API ──────────────────────────────────────────────────────────

    public static function register_rest_routes() : void {
        register_rest_route( 'avision/v1', '/heatmap-schedule', array(
            array(
                'methods'             => 'GET',
                'callback'            => array( __CLASS__, 'rest_list' ),
                'permission_callback' => array( __CLASS__, 'rest_permission' ),
                'args'                => array(
                    'profile_id' => array( 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint' ),
                ),
            ),
            array(
                'methods'             => 'POST',
                'callback'            => array( __CLASS__, 'rest_create' ),
                'permission_callback' => array( __CLASS__, 'rest_permission' ),
            ),
        ) );
        register_rest_route( 'avision/v1', '/heatmap-schedule/(?P<id>\d+)', array(
            array(
                'methods'             => 'PUT',
                'callback'            => array( __CLASS__, 'rest_update' ),
                'permission_callback' => array( __CLASS__, 'rest_permission' ),
            ),
            array(
                'methods'             => 'DELETE',
                'callback'            => array( __CLASS__, 'rest_delete' ),
                'permission_callback' => array( __CLASS__, 'rest_permission' ),
            ),
        ) );
    }

    public static function rest_permission( \WP_REST_Request $request ) : bool|\WP_Error {
        // #Sprint44-Plugin — Auth embed token (mode iframe public)
        $embed_token = $request->get_header( 'X-Embed-Token' );
        if ( $embed_token === '' || $embed_token === null ) {
            $embed_token = $request->get_param( 'embed_token' );
        }
        $profile_id = absint( $request->get_param( 'profile_id' ) );
        if ( $embed_token && $profile_id ) {
            if ( class_exists( 'Avision_Embed' ) &&
                Avision_Embed::verify_embed_token( (string) $embed_token, $profile_id )
            ) {
                return true;
            }
        }

        if ( ! is_user_logged_in() ) {
            return new \WP_Error( 'unauthorized', 'Authentication required', array( 'status' => 401 ) );
        }
        $nonce = $request->get_header( 'X-WP-Nonce' ) ?: $request->get_param( '_wpnonce' );
        if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
            return new \WP_Error( 'bad_nonce', 'Nonce invalide', array( 'status' => 403 ) );
        }
        return true;
    }

    public static function rest_list( \WP_REST_Request $request ) : \WP_REST_Response {
        $profile_id = (int) $request->get_param( 'profile_id' );
        return new \WP_REST_Response( self::get_by_profile( $profile_id ), 200 );
    }

    public static function rest_create( \WP_REST_Request $request ) : \WP_REST_Response|\WP_Error {
        $body        = $request->get_json_params() ?: array();
        $profile_id  = absint( $body['profile_id'] ?? 0 );
        $keywords    = array_filter( (array) ( $body['keywords'] ?? array() ) );
        $grid_size   = absint( $body['grid_size'] ?? 7 );
        $radius_km   = (float) ( $body['radius_km'] ?? 5.0 );
        $frequency   = sanitize_key( $body['frequency'] ?? 'weekly' );
        $notify_email = sanitize_email( $body['notify_email'] ?? '' );

        if ( ! $profile_id || empty( $keywords ) ) {
            return new \WP_Error( 'invalid_params', 'profile_id et keywords requis', array( 'status' => 422 ) );
        }
        $id = self::create( $profile_id, $keywords, $grid_size, $radius_km, $frequency, $notify_email );
        if ( ! $id ) {
            return new \WP_Error( 'create_failed', 'Création échouée', array( 'status' => 500 ) );
        }
        return new \WP_REST_Response( array( 'id' => $id, 'status' => 'active' ), 201 );
    }

    public static function rest_update( \WP_REST_Request $request ) : \WP_REST_Response|\WP_Error {
        $schedule_id = (int) $request->get_param( 'id' );
        $body        = $request->get_json_params() ?: array();
        $ok = self::update( $schedule_id, $body );
        return new \WP_REST_Response( array( 'updated' => $ok ), $ok ? 200 : 500 );
    }

    public static function rest_delete( \WP_REST_Request $request ) : \WP_REST_Response {
        $schedule_id = (int) $request->get_param( 'id' );
        $ok = self::delete( $schedule_id );
        return new \WP_REST_Response( array( 'deleted' => $ok ), 200 );
    }
}

// Auto-init
add_action( 'plugins_loaded', array( 'Avision_Heatmap_Scheduler', 'init' ), 15 );
