<?php
/**
 * cliImportDocsResidentsFTM.php
 *
 * Importe l'arborescence de documents PDF rangee dans le dossier source
 * "FTM METZ BLIDA/<bat>/Résidents/<NOM ...=<codeSite>_<numDossier>>/<categorie...>/fichier"
 * vers la gestion documentaire de l'application, en respectant EXACTEMENT la
 * politique d'un upload manuel (cf. ListeController::saveDocumentResidentAction /
 * uploadfichedocumentAction) :
 *
 *   1) fichier physique  -> public/docs/locataires/<idLocataire>/uploads/<idCategorie>/<nom>
 *      nom = removeAccent(base)."_".date('Y-m-d-his')."_".<seq>.".".<ext>
 *   2) ligne `documents`  -> nom, libelle, idCategorie, isResident=1, typeDoc='statique',
 *      langue='fr_FR', etatResa (etat courant de la resa ; +10 si 1 present),
 *      dateCreation=now, idUser, ordre=0
 *   3) ligne `document_locataire` -> (idDocument, idReservation)
 *
 * Choix valides avec le demandeur :
 *   - categorie introuvable (nom/chaine absent de categorie_document) : ON SAUTE + log
 *   - etatResa : etat courant de la reservation (regle 1->+10)
 *   - plusieurs reservations pour codeSite+numDossier : la PLUS RECENTE (id max)
 *   - la chaine de dossiers est resolue en ENTIER, chaque niveau etant une categorie.
 *     Ex: "Contrat résident/Contrat location/Bail" -> "Contrat résident" > "Contrat location" > "Bail"
 *
 * Garanties :
 *   - NON destructif : on COPIE (jamais move/unlink), on n'ecrase aucun fichier.
 *   - Idempotent : un doc deja importe (meme resa + meme categorie + meme libelle,
 *     isResident=1, non supprime) est saute -> relançable sans doublon.
 *   - DRY-RUN par defaut : n'ecrit ni fichier ni base tant que --go n'est pas passe.
 *
 * Identifiants DB retires du depot : a renseigner sur le serveur avant execution.
 *
 * Usage :
 *   php5.6 cli/cliImportDocsResidentsFTM.php                    # DRY-RUN (n'ecrit rien)
 *   php5.6 cli/cliImportDocsResidentsFTM.php --go               # applique
 *   php5.6 cli/cliImportDocsResidentsFTM.php --num=6615         # limite a un numDossier
 *   php5.6 cli/cliImportDocsResidentsFTM.php --source="/chemin/FTM METZ BLIDA"
 */

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

// Profondeur max de l'arbre des categories geree par l'application :
// fetchAllParents() ne propose comme parent que les categories de level < 2,
// ce qui plafonne l'arborescence a 3 niveaux (level 0, 1, 2).
define('NIVEAU_MAX', 3);

// ---- A renseigner sur le serveur -------------------------------------------
$DB_HOST = 'localhost';
$DB_NAME = 'weasyloc_amli_test';   // base CIBLE (test d'abord, prod ensuite)
$DB_USER = 'root';
$DB_PASS = 'Dy6rqti7lM50w41';
// ----------------------------------------------------------------------------

// ---- Parametres ------------------------------------------------------------
// Le codeSite N'EST PAS en dur : il est lu dans chaque nom de dossier, entre '=' et '_'
// (ex: "ADBIB Moustafa 01-08-1967=J1_06615" -> codeSite=J1, numDossier=6615).
// $FILTER_SITE permet, en option (--codeSite=XX), de ne traiter que les dossiers d'un site.
$FILTER_SITE    = '';                                    // '' = tous les sites
$SOURCE_DIR     = getcwd().'/FTM METZ BLIDA';             // racine des dossiers source
$DOCS_ROOT      = getcwd().'/public/docs/locataires';     // racine de stockage appli
$IMPORT_USER_ID = 1;                                      // idUser attribue aux docs crees
$ALLOWED_EXT    = array('pdf','jpg','jpeg','png','doc','docx','odt','xls','xlsx');

