#!/usr/bin/php -q
<?php
/**
 * cliArchiverCategoriesDocsLogements.php
 *
 * Deplace les documents des categories choisies de l'onglet "Documents" de la
 * FICHE LOGEMENT vers une categorie d'archivage unique ("Historique" par defaut),
 * avec suppression optionnelle de la categorie source une fois videe.
 *
 * ATTENTION : ne concerne QUE les documents LOGEMENT, jamais ceux des locataires
 * (categorie_document / documents / document_locataire ne sont pas touches).
 *
 * Rappel du modele (verifie dans LogementController::loadDocumentsLogeTab) :
 *   - un document logement n'a AUCUNE ligne en base, c'est uniquement un fichier :
 *       public/docs/logements/<idLogement>/uploads/<idCategorie>/<fichier>
 *       public/docs/logements/<idLogement>/uploads/<fichier>     <- pseudo-categorie "Sans catégorie"
 *   - les categories sont dans `categorie_document_loge` (id, nom, idParent, deleted, ordre),
 *     arbre plafonne a 3 niveaux par l'appli (fetchAllParents ne propose que level < 2).
 *   => "deplacer un document" = deplacer un fichier d'un dossier a l'autre, rien de plus.
 *
 * Garanties :
 *   - DRY-RUN par defaut : rien n'est ecrit tant que --go n'est pas passe.
 *   - Aucun ecrasement : en cas d'homonyme dans la cible, suffixe _1, _2, ...
 *   - Aucune suppression de fichier : uniquement des deplacements (rename).
 *   - Une categorie n'est archivee (deleted=1) que si elle est REELLEMENT vide
 *     sur la TOTALITE des logements, y compris hors du filtre --logement.
 *   - Relançable : un second passage ne trouve plus rien a deplacer.
 *
 * Usage :
 *   php5.6 cli/cliArchiverCategoriesDocsLogements.php --toutes                    # DRY-RUN complet
 *   php5.6 cli/cliArchiverCategoriesDocsLogements.php --cat=1,5,18 --detail       # DRY-RUN detaille
 *   php5.6 cli/cliArchiverCategoriesDocsLogements.php --cat=1,5,18 --go           # applique
 *   php5.6 cli/cliArchiverCategoriesDocsLogements.php --toutes --prefixer \
 *          --supprimer-categorie --csv=/tmp/archivage.csv --go
 *
 * Se lance depuis la racine du projet (le chemin des docs et la config BDD en dependent).
 */

error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
mb_internal_encoding('UTF-8');

$Script = basename($_SERVER['argv'][0]);

