<?php

// +-------------------------------------------------+
// � 2002-2025 PMB Services / www.sigb.net pmb@sigb.net et contributeurs (voir www.sigb.net)
// +-------------------------------------------------+
// Author: Gregor Neveu
// Script CLI : Indexation AI des documents numeriques
// Usage: php AiIndexationDocnumCli.php [options]

use Pmb\AI\Orm\AISettingsOrm;
use Pmb\AI\Library\Source\Semantic;

// Constants
const PROGRESS_BAR_LENGTH = 50;
const BATCH_SLEEP_MICROSECONDS = 100000;
const MAX_BATCH_SIZE = 10000;
const MIN_BATCH_SIZE = 1;
const DEFAULT_BATCH_SIZE = 10;
const MAX_IDLE_ITERATIONS = 3;
const LOCK_FILE_PATH = '/tmp/pmb_ai_indexation_docnum.lock';

if (PHP_SAPI !== 'cli') {
    die('This script can only be run from the command line.');
}

$base_path = __DIR__ . '/../..';
$class_path = $base_path . '/classes';
$include_path = $base_path . '/includes';

$base_noheader = 1;
$base_nocheck = 1;
$base_nobody = 1;
$base_nosession = 1;

ini_set('display_errors', 1);
error_reporting(1);

$_SERVER['REQUEST_URI'] = '';
$_SERVER['HTTP_USER_AGENT'] = '';

require_once($base_path . "/includes/init.inc.php");

// Check if AI is active
global $ai_active;
if (!$ai_active) {
    echo 'AI is not active' . PHP_EOL;
    exit(1);
}

// Verrou d'execution : empeche deux instances de tourner en parallele
// (meme indexation declenchee via cron + CLI manuel => double ecriture ai_embeddings).
$lockHandle = fopen(LOCK_FILE_PATH, 'c');
if ($lockHandle === false || !flock($lockHandle, LOCK_EX | LOCK_NB)) {
    echo 'Another instance is already running (lock: ' . LOCK_FILE_PATH . ')' . PHP_EOL;
    exit(1);
}
register_shutdown_function(static function () use ($lockHandle) {
    if (is_resource($lockHandle)) {
        flock($lockHandle, LOCK_UN);
        fclose($lockHandle);
    }
});

// ============================================
// FONCTIONS CLI
// ============================================

/**
 * Display help for the script
 *
 * @return void
 */
function print_help(): void
{
    echo "\n";
    echo "Usage: php AiIndexationDocnumCli.php [options]\n";
    echo "\n";
    echo "Options:\n";
    echo "  --help, -h          : Display this help\n";
    echo "  --verbose, -v       : Verbose mode\n";
    echo "  --batch-size <n>    : Number of elements per batch\n";
    echo "  --limit <n>         : Maximum number of elements to process\n";
    echo "  --clean             : Clean indexation before starting\n";
    echo "\n";
    echo "Description:\n";
    echo "  Index digital documents (explnum) using the AI configured in PMB\n";
    echo "  Uses the active AI configuration defined in PMB administration\n";
    echo "\n";
    echo "Examples:\n";
    echo "  php AiIndexationDocnumCli.php\n";
    echo "  php AiIndexationDocnumCli.php --verbose\n";
    echo "  php AiIndexationDocnumCli.php --clean --verbose\n";
    echo "  php AiIndexationDocnumCli.php --batch-size 50\n";
    echo "  php AiIndexationDocnumCli.php --limit 20 --batch-size 10\n";
    echo "\n";
    exit(0);
}

/**
 * Parse command line arguments
 *
 * @return array<string, mixed> Associative array with: verbose, batch_size, limit, clean
 */
