<?php
/**
 * Demo Oracle — accumulation des echantillons du SQL Monitor.
 *
 * Sans Diagnostic Pack il n'y a pas d'ASH : un appel a GV$SESSION ne donne
 * qu'un instantane, qui ne dit rien seul. C'est leur ACCUMULATION, d'un
 * rafraichissement a l'autre, qui reconstitue l'histogramme « ou le temps est
 * passe ». D'ou un etat sur disque entre deux requetes HTTP.
 *
 * Ce n'est pas un cache : un cache expire et se reconstruit, celui-ci ne se
 * reconstruit pas — perdu, l'histogramme repart de zero.
 *
 * CE QU'IL CONTIENT, ET RIEN D'AUTRE : des identifiants de ligne de plan, des
 * noms de classes d'attente, des compteurs, et le premier releve des
 * statistiques de session. Ni texte SQL, ni nom d'utilisateur, ni nom de
 * machine. Le fichier vit dans un repertoire temporaire partage : cette
 * propriete doit rester vraie.
 *
 * LIMITE ASSUMEE : deux visiteurs sur la meme execution ecrivent tour a tour.
 * Le verrou exclusif evite un fichier corrompu, pas la mise a jour perdue.
 * C'est un histogramme, pas une comptabilite.
 *
 * Portage de modules/oracle/lib/OracleSqlMonAccumulator.php — copie stricte,
 * hors le prefixe de fichier et la duree de vie.
 *
 * Safe to include multiple times (guard sur class_exists).
 */
/*
 * Les dependances sont chargees AVANT la garde, et c'est indispensable.
 *
 * PHP lie les declarations de classe a la COMPILATION du fichier, donc avant
 * d'en executer la premiere instruction. Quand la garde s'evalue, la classe
 * definie plus bas existe deja : la garde `return` systematiquement, et tout
 * ce qui la suit est du code mort. Un require_once place apres elle ne
 * s'execute jamais.
 */
require_once __DIR__ . '/DemoOracleConfig.php';

if (class_exists('DemoOracleSqlMonAccumulator')) return;

class DemoOracleSqlMonAccumulator
{
    /** Prefixe de nommage : sert aussi d'assiette au ramasse-miettes. */
    private const PREFIX = 'demo_oracle_sqlmon_';

    /** Au-dela, l'execution est consideree abandonnee. Un quart d'heure
        dans la demonstration, une heure dans le module complet. */
    private const TTL = DEMO_ORACLE_SQLMON_SAMPLE_TTL;

    /** Plafond de fichiers, pour ne pas remplir le repertoire temporaire. */
    private const MAX_FILES = 200;

    /**
     * L'identite d'un histogramme.
     *
     * Hachee : le sql_id et la base n'ont pas a apparaitre dans un nom de
     * fichier d'un repertoire partage.
     */
    public static function key(string $dbKey, string $sqlId, int $execId, string $execStart): string
    {
        return md5($dbKey . '|' . $sqlId . '|' . $execId . '|' . $execStart);
    }

    /**
     * L'etat accumule, ou l'etat vide si rien n'a encore ete releve.
     *
     * @return array{first:int, last:int, n:int, lines:array<string,array<string,int>>, stat0:array<string,int>}
     */
    public static function load(string $key): array
    {
        $path = self::path($key);
        $empty = ['first' => 0, 'last' => 0, 'n' => 0, 'lines' => [], 'stat0' => []];

        if (!is_file($path)) return $empty;

        $raw = @file_get_contents($path);
        if ($raw === false) return $empty;

        $state = json_decode($raw, true);
        if (!is_array($state)) return $empty;

        /* Un fichier tronque par une ecriture interrompue ne doit pas faire
           tomber la page : on repart de l'etat vide plutot que de propager
           des cles manquantes. */
        return [
            'first' => (int)($state['first'] ?? 0),
            'last'  => (int)($state['last']  ?? 0),
            'n'     => (int)($state['n']     ?? 0),
            'lines' => is_array($state['lines'] ?? null) ? $state['lines'] : [],
            'stat0' => is_array($state['stat0'] ?? null) ? $state['stat0'] : [],
        ];
    }