## ----------------------------------------------------------------------------------------------------
## Usage
## ----------------------------------------------------------------------------------------------------
function usage()
{
	global $Script;

	echo "$Script
DESCRIPTION :
	Deplace les documents des categories de l'onglet Documents de la FICHE LOGEMENT
	vers une categorie d'archivage unique, et peut ensuite archiver la categorie videe.
	Ne touche pas aux documents des locataires.

CHOIX DES CATEGORIES A VIDER (au moins un obligatoire) :
	--cat=<ids>            : ids de categories, separes par des virgules (ex: --cat=1,5,18)
	--cat-nom=<motif>      : categories dont le nom correspond au motif SQL LIKE (ex: --cat-nom='DEVIS%')
	--toutes               : toutes les categories actives, sauf la cible
	--inclure-enfants      : ajoute recursivement les sous-categories des categories choisies
	--inclure-racine       : traite aussi les fichiers poses directement dans uploads/,
	                         affiches sous \"Sans catégorie\" dans l'onglet

CATEGORIE CIBLE :
	--cible=<nom|id>       : categorie d'arrivee (defaut: 'Historique')
	                         nom inconnu -> creee a la racine de l'arbre
	                         id          -> doit exister et etre active
	--prefixer             : prefixe chaque fichier deplace par sa categorie d'origine
	                         (ex: ETAT_LIEUX__edl_2024-01-05-101500.pdf)

APRES DEPLACEMENT :
	--supprimer-categorie  : passe la categorie videe a deleted=1 (comme l'appli)
	--purger-dossiers      : supprime en plus les dossiers source devenus vides

PORTEE / SORTIE :
	--logement=<ids>       : limite le deplacement a ces logements (ex: --logement=352,353)
	--racine=<chemin>      : racine des documents (defaut: <projet>/public/docs/logements)
	--db=<base>            : force la base (defaut: celle de config/autoload/global.php)
	--detail               : liste chaque fichier au lieu du seul resume
	--csv=<fichier>        : ecrit la trace complete des deplacements au format CSV
	--go                   : APPLIQUE les changements (sans ce parametre : simulation)
	--help                 : cette aide

phpVersion : ".phpversion()."\n";
	exit(1);
}

## ----------------------------------------------------------------------------------------------------
## Helpers
## ----------------------------------------------------------------------------------------------------

/**
 * Lit la configuration BDD du projet (meme methode que cliMajSql.php).
 */
function lireConfigBdd($dbForce = '')
{
	$conf = array('name' => '', 'user' => '', 'pass' => '', 'host' => 'localhost');

	$globalPath = './config/autoload/global.php';
	$file = @file($globalPath);
	if (!$file) {
		fwrite(STDERR, "ERREUR: impossible de lire $globalPath (lancez le script depuis la racine du projet)\n");
		exit(1);
	}
	foreach ($file as $line) {
		if (preg_match("@^\s*'dsn'\s*=>.*dbname=(.+);host=([^']*)@", $line, $m)) {
			$conf['name'] = $m[1];
			$conf['host'] = $m[2] ? $m[2] : 'localhost';
			break;
		}
	}

	$localPath = './config/autoload/local.php';
	$file = @file($localPath);
	if (!$file) {
		fwrite(STDERR, "ERREUR: impossible de lire $localPath\n");
		exit(1);
	}
	foreach ($file as $line) {
		if (!$conf['user'] && preg_match("@^.*'username'\s*=>\s*'(.+)'.*$@", $line, $m)) { $conf['user'] = $m[1]; }
		if (!$conf['pass'] && preg_match("@^.*'password'\s*=>\s*'(.+)'.*$@", $line, $m)) { $conf['pass'] = $m[1]; }
		if ($conf['user'] && $conf['pass']) { break; }
	}

	if ($dbForce !== '') { $conf['name'] = $dbForce; }

	if (!$conf['name'] || !$conf['user']) {
		fwrite(STDERR, "ERREUR: base ou identifiants introuvables dans config/autoload/\n");
		exit(1);
	}
	return $conf;
}

/**
 * Nettoie une chaine pour en faire un morceau de nom de fichier
 * (les noms de categories contiennent des '/', des accents, des apostrophes...).
 */
function removeAccent($str)
{
	$str  = (string) $str;
	$conv = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
	if ($conv !== false && $conv !== '') { $str = $conv; }
	$str = preg_replace('/[^A-Za-z0-9\-_. ]/', '', $str);
	$str = preg_replace('/\s+/', '_', trim($str));
	$str = trim($str, '_');
	return $str !== '' ? $str : 'cat';
}

/**
 * Liste les documents d'un repertoire, EXACTEMENT comme Tools::lireDocumentsDeRepertoire :
 * fichiers (pas de dossier) possedant une extension.
 */
function lireDocumentsDeRepertoire($dir)
{
	$docs = array();
	if (is_dir($dir) && ($dh = @opendir($dir))) {
		while (($file = readdir($dh)) !== false) {
			if ($file === '.' || $file === '..') { continue; }
			if (is_dir($dir.'/'.$file)) { continue; }
			if (pathinfo($file, PATHINFO_EXTENSION)) { $docs[] = $file; }
		}
		closedir($dh);
		sort($docs);
	}
	return $docs;
}

/**
 * Cree un dossier en reprenant droits et proprietaire de son parent : sans cela, un
 * dossier cree par root serait inaccessible en ecriture a Apache lors des uploads suivants.
 */
function creerDossier($dir)
{
	if (is_dir($dir)) { return true; }
	$parent = dirname($dir);
	$perms  = is_dir($parent) ? (fileperms($parent) & 0777) : 0775;
	if (!@mkdir($dir, $perms, true)) { return false; }
	@chmod($dir, $perms);
	if (is_dir($parent) && function_exists('posix_geteuid') && posix_geteuid() === 0) {
		@chown($dir, fileowner($parent));
		@chgrp($dir, filegroup($parent));
	}
	return true;
}

/**
 * Retourne un nom de fichier libre dans $dossier : jamais d'ecrasement.
 * $reserves memorise les noms deja attribues pendant ce meme passage (dry-run compris).
 */
function nomLibre($dossier, $nom, &$reserves)
{
	$cle = $dossier.'/'.$nom;
	if (!file_exists($cle) && !isset($reserves[$cle])) {
		$reserves[$cle] = true;
		return array($nom, false);
	}
	$ext  = pathinfo($nom, PATHINFO_EXTENSION);
	$base = pathinfo($nom, PATHINFO_FILENAME);
	for ($i = 1; $i < 10000; $i++) {
		$essai = $base.'_'.$i.($ext !== '' ? '.'.$ext : '');
		$cle   = $dossier.'/'.$essai;
		if (!file_exists($cle) && !isset($reserves[$cle])) {
			$reserves[$cle] = true;
			return array($essai, true);
		}
	}
	return array(null, true);
}

/**
 * Tronque et cadre une chaine sur $len caracteres : sprintf('%-45s') compte en OCTETS
 * et desaligne toutes les colonnes des que le nom porte un accent.
 */
function padMb($str, $len)
{
	$str = mb_substr((string) $str, 0, $len);
	$manque = $len - mb_strlen($str);
	return $str.($manque > 0 ? str_repeat(' ', $manque) : '');
}

function formatTaille($octets)
{
	$unites = array('o', 'Ko', 'Mo', 'Go');
	$i = 0;
	while ($octets >= 1024 && $i < count($unites) - 1) { $octets /= 1024; $i++; }
	return sprintf('%.1f %s', $octets, $unites[$i]);
}

/**
 * Descendants d'une categorie (recursif), pour --inclure-enfants et les controles de coherence.
 */
function descendants($idCat, $enfants, $seulementActives = true, $cats = array())
{
	$out = array();
	if (empty($enfants[$idCat])) { return $out; }
	foreach ($enfants[$idCat] as $idFils) {
		if ($seulementActives && !empty($cats[$idFils]) && $cats[$idFils]['deleted']) { continue; }
		$out[] = $idFils;
		$out = array_merge($out, descendants($idFils, $enfants, $seulementActives, $cats));
	}
	return $out;
}

## ----------------------------------------------------------------------------------------------------
## Parametres
## ----------------------------------------------------------------------------------------------------
$opts = getopt('', array(
	'go', 'help', 'detail', 'toutes', 'prefixer', 'inclure-enfants', 'inclure-racine',
	'supprimer-categorie', 'purger-dossiers',
	'cat::', 'cat-nom::', 'cible::', 'logement::', 'racine::', 'db::', 'csv::',
));

if (isset($opts['help']) || $_SERVER['argc'] < 2) { usage(); }

function optVal($opts, $cle, $defaut = '')
{
	return (isset($opts[$cle]) && $opts[$cle] !== false) ? $opts[$cle] : $defaut;
}

$apply          = isset($opts['go']);
$detail         = isset($opts['detail']);
$toutes         = isset($opts['toutes']);
$prefixer       = isset($opts['prefixer']);
$inclureEnfants = isset($opts['inclure-enfants']);
$inclureRacine  = isset($opts['inclure-racine']);
$supprimerCat   = isset($opts['supprimer-categorie']);
$purgerDossiers = isset($opts['purger-dossiers']);

$catIds    = array();
if (optVal($opts, 'cat') !== '') {
	foreach (explode(',', optVal($opts, 'cat')) as $v) {
		$v = (int) trim($v);
		if ($v > 0) { $catIds[$v] = $v; }
	}
}
$catNom    = optVal($opts, 'cat-nom');
$cibleArg  = optVal($opts, 'cible', 'Historique');
$csvPath   = optVal($opts, 'csv');
$dbForce   = optVal($opts, 'db');
$racine    = optVal($opts, 'racine', getcwd().'/public/docs/logements');
$racine    = rtrim($racine, '/');

$logeFiltre = array();
if (optVal($opts, 'logement') !== '') {
	foreach (explode(',', optVal($opts, 'logement')) as $v) {
		$v = (int) trim($v);
		if ($v > 0) { $logeFiltre[$v] = $v; }
	}
}

if (empty($catIds) && $catNom === '' && !$toutes && !$inclureRacine) {
	fwrite(STDERR, "ERREUR: aucune categorie choisie. Utilisez --cat=, --cat-nom=, --toutes ou --inclure-racine.\n\n");
	usage();
}

if (!is_dir($racine)) {
	fwrite(STDERR, "ERREUR: racine des documents introuvable : $racine\n");
	exit(1);
}

## ----------------------------------------------------------------------------------------------------
## Connexion
## ----------------------------------------------------------------------------------------------------
$cfg = lireConfigBdd($dbForce);
try {
	$pdo = new PDO(
		"mysql:host=".$cfg['host'].";dbname=".$cfg['name'].";charset=utf8",
		$cfg['user'], $cfg['pass'],
		array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC)
	);
} catch (Exception $e) {
	fwrite(STDERR, "ERREUR connexion BDD (".$cfg['name'].") : ".$e->getMessage()."\n");
	exit(1);
}

