<?php
/**
 * DokuWiki Plugin filelisting (Action Component - Sync)
 *
 * Sincroniza archivos subidos/eliminados con comentarios del plugin Discussion
 * Escribe directamente al archivo .comments en /meta/
 *
 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
 * @author  SIE Asesorias
 */

if(!defined('DOKU_INC')) die();

class action_plugin_filelisting_sync extends DokuWiki_Action_Plugin {

    /** @var string Archivo de cache para mapeo namespace->paginas */
    const CACHE_FILE = 'filelisting_ns_map.php';

    /**
     * Registra los hooks de eventos
     */
    public function register(Doku_Event_Handler $controller) {
        // Evento cuando se sube un archivo
        $controller->register_hook('MEDIA_UPLOAD_FINISH', 'AFTER', $this, 'handleMediaUpload');

        // Evento cuando se elimina un archivo
        $controller->register_hook('MEDIA_DELETE_FILE', 'AFTER', $this, 'handleMediaDelete');

        // Evento cuando se renderiza una pagina (para registrar el mapeo)
        $controller->register_hook('PARSER_METADATA_RENDER', 'AFTER', $this, 'handleMetadataRender');
    }

    /**
     * Maneja el evento de subida de archivo
     * Estructura de $event->data para MEDIA_UPLOAD_FINISH:
     * [0] = nombre archivo temporal
     * [1] = ruta completa destino
     * [2] = ID del media (ej: namespace:archivo.pdf)
     * [3] = mime type
     * [4] = overwrite (bool)
     * [5] = move (si es move o copy)
     */
    public function handleMediaUpload(Doku_Event $event, $param) {
        $mediaId = $event->data[2];
        $this->logDebug("UPLOAD detectado: $mediaId");
        $this->syncDiscussion($mediaId);
    }

    /**
     * Log para debug
     */
    protected function logDebug($msg) {
        global $conf;
        $logFile = $conf['cachedir'] . '/filelisting_sync.log';
        $line = date('Y-m-d H:i:s') . " | $msg\n";
        io_saveFile($logFile, $line, true);
    }

    /**
     * Maneja el evento de eliminacion de archivo
     * Estructura de $event->data para MEDIA_DELETE_FILE:
     * ['id'] = ID del archivo
     * ['name'] = nombre
     * ['path'] = ruta completa
     * ['size'] = tamano
     */
    public function handleMediaDelete(Doku_Event $event, $param) {
        $mediaId = $event->data['id'];
        $this->syncDiscussion($mediaId);
    }

    /**
     * Registra el mapeo namespace->pagina cuando se renderiza metadata
     * Detecta paginas que usan {{filelisting>namespace}}
     */
    public function handleMetadataRender(Doku_Event $event, $param) {
        global $ID;

        // Verificar si la pagina tiene filelisting en su metadata
        if (!empty($event->data['current']['filelisting'])) {
            $content = rawWiki($ID);

            // Buscar namespaces usados en filelisting
            // Patron: {{filelisting}} o {{filelisting>namespace}}
            if (preg_match_all('/\{\{filelisting>?([^}]*)\}\}/', $content, $matches)) {
                foreach ($matches[1] as $nsPattern) {
                    $resolvedNs = $this->resolveNamespace($ID, $nsPattern);
                    $this->registerNamespaceMapping($resolvedNs, $ID);
                }
            }
        }
    }

    /**
     * Sincroniza el comentario de Discussion con los archivos del namespace
     *
     * @param string $mediaId ID del archivo de media que disparo el evento
     */
    protected function syncDiscussion($mediaId) {
        // Obtener el namespace del archivo
        $mediaNs = getNS($mediaId);
        $this->logDebug("Namespace del archivo: '$mediaNs'");

        if (empty($mediaNs)) {
            $this->logDebug("Namespace vacio, saliendo");
            return; // Archivo en raiz, no procesamos
        }

        // Buscar paginas wiki que usen este namespace en filelisting
        $pages = $this->findPagesWithFilelisting($mediaNs);
        $this->logDebug("Paginas en cache: " . count($pages) . " -> " . implode(', ', $pages));

        // Si no hay en cache, buscar la pagina directamente por convencion
        if (empty($pages)) {
            $pages = $this->findPagesByConvention($mediaNs);
            $this->logDebug("Paginas por convencion: " . count($pages) . " -> " . implode(', ', $pages));
        }

        if (empty($pages)) {
            $this->logDebug("No se encontraron paginas, saliendo");
            return;
        }

        /** @var helper_plugin_filelisting $helper */
        $helper = plugin_load('helper', 'filelisting');
        if (!$helper) {
            $this->logDebug("ERROR: No se pudo cargar el helper");
            return;
        }

        foreach ($pages as $pageId) {
            $this->logDebug("Verificando pagina: $pageId");
            // Verificar si la pagina tiene ~~DISCUSSION~~
            if ($this->pageHasDiscussion($pageId)) {
                $this->logDebug("Pagina tiene DISCUSSION, sincronizando...");
                // Llamar al helper para sincronizar
                $result = $helper->syncToDiscussion($pageId, $mediaNs);
                $this->logDebug("Resultado syncToDiscussion: " . ($result ? 'OK' : 'Sin cambios'));
            } else {
                $this->logDebug("Pagina NO tiene DISCUSSION");
            }
        }
    }