// Dossiers de la source qui ne correspondent a AUCUNE categorie en base : ce sont de
// simples niveaux de rangement, on les retire de la chaine avant resolution.
// Comparaison insensible a la casse, aux accents et a la ponctuation.
// Vide = toute la chaine de dossiers est resolue en categories (cas actuel FTM).
// Ex. pour neutraliser un niveau : array('Contrat résident')
$SEGMENTS_IGNORES = array();
// ----------------------------------------------------------------------------

$opts   = getopt('', array('go', 'num::', 'source::', 'codeSite::'));
$apply  = isset($opts['go']);
$onlyNum = (isset($opts['num']) && $opts['num'] !== false) ? (int) $opts['num'] : 0;
if (isset($opts['source']) && $opts['source'] !== false) { $SOURCE_DIR = rtrim($opts['source'], '/'); }
if (isset($opts['codeSite']) && $opts['codeSite'] !== false) { $FILTER_SITE = $opts['codeSite']; }

echo "==========================================================\n";
echo ($apply ? "MODE REEL (--go)\n" : "MODE DRY-RUN (aucune ecriture) — ajouter --go pour appliquer\n");
echo "Source  : $SOURCE_DIR\n";
echo "Cible   : $DOCS_ROOT\n";
echo "Site    : ".($FILTER_SITE !== '' ? $FILTER_SITE." (filtre)" : "lu par dossier (entre = et _)")."\n";
if (!empty($SEGMENTS_IGNORES)) { echo "Ignore  : ".implode(', ', $SEGMENTS_IGNORES)." (dossiers non-categorie)\n"; }
if ($onlyNum) { echo "Filtre  : numDossier = $onlyNum\n"; }
echo "==========================================================\n";

if (!is_dir($SOURCE_DIR)) { fwrite(STDERR, "ERREUR: dossier source introuvable: $SOURCE_DIR\n"); exit(1); }

try {
    $pdo = new PDO("mysql:host=$DB_HOST;dbname=$DB_NAME;charset=utf8mb4", $DB_USER, $DB_PASS, array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ));
} catch (Exception $e) {
    fwrite(STDERR, "ERREUR connexion DB: ".$e->getMessage()."\n"); exit(1);
}

// ------- helpers ------------------------------------------------------------
function removeAccent($str)
{
    $str = (string) $str;
    $conv = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
    if ($conv !== false && $conv !== '') { $str = $conv; }
    // nettoyage : garde lettres/chiffres/-_ et espaces -> '_'
    $str = preg_replace('/[^A-Za-z0-9\-_. ]/', '', $str);
    $str = preg_replace('/\s+/', '_', trim($str));
    return $str !== '' ? $str : 'doc';
}

/**
 * Normalise un segment de chemin pour comparaison insensible casse/accents/ponctuation.
 */
function normSegment($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);
    return strtolower(trim(preg_replace('/\s+/', ' ', $str)));
}

/**
 * Resout la chaine de categories (ex: "Contrat résident/Contrat location/Bail")
 * vers l'id de la categorie la plus profonde, en descendant niveau par niveau via idParent.
 *
 * Depuis le 29/07/2026 (ALTER TABLE categorie_document DROP INDEX `nom`), un meme nom
 * peut exister a plusieurs endroits de l'arbre : la resolution DOIT donc se faire sur le
 * chemin complet (couple nom+idParent a chaque niveau), jamais sur le nom seul.
 * Corollaire : plus rien n'empeche en base deux categories de meme nom sous le MEME parent.
 * Dans ce cas on refuse de choisir au hasard -> retour 0 + message dans $err.
 *
 * Retourne l'id de la categorie feuille, ou 0 (motif detaille dans $err).
 * La comparaison des noms suit la collation MySQL (insensible casse/accents).
 */