echo "==============================================================================\n";
echo ($apply ? "MODE REEL (--go) : les fichiers vont etre deplaces\n"
             : "MODE DRY-RUN : aucune ecriture — ajouter --go pour appliquer\n");
echo "Base    : ".$cfg['name']." sur ".$cfg['host']."\n";
echo "Racine  : $racine\n";
if (!empty($logeFiltre)) { echo "Filtre  : logements ".implode(',', $logeFiltre)."\n"; }
echo "Perimetre : <idLogement>/uploads/ UNIQUEMENT.\n";
echo "            Les dossiers voisins (edl-*, suivis/) et les fichiers ranges directement\n";
echo "            sous <idLogement>/ ne sont ni lus ni deplaces.\n";
echo "==============================================================================\n\n";

## ----------------------------------------------------------------------------------------------------
## Schema de categorie_document_loge
## ----------------------------------------------------------------------------------------------------
## La table a ete creee avec les seules colonnes (id, nom) ; idParent, deleted et ordre ont ete
## ajoutees plus tard (majsql.txt, 14-07-2026). Les sites qui n'ont pas rejoue ces ALTER n'ont
## donc que 2 colonnes : on s'adapte au lieu de planter sur une erreur SQL 1054.
$colonnes = array();
foreach ($pdo->query("SHOW COLUMNS FROM categorie_document_loge") as $r) {
	$colonnes[$r['Field']] = true;
}
$aParent  = isset($colonnes['idParent']);
$aDeleted = isset($colonnes['deleted']);
$aOrdre   = isset($colonnes['ordre']);

