<?php

class helper_plugin_niccstyleconfig extends DokuWiki_Plugin {

    /** @var helper_plugin_sqlite */
    protected $sqlite = null;
    protected $dbReady = false;

    // ── Base path ──────────────────────────────────────────────
    protected $cpanelUser = 'systeng3';

    /** Templates válidos — whitelist para prevenir path traversal */
    protected static $validTemplates = ['20cones', '21cones'];

    public function getBasePath() {
        return '/home/' . $this->cpanelUser . '/public_html';
    }

    /**
     * Valida que el nombre de template sea seguro (solo 20cones o 21cones).
     * Previene path traversal en readStyleIni/writeStyleIni.
     */
    public function isValidTemplate($template) {
        return in_array($template, self::$validTemplates, true);
    }

    // ── Database ───────────────────────────────────────────────

    public function getDB() {
        if ($this->dbReady) return $this->sqlite;

        $this->sqlite = plugin_load('helper', 'sqlite');
        if (!$this->sqlite) {
            msg('niccstyleconfig: No se pudo cargar el plugin sqlite', -1);
            return null;
        }

        // __DIR__ = ruta real de este archivo (helper.php), siempre confiable
        $dbDir = __DIR__ . '/db/';

        $ok = $this->sqlite->init('niccstyleconfig', $dbDir);
        if (!$ok) {
            msg('niccstyleconfig: Error inicializando DB (dbDir=' . $dbDir . ')', -1);
            $this->sqlite = null;
            return null;
        }

        $this->dbReady = true;
        return $this->sqlite;
    }

    public function queryAll($sql, $params = []) {
        $db = $this->getDB();
        if (!$db) return [];
        $res = $db->query($sql, $params);
        if (!$res) return [];
        return $db->res2arr($res);
    }

    public function queryRow($sql, $params = []) {
        $rows = $this->queryAll($sql, $params);
        return $rows ? $rows[0] : null;
    }

    public function queryValue($sql, $params = []) {
        $db = $this->getDB();
        if (!$db) return null;
        $res = $db->query($sql, $params);
        if (!$res) return null;
        $row = $db->res2row($res);
        return $row ? reset($row) : null;
    }

    // ── System Scanning ────────────────────────────────────────

    public function isDokuWiki($path) {
        return file_exists($path . '/conf/local.php') && is_dir($path . '/data');
    }

    public function readConfValue($systemPath, $key) {
        $localPhp = $systemPath . '/conf/local.php';
        if (!file_exists($localPhp)) return null;
        $content = @file_get_contents($localPhp);
        if (!$content) return null;
        if (preg_match('/\$conf\s*\[\s*[\'"]' . preg_quote($key, '/') . '[\'"]\s*\]\s*=\s*[\'"]([^\'"]*)[\'"]/', $content, $m)) {
            return $m[1];
        }
        return null;
    }

    public function readTemplate($systemPath) {
        return $this->readConfValue($systemPath, 'template') ?: '20cones';
    }

    public function readTitle($systemPath) {
        return $this->readConfValue($systemPath, 'title') ?: '';
    }

    public function scanServerSystems() {
        $basePath = $this->getBasePath();
        if (!is_dir($basePath)) {
            return ['error' => 'Ruta base no accesible: ' . $basePath, 'systems' => []];
        }

        $entries = @scandir($basePath);
        if (!$entries) {
            return ['error' => 'No se pudo leer: ' . $basePath, 'systems' => []];
        }

        $systems = [];
        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..' || $entry[0] === '.' || $entry[0] === '_') continue;
            $fullPath = $basePath . '/' . $entry;
            if (!is_dir($fullPath)) continue;
            if (!$this->isDokuWiki($fullPath)) continue;

            $template = $this->readTemplate($fullPath);
            $title = $this->readTitle($fullPath);
            $hasStyle20 = file_exists($fullPath . '/conf/tpl/20cones/style.ini') ? 1 : 0;
            $hasStyle21 = file_exists($fullPath . '/conf/tpl/21cones/style.ini') ? 1 : 0;
            $writable = is_writable($fullPath . '/conf/') ? 1 : 0;

            $systems[] = [
                'system_name'      => $entry,
                'system_path'      => $fullPath,
                'system_title'     => $title,
                'template'         => $template,
                'has_style_ini_20' => $hasStyle20,
                'has_style_ini_21' => $hasStyle21,
                'is_writable'      => $writable,
            ];
        }