function resolveCategorie(PDO $pdo, array $segments, &$cacheStmt, &$err = null)
{
    $err = '';
    if (empty($segments)) { $err = 'chaine vide'; return 0; }
    if (count($segments) > NIVEAU_MAX) {
        $err = 'profondeur '.count($segments).' niveaux > '.NIVEAU_MAX.' gere par l\'application';
        return 0;
    }
    if ($cacheStmt === null) {
        $cacheStmt = $pdo->prepare(
            "SELECT id FROM categorie_document WHERE nom = :nom AND idParent = :par AND deleted = 0"
        );
    }
    $parent = 0;
    $chemin = array();
    foreach ($segments as $seg) {
        $seg = trim($seg);
        if ($seg === '') { continue; }
        $chemin[] = $seg;
        $cacheStmt->execute(array(':nom' => $seg, ':par' => $parent));
        $rows = $cacheStmt->fetchAll();
        if (!$rows) {
            $err = 'maillon absent de la base : '.implode(' > ', $chemin);
            return 0;
        }
        if (count($rows) > 1) {
            $ids = array();
            foreach ($rows as $r) { $ids[] = $r['id']; }
            $err = 'AMBIGU : '.implode(' > ', $chemin).' correspond a '.count($rows)
                 .' categories sous le meme parent (id '.implode(', ', $ids).') -> a dedoublonner en base';
            return 0;
        }
        $parent = (int) $rows[0]['id'];
    }
    return $parent;
}

// ------- 0) controles prealables sur la base cible ---------------------------
// L'index UNIQUE sur categorie_document.nom a ete supprime le 29/07/2026
// (majsql.txt : ALTER TABLE `categorie_document` DROP INDEX `nom`) pour autoriser
// un meme nom a des niveaux differents. L'arbo FTM en a BESOIN : "Carte d'identite"
// doit exister a la fois en racine ET sous "Contrat résident".
try {
    $idx = $pdo->query("SHOW INDEX FROM categorie_document WHERE Key_name = 'nom'")->fetchAll();
    if (!empty($idx)) {
        echo "!! ATTENTION : l'index UNIQUE `nom` existe encore sur categorie_document.\n";
        echo "   Les categories de meme nom a des niveaux differents sont IMPOSSIBLES a creer.\n";
        echo "   Appliquer d'abord : ALTER TABLE `categorie_document` DROP INDEX `nom`;\n\n";
    }
    // doublons sous un meme parent : rendraient la resolution ambigue
    $dbl = $pdo->query(
        "SELECT nom, idParent, COUNT(*) n FROM categorie_document
         WHERE deleted = 0 GROUP BY nom, idParent HAVING n > 1"
    )->fetchAll();
    if (!empty($dbl)) {
        echo "!! ATTENTION : ".count($dbl)." doublon(s) (nom, idParent) en base :\n";
        foreach ($dbl as $d) { echo "   - \"".$d['nom']."\" sous idParent=".$d['idParent']." x".$d['n']."\n"; }
        echo "   Les chaines concernees seront SAUTEES (pas de choix au hasard).\n\n";
    }
} catch (Exception $e) {
    echo "!! Controles prealables non effectues : ".$e->getMessage()."\n\n";
}

// ------- 1) recensement des dossiers residents ------------------------------
// On cherche les dossiers .../Résidents/<...=CODESITE_NUM>
// codeSite et numDossier sont lus dans le nom, entre '=' et '_' (codeSite) puis
// derriere le dernier '_' (numDossier).
$residents = array(); // "code|num" => array(code, num, path)
$it = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($SOURCE_DIR, FilesystemIterator::SKIP_DOTS),
    RecursiveIteratorIterator::SELF_FIRST
);
foreach ($it as $node) {
    if (!$node->isDir()) { continue; }
    $path = $node->getPathname();
    // parent doit s'appeler "Résidents" (insensible a la casse via /i ; accent e/é tolere)
    if (!preg_match('#/r[ée]sidents/[^/]+$#iu', $path)) { continue; }
    $base = basename($path);
    // =<codeSite>_<numDossier> en fin de nom ; codeSite = tout sauf '=', '_' et '/'
    if (preg_match('/=\s*([^=_\/\s]+)\s*_0*([0-9]+)\s*$/u', $base, $m)) {
        $code = $m[1];
        $num  = (int) $m[2];
        if ($onlyNum && $num !== $onlyNum) { continue; }
        if ($FILTER_SITE !== '' && strcasecmp($code, $FILTER_SITE) !== 0) { continue; }
        $residents[$code.'|'.$num] = array('code' => $code, 'num' => $num, 'path' => $path);
    }
}
ksort($residents);
echo "Dossiers residents detectes : ".count($residents)."\n\n";