if (!$aParent || !$aDeleted) {
	echo "SCHEMA REDUIT : la table categorie_document_loge n'a pas la colonne "
		.(!$aParent ? "`idParent`" : "")
		.(!$aParent && !$aDeleted ? " ni " : "")
		.(!$aDeleted ? "`deleted`" : "")."\n";
	echo "  -> les categories sont traitees comme un arbre plat, toutes actives.\n";
	echo "  -> a noter : l'onglet Documents de la fiche logement ne fonctionne pas non plus\n";
	echo "     sur ce site tant que les ALTER de majsql.txt (14-07-2026) ne sont pas passes.\n\n";
}

## ----------------------------------------------------------------------------------------------------
## Lecture des categories
## ----------------------------------------------------------------------------------------------------
$champsSel = 'id, nom'.($aParent ? ', idParent' : '').($aDeleted ? ', deleted' : '').($aOrdre ? ', ordre' : '');
$cats    = array();
$enfants = array();
foreach ($pdo->query("SELECT $champsSel FROM categorie_document_loge ORDER BY id") as $r) {
	$id = (int) $r['id'];
	## colonnes absentes : arbre plat (idParent=0) et categorie active (deleted=0)
	$idParent = $aParent  ? (int) $r['idParent'] : 0;
	$cats[$id] = array(
		'id'       => $id,
		'nom'      => $r['nom'],
		'idParent' => $idParent,
		'deleted'  => $aDeleted ? (int) $r['deleted'] : 0,
	);
	$enfants[$idParent][] = $id;
}

## Numeros de logements, pour un rapport lisible
$numLoge = array();
foreach ($pdo->query("SELECT id, numero FROM logements") as $r) {
	$numLoge[(int) $r['id']] = $r['numero'];
}

## ----------------------------------------------------------------------------------------------------
## Categorie cible
## ----------------------------------------------------------------------------------------------------
$cibleId    = 0;
$cibleNom   = '';
$cibleACreer = false;

if (ctype_digit((string) $cibleArg)) {
	$cibleId = (int) $cibleArg;
	if (empty($cats[$cibleId])) {
		fwrite(STDERR, "ERREUR: categorie cible #$cibleId inexistante.\n");
		exit(1);
	}
	if ($cats[$cibleId]['deleted']) {
		fwrite(STDERR, "ERREUR: categorie cible #$cibleId est archivee (deleted=1), elle n'apparaitrait pas dans l'onglet.\n");
		exit(1);
	}
	$cibleNom = $cats[$cibleId]['nom'];
} else {
	$cibleNom  = trim($cibleArg);
	$trouvees = array();
	foreach ($cats as $c) {
		if (!$c['deleted'] && $c['nom'] === $cibleNom) { $trouvees[] = $c['id']; }
	}
	if (count($trouvees) > 1) {
		fwrite(STDERR, "ERREUR: plusieurs categories actives se nomment '$cibleNom' (#".implode(', #', $trouvees)."). Utilisez --cible=<id>.\n");
		exit(1);
	}
	if (count($trouvees) === 1) {
		$cibleId = $trouvees[0];
	} else {
		$cibleACreer = true;
	}
}

## ----------------------------------------------------------------------------------------------------
## Categories sources
## ----------------------------------------------------------------------------------------------------
$sources = array();   // idCat => true   (0 = pseudo-categorie "Sans catégorie")

if ($toutes) {
	foreach ($cats as $c) {
		if (!$c['deleted']) { $sources[$c['id']] = true; }
	}
}
foreach ($catIds as $id) {
	if (empty($cats[$id])) {
		## Pas de ligne en base : ce peut etre un dossier orphelin (categorie supprimee
		## physiquement un jour) dont les documents sont devenus invisibles dans l'onglet.
		## On accepte de le vider s'il existe reellement sur le disque.
		$vu = false;
		foreach (scandir($racine) as $d) {
			if (ctype_digit($d) && is_dir($racine.'/'.$d.'/uploads/'.$id)) { $vu = true; break; }
		}
		if (!$vu) {
			fwrite(STDERR, "ERREUR: categorie #$id inexistante en base et aucun dossier correspondant sous $racine.\n");
			exit(1);
		}
		$cats[$id] = array('id' => $id, 'nom' => "(categorie #$id disparue de la base)",
			'idParent' => 0, 'deleted' => 1, 'orpheline' => true);
	}
	$sources[$id] = true;
}
if ($catNom !== '') {
	$st = $pdo->prepare("SELECT id FROM categorie_document_loge WHERE ".($aDeleted ? "deleted = 0 AND " : "")."nom LIKE :m");
	$st->execute(array(':m' => $catNom));
	$n = 0;
	foreach ($st as $r) { $sources[(int) $r['id']] = true; $n++; }
	if (!$n) { echo "AVERTISSEMENT: aucune categorie active ne correspond a --cat-nom='$catNom'\n\n"; }
}
if ($inclureEnfants) {
	foreach (array_keys($sources) as $id) {
		if ($id === 0) { continue; }
		foreach (descendants($id, $enfants, true, $cats) as $idFils) { $sources[$idFils] = true; }
	}
}
if ($inclureRacine) { $sources[0] = true; }