        // Cache results in transaction (prevent partial cache on failure)
        $db = $this->getDB();
        if ($db) {
            $db->query('BEGIN TRANSACTION');
            $db->query('DELETE FROM style_systems_cache');
            foreach ($systems as $s) {
                $db->query(
                    'INSERT INTO style_systems_cache (system_name, system_path, system_title, template, has_style_ini_20, has_style_ini_21, is_writable, scanned_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime("now"))',
                    [$s['system_name'], $s['system_path'], $s['system_title'], $s['template'], $s['has_style_ini_20'], $s['has_style_ini_21'], $s['is_writable']]
                );
            }
            $db->query('COMMIT');
        }

        $this->addLog('scan', null, json_encode(['total' => count($systems)]), count($systems), 'ok');

        return ['error' => null, 'systems' => $systems];
    }

    public function getCachedSystems() {
        return $this->queryAll('SELECT * FROM style_systems_cache ORDER BY system_name ASC');
    }

    public function getCacheDate() {
        return $this->queryValue('SELECT MAX(scanned_at) FROM style_systems_cache');
    }

    // ── Group Management ───────────────────────────────────────

    public function createGroup($label, $color = '#3b82f6', $description = '') {
        $db = $this->getDB();
        if (!$db) return ['ok' => false, 'reason' => 'db_unavailable'];
        $label = trim($label);
        if ($label === '') return ['ok' => false, 'reason' => 'empty_label'];

        // Check unique
        $existing = $this->queryValue('SELECT id FROM style_groups WHERE label = ?', [$label]);
        if ($existing) return ['ok' => false, 'reason' => 'duplicate'];

        $res = $db->query(
            'INSERT INTO style_groups (label, color, description, is_active) VALUES (?, ?, ?, 1)',
            [$label, $color, $description]
        );
        if ($res === false) return ['ok' => false, 'reason' => 'insert_failed'];

        $this->addLog('group_create', $label, $description, 0, 'ok');
        return ['ok' => true];
    }

    public function updateGroup($id, $label, $color, $description, $isActive) {
        $db = $this->getDB();
        if (!$db) return false;
        $id = (int)$id;

        // Check unique label (excluding self)
        $existing = $this->queryValue('SELECT id FROM style_groups WHERE label = ? AND id != ?', [$label, $id]);
        if ($existing) return false;

        $db->query(
            'UPDATE style_groups SET label = ?, color = ?, description = ?, is_active = ?, updated_at = datetime("now") WHERE id = ?',
            [$label, $color, $description, (int)$isActive, $id]
        );
        return true;
    }

    public function deleteGroup($id) {
        $db = $this->getDB();
        if (!$db) return false;
        $id = (int)$id;
        $group = $this->getGroup($id);
        if (!$group) return false;

        $db->query('DELETE FROM style_origins WHERE group_id = ?', [$id]);
        $db->query('DELETE FROM style_destinations WHERE group_id = ?', [$id]);
        $db->query('DELETE FROM style_groups WHERE id = ?', [$id]);
        $this->addLog('group_delete', $group['label'], '', 0, 'ok');
        return true;
    }

    public function getGroup($id) {
        return $this->queryRow('SELECT * FROM style_groups WHERE id = ?', [(int)$id]);
    }

    public function getAllGroups() {
        $groups = $this->queryAll('SELECT * FROM style_groups ORDER BY label ASC');
        foreach ($groups as &$g) {
            $g['origin'] = $this->getOrigin($g['id']);
            $g['dest_count'] = (int)$this->queryValue(
                'SELECT COUNT(*) FROM style_destinations WHERE group_id = ?', [$g['id']]
            );
        }
        unset($g); // release reference
        return $groups;
    }

    public function toggleGroupActive($id, $active) {
        $db = $this->getDB();
        if (!$db) return false;
        $db->query('UPDATE style_groups SET is_active = ?, updated_at = datetime("now") WHERE id = ?', [(int)$active, (int)$id]);
        return true;
    }

    // ── Origin / Destination Assignment ────────────────────────

    public function setOrigin($groupId, $systemName, $systemPath, $title, $template) {
        $db = $this->getDB();
        if (!$db) return false;
        $groupId = (int)$groupId;

        // Remove existing origin
        $db->query('DELETE FROM style_origins WHERE group_id = ?', [$groupId]);

        $db->query(
            'INSERT INTO style_origins (group_id, system_name, system_path, system_title, template) VALUES (?, ?, ?, ?, ?)',
            [$groupId, $systemName, $systemPath, $title, $template]
        );
        return true;
    }

    public function getOrigin($groupId) {
        return $this->queryRow('SELECT * FROM style_origins WHERE group_id = ?', [(int)$groupId]);
    }

    public function removeOrigin($groupId) {
        $db = $this->getDB();
        if (!$db) return false;
        $db->query('DELETE FROM style_origins WHERE group_id = ?', [(int)$groupId]);
        return true;
    }

    public function addDestination($groupId, $systemName, $systemPath, $title, $template) {
        $db = $this->getDB();
        if (!$db) return false;

        // Check not already assigned
        $existing = $this->queryValue(
            'SELECT id FROM style_destinations WHERE group_id = ? AND system_name = ?',
            [(int)$groupId, $systemName]
        );
        if ($existing) return false;

        $db->query(
            'INSERT INTO style_destinations (group_id, system_name, system_path, system_title, template) VALUES (?, ?, ?, ?, ?)',
            [(int)$groupId, $systemName, $systemPath, $title, $template]
        );
        return true;
    }

    public function addDestinations($groupId, $systemNames) {
        $db = $this->getDB();
        if (!$db) return 0;
        $added = 0;
        foreach ($systemNames as $name) {
            $cached = $this->queryRow('SELECT * FROM style_systems_cache WHERE system_name = ?', [$name]);
            if (!$cached) continue;
            $ok = $this->addDestination($groupId, $cached['system_name'], $cached['system_path'], $cached['system_title'], $cached['template']);
            if ($ok) $added++;
        }
        return $added;
    }

    public function removeDestination($groupId, $systemName) {
        $db = $this->getDB();
        if (!$db) return false;
        $db->query('DELETE FROM style_destinations WHERE group_id = ? AND system_name = ?', [(int)$groupId, $systemName]);
        return true;
    }

    public function getDestinations($groupId) {
        return $this->queryAll('SELECT * FROM style_destinations WHERE group_id = ? ORDER BY system_name ASC', [(int)$groupId]);
    }

    public function getSystemAssignments($systemName) {
        $asOrigin = $this->queryAll(
            'SELECT g.* FROM style_groups g JOIN style_origins o ON g.id = o.group_id WHERE o.system_name = ?',
            [$systemName]
        );
        $asDest = $this->queryAll(
            'SELECT g.* FROM style_groups g JOIN style_destinations d ON g.id = d.group_id WHERE d.system_name = ?',
            [$systemName]
        );
        return ['as_origin' => $asOrigin, 'as_destination' => $asDest];
    }

    // ── Style.ini Operations ───────────────────────────────────

    /**
     * Lee el style.ini de un sistema.
     * DokuWiki guarda los estilos personalizados en conf/tpl/{template}/style.ini
     * (NO en lib/tpl/{template}/style.ini que es el template original).
     */
    public function readStyleIni($systemPath, $template) {
        if (!$this->isValidTemplate($template)) return null;
        $file = $systemPath . '/conf/tpl/' . $template . '/style.ini';
        if (!file_exists($file)) return null;
        return @file_get_contents($file);
    }

    public function parseStyleIni($content) {
        if (!$content) return [];
        $sections = [];
        $currentSection = '_default';
        foreach (explode("\n", $content) as $line) {
            $line = trim($line);
            if ($line === '' || $line[0] === ';') continue;
            if (preg_match('/^\[(.+)\]$/', $line, $m)) {
                $currentSection = $m[1];
                continue;
            }
            if (strpos($line, '=') !== false) {
                list($key, $val) = array_map('trim', explode('=', $line, 2));
                $val = trim($val, '"\'');
                $sections[$currentSection][$key] = $val;
            }
        }
        return $sections;
    }

    public function writeStyleIni($systemPath, $template, $content) {
        if (!$this->isValidTemplate($template)) {
            return ['ok' => false, 'error' => 'Template invalido: ' . $template];
        }
        $dir = $systemPath . '/conf/tpl/' . $template;
        $file = $dir . '/style.ini';

        // Crear directorio conf/tpl/{template}/ si no existe (DokuWiki lo crea on-demand)
        if (!is_dir($dir)) {
            if (!@mkdir($dir, 0755, true)) {
                return ['ok' => false, 'error' => 'No se pudo crear directorio: ' . $dir];
            }
        }
        if (file_exists($file) && !is_writable($file)) return ['ok' => false, 'error' => 'Sin permisos de escritura: ' . $file];
        if (!file_exists($file) && !is_writable($dir)) return ['ok' => false, 'error' => 'Sin permisos de escritura en directorio: ' . $dir];

        $bytes = @file_put_contents($file, $content, LOCK_EX);
        if ($bytes === false) return ['ok' => false, 'error' => 'Error escribiendo: ' . $file];

        return ['ok' => true, 'bytes' => $bytes];
    }

    // ── Apply / Copy ───────────────────────────────────────────

    public function applyGroup($groupId) {
        $group = $this->getGroup($groupId);
        if (!$group) return ['ok' => false, 'error' => 'Grupo no encontrado'];
        if (!$group['is_active']) return ['ok' => false, 'error' => 'Grupo inactivo'];

        $origin = $this->getOrigin($groupId);
        if (!$origin) return ['ok' => false, 'error' => 'No hay sistema origen asignado'];

        // Read style.ini from origin - check both templates
        $content20 = $this->readStyleIni($origin['system_path'], '20cones');
        $content21 = $this->readStyleIni($origin['system_path'], '21cones');

        if (!$content20 && !$content21) {
            return ['ok' => false, 'error' => 'El origen no tiene style.ini en ningún template'];
        }

        // Use whatever content exists to apply to BOTH templates
        // If origin only has one, use that same content for both
        $styleContent = $content20 ?: $content21;

        $destinations = $this->getDestinations($groupId);
        if (empty($destinations)) {
            return ['ok' => false, 'error' => 'No hay sistemas destino asignados'];
        }

        $results = ['success' => [], 'failed' => [], 'skipped' => []];

        foreach ($destinations as $dest) {
            $destPath = $dest['system_path'];
            $destName = $dest['system_name'];

            if (!is_dir($destPath)) {
                $results['failed'][] = ['system' => $destName, 'error' => 'Ruta no accesible'];
                continue;
            }

            $errors = [];

            // Apply to BOTH templates always (20cones and 21cones)
            foreach (self::$validTemplates as $tpl) {
                $existing = $this->readStyleIni($destPath, $tpl);
                if ($existing !== null) {
                    $this->createBackup($destName, $destPath, $tpl, 'pre-apply', $group['label']);
                }
                $r = $this->writeStyleIni($destPath, $tpl, $styleContent);
                if (!$r['ok']) $errors[] = $tpl . ': ' . $r['error'];
            }

            if (empty($errors)) {
                $results['success'][] = $destName;
            } else {
                $results['failed'][] = ['system' => $destName, 'error' => implode('; ', $errors)];
            }
        }

        $status = empty($results['failed']) ? 'ok' : (empty($results['success']) ? 'error' : 'partial');
        $this->addLog(
            'apply',
            $group['label'],
            json_encode([
                'origin'  => $origin['system_name'],
                'success' => count($results['success']),
                'failed'  => count($results['failed']),
                'details' => $results['failed'],
            ]),
            count($results['success']),
            $status
        );

        return ['ok' => true, 'results' => $results];
    }

    public function applyAllActive() {
        $groups = $this->queryAll('SELECT * FROM style_groups WHERE is_active = 1 ORDER BY label');
        $allResults = [];
        foreach ($groups as $g) {
            $allResults[$g['label']] = $this->applyGroup($g['id']);
        }
        return $allResults;
    }

    // ── Backup / Rollback ──────────────────────────────────────

    public function createBackup($systemName, $systemPath, $template, $reason, $groupLabel = '') {
        $content = $this->readStyleIni($systemPath, $template);
        if ($content === null) return false;

        $db = $this->getDB();
        if (!$db) return false;

        $lastVersion = (int)$this->queryValue(
            'SELECT MAX(version) FROM style_backups WHERE system_name = ? AND template = ?',
            [$systemName, $template]
        );

        $db->query(
            'INSERT INTO style_backups (system_name, system_path, template, style_ini_content, backup_reason, group_label, version) VALUES (?, ?, ?, ?, ?, ?, ?)',
            [$systemName, $systemPath, $template, $content, $reason, $groupLabel, $lastVersion + 1]
        );
        return true;
    }

    public function getBackups($systemName = null, $limit = 50) {
        if ($systemName) {
            return $this->queryAll(
                'SELECT id, system_name, template, backup_reason, group_label, version, created_at FROM style_backups WHERE system_name = ? ORDER BY created_at DESC LIMIT ?',
                [$systemName, (int)$limit]
            );
        }
        return $this->queryAll(
            'SELECT id, system_name, template, backup_reason, group_label, version, created_at FROM style_backups ORDER BY created_at DESC LIMIT ?',
            [(int)$limit]
        );
    }

    public function getBackupById($id) {
        return $this->queryRow('SELECT * FROM style_backups WHERE id = ?', [(int)$id]);
    }

    public function rollbackSystem($backupId) {
        $backup = $this->getBackupById($backupId);
        if (!$backup) return ['ok' => false, 'error' => 'Backup no encontrado'];

        $systemPath = $backup['system_path'];
        $template = $backup['template'];

        if (!is_dir($systemPath)) return ['ok' => false, 'error' => 'Ruta del sistema no accesible'];

        // Backup current state before rollback
        $this->createBackup($backup['system_name'], $systemPath, $template, 'pre-rollback', '');

        // Write backup content
        $r = $this->writeStyleIni($systemPath, $template, $backup['style_ini_content']);
        if (!$r['ok']) return $r;

        $this->addLog(
            'rollback',
            '',
            json_encode([
                'system' => $backup['system_name'],
                'template' => $template,
                'restored_version' => $backup['version'],
            ]),
            1,
            'ok'
        );

        return ['ok' => true];
    }

    public function cleanOldBackups($days = 90) {
        $db = $this->getDB();
        if (!$db) return 0;
        $days = (int)$days;
        $cutoff = date('Y-m-d H:i:s', time() - ($days * 86400));

        // Delete old backups but keep at least 5 most recent per system+template.
        // Step 1: Get IDs to protect (5 most recent per system+template)
        $protected = $this->queryAll(
            'SELECT system_name, template, id FROM style_backups ORDER BY system_name, template, created_at DESC'
        );
        $keep = [];
        $counts = [];
        foreach ($protected as $row) {
            $key = $row['system_name'] . '|' . $row['template'];
            if (!isset($counts[$key])) $counts[$key] = 0;
            $counts[$key]++;
            if ($counts[$key] <= 5) $keep[] = (int)$row['id'];
        }

        if (empty($keep)) return true;

        // Step 2: Get IDs to delete (old entries NOT in protected set)
        $allOld = $this->queryAll(
            'SELECT id FROM style_backups WHERE created_at < ?',
            [$cutoff]
        );
        $keepSet = array_flip($keep);
        $toDelete = [];
        foreach ($allOld as $row) {
            if (!isset($keepSet[(int)$row['id']])) {
                $toDelete[] = (int)$row['id'];
            }
        }

        if (empty($toDelete)) return true;

        // Step 3: Delete in batches of 500 to stay within SQLite parameter limit (999)
        $batches = array_chunk($toDelete, 500);
        foreach ($batches as $batch) {
            $placeholders = implode(',', array_fill(0, count($batch), '?'));
            $db->query("DELETE FROM style_backups WHERE id IN ($placeholders)", $batch);
        }
        return true;
    }

    // ── Logging ────────────────────────────────────────────────

    public function addLog($action, $groupLabel, $details, $affected, $status) {
        $db = $this->getDB();
        if (!$db) return false;

        $userId = isset($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'] : 'unknown';
        $db->query(
            'INSERT INTO style_logs (action, group_label, details, systems_affected, status, user_id) VALUES (?, ?, ?, ?, ?, ?)',
            [$action, $groupLabel, $details, (int)$affected, $status, $userId]
        );
        return true;
    }

    public function getLogs($limit = 50, $action = null) {
        if ($action) {
            return $this->queryAll(
                'SELECT * FROM style_logs WHERE action = ? ORDER BY created_at DESC LIMIT ?',
                [$action, (int)$limit]
            );
        }
        return $this->queryAll('SELECT * FROM style_logs ORDER BY created_at DESC LIMIT ?', [(int)$limit]);
    }

    // ── Preview ────────────────────────────────────────────────

    public function getColorPreview($systemPath, $template) {
        $content = $this->readStyleIni($systemPath, $template);
        if (!$content) return [];
        $parsed = $this->parseStyleIni($content);
        $colors = [];
        if (isset($parsed['replacements'])) {
            foreach ($parsed['replacements'] as $k => $v) {
                if (preg_match('/^#[0-9a-fA-F]{3,8}$/', $v)) {
                    $colors[$k] = $v;
                }
            }
        }
        return $colors;
    }
}