// ------- prepared statements ------------------------------------------------
$stmtResa = $pdo->prepare(
    "SELECT id, idLocataire, etat FROM reservations
     WHERE codeSite = :cs AND numDossier + 0 = :num
     ORDER BY id DESC LIMIT 1"
);
$stmtDejaLa = $pdo->prepare(
    "SELECT d.id FROM documents d
     JOIN document_locataire dl ON dl.idDocument = d.id
     WHERE dl.idReservation = :idResa AND d.idCategorie = :idCat
       AND d.libelle = :libelle AND d.isResident = 1 AND d.deleted = '0'
     LIMIT 1"
);
$stmtInsDoc = $pdo->prepare(
    "INSERT INTO documents (nom, libelle, langue, isResident, idModele, idCategorie,
                            etatResa, typeDoc, dateCreation, idUser, ordre)
     VALUES (:nom, :libelle, 'fr_FR', 1, NULL, :idCat, :etatResa, 'statique', :dc, :idUser, 0)"
);
$stmtInsLien = $pdo->prepare(
    "INSERT INTO document_locataire (idDocument, idReservation) VALUES (:idDoc, :idResa)"
);

// ------- compteurs ----------------------------------------------------------
$IGNORES_NORM = array();
foreach ($SEGMENTS_IGNORES as $s) {
    $n = normSegment($s);
    if ($n !== '') { $IGNORES_NORM[] = $n; }
}
$catStmt = null;
$seq = 0;
$nbImportes = 0; $nbDejaLa = 0; $nbSautCat = 0; $nbSansResa = 0; $nbSansExt = 0; $nbResaKO = 0;
$catsIntrouvables = array();
$sansResa = array();