function parse_arguments(): array
{
    global $argv, $argc;

    $args = [
        'verbose' => false,
        'batch_size' => null,
        'limit' => null,
        'clean' => false
    ];

    for ($i = 1; $i < $argc; $i++) {
        switch ($argv[$i]) {
            case '--help':
            case '-h':
                print_help();
                break;

            case '--verbose':
            case '-v':
                $args['verbose'] = true;
                break;

            case '--clean':
                $args['clean'] = true;
                break;

            case '--batch-size':
                if (isset($argv[$i + 1])) {
                    $batch_size = (int)$argv[$i + 1];
                    // Validate batch size is within acceptable range
                    if ($batch_size < MIN_BATCH_SIZE || $batch_size > MAX_BATCH_SIZE) {
                        echo sprintf(
                            "ERROR: batch-size must be between %d and %d\n",
                            MIN_BATCH_SIZE,
                            MAX_BATCH_SIZE
                        );
                        exit(1);
                    }
                    $args['batch_size'] = $batch_size;
                    $i++;
                } else {
                    echo "ERROR: --batch-size requires a numeric argument\n";
                    exit(1);
                }
                break;

            case '--limit':
                if (isset($argv[$i + 1])) {
                    $limit = (int)$argv[$i + 1];
                    // Validate limit is positive
                    if ($limit <= 0) {
                        echo "ERROR: limit must be a positive number\n";
                        exit(1);
                    }
                    $args['limit'] = $limit;
                    $i++;
                } else {
                    echo "ERROR: --limit requires a numeric argument\n";
                    exit(1);
                }
                break;
        }
    }

    return $args;
}

/**
 * Log a message (respects verbose mode)
 *
 * @param string $message The message to log
 * @param bool $force Force output regardless of verbose mode
 * @return void
 */
function log_message(string $message, bool $force = false): void
{
    global $verbose;
    if ($verbose || $force) {
        echo "[" . date('Y-m-d H:i:s') . "] " . $message . "\n";
    }
}

/**
 * Display a CLI progress bar
 *
 * @param float $progress Progress percentage (0-100)
 * @param int|null $totalEntries Total number of entries
 * @param int|null $countIndexed Number of indexed entries
 * @return void
 */
function print_progress(float $progress, ?int $totalEntries = null, ?int $countIndexed = null): void
{
    $barLength = PROGRESS_BAR_LENGTH;
    $filledLength = (int)($barLength * $progress / 100);
    $bar = str_repeat('=', $filledLength) . str_repeat('-', $barLength - $filledLength);

    $info = '';
    if ($totalEntries !== null && $countIndexed !== null) {
        $info = " ($countIndexed/$totalEntries)";
    }

    echo "\r[" . $bar . "] " . number_format($progress, 2) . "%$info";
    if (ob_get_level() > 0) {
        @ob_flush();
    }
    flush();

    if ($progress >= 100) {
        echo "\n";
    }
}

// ============================================
// LOGIQUE METIER (conservee du script original)
// ============================================

/**
 * Initialize AI indexation
 *
 * @param bool $doClean Whether to clean existing indexation
 * @return int|false The AI settings ID or false on error
 */
function startIndexationAI(bool $doClean = false): int|false
{
    global $verbose;

    log_message("=== Initialisation de l'indexation AI ===", true);

    // Verify required constant is defined
    if (!defined('TYPE_EXPLNUM')) {
        log_message("ERROR: TYPE_EXPLNUM constant not defined", true);
        return false;
    }

    // Fetch active AI configuration
    $idSettings = AISettingsOrm::fetchActiveAiSettings();
    if (empty($idSettings)) {
        log_message("ERREUR: Aucune configuration AI active trouvee", true);
        log_message("Veuillez activer une configuration AI dans l'administration PMB", true);
        return false;
    }

    log_message("Configuration AI active: ID $idSettings", $verbose);

    $semantic = new Semantic($idSettings);

    if ($doClean) {
        log_message("Nettoyage de l'indexation existante...", true);
        try {
            $semantic->cleanIndexation(TYPE_EXPLNUM);
            log_message("Nettoyage termine", true);
        } catch (Exception $e) {
            log_message("ERROR during cleanup: " . $e->getMessage(), true);
            return false;
        }
    }

    return $idSettings;
}

/**
 * Run AI indexation in batches
 *
 * @param int $idSettings The AI settings ID
 * @param int|null $batchSize Optional custom batch size
 * @param int|null $limit Optional maximum number of elements to process
 * @return int Total number of indexed elements
 */