    /**
     * Busca paginas por convencion de nombres
     * Intenta encontrar paginas que probablemente usen el namespace
     *
     * @param string $mediaNs Namespace del media
     * @return array Lista de paginas encontradas
     */
    protected function findPagesByConvention($mediaNs) {
        $pages = array();

        // Opcion 1: Pagina con mismo ID que el namespace (ej: varios:docs)
        if (page_exists($mediaNs)) {
            $content = rawWiki($mediaNs);
            if (strpos($content, '{{filelisting') !== false) {
                $pages[] = $mediaNs;
            }
        }

        // Opcion 2: Pagina "start" dentro del namespace (ej: varios:docs:start)
        $startPage = $mediaNs . ':start';
        if (page_exists($startPage)) {
            $content = rawWiki($startPage);
            if (strpos($content, '{{filelisting') !== false) {
                $pages[] = $startPage;
            }
        }

        // Opcion 3: Buscar en el namespace padre
        $parentNs = getNS($mediaNs);
        if (!empty($parentNs)) {
            // Pagina con nombre del ultimo segmento en namespace padre
            $pageName = noNS($mediaNs);
            $parentPage = $parentNs . ':' . $pageName;
            if (page_exists($parentPage)) {
                $content = rawWiki($parentPage);
                if (strpos($content, '{{filelisting') !== false) {
                    $pages[] = $parentPage;
                }
            }
        }

        return array_unique($pages);
    }

    /**
     * Resuelve un namespace relativo a uno absoluto
     *
     * @param string $pageId ID de la pagina actual
     * @param string $ns Namespace (puede ser relativo o vacio)
     * @return string Namespace absoluto
     */
    protected function resolveNamespace($pageId, $ns) {
        $ns = trim($ns);

        if (empty($ns)) {
            // {{filelisting}} sin parametro usa el namespace de la pagina
            return getNS($pageId);
        }

        // Usar el resolver de DokuWiki para namespaces relativos
        $resolver = new \dokuwiki\File\PageResolver($pageId);
        return cleanID($resolver->resolveId($ns));
    }

    /**
     * Registra el mapeo namespace -> pagina en cache
     *
     * @param string $ns Namespace de media
     * @param string $pageId ID de la pagina wiki
     */
    protected function registerNamespaceMapping($ns, $pageId) {
        global $conf;

        $map = $this->loadNamespaceMap();

        $ns = cleanID($ns);
        if (!isset($map[$ns])) {
            $map[$ns] = array();
        }

        if (!in_array($pageId, $map[$ns])) {
            $map[$ns][] = $pageId;
            $this->saveNamespaceMap($map);
        }
    }

    /**
     * Carga el mapeo namespace -> paginas desde cache
     *
     * @return array
     */
    protected function loadNamespaceMap() {
        global $conf;

        $cacheFile = $conf['cachedir'] . '/' . self::CACHE_FILE;

        if (file_exists($cacheFile)) {
            $content = io_readFile($cacheFile, false);
            $map = @unserialize($content, ['allowed_classes' => false]);
            if (is_array($map)) {
                return $map;
            }
        }

        return array();
    }

    /**
     * Guarda el mapeo namespace -> paginas en cache
     *
     * @param array $map
     */
    protected function saveNamespaceMap($map) {
        global $conf;

        $cacheFile = $conf['cachedir'] . '/' . self::CACHE_FILE;
        io_saveFile($cacheFile, serialize($map));
    }

    /**
     * Busca paginas que usen el namespace dado en filelisting
     *
     * @param string $targetNs Namespace de media a buscar
     * @return array Lista de IDs de paginas
     */
    protected function findPagesWithFilelisting($targetNs) {
        $targetNs = cleanID($targetNs);
        $pages = array();

        // Buscar en cache
        $map = $this->loadNamespaceMap();

        // Buscar coincidencias exactas o parciales (namespace padre)
        foreach ($map as $ns => $pageIds) {
            // Coincidencia exacta
            if ($ns === $targetNs) {
                $pages = array_merge($pages, $pageIds);
                continue;
            }

            // El archivo esta en un sub-namespace del filelisting
            if (strpos($targetNs, $ns . ':') === 0) {
                $pages = array_merge($pages, $pageIds);
            }
        }

        // Filtrar paginas que ya no existen
        $pages = array_filter($pages, function($pageId) {
            return page_exists($pageId);
        });

        return array_unique($pages);
    }

    /**
     * Verifica si una pagina tiene ~~DISCUSSION~~ habilitado
     *
     * @param string $pageId ID de la pagina
     * @return bool
     */
    protected function pageHasDiscussion($pageId) {
        // Verificar si existe el archivo .comments
        $commentsFile = metaFN($pageId, '.comments');
        if (file_exists($commentsFile)) {
            $data = @unserialize(io_readFile($commentsFile, false), ['allowed_classes' => false]);
            // Si status es 0, las discusiones estan deshabilitadas
            if (is_array($data) && isset($data['status']) && $data['status'] > 0) {
                return true;
            }
        }

        // Verificar en el contenido de la pagina
        $content = rawWiki($pageId);
        if (strpos($content, '~~DISCUSSION~~') !== false) {
            return true;
        }
        if (preg_match('/~~DISCUSSION[:\|]?[^~]*~~/', $content)) {
            return true;
        }

        return false;
    }

}