// ------- 2) traitement ------------------------------------------------------
foreach ($residents as $entry) {
    $code   = $entry['code'];
    $num    = $entry['num'];
    $resDir = $entry['path'];
    $tag    = $code.'/'.$num;

    $stmtResa->execute(array(':cs' => $code, ':num' => $num));
    $resa = $stmtResa->fetch();
    if (!$resa) {
        $nbSansResa++; $sansResa[] = $tag;
        echo "[$tag] AUCUNE reservation -> lot ignore\n";
        continue;
    }
    $idResa = (int) $resa['id'];
    $idLoc  = (int) $resa['idLocataire'];
    $etat   = (int) $resa['etat'];
    if (!$idLoc) { $nbResaKO++; echo "[$tag] resa $idResa sans idLocataire -> ignore\n"; continue; }

    // etatResa = etat courant (+10 si 1)
    $etats = array($etat);
    if (in_array(1, $etats)) { $etats[] = 10; }
    $etatResa = implode(',', $etats);

    echo "[$tag] resa=$idResa loc=$idLoc etat=$etat\n";

    // parcours des fichiers du resident
    $fit = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($resDir, FilesystemIterator::SKIP_DOTS)
    );
    foreach ($fit as $file) {
        if (!$file->isFile()) { continue; }
        $abs = $file->getPathname();
        // chaine de categories = chemin du dossier parent, relatif au dossier resident
        $relDir = trim(str_replace($resDir, '', dirname($abs)), '/');
        if ($relDir === '') {
            echo "    - IGNORE (fichier a la racine du resident, pas de categorie): ".basename($abs)."\n";
            $nbSautCat++; continue;
        }
        // on retire les niveaux de rangement qui ne sont pas des categories
        $segments = array();
        foreach (explode('/', $relDir) as $seg) {
            $seg = trim($seg);
            if ($seg === '') { continue; }
            if (in_array(normSegment($seg), $IGNORES_NORM, true)) { continue; }
            $segments[] = $seg;
        }
        if (empty($segments)) {
            echo "    - IGNORE (aucune categorie apres retrait des dossiers de rangement): $relDir / ".basename($abs)."\n";
            $nbSautCat++; continue;
        }
        $chaine = implode(' > ', $segments);
        $errCat = '';
        $idCat = resolveCategorie($pdo, $segments, $catStmt, $errCat);
        if (!$idCat) {
            $nbSautCat++;
            $catsIntrouvables[$chaine] = isset($catsIntrouvables[$chaine])
                ? array($catsIntrouvables[$chaine][0] + 1, $errCat)
                : array(1, $errCat);
            echo "    - SAUTE ($errCat) : ".basename($abs)."\n";
            continue;
        }

        $originalName = basename($abs);              // libelle lisible
        // idempotence
        $stmtDejaLa->execute(array(':idResa' => $idResa, ':idCat' => $idCat, ':libelle' => $originalName));
        if ($stmtDejaLa->fetch()) {
            $nbDejaLa++;
            echo "    - deja importe (skip): [cat $idCat] $originalName\n";
            continue;
        }

        $ext  = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
        $baseNoExt = pathinfo($originalName, PATHINFO_FILENAME);
        if ($ext === '') { $nbSansExt++; echo "    - ! fichier sans extension: $originalName (importe tel quel)\n"; }
        elseif (!in_array($ext, $ALLOWED_EXT)) { echo "    - ! extension inhabituelle .$ext (importe quand meme)\n"; }

        // nom cible facon appli
        $seq++;
        $nom = removeAccent($baseNoExt).'_'.date('Y-m-d-his').'_'.$seq.($ext !== '' ? '.'.$ext : '');
        $destDir = $DOCS_ROOT.'/'.$idLoc.'/uploads/'.$idCat;
        // anti-collision physique
        $try = 0;
        while (file_exists($destDir.'/'.$nom)) {
            $try++;
            $nom = removeAccent($baseNoExt).'_'.date('Y-m-d-his').'_'.$seq.'_'.$try.($ext !== '' ? '.'.$ext : '');
        }
        $dest = $destDir.'/'.$nom;

        if (!$apply) {
            echo "    - [DRY] copierait -> $dest  (+documents cat=$idCat etatResa=$etatResa +lien resa=$idResa)\n";
            $nbImportes++;
            continue;
        }

        // REEL : copie non destructive
        if (!is_dir($destDir)) {
            if (!mkdir($destDir, 0775, true) && !is_dir($destDir)) {
                echo "    - ERREUR mkdir $destDir -> fichier ignore\n"; continue;
            }
        }
        if (!copy($abs, $dest)) {
            echo "    - ERREUR copie vers $dest -> ignore\n"; continue;
        }

        try {
            $pdo->beginTransaction();
            $stmtInsDoc->execute(array(
                ':nom' => $nom, ':libelle' => $originalName, ':idCat' => $idCat,
                ':etatResa' => $etatResa, ':dc' => date('Y-m-d H:i:s'), ':idUser' => $IMPORT_USER_ID,
            ));
            $idDoc = (int) $pdo->lastInsertId();
            $stmtInsLien->execute(array(':idDoc' => $idDoc, ':idResa' => $idResa));
            $pdo->commit();
            $nbImportes++;
            echo "    - OK doc=$idDoc [cat $idCat] $originalName -> ".basename($dest)."\n";
        } catch (Exception $e) {
            if ($pdo->inTransaction()) { $pdo->rollBack(); }
            @unlink($dest); // on retire le fichier copie si l'insert echoue
            echo "    - ERREUR insert DB (".$e->getMessage().") -> fichier retire\n";
        }
    }
}

// ------- rapport ------------------------------------------------------------
echo "\n==========================================================\n";
echo "RESUME ".($apply ? "(REEL)" : "(DRY-RUN)")."\n";
echo "  Documents ".($apply ? "importes" : "a importer")." : $nbImportes\n";
echo "  Deja presents (skip)                : $nbDejaLa\n";
echo "  Fichiers sautes (categorie KO)      : $nbSautCat\n";
echo "  Lots sans reservation               : $nbSansResa\n";
echo "  Resa sans locataire                 : $nbResaKO\n";
echo "  Fichiers sans extension             : $nbSansExt\n";
if (!empty($catsIntrouvables)) {
    echo "\n  Chaines de categories non resolues (a creer/corriger en base avant relance) :\n";
    foreach ($catsIntrouvables as $chain => $info) {
        echo "    - [".$info[0]." fichier(s)] $chain\n";
        echo "        motif : ".$info[1]."\n";
    }
}
if (!empty($sansResa)) {
    echo "\n  numDossier sans reservation en base : ".implode(', ', $sansResa)."\n";
}
echo "==========================================================\n";
if (!$apply) { echo "DRY-RUN termine. Relancer avec --go pour appliquer.\n"; }