function runIndexationAI(int $idSettings, ?int $batchSize = null, ?int $limit = null): int
{
    global $verbose, $ai_index_nb_elements;

    $defaultBatch = (isset($ai_index_nb_elements) && is_int($ai_index_nb_elements) && $ai_index_nb_elements > 0)
        ? $ai_index_nb_elements
        : DEFAULT_BATCH_SIZE;

    $semantic = new Semantic($idSettings);
    $semantic->setIndexationType(TYPE_EXPLNUM);

    $elementsPerBatch = $batchSize ?? $defaultBatch;

    $limitMessage = $limit !== null ? " (limite: $limit elements)" : "";
    log_message("Debut de l'indexation (lots de $elementsPerBatch elements)$limitMessage...", true);

    $totalProcessedInThisRun = 0;
    $previousCountIndexed = null; // initialise a la 1re iteration (source unique : resultat du service)
    $iteration = 0;
    $idleIterations = 0; // garde anti-boucle infinie : nb d'iterations consecutives sans progres

    while (true) {
        $iteration++;
        log_message("Iteration $iteration...", $verbose);

        try {
            $result = $semantic->indexation($elementsPerBatch);
        } catch (Exception $e) {
            log_message("ERROR during indexation: " . $e->getMessage(), true);
            break;
        }

        $totalEntries = (int) ($result["count"] ?? 0);
        $countIndexed = (int) ($result["countIndexed"] ?? 0);

        // Baseline sur la 1re iteration : on utilise la meme source de verite
        // que $countIndexed (cad AiEmbeddingOrm::countByType, cote service)
        // plutot qu'une requete SQL locale, pour eviter toute derive semantique.
        if ($previousCountIndexed === null) {
            $previousCountIndexed = $countIndexed;
        }

        $processedThisIteration = $countIndexed - $previousCountIndexed;
        if ($processedThisIteration < 0) {
            // Cas theorique : si un nettoyage concurrent a reduit countIndexed, on
            // ne veut pas polluer le total. On reinitialise simplement la baseline.
            $processedThisIteration = 0;
        }
        $totalProcessedInThisRun += $processedThisIteration;
        $previousCountIndexed = $countIndexed;

        if ($totalEntries === 0) {
            print_progress(100);
            log_message("Aucun element a indexer", true);
            break;
        }

        $progress = min(100.0, max(0.0, round(($countIndexed * 100) / $totalEntries, 2)));
        print_progress($progress, $totalEntries, $countIndexed);

        if ($verbose) {
            log_message("Elements traites dans cette execution: $totalProcessedInThisRun", true);
        }

        // Limite utilisateur atteinte
        if ($limit !== null && $totalProcessedInThisRun >= $limit) {
            log_message("\nLimite atteinte ($limit elements traites dans cette execution)", true);
            log_message("Total d'elements indexes dans cette execution: $totalProcessedInThisRun", true);
            log_message("Total global d'elements indexes: $countIndexed", true);
            break;
        }

        // Fin normale : tout est indexe
        if ($countIndexed >= $totalEntries) {
            print_progress(100, $totalEntries, $countIndexed);
            log_message("\nIndexation terminee avec succes!", true);
            log_message("Total d'elements indexes dans cette execution: $totalProcessedInThisRun", true);
            log_message("Total global d'elements indexes: $countIndexed", true);
            break;
        }

        // Garde anti-boucle infinie : si aucun progres depuis MAX_IDLE_ITERATIONS
        // iterations, on abandonne plutot que tourner en rond (API en erreur silencieuse,
        // entites bloquees sur un probleme d'extraction, etc.)
        if ($processedThisIteration === 0) {
            $idleIterations++;
            if ($idleIterations >= MAX_IDLE_ITERATIONS) {
                log_message(
                    "\nAucun progres apres " . MAX_IDLE_ITERATIONS . " iterations - arret (reste "
                    . ($totalEntries - $countIndexed) . " element(s) non indexable(s))",
                    true
                );
                break;
            }
        } else {
            $idleIterations = 0;
        }

        usleep(BATCH_SLEEP_MICROSECONDS);
    }

    return $totalProcessedInThisRun;
}

// ============================================
// EXECUTION PRINCIPALE
// ============================================

try {
    $args = parse_arguments();
    $verbose = $args['verbose'];

    log_message("=== Script d'indexation AI des documents numeriques ===", true);
    log_message("Demarrage: " . date('Y-m-d H:i:s'), true);

    $idSettings = startIndexationAI($args['clean']);
    if ($idSettings === false) {
        exit(1);
    }

    runIndexationAI($idSettings, $args['batch_size'], $args['limit']);

    log_message("=== Indexation terminee ===", true);
    log_message("Fin: " . date('Y-m-d H:i:s'), true);

    exit(0);
} catch (Exception $e) {
    log_message("ERREUR CRITIQUE: " . $e->getMessage(), true);
    log_message("Trace: " . $e->getTraceAsString(), true);
    exit(1);
}