## La cible ne peut pas etre sa propre source
if ($cibleId > 0 && isset($sources[$cibleId])) {
	if (isset($catIds[$cibleId])) {
		fwrite(STDERR, "ERREUR: la categorie cible #$cibleId est aussi listee dans --cat.\n");
		exit(1);
	}
	unset($sources[$cibleId]);   // cas --toutes : on l'exclut simplement
}

if (empty($sources)) {
	fwrite(STDERR, "ERREUR: aucune categorie source retenue.\n");
	exit(1);
}

## ----------------------------------------------------------------------------------------------------
## Controles de coherence AVANT toute ecriture
## ----------------------------------------------------------------------------------------------------
$bloquants = array();

## Archiver, c'est passer deleted a 1 : sans la colonne, l'operation n'a pas de sens
if ($supprimerCat && !$aDeleted) {
	$bloquants[] = "--supprimer-categorie demande, mais la table categorie_document_loge n'a pas la"
		." colonne `deleted` sur ce site : impossible d'archiver une categorie. Passez les ALTER de"
		." majsql.txt (14-07-2026), ou relancez sans --supprimer-categorie (les fichiers seront"
		." quand meme regroupes)";
}

if ($supprimerCat) {
	## Archiver un parent sans archiver ses enfants les rendrait invisibles :
	## getCategoriesTree() descend depuis idParent=0 et ne reverrait jamais l'enfant.
	foreach (array_keys($sources) as $id) {
		if ($id === 0) { continue; }
		foreach (descendants($id, $enfants, true, $cats) as $idFils) {
			if (empty($sources[$idFils])) {
				$bloquants[] = "la categorie #$id (".$cats[$id]['nom'].") a la sous-categorie active #$idFils ("
					.$cats[$idFils]['nom'].") qui n'est pas traitee : l'archiver rendrait ses documents invisibles"
					." (ajoutez --inclure-enfants, ou retirez #$id)";
			}
		}
	}
	## La cible ne doit pas etre un descendant d'une categorie archivee
	if ($cibleId > 0) {
		$p = $cats[$cibleId]['idParent'];
		while ($p > 0 && !empty($cats[$p])) {
			if (isset($sources[$p])) {
				$bloquants[] = "la categorie cible #$cibleId ($cibleNom) descend de #$p (".$cats[$p]['nom']
					.") qui serait archivee : la cible deviendrait invisible";
				break;
			}
			$p = $cats[$p]['idParent'];
		}
	}
}

if (!empty($bloquants)) {
	echo "ARRET : incoherences detectees\n";
	foreach ($bloquants as $b) { echo "  - $b\n"; }
	echo "\nAucune modification effectuee.\n";
	exit(1);
}

## ----------------------------------------------------------------------------------------------------
## Scan du disque et construction du plan
## ----------------------------------------------------------------------------------------------------
$logements = array();
foreach (scandir($racine) as $d) {
	if ($d === '.' || $d === '..' || !ctype_digit($d)) { continue; }
	if (is_dir($racine.'/'.$d.'/uploads')) { $logements[] = (int) $d; }
}
sort($logements);

$plan       = array();   // deplacements a effectuer
$reserves   = array();   // noms deja attribues dans la cible (anti-ecrasement)
$statsCat   = array();   // idCat => compteurs
$horsFiltre = array();   // idCat => nb de fichiers laisses de cote par --logement
$orphelins  = array();   // dossiers dont la categorie n'existe pas / est archivee
$nonCats    = array();   // dossiers non numeriques dans uploads/ : ne sont pas des categories