    /**
     * Ajoute un releve et rend l'etat mis a jour.
     *
     * @param array $samples retour de sampleSessionPlanLines() : une entree
     *                       par session active de l'execution
     * @param array $stats   retour de getSessionStatDelta(), premier releve
     *                       conserve pour pouvoir publier une difference
     * @param int   $now     horodatage du releve (injecte, jamais devine :
     *                       c'est ce qui rend la methode testable)
     */
    public static function add(string $key, array $samples, array $stats, int $now): array
    {
        $state = self::load($key);

        if ($state['first'] === 0) $state['first'] = $now;
        $state['last'] = $now;
        $state['n']++;

        /* Une session = un echantillon. Les sessions sans ligne de plan
           connue tombent sous la cle '' : leur temps est reel, il ne serait
           pas honnete de le jeter au motif qu'on ne sait pas ou l'imputer. */
        foreach ($samples as $s) {
            $line = ($s['line'] === null) ? '' : (string)(int)$s['line'];
            $wc   = (string)($s['wait_class'] ?? 'CPU');
            if (!isset($state['lines'][$line])) $state['lines'][$line] = [];
            $state['lines'][$line][$wc] = ($state['lines'][$line][$wc] ?? 0) + 1;
        }

        /* Le PREMIER releve seulement : c'est la reference du delta. */
        if (empty($state['stat0']) && !empty($stats)) {
            $state['stat0'] = $stats;
        }

        self::write($key, $state);
        return $state;
    }

    /** Oublie un histogramme — la page en a fini avec l'execution. */
    public static function drop(string $key): void
    {
        $path = self::path($key);
        if (is_file($path)) @unlink($path);
    }

    /**
     * Le delta des statistiques depuis le premier releve.
     *
     * Un compteur qui a recule (session recyclee, esclave PX disparu) rend 0
     * plutot qu'un nombre negatif : mieux vaut sous-estimer que d'afficher
     * une consommation impossible.
     */
    public static function delta(array $state, array $stats): array
    {
        $base = $state['stat0'] ?? [];
        $out  = [];
        foreach ($stats as $name => $value) {
            $out[$name] = max(0, (int)$value - (int)($base[$name] ?? 0));
        }
        return $out;
    }

    // ── prive ────────────────────────────────────────────────────────────

    private static function path(string $key): string
    {
        return rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR
             . self::PREFIX . preg_replace('/[^a-f0-9]/', '', $key) . '.json';
    }

    private static function write(string $key, array $state): void
    {
        $path = self::path($key);
        $new  = !is_file($path);

        if (@file_put_contents($path, json_encode($state), LOCK_EX) === false) return;
        if ($new) @chmod($path, 0600);

        /* Le ramasse-miettes ne tourne qu'a la creation : le passer a chaque
           poll ferait un glob() toutes les 5 secondes pour rien. */
        if ($new) self::gc();
    }

    private static function gc(): void
    {
        $files = @glob(rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR
                       . self::PREFIX . '*.json');
        if (!is_array($files) || count($files) === 0) return;

        $now   = time();
        $alive = [];
        foreach ($files as $f) {
            $mtime = @filemtime($f);
            if ($mtime === false) continue;
            if ($now - $mtime > self::TTL) { @unlink($f); continue; }
            $alive[$f] = $mtime;
        }

        /* Toujours trop de fichiers vivants : on sacrifie les plus anciens.
           Un histogramme perdu se reconstruit au poll suivant, en repartant
           de zero — degradation visible, pas silencieuse. */
        if (count($alive) > self::MAX_FILES) {
            asort($alive);
            $excess = array_slice(array_keys($alive), 0, count($alive) - self::MAX_FILES);
            foreach ($excess as $f) @unlink($f);
        }
    }
}