foreach ($logements as $idLoge) {
	$baseLoge = $racine.'/'.$idLoge.'/uploads';

	## Detection des dossiers orphelins (categorie disparue ou archivee) et de tout ce qui,
	## dans uploads/, ne correspond a aucune categorie : rien ne doit etre ignore en silence.
	foreach (scandir($baseLoge) as $d) {
		if ($d === '.' || $d === '..') { continue; }
		if (!is_dir($baseLoge.'/'.$d)) { continue; }   // fichier = pseudo-categorie "Racine"
		if (!ctype_digit($d)) {
			## un dossier non numerique n'a jamais ete cree par la gestion documentaire
			if (!isset($nonCats[$d])) { $nonCats[$d] = array('nb' => 0, 'loge' => 0); }
			$nonCats[$d]['nb'] += count(lireDocumentsDeRepertoire($baseLoge.'/'.$d));
			$nonCats[$d]['loge']++;
			continue;
		}
		$idc = (int) $d;
		if (empty($cats[$idc]) || $cats[$idc]['deleted']) {
			$nb = count(lireDocumentsDeRepertoire($baseLoge.'/'.$d));
			if ($nb > 0 && empty($sources[$idc])) {
				$k = $idc;
				if (!isset($orphelins[$k])) { $orphelins[$k] = array('nb' => 0, 'loge' => 0); }
				$orphelins[$k]['nb'] += $nb;
				$orphelins[$k]['loge']++;
			}
		}
	}

	foreach (array_keys($sources) as $idCat) {
		$dirSrc = ($idCat === 0) ? $baseLoge : $baseLoge.'/'.$idCat;
		if (!is_dir($dirSrc)) { continue; }

		$fichiers = lireDocumentsDeRepertoire($dirSrc);
		if (empty($fichiers)) { continue; }

		if (!isset($statsCat[$idCat])) {
			$statsCat[$idCat] = array('fichiers' => 0, 'octets' => 0, 'logements' => 0, 'collisions' => 0);
		}

		## Logement exclu par --logement : on compte, mais on ne deplace pas
		if (!empty($logeFiltre) && empty($logeFiltre[$idLoge])) {
			$horsFiltre[$idCat] = (isset($horsFiltre[$idCat]) ? $horsFiltre[$idCat] : 0) + count($fichiers);
			continue;
		}

		$statsCat[$idCat]['logements']++;
		$dirDst = $baseLoge.'/'.($cibleACreer ? '__CIBLE__' : $cibleId);

		foreach ($fichiers as $f) {
			$nomDst = $f;
			if ($prefixer) {
				$nomDst = removeAccent($idCat === 0 ? 'Sans catégorie' : $cats[$idCat]['nom']).'__'.$f;
			}
			list($nomFinal, $collision) = nomLibre($dirDst, $nomDst, $reserves);
			if ($nomFinal === null) {
				echo "AVERTISSEMENT: impossible de trouver un nom libre pour $dirSrc/$f — fichier ignore\n";
				continue;
			}
			$taille = @filesize($dirSrc.'/'.$f);
			$plan[] = array(
				'idLoge'  => $idLoge,
				'idCat'   => $idCat,
				'src'     => $dirSrc.'/'.$f,
				'nomSrc'  => $f,
				'dirDst'  => $dirDst,
				'nomDst'  => $nomFinal,
				'octets'  => $taille ? $taille : 0,
			);
			$statsCat[$idCat]['fichiers']++;
			$statsCat[$idCat]['octets'] += $taille ? $taille : 0;
			if ($collision) { $statsCat[$idCat]['collisions']++; }
		}
	}
}

## ----------------------------------------------------------------------------------------------------
## Rapport
## ----------------------------------------------------------------------------------------------------
echo "CATEGORIE CIBLE\n";
echo "---------------\n";
if ($cibleACreer) {
	echo "  '$cibleNom' : ABSENTE — ".($apply ? "elle va etre creee" : "elle serait creee")." a la racine de l'arbre\n";
} else {
	echo "  #$cibleId '$cibleNom'".($cats[$cibleId]['idParent'] > 0 ? " (sous-categorie de #".$cats[$cibleId]['idParent'].")" : "")."\n";
}
echo "\n";

echo "CATEGORIES A VIDER\n";
echo "------------------\n";
$totFichiers = 0; $totOctets = 0; $totCollisions = 0;
$idsTries = array_keys($sources);
sort($idsTries);
foreach ($idsTries as $idCat) {
	## libelle nommant a la fois l'etiquette de l'onglet et l'emplacement physique : reste
	## juste sur un site en version anterieure, ou l'onglet affiche encore "Racine"
	$nom = ($idCat === 0) ? 'Sans catégorie (racine de uploads/)' : $cats[$idCat]['nom'];
	$sfx = '';
	if ($idCat !== 0 && !empty($cats[$idCat]['orpheline'])) {
		$sfx = '  [dossier orphelin, aucune ligne en base]';
	} elseif ($idCat !== 0 && $cats[$idCat]['deleted']) {
		$sfx = '  [deja archivee, deleted=1]';
	}
	if (empty($statsCat[$idCat]) || !$statsCat[$idCat]['fichiers']) {
		$reste = isset($horsFiltre[$idCat]) ? $horsFiltre[$idCat] : 0;
		echo sprintf("  #%-4s %s aucun fichier a deplacer%s%s\n", $idCat, padMb($nom, 45), ($reste ? " ($reste hors filtre)" : ""), $sfx);
		continue;
	}
	$s = $statsCat[$idCat];
	echo sprintf("  #%-4s %s %4d fichier(s)  %10s  sur %d logement(s)%s\n",
		$idCat, padMb($nom, 45), $s['fichiers'], formatTaille($s['octets']), $s['logements'], $sfx);
	if ($s['collisions']) {
		echo sprintf("        -> %d homonyme(s) dans la cible : renommes en _1, _2, ... (aucun ecrasement)\n", $s['collisions']);
	}
	if (!empty($horsFiltre[$idCat])) {
		echo sprintf("        -> %d fichier(s) laisses de cote par --logement : la categorie ne sera pas archivee\n", $horsFiltre[$idCat]);
	}
	$totFichiers += $s['fichiers'];
	$totOctets   += $s['octets'];
	$totCollisions += $s['collisions'];
}
echo "\n";
echo "  TOTAL : $totFichiers fichier(s), ".formatTaille($totOctets).", $totCollisions renommage(s) pour homonymie\n\n";

if ($detail) {
	echo "DETAIL DES DEPLACEMENTS\n";
	echo "-----------------------\n";
	foreach ($plan as $p) {
		$num = isset($numLoge[$p['idLoge']]) ? $numLoge[$p['idLoge']] : '?';
		echo sprintf("  logement %-6s (%-12s) cat #%-4s : %s\n", $p['idLoge'], $num, $p['idCat'], $p['nomSrc']);
		echo sprintf("        -> %s/%s\n", str_replace($racine, '...', $p['dirDst']), $p['nomDst']);
	}
	echo "\n";
}

if (!empty($orphelins) || !empty($nonCats)) {
	echo "ANOMALIES (contenu de uploads/ sans categorie active, non traite)\n";
	echo "----------------------------------------------------------------\n";
	foreach ($nonCats as $nom => $o) {
		echo sprintf("  %s %d fichier(s) sur %d logement(s) — ignore (pas une categorie)\n",
			padMb("dossier '".$nom."' non numerique dans uploads/", 60), $o['nb'], $o['loge']);
	}
	foreach ($orphelins as $idc => $o) {
		$quoi = empty($cats[$idc]) ? "categorie #$idc disparue de la base" : "categorie #$idc '".$cats[$idc]['nom']."' archivee (deleted=1)";
		echo sprintf("  %s %d fichier(s) sur %d logement(s) — invisibles dans l'onglet\n", padMb($quoi, 60), $o['nb'], $o['loge']);
	}
	if (!empty($orphelins)) {
		echo "  (--cat=<id> permet de recuperer ces documents vers la categorie cible)\n";
	}
	if (!empty($nonCats)) {
		echo "  (un dossier non numerique n'a pas ete cree par la gestion documentaire : a traiter a la main)\n";
	}
	echo "\n";
}

if (!$apply) {
	echo "==============================================================================\n";
	echo "DRY-RUN termine : aucune modification. Relancer avec --go pour appliquer.\n";
	echo "==============================================================================\n";
	if ($csvPath !== '') { ecrireCsv($csvPath, $plan, $cats, $numLoge, $cibleNom, 'simule'); echo "CSV ecrit : $csvPath\n"; }
	exit(0);
}

## ----------------------------------------------------------------------------------------------------
## Application
## ----------------------------------------------------------------------------------------------------
if (empty($plan) && !$supprimerCat) {
	echo "Rien a faire.\n";
	exit(0);
}

## 1) creation de la categorie cible si besoin
if ($cibleACreer) {
	$colsIns = array('nom');
	$valsIns = array(':n');
	if ($aParent)  { $colsIns[] = 'idParent'; $valsIns[] = '0'; }
	if ($aDeleted) { $colsIns[] = 'deleted';  $valsIns[] = '0'; }
	if ($aOrdre)   { $colsIns[] = 'ordre';    $valsIns[] = '0'; }
	$st = $pdo->prepare("INSERT INTO categorie_document_loge (".implode(', ', $colsIns).") "
		."VALUES (".implode(', ', $valsIns).")");
	$st->execute(array(':n' => $cibleNom));
	$cibleId = (int) $pdo->lastInsertId();
	$cats[$cibleId] = array('id' => $cibleId, 'nom' => $cibleNom, 'idParent' => 0, 'deleted' => 0);
	echo "Categorie cible creee : #$cibleId '$cibleNom'\n";
	foreach ($plan as $k => $p) {
		$plan[$k]['dirDst'] = str_replace('/__CIBLE__', '/'.$cibleId, $p['dirDst']);
	}
}

## 2) deplacement des fichiers
$okDep = 0; $koDep = 0;
$statuts = array();
foreach ($plan as $k => $p) {
	if (!creerDossier($p['dirDst'])) {
		echo "ERREUR: creation impossible du dossier ".$p['dirDst']."\n";
		$statuts[$k] = 'echec-dossier'; $koDep++;
		continue;
	}
	$dst = $p['dirDst'].'/'.$p['nomDst'];
	if (file_exists($dst)) {   // securite ultime : on ne touche jamais a un fichier existant
		echo "ERREUR: la cible existe deja, deplacement annule : $dst\n";
		$statuts[$k] = 'cible-existante'; $koDep++;
		continue;
	}
	$ok = @rename($p['src'], $dst);
	if (!$ok) {   // systemes de fichiers differents : copie puis suppression, avec verification
		if (@copy($p['src'], $dst) && file_exists($dst) && filesize($dst) === filesize($p['src'])) {
			$ok = @unlink($p['src']);
		}
	}
	if ($ok && file_exists($dst) && !file_exists($p['src'])) {
		$statuts[$k] = 'deplace'; $okDep++;
	} else {
		echo "ERREUR: deplacement echoue : ".$p['src']." -> $dst\n";
		$statuts[$k] = 'echec'; $koDep++;
	}
}
echo "\nDeplacements : $okDep reussi(s), $koDep echec(s)\n";

## 3) archivage des categories reellement vides (TOUS logements, filtre compris)
if ($supprimerCat) {
	echo "\nARCHIVAGE DES CATEGORIES VIDEES\n";
	echo "-------------------------------\n";
	foreach ($idsTries as $idCat) {
		if ($idCat === 0) {
			echo "  Sans catégorie (racine de uploads/) : pas une categorie, rien a archiver\n";
			continue;
		}
		if (!empty($cats[$idCat]['orpheline'])) {
			echo sprintf("  #%-4s %s aucune ligne en base : rien a archiver\n", $idCat, padMb($cats[$idCat]['nom'], 40));
			if ($purgerDossiers) { echo "        ".purgerDossiersVides($racine, $logements, $idCat)." dossier(s) vide(s) supprime(s)\n"; }
			continue;
		}
		$reste = 0;
		foreach ($logements as $idLoge) {
			$d = $racine.'/'.$idLoge.'/uploads/'.$idCat;
			$reste += count(lireDocumentsDeRepertoire($d));
		}
		if ($reste > 0) {
			echo sprintf("  #%-4s %s NON archivee : %d fichier(s) restant(s)\n", $idCat, padMb($cats[$idCat]['nom'], 40), $reste);
			continue;
		}
		$pdo->prepare("UPDATE categorie_document_loge SET deleted = 1 WHERE id = :id")->execute(array(':id' => $idCat));
		echo sprintf("  #%-4s %s archivee (deleted=1)\n", $idCat, padMb($cats[$idCat]['nom'], 40));

		if ($purgerDossiers) {
			$n = purgerDossiersVides($racine, $logements, $idCat);
			if ($n) { echo "        $n dossier(s) vide(s) supprime(s)\n"; }
		}
	}
}

if ($csvPath !== '') {
	ecrireCsv($csvPath, $plan, $cats, $numLoge, $cibleNom, $statuts);
	echo "\nCSV ecrit : $csvPath\n";
}

echo "\n==============================================================================\n";
echo "Termine.\n";
echo "==============================================================================\n";
exit($koDep > 0 ? 2 : 0);

## ----------------------------------------------------------------------------------------------------
/**
 * Supprime, pour chaque logement, le dossier de la categorie s'il ne contient plus aucun document.
 */
function purgerDossiersVides($racine, $logements, $idCat)
{
	$n = 0;
	foreach ($logements as $idLoge) {
		$d = $racine.'/'.$idLoge.'/uploads/'.$idCat;
		if (is_dir($d) && count(lireDocumentsDeRepertoire($d)) === 0 && @rmdir($d)) { $n++; }
	}
	return $n;
}

function ecrireCsv($path, $plan, $cats, $numLoge, $cibleNom, $statuts)
{
	$fh = @fopen($path, 'w');
	if (!$fh) { echo "AVERTISSEMENT: ecriture CSV impossible : $path\n"; return; }
	fwrite($fh, "\xEF\xBB\xBF");   // BOM UTF-8 pour Excel
	fputcsv($fh, array('idLogement', 'numeroLogement', 'idCategorieSource', 'nomCategorieSource',
		'fichierSource', 'categorieCible', 'fichierCible', 'octets', 'statut'), ';');
	foreach ($plan as $k => $p) {
		$statut = is_array($statuts) ? (isset($statuts[$k]) ? $statuts[$k] : 'non-traite') : $statuts;
		fputcsv($fh, array(
			$p['idLoge'],
			isset($numLoge[$p['idLoge']]) ? $numLoge[$p['idLoge']] : '',
			$p['idCat'],
			$p['idCat'] === 0 ? 'Sans catégorie' : $cats[$p['idCat']]['nom'],
			$p['nomSrc'],
			$cibleNom,
			$p['nomDst'],
			$p['octets'],
			$statut,
		), ';');
	}
	fclose($fh);
}
