<?php
/**
 * @intranet-file base/plugins/index.php
 * @intranet-file-version 2026.03.13.1
 * @intranet-file-date 2026-03-13
 * @intranet-file-owner CSIF Intranet
 */

declare(strict_types=1);

require_once dirname(__DIR__, 2) . '/common/app_guard.php';
require_once dirname(__DIR__, 2) . '/common/url.php';
require_once dirname(__DIR__, 2) . '/common/roles.php';
checkAppAccess();

require_once dirname(__DIR__, 2) . '/common/catalogo_db.php';
require_once dirname(__DIR__, 2) . '/common/plugin_store.php';

if (!catalogo_is_admin()) {
  http_response_code(403);
  die('Acceso restringido');
}

$pdo = db();
catalogo_schema_ensure($pdo);

function h($s): string
{
  return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function pm_store_published_raw(array $meta): string
{
  return trim((string) ($meta['published_at'] ?? ''));
}

function pm_store_published_human(array $meta): string
{
  $raw = pm_store_published_raw($meta);
  if ($raw === '') {
    return '-';
  }

  $ts = strtotime($raw);
  if ($ts === false) {
    return $raw;
  }

  return date('d/m/Y H:i', $ts);
}

function pm_store_published_sort(array $meta): string
{
  $raw = pm_store_published_raw($meta);
  if ($raw === '') {
    return '';
  }

  $ts = strtotime($raw);
  if ($ts === false) {
    return $raw;
  }

  return date('Y-m-d H:i:s', $ts);
}

function pm_local_manifest(string $pluginDir): ?array
{
  $path = rtrim($pluginDir, '/\\') . DIRECTORY_SEPARATOR . 'plugin.json';
  if (!is_file($path)) {
    return null;
  }

  $raw = (string) @file_get_contents($path);
  if ($raw === '') {
    return null;
  }

  $data = json_decode($raw, true);
  return is_array($data) ? $data : null;
}

function pm_store_plugin_meta(array $store, string $id): ?array
{
  $list = $store['plugins'] ?? $store;
  if (!is_array($list)) {
    return null;
  }

  foreach ($list as $plugin) {
    if (is_array($plugin) && (string) ($plugin['id'] ?? '') === $id) {
      return $plugin;
    }
  }

  return null;
}

function pm_changelog_normalize(array $entries): array
{
  $out = [];
  foreach ($entries as $entry) {
    if (!is_array($entry)) {
      continue;
    }
    $version = trim((string) ($entry['version'] ?? ''));
    $changes = $entry['changes'] ?? [];
    if (!is_array($changes)) {
      $changes = [];
    }
    $clean = [];
    foreach ($changes as $change) {
      $change = trim((string) $change);
      if ($change !== '') {
        $clean[] = $change;
      }
    }
    if ($version === '' || empty($clean)) {
      continue;
    }
    $out[] = [
      'version' => $version,
      'date' => trim((string) ($entry['date'] ?? '')),
      'changes' => array_values(array_unique($clean)),
    ];
  }

  usort($out, static function (array $a, array $b): int {
    return version_compare((string) ($b['version'] ?? '0.0.0'), (string) ($a['version'] ?? '0.0.0'));
  });

  return $out;
}

function pm_changelog_latest(array $meta): ?array
{
  $list = pm_changelog_normalize((array) ($meta['changelog'] ?? []));
  return $list[0] ?? null;
}

$errors = [];
$notices = [];
$pluginActionResult = null;
$pluginCompareResult = null;

$pluginsDir = realpath(__DIR__ . '/../../plugins');
if ($pluginsDir === false) {
  $pluginsDir = __DIR__ . '/../../plugins';
  @mkdir($pluginsDir, 0775, true);
}

$csrf = catalogo_csrf_token('plugins_manager_csrf');

$pluginNoticeMap = [
  'installed' => 'Plugin instalado/actualizado correctamente.',
  'installed_skip_sql' => 'Plugin instalado/actualizado sin ejecutar SQL ni tocar tablas.',
  'toggled_on' => 'Plugin activado.',
  'toggled_off' => 'Plugin desactivado.',
  'uninstalled' => 'Plugin desinstalado.',
  'synced_current' => 'Plugin marcado como actualizado tras comprobar que coincide con el Store.',
  'core_uploaded' => 'Paquete de actualización del núcleo subido correctamente.',
  'core_installed' => 'Actualización del núcleo aplicada correctamente.',
  'core_deleted' => 'Paquete local eliminado.'
];
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST' && isset($_GET['okp'])) {
  $k = (string) $_GET['okp'];
  if (isset($pluginNoticeMap[$k])) {
    $notices[] = $pluginNoticeMap[$k];
  }
}


function plugins_redirect_ok(string $status): void
{
  $target = asset_url('dashboard.php') . '?open=' . rawurlencode('base/plugins/index.php?okp=' . $status)
    . '&full_reload=1&ts=' . time();

  if (!headers_sent()) {
    header('Content-Type: text/html; charset=utf-8');
  }

  echo '<!doctype html><html lang="es"><head><meta charset="utf-8"><title>Redirigiendo...</title></head><body>';
  echo '<script>';
  echo 'window.top.location.replace(' . json_encode($target, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');';
  echo '</script>';
  echo '<noscript><meta http-equiv="refresh" content="0;url=' . h($target) . '"></noscript>';
  echo '</body></html>';
  exit;
}

function pm_human_size(int $bytes): string
{
  if ($bytes < 1024) return $bytes . ' B';
  if ($bytes < 1048576) return number_format($bytes / 1024, 1, ',', '.') . ' KB';
  return number_format($bytes / 1048576, 2, ',', '.') . ' MB';
}

function pm_admin_db_from_post(array $post): array
{
  return [
    'host' => trim((string) ($post['db_admin_host'] ?? (defined('DB_HOST') ? DB_HOST : 'localhost'))),
    'port' => (int) ($post['db_admin_port'] ?? (defined('DB_PORT') ? DB_PORT : 3306)),
    'user' => trim((string) ($post['db_admin_user'] ?? '')),
    'pass' => (string) ($post['db_admin_pass'] ?? ''),
  ];
}

function pm_admin_pdo_connect(array $adminDb): PDO
{
  $host = $adminDb['host'] !== '' ? $adminDb['host'] : (defined('DB_HOST') ? DB_HOST : 'localhost');
  $port = (int) ($adminDb['port'] ?: (defined('DB_PORT') ? DB_PORT : 3306));
  $user = trim((string) ($adminDb['user'] ?? ''));
  $pass = (string) ($adminDb['pass'] ?? '');

  if ($user === '') {
    throw new RuntimeException('Debes indicar un usuario administrador de MySQL para ejecutar el SQL del plugin con privilegios elevados.');
  }

  $dsn = "mysql:host={$host};port={$port};charset=utf8mb4";
  return new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  ]);
}

function pm_read_sql_file_payload(string $path, string $stage = 'install', string $mode = 'auto'): ?array
{
  if (!is_file($path)) {
    return null;
  }

  $sql = (string) file_get_contents($path);
  return [
    'file' => basename($path),
    'path' => $path,
    'exists' => true,
    'order' => 0,
    'stage' => $stage,
    'mode' => $mode,
    'sql' => $sql,
  ];
}

function pm_split_sql_statements(string $sql): array
{
  $statements = [];
  $buffer = '';
  $length = strlen($sql);
  $inSingle = false;
  $inDouble = false;
  $inBacktick = false;
  $inLineComment = false;
  $inBlockComment = false;

  for ($i = 0; $i < $length; $i++) {
    $char = $sql[$i];
    $next = ($i + 1 < $length) ? $sql[$i + 1] : '';

    if ($inLineComment) {
      $buffer .= $char;
      if ($char === "\n") {
        $inLineComment = false;
      }
      continue;
    }

    if ($inBlockComment) {
      $buffer .= $char;
      if ($char === '*' && $next === '/') {
        $buffer .= $next;
        $i++;
        $inBlockComment = false;
      }
      continue;
    }

    if (!$inSingle && !$inDouble && !$inBacktick) {
      if ($char === '-' && $next === '-' && (($i + 2 >= $length) || ctype_space($sql[$i + 2]))) {
        $buffer .= $char . $next;
        $i++;
        $inLineComment = true;
        continue;
      }
      if ($char === '#') {
        $buffer .= $char;
        $inLineComment = true;
        continue;
      }
      if ($char === '/' && $next === '*') {
        $buffer .= $char . $next;
        $i++;
        $inBlockComment = true;
        continue;
      }
    }

    if ($char === "'" && !$inDouble && !$inBacktick) {
      $escaped = ($i > 0 && $sql[$i - 1] === '\\');
      if (!$escaped) {
        $inSingle = !$inSingle;
      }
      $buffer .= $char;
      continue;
    }

    if ($char === '"' && !$inSingle && !$inBacktick) {
      $escaped = ($i > 0 && $sql[$i - 1] === '\\');
      if (!$escaped) {
        $inDouble = !$inDouble;
      }
      $buffer .= $char;
      continue;
    }

    if ($char === '`' && !$inSingle && !$inDouble) {
      $inBacktick = !$inBacktick;
      $buffer .= $char;
      continue;
    }

    if ($char === ';' && !$inSingle && !$inDouble && !$inBacktick) {
      $statement = trim($buffer);
      if ($statement !== '') {
        $statements[] = $statement;
      }
      $buffer = '';
      continue;
    }

    $buffer .= $char;
  }

  $tail = trim($buffer);
  if ($tail !== '') {
    $statements[] = $tail;
  }

  return $statements;
}

function pm_exec_sql_payload(PDO $pdo, array $payload, string $connectionLabel): array
{
  $count = 0;
  foreach (pm_split_sql_statements((string) ($payload['sql'] ?? '')) as $statement) {
    $trimmed = trim($statement);
    if ($trimmed === '' || preg_match('/^(--|#)/', $trimmed)) {
      continue;
    }
    $pdo->exec($statement);
    $count++;
  }

  $payload['statement_count'] = $count;
  $payload['connection'] = $connectionLabel;
  return $payload;
}

function pm_merge_sql_results(array $base, array $extra): array
{
  $merged = [
    'ok' => !empty($base['ok']) && !empty($extra['ok']),
    'executed' => array_merge($base['executed'] ?? [], $extra['executed'] ?? []),
    'manual' => array_merge($base['manual'] ?? [], $extra['manual'] ?? []),
    'errors' => array_merge($base['errors'] ?? [], $extra['errors'] ?? []),
    'connection' => (string) ($extra['connection'] ?? ($base['connection'] ?? '')),
  ];

  if ($merged['connection'] === '') {
    $merged['connection'] = (string) ($base['connection'] ?? '');
  }

  return $merged;
}

function pm_plugin_ai_excerpt(string $text, int $maxLen = 180): string
{
  $text = trim(preg_replace('/\s+/', ' ', $text) ?? '');
  if ($text === '') {
    return '';
  }
  if (function_exists('mb_strlen') && function_exists('mb_substr')) {
    if (mb_strlen($text, 'UTF-8') <= $maxLen) {
      return $text;
    }
    return rtrim(mb_substr($text, 0, $maxLen - 1, 'UTF-8')) . '…';
  }
  if (strlen($text) <= $maxLen) {
    return $text;
  }
  return rtrim(substr($text, 0, $maxLen - 1)) . '...';
}

function pm_plugin_ai_analysis(array $meta, ?string $pluginDir = null): array
{
  $name = (string) ($meta['name'] ?? $meta['id'] ?? 'plugin');
  $description = trim((string) ($meta['description'] ?? ''));
  $appFolder = (string) ($meta['app_folder'] ?? ($meta['app']['folder'] ?? ''));
  $pluginId = (string) ($meta['plugin_id'] ?? $meta['id'] ?? $appFolder);
  $sqlScripts = $meta['sql_scripts'] ?? [];
  $roles = $meta['roles_json'] ?? [];
  if (!is_array($roles)) {
    $roles = [];
  }

  $signals = strtolower(trim($name . ' ' . $description . ' ' . $pluginId . ' ' . $appFolder));
  $readmeSnippet = '';
  $topFiles = [];

  if ($pluginDir !== null && is_dir($pluginDir)) {
    foreach (['README.md', 'README.txt', 'plugin.json'] as $candidate) {
      $path = rtrim($pluginDir, '/\\') . DIRECTORY_SEPARATOR . $candidate;
      if (is_file($path)) {
        $readmeSnippet = (string) file_get_contents($path);
        break;
      }
    }

    $appDir = $appFolder !== '' ? rtrim($pluginDir, '/\\') . '/apps/' . $appFolder : '';
    if ($appDir !== '' && is_dir($appDir)) {
      $entries = @scandir($appDir) ?: [];
      foreach ($entries as $entry) {
        if ($entry === '.' || $entry === '..') {
          continue;
        }
        $full = $appDir . '/' . $entry;
        if (is_file($full)) {
          $topFiles[] = $entry;
        }
      }
    }
  }

  $signals .= ' ' . strtolower($readmeSnippet . ' ' . implode(' ', $topFiles));
  $capabilities = [];

  $keywordMap = [
    'incidenc' => 'Gestiona incidencias o tickets internos.',
    'ticket' => 'Gestiona incidencias o tickets internos.',
    'irpf' => 'Genera certificados y gestiona envíos relacionados con IRPF.',
    'certificado' => 'Genera certificados y documentación descargable.',
    'iban' => 'Calcula o valida IBAN y códigos bancarios.',
    'bic' => 'Procesa ficheros bancarios o códigos BIC/SWIFT.',
    'swift' => 'Procesa ficheros bancarios o códigos BIC/SWIFT.',
    'listin' => 'Mantiene listines o directorios de contactos.',
    'contact' => 'Mantiene listines o directorios de contactos.',
    'entrega' => 'Registra entregas de materiales u objetos.',
    'gasto' => 'Gestiona hojas de gasto o justificantes.',
    'tarea' => 'Organiza tareas, seguimiento e histórico de trabajo.',
    'compra' => 'Gestiona solicitudes de compra o material.',
    'material' => 'Gestiona solicitudes de compra o material.',
    'alta' => 'Tramita altas de usuarios, cuentas o accesos.',
    'usuario' => 'Tramita altas de usuarios, cuentas o accesos.',
    'ldap' => 'Se integra con LDAP o Active Directory.',
    'active directory' => 'Se integra con LDAP o Active Directory.',
    'ad-' => 'Se integra con LDAP o Active Directory.',
    'enlace' => 'Publica enlaces o recursos compartidos.',
    'novedad' => 'Publica novedades, avisos o informes.',
  ];

  foreach ($keywordMap as $keyword => $message) {
    if (strpos($signals, $keyword) !== false && !in_array($message, $capabilities, true)) {
      $capabilities[] = $message;
    }
  }

  if (!empty($sqlScripts)) {
    $capabilities[] = 'Incluye scripts SQL para crear o actualizar esquema.';
  }
  if (strpos($signals, 'mail') !== false || strpos($signals, 'smtp') !== false || strpos($signals, 'correo') !== false) {
    $capabilities[] = 'Puede enviar correos o notificaciones.';
  }
  if (strpos($signals, 'pdf') !== false) {
    $capabilities[] = 'Genera documentos PDF.';
  }
  if (strpos($signals, 'api') !== false) {
    $capabilities[] = 'Expone endpoints o acciones API.';
  }
  if (strpos($signals, 'worker') !== false || strpos($signals, 'cron') !== false) {
    $capabilities[] = 'Incluye procesos batch o tareas en segundo plano.';
  }
  if (in_array('administradores', $roles, true) && count($roles) === 1) {
    $capabilities[] = 'Pensado principalmente para administradores.';
  }

  $capabilities = array_values(array_unique($capabilities));
  $summary = $capabilities[0] ?? '';
  if ($summary === '' && $description !== '') {
    $summary = pm_plugin_ai_excerpt($description, 150);
  }
  if ($summary === '') {
    $summary = 'Plugin sin descripción clara; conviene revisar su app principal y sus scripts SQL.';
  }

  return [
    'summary' => $summary,
    'capabilities' => array_slice($capabilities, 0, 4),
    'raw_description' => pm_plugin_ai_excerpt($description, 180),
  ];
}

function plugin_find_root_dir(string $destDir): string
{
  $manifest = $destDir . '/plugin.json';
  if (is_file($manifest)) {
    return $destDir;
  }

  $items = @scandir($destDir);
  if (!is_array($items)) {
    return $destDir;
  }

  $dirs = [];
  foreach ($items as $it) {
    if ($it === '.' || $it === '..') {
      continue;
    }
    $p = $destDir . DIRECTORY_SEPARATOR . $it;
    if (is_dir($p)) {
      $dirs[] = $p;
    }
  }

  if (count($dirs) === 1 && is_file($dirs[0] . '/plugin.json')) {
    return $dirs[0];
  }

  return $destDir;
}

function plugin_move_contents_up(string $fromDir, string $toDir): void
{
  if ($fromDir === $toDir) {
    return;
  }

  $items = scandir($fromDir);
  if ($items === false) {
    return;
  }

  foreach ($items as $it) {
    if ($it === '.' || $it === '..') {
      continue;
    }
    $src = $fromDir . DIRECTORY_SEPARATOR . $it;
    $dst = $toDir . DIRECTORY_SEPARATOR . $it;
    @rename($src, $dst);
  }

  @rmdir($fromDir);
}

function pm_compare_collect_files(string $dir): array
{
  $real = realpath($dir);
  if ($real === false || !is_dir($real)) {
    return [];
  }

  $base = rtrim(str_replace('\\', '/', $real), '/');
  $files = [];
  $it = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($real, FilesystemIterator::SKIP_DOTS)
  );

  foreach ($it as $file) {
    if (!$file->isFile()) {
      continue;
    }
    $path = $file->getPathname();
    $normalized = str_replace('\\', '/', $path);
    $rel = ltrim(substr($normalized, strlen($base)), '/');
    if ($rel === '') {
      continue;
    }
    $files[$rel] = [
      'path' => $path,
      'size' => (int) $file->getSize(),
      'sha256' => (string) hash_file('sha256', $path),
    ];
  }

  ksort($files, SORT_NATURAL | SORT_FLAG_CASE);
  return $files;
}

function pm_json_canonicalize($value)
{
  if (is_array($value)) {
    $isList = array_keys($value) === range(0, count($value) - 1);
    if (!$isList) {
      ksort($value, SORT_STRING);
    }
    foreach ($value as $key => $item) {
      $value[$key] = pm_json_canonicalize($item);
    }
  }

  return $value;
}

function pm_compare_normalize_content(string $relativePath, string $content): ?string
{
  $relativePath = str_replace('\\', '/', ltrim($relativePath, '/'));

  if (basename($relativePath) === '.htaccess') {
    return null;
  }

  if ($relativePath === 'plugin.json') {
    $data = json_decode($content, true);
    if (is_array($data)) {
      unset($data['version'], $data['published_at']);
      return (string) json_encode(pm_json_canonicalize($data), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
  }

  if (preg_match('~^apps/[^/]+/app\.json$~', $relativePath)) {
    $data = json_decode($content, true);
    if (is_array($data)) {
      unset($data['version'], $data['updated_at']);
      return (string) json_encode(pm_json_canonicalize($data), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
  }

  if ($relativePath === 'README.md') {
    return (string) preg_replace('/\(\d+\.\d+\.\d+\)/', '(__VERSION__)', $content);
  }

  if (preg_match('~^install/(?:install|installer|uninstall|uninstaller)\.php$~', $relativePath)) {
    return (string) preg_replace('/("version"\s*:\s*")([^"]+)(")/', '$1__VERSION__$3', $content);
  }

  return $content;
}

function pm_compare_plugin_with_store(string $pluginsDir, array $meta, array &$errors): array
{
  $id = trim((string) ($meta['id'] ?? ''));
  if ($id === '') {
    throw new RuntimeException('Plugin no valido para comparar.');
  }

  $zip = trim((string) ($meta['zip'] ?? ''));
  if ($zip === '') {
    throw new RuntimeException('El plugin del store no define URL de ZIP.');
  }

  $localRoot = plugin_safe_join($pluginsDir, $id);
  if (!is_dir($localRoot)) {
    throw new RuntimeException('No existe la carpeta local del plugin.');
  }

  $zipUrl = plugin_store_join_url(plugin_store_base_url(), $zip);
  $tmpZip = plugin_download_to_tmp($zipUrl, '', $errors);
  if (!$tmpZip) {
    throw new RuntimeException('No se pudo descargar el ZIP del plugin para comparacion.');
  }

  $tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'plugin_compare_' . bin2hex(random_bytes(6));
  try {
    $actualSha = strtolower((string) hash_file('sha256', $tmpZip));
    $expectedSha = strtolower(trim((string) ($meta['sha256'] ?? '')));
    $shaMatches = $expectedSha === '' ? null : plugin_sha256_matches_catalog($expectedSha, $actualSha);

    if (!plugin_extract_zip($tmpZip, $tmpDir, $errors)) {
      throw new RuntimeException('No se pudo descomprimir el ZIP del store para comparacion.');
    }

    $remoteRoot = plugin_find_root_dir($tmpDir);
    $remoteManifest = plugin_read_manifest($remoteRoot, $errors);
    if (!is_array($remoteManifest)) {
      throw new RuntimeException('El ZIP del store no contiene un plugin.json valido.');
    }
    if (is_array($remoteManifest) && (string) ($remoteManifest['id'] ?? '') !== $id) {
      throw new RuntimeException('El plugin.json del ZIP no coincide con el id solicitado.');
    }

    $localFiles = pm_compare_collect_files($localRoot);
    $remoteFiles = pm_compare_collect_files($remoteRoot);
    $all = array_values(array_unique(array_merge(array_keys($localFiles), array_keys($remoteFiles))));
    sort($all, SORT_NATURAL | SORT_FLAG_CASE);

    $same = [];
    $changed = [];
    $onlyLocal = [];
    $onlyStore = [];
    $normalizedLocal = [];
    $normalizedStore = [];

    foreach ($all as $rel) {
      $local = $localFiles[$rel] ?? null;
      $remote = $remoteFiles[$rel] ?? null;

      if ($local === null) {
        $remoteRaw = is_array($remote) ? (string) @file_get_contents((string) $remote['path']) : '';
        $remoteNormalized = pm_compare_normalize_content($rel, $remoteRaw);
        if ($remoteNormalized !== null) {
          $normalizedStore[$rel] = hash('sha256', $remoteNormalized);
        }
        $onlyStore[] = [
          'file' => $rel,
          'store_size' => (int) ($remote['size'] ?? 0),
          'store_sha256' => (string) ($remote['sha256'] ?? ''),
        ];
        continue;
      }

      if ($remote === null) {
        $localRaw = is_array($local) ? (string) @file_get_contents((string) $local['path']) : '';
        $localNormalized = pm_compare_normalize_content($rel, $localRaw);
        if ($localNormalized !== null) {
          $normalizedLocal[$rel] = hash('sha256', $localNormalized);
        }
        $onlyLocal[] = [
          'file' => $rel,
          'local_size' => (int) ($local['size'] ?? 0),
          'local_sha256' => (string) ($local['sha256'] ?? ''),
        ];
        continue;
      }

      $localRaw = (string) @file_get_contents((string) $local['path']);
      $remoteRaw = (string) @file_get_contents((string) $remote['path']);
      $localNormalized = pm_compare_normalize_content($rel, $localRaw);
      $remoteNormalized = pm_compare_normalize_content($rel, $remoteRaw);
      if ($localNormalized !== null) {
        $normalizedLocal[$rel] = hash('sha256', $localNormalized);
      }
      if ($remoteNormalized !== null) {
        $normalizedStore[$rel] = hash('sha256', $remoteNormalized);
      }

      if ((string) $local['sha256'] === (string) $remote['sha256']) {
        $same[] = [
          'file' => $rel,
          'size' => (int) ($local['size'] ?? 0),
          'sha256' => (string) ($local['sha256'] ?? ''),
        ];
        continue;
      }

      $changed[] = [
        'file' => $rel,
        'local_size' => (int) ($local['size'] ?? 0),
        'store_size' => (int) ($remote['size'] ?? 0),
        'local_sha256' => (string) ($local['sha256'] ?? ''),
        'store_sha256' => (string) ($remote['sha256'] ?? ''),
      ];
    }

    ksort($normalizedLocal, SORT_STRING);
    ksort($normalizedStore, SORT_STRING);
    $contentSame = hash_equals(
      hash('sha256', json_encode($normalizedLocal, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
      hash('sha256', json_encode($normalizedStore, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))
    );
    $differentCount = count($changed) + count($onlyLocal) + count($onlyStore);
    return [
      'plugin_id' => $id,
      'plugin_name' => (string) ($meta['name'] ?? $id),
      'store_version' => (string) ($meta['version'] ?? ''),
      'local_version' => (string) ((pm_local_manifest($localRoot)['version'] ?? '') ?: ''),
      'zip_url' => $zipUrl,
      'expected_sha256' => $expectedSha,
      'actual_sha256' => $actualSha,
      'sha_matches' => $shaMatches,
      'same' => $same,
      'changed' => $changed,
      'only_local' => $onlyLocal,
      'only_store' => $onlyStore,
      'content_same' => $contentSame,
      'summary' => $differentCount === 0
        ? 'El plugin local coincide fichero a fichero con el ZIP publicado en el store.'
        : ($contentSame
          ? 'Solo hay diferencias de metadatos normalizados entre el plugin local y el ZIP publicado.'
          : 'Se han encontrado diferencias entre el plugin local y el ZIP publicado en el store.'),
    ];
  } finally {
    @unlink($tmpZip);
    if (is_dir($tmpDir)) {
      plugin_rrmdir($tmpDir);
    }
  }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  try {
    catalogo_csrf_validate($_POST['csrf'] ?? null, 'plugins_manager_csrf');
    $action = (string) ($_POST['action'] ?? '');

    if ($action === 'compare_store') {
      $id = trim((string) ($_POST['plugin_id'] ?? ''));
      if ($id === '') {
        throw new RuntimeException('Plugin no valido.');
      }

      $store = plugin_store_fetch_index($errors);
      if (!$store) {
        throw new RuntimeException('No se pudo leer el catalogo del store.');
      }

      $meta = pm_store_plugin_meta($store, $id);
      if (!$meta) {
        throw new RuntimeException('El plugin no existe en el store.');
      }

      $pluginCompareResult = pm_compare_plugin_with_store($pluginsDir, $meta, $errors);
    }

    if ($action === 'mark_store_current') {
      $id = trim((string) ($_POST['plugin_id'] ?? ''));
      if ($id === '') {
        throw new RuntimeException('Plugin no valido.');
      }

      $store = plugin_store_fetch_index($errors);
      if (!$store) {
        throw new RuntimeException('No se pudo leer el catalogo del store.');
      }

      $meta = pm_store_plugin_meta($store, $id);
      if (!$meta) {
        throw new RuntimeException('El plugin no existe en el store.');
      }

      $pluginCompareResult = pm_compare_plugin_with_store($pluginsDir, $meta, $errors);
      if (empty($pluginCompareResult['content_same'])) {
        throw new RuntimeException('No se puede marcar como actualizado porque hay diferencias reales de contenido con el Store.');
      }

      $installedRows = plugin_installed_all($pdo);
      $installedRow = $installedRows[$id] ?? null;
      if (!is_array($installedRow)) {
        throw new RuntimeException('El plugin no esta registrado como instalado.');
      }

      $zipPath = trim((string) ($meta['zip'] ?? ''));
      if ($zipPath === '') {
        $zipPath = 'download.php?id=' . rawurlencode($id);
      }
      $zipUrl = plugin_store_join_url(plugin_store_base_url(), $zipPath);
      plugin_upsert_installed(
        $pdo,
        $id,
        (string) ($meta['name'] ?? ($installedRow['name'] ?? $id)),
        (string) ($meta['version'] ?? ''),
        (int) ($installedRow['enabled'] ?? 1),
        $zipUrl,
        (string) ($meta['sha256'] ?? '')
      );

      plugins_redirect_ok('synced_current');
    }

    if ($action === 'install' || $action === 'update') {
      $id = trim((string) ($_POST['plugin_id'] ?? ''));
      if ($id === '') {
        throw new RuntimeException('Plugin no válido.');
      }

      $store = plugin_store_fetch_index($errors);
      if (!$store) {
        throw new RuntimeException('No se pudo leer el catálogo del store.');
      }

      $list = $store['plugins'] ?? $store;
      if (!is_array($list)) {
        throw new RuntimeException('Catálogo inválido.');
      }

      $meta = null;
      foreach ($list as $p) {
        if (is_array($p) && (string) ($p['id'] ?? '') === $id) {
          $meta = $p;
          break;
        }
      }
      if (!$meta) {
        throw new RuntimeException('El plugin no existe en el store.');
      }

      $zipUrl = (string) ($meta['zip'] ?? '');
      if ($zipUrl === '') {
        throw new RuntimeException('El plugin del store no define URL de ZIP.');
      }

      $zipUrl = plugin_store_join_url(plugin_store_base_url(), $zipUrl);

      $sha256 = (string) ($meta['sha256'] ?? '');
      $tmpZip = plugin_download_to_tmp($zipUrl, $sha256, $errors);
      if (!$tmpZip) {
        throw new RuntimeException('No se pudo descargar el ZIP del plugin.');
      }

      $dest = plugin_safe_join($pluginsDir, $id);

      if (is_dir($dest)) {
        plugin_rrmdir($dest);
      }

      if (!@mkdir($dest, 0775, true) && !is_dir($dest)) {
        @unlink($tmpZip);
        throw new RuntimeException('No se pudo crear el directorio del plugin.');
      }

      if (!plugin_extract_zip($tmpZip, $dest, $errors)) {
        @unlink($tmpZip);
        throw new RuntimeException('No se pudo extraer el plugin.');
      }
      @unlink($tmpZip);

      $root = plugin_find_root_dir($dest);
      if ($root !== $dest) {
        plugin_move_contents_up($root, $dest);
      }

      $manifest = plugin_read_manifest($dest, $errors);
      if (!$manifest) {
        throw new RuntimeException('plugin.json inválido.');
      }
      if ((string) $manifest['id'] !== $id) {
        throw new RuntimeException('plugin.json id no coincide con el id del store.');
      }
      $rolesJson = intranet_roles_from_post($_POST['roles_json'] ?? ($manifest['roles_json'] ?? []), ['administradores']);
      $manifest['roles_json'] = $rolesJson;
      $selectedGroupName = catalogo_normalize_group_name(
        $_POST['group_name'] ?? ($manifest['default_group_name'] ?? ($manifest['app']['default_group_name'] ?? 'Mis apps')),
        'apps'
      );
      if (!catalogo_group_exists($pdo, 'apps', $selectedGroupName)) {
        throw new RuntimeException('El grupo seleccionado no existe en el catalogo de aplicaciones.');
      }
      $manifest['default_group_name'] = $selectedGroupName;
      if (isset($manifest['app']) && is_array($manifest['app'])) {
        $manifest['app']['default_group_name'] = $selectedGroupName;
      }

      $sqlStrategy = (string) ($_POST['sql_strategy'] ?? 'auto');
      if (!in_array($sqlStrategy, ['auto', 'show', 'admin', 'skip'], true)) {
        $sqlStrategy = 'auto';
      }
      $adminDb = pm_admin_db_from_post($_POST);

      $sqlResult = [
        'ok' => true,
        'executed' => [],
        'manual' => [],
        'errors' => [],
        'connection' => '',
      ];

      $sqlInstall = $dest . '/install/install.sql';
      $legacySql = pm_read_sql_file_payload($sqlInstall, 'install', 'auto');
      if (is_array($legacySql)) {
        if ($sqlStrategy === 'show') {
          $legacySql['mode'] = 'manual';
          $sqlResult['manual'][] = $legacySql;
          $sqlResult['ok'] = true;
        } elseif ($sqlStrategy === 'skip') {
          $sqlResult['ok'] = true;
        } else {
          $sqlPdo = $pdo;
          $connectionLabel = 'usuario de aplicacion';
          if ($sqlStrategy === 'admin') {
            $sqlPdo = pm_admin_pdo_connect($adminDb);
            $connectionLabel = 'usuario administrador';
          }
          $executedPayload = pm_exec_sql_payload($sqlPdo, $legacySql, $connectionLabel);
          $sqlResult['executed'][] = $executedPayload;
          $sqlResult['connection'] = $connectionLabel;
        }
      }

      $phpInstall = $dest . '/install/install.php';
      if (!is_file($phpInstall)) {
        $phpInstall = $dest . '/install/installer.php';
      }

      $pluginInstallResult = null;
      if (is_file($phpInstall)) {
        try {
          $pdo_for_plugin = $pdo;
          $PLUGIN_INSTALL_CTX = [
            'public_root' => realpath(__DIR__ . '/../..') ?: (__DIR__ . '/../..'),
            'plugin_root' => $dest,
            'plugin_id' => $id,
            'group_name' => $selectedGroupName,
            'app_folder' => (string) ($manifest['app_folder'] ?? ($manifest['app']['folder'] ?? '')),
            'version' => (string) ($manifest['version'] ?? ''),
            'source_url' => $zipUrl,
            'sha256' => $sha256,
            'roles_json' => $rolesJson,
            'sql_strategy' => $sqlStrategy,
            'admin_db' => $adminDb,
          ];
          $PLUGIN_INSTALL_RESULT = null;
          require $phpInstall;
          $pluginInstallResult = is_array($PLUGIN_INSTALL_RESULT ?? null) ? $PLUGIN_INSTALL_RESULT : null;
          unset($PLUGIN_INSTALL_RESULT, $PLUGIN_INSTALL_CTX, $pdo_for_plugin);
        } catch (Throwable $e) {
          throw new RuntimeException('Fallo en el instalador PHP del plugin: ' . $e->getMessage());
        }
      }

      if (is_array($pluginInstallResult) && isset($pluginInstallResult['sql']) && is_array($pluginInstallResult['sql'])) {
        $sqlResult = pm_merge_sql_results($sqlResult, $pluginInstallResult['sql']);
      }

      $sqlOnly = false;
      $installOk = empty($sqlResult['errors']);
      $message = 'Plugin instalado/actualizado correctamente.';
      if (is_array($pluginInstallResult)) {
        $installOk = $installOk && !empty($pluginInstallResult['ok']);
        $sqlOnly = !empty($pluginInstallResult['sql_only']);
        $message = (string) ($pluginInstallResult['msg'] ?? $message);
      }

      if ($sqlStrategy === 'show' && (!empty($sqlResult['manual']) || is_array($pluginInstallResult))) {
        $sqlOnly = true;
        if ($message === 'Plugin instalado/actualizado correctamente.') {
          $message = 'SQL preparados para ejecucion manual.';
        }
      }

      if (!$installOk || $sqlOnly) {
        $pluginActionResult = [
          'ok' => $installOk,
          'sql_only' => $sqlOnly,
          'msg' => $message,
          'plugin_id' => $id,
          'plugin_name' => (string) ($manifest['name'] ?? $id),
          'sql' => $sqlResult,
        ];
      } else {
        plugin_upsert_installed(
          $pdo,
          (string) $manifest['id'],
          (string) $manifest['name'],
          (string) $manifest['version'],
          1,
          $zipUrl,
          $sha256
        );

        plugin_app_register($pdo, $manifest, 1);

        plugins_redirect_ok($sqlStrategy === 'skip' ? 'installed_skip_sql' : 'installed');
      }
    }

    if ($action === 'toggle') {
      $id = trim((string) ($_POST['plugin_id'] ?? ''));
      $enabled = (int) ($_POST['enabled'] ?? 0) ? 1 : 0;
      if ($id === '') {
        throw new RuntimeException('Plugin no válido.');
      }

      $st = $pdo->prepare('UPDATE intranet_plugins SET enabled=?, updated_at=NOW() WHERE id=?');
      $st->execute([$enabled, $id]);

      $manifestPath = plugin_safe_join($pluginsDir, $id) . '/plugin.json';
      if (is_file($manifestPath)) {
        $manifest = json_decode((string) file_get_contents($manifestPath), true);
        if (is_array($manifest)) {
          plugin_app_register($pdo, $manifest, $enabled);
        }
      }

      plugins_redirect_ok($enabled ? 'toggled_on' : 'toggled_off');
    }

    if ($action === 'uninstall') {
      $id = trim((string) ($_POST['plugin_id'] ?? ''));
      if ($id === '') {
        throw new RuntimeException('Plugin no válido.');
      }

      $manifest = null;
      $manifestPath = plugin_safe_join($pluginsDir, $id) . '/plugin.json';
      if (is_file($manifestPath)) {
        $manifest = json_decode((string) file_get_contents($manifestPath), true);
      }

      $sqlUn = plugin_safe_join($pluginsDir, $id) . '/install/uninstall.sql';
      if (is_file($sqlUn)) {
        plugin_exec_sql_file($pdo, $sqlUn, $errors);
      }

      $phpUn = plugin_safe_join($pluginsDir, $id) . '/install/uninstall.php';
      if (!is_file($phpUn)) {
        $phpUn = plugin_safe_join($pluginsDir, $id) . '/install/uninstaller.php';
      }

      if (is_file($phpUn)) {
        try {
          $pdo_for_plugin = $pdo;
          $PLUGIN_UNINSTALL_CTX = [
            'public_root' => realpath(__DIR__ . '/../..') ?: (__DIR__ . '/../..'),
            'plugin_root' => plugin_safe_join($pluginsDir, $id),
          ];
          require $phpUn;
          unset($PLUGIN_UNINSTALL_CTX, $pdo_for_plugin);
        } catch (Throwable $e) {
          $errors[] = 'Aviso uninstall.php: ' . $e->getMessage();
        }
      }

      if (is_array($manifest)) {
        plugin_app_unregister($pdo, $manifest);
      }

      plugin_delete_installed($pdo, $id);

      $dir = plugin_safe_join($pluginsDir, $id);
      plugin_rrmdir($dir);

      plugins_redirect_ok('uninstalled');
    }

    if ($action === 'core_upload') {
      if (empty($_FILES['core_zip'])) {
        throw new RuntimeException('No se ha recibido el ZIP de actualización.');
      }
      $saved = core_update_save_uploaded_zip($_FILES['core_zip'], $errors);
      if (!$saved) {
        throw new RuntimeException('No se pudo guardar el paquete de actualización.');
      }
      $tmpErrors = [];
      $tmp = core_update_extract_to_tmp($saved, $tmpErrors);
      if (!$tmp) {
        @unlink($saved);
        throw new RuntimeException('El ZIP subido no es válido como actualización del núcleo.');
      }
      $manifest = core_update_read_manifest_from_dir($tmp, $tmpErrors);
      plugin_rrmdir($tmp);
      if (!$manifest) {
        @unlink($saved);
        throw new RuntimeException('El paquete subido no contiene un core-update válido.');
      }
      plugins_redirect_ok('core_uploaded');
    }

    if ($action === 'core_delete_local') {
      $file = basename((string) ($_POST['package_file'] ?? ''));
      if ($file === '') {
        throw new RuntimeException('Paquete no válido.');
      }
      $path = core_updates_base_dir() . '/packages/' . $file;
      if (is_file($path)) {
        @unlink($path);
      }
      plugins_redirect_ok('core_deleted');
    }

    if ($action === 'core_install_local') {
      $file = basename((string) ($_POST['package_file'] ?? ''));
      if ($file === '') {
        throw new RuntimeException('Paquete no válido.');
      }
      $path = core_updates_base_dir() . '/packages/' . $file;
      if (!is_file($path)) {
        throw new RuntimeException('No existe el paquete local seleccionado.');
      }
      $res = core_update_apply_from_zip($pdo, $path, (string) ($_SESSION['username'] ?? 'admin'), $errors);
      if (!$res) {
        throw new RuntimeException('No se pudo aplicar la actualización del núcleo.');
      }
      plugins_redirect_ok('core_installed');
    }

    if ($action === 'core_install_remote') {
      $id = trim((string) ($_POST['core_id'] ?? ''));
      if ($id === '') {
        throw new RuntimeException('Actualización no válida.');
      }
      $remote = core_update_fetch_index($errors);
      $meta = null;
      foreach ($remote as $item) {
        if ((string) ($item['id'] ?? '') === $id) {
          $meta = $item;
          break;
        }
      }
      if (!$meta) {
        throw new RuntimeException('La actualización no existe en el store.');
      }
      $zipUrl = (string) ($meta['zip'] ?? '');
      if ($zipUrl === '') {
        throw new RuntimeException('La actualización no define URL de ZIP.');
      }
      $zipUrl = plugin_store_join_url(plugin_store_base_url(), $zipUrl);
      $sha256 = (string) ($meta['sha256'] ?? '');
      $tmpZip = plugin_download_to_tmp($zipUrl, $sha256, $errors);
      if (!$tmpZip) {
        throw new RuntimeException('No se pudo descargar el ZIP de actualización.');
      }
      $res = core_update_apply_from_zip($pdo, $tmpZip, (string) ($_SESSION['username'] ?? 'admin'), $errors);
      @unlink($tmpZip);
      if (!$res) {
        throw new RuntimeException('No se pudo aplicar la actualización del núcleo.');
      }
      plugins_redirect_ok('core_installed');
    }

  } catch (Throwable $e) {
    $errors[] = $e->getMessage();
  }
}

$installed = plugin_installed_all($pdo);
$store = plugin_store_fetch_index($errors);
$coreRemoteUpdates = core_update_fetch_index($errors);
$coreLocalUpdates = core_update_list_local_packages($errors);

$storePlugins = [];
if (is_array($store)) {
  $list = $store['plugins'] ?? $store;
  if (is_array($list)) {
    foreach ($list as $p) {
      if (!is_array($p)) {
        continue;
      }
      $pid = (string) ($p['id'] ?? '');
      if ($pid === '') {
        continue;
      }
      $storePlugins[$pid] = $p;
    }
  }
}

$allIds = array_unique(array_merge(array_keys($storePlugins), array_keys($installed)));
sort($allIds);
$availableRoles = intranet_available_roles($pdo);
$appGroupOptions = [];
foreach (catalogo_groups_fetch($pdo, 'apps') as $groupRow) {
  $groupName = catalogo_normalize_group_name($groupRow['group_name'] ?? 'Mis apps', 'apps');
  if ($groupName !== '') {
    $appGroupOptions[] = $groupName;
  }
}
$appGroupOptions = array_values(array_unique($appGroupOptions));
if (empty($appGroupOptions)) {
  $appGroupOptions[] = 'Mis apps';
}

?><!doctype html>
<html lang="es" class="app-html" <?= intranet_theme_html_attr(); ?>>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Administración · Plugins</title>
  <?= intranet_theme_bootstrap_script_tag(); ?>
  <link rel="stylesheet" href="<?= h(asset_url('styles/css/style.css')) ?>">
  <link rel="stylesheet" href="<?= h(asset_url('styles/css/theme-vars.css')) ?>">
  <script src="<?= h(asset_url('styles/js/table_sort.js')) ?>" defer></script>
</head>

<body class="app-body">
  <div class="app-container">
    <header class="header-top">
      <div>
        <h2 style="margin:0;">Administración · Plugins</h2>
        <p style="margin:5px 0 0 0; opacity:0.9;">Catálogo e instalación desde el Plugin Store</p>
        <p style="margin:6px 0 0 0; font-size:.9rem; opacity:.85;">Store: <code><?= h(plugin_store_base_url()) ?></code>
        </p>
      </div>
      <div class="header-actions">
        <a href="<?= h(asset_url('dashboard.php')) ?>" target="_top" class="btn-back-dashboard">Volver al inicio</a>
      </div>
    </header>

    <?php foreach ($errors as $e): ?>
      <div class="alert alert-error" style="margin-bottom:12px;">
        <?= h($e) ?>
      </div>
    <?php endforeach; ?>
    <?php foreach ($notices as $n): ?>
      <div class="alert admin-notice" style="margin-bottom:12px;">
        <?= h($n) ?>
      </div>
    <?php endforeach; ?>

    <?php if (is_array($pluginActionResult)): ?>
      <section class="card" style="margin-bottom:20px;">
        <div class="card-title"><?= !empty($pluginActionResult['ok']) ? 'Resultado del plugin' : 'Instalacion incompleta' ?></div>
        <p style="margin:0 0 12px 0;">
          <strong><?= h((string) ($pluginActionResult['plugin_name'] ?? $pluginActionResult['plugin_id'] ?? 'Plugin')) ?></strong>
          <?php if (!empty($pluginActionResult['msg'])): ?>
            <span style="display:block; margin-top:6px; color:var(--text-muted);"><?= h((string) $pluginActionResult['msg']) ?></span>
          <?php endif; ?>
        </p>

        <?php if (!empty($pluginActionResult['sql']['errors'])): ?>
          <div class="alert alert-error" style="margin-bottom:12px;">
            <?php foreach ($pluginActionResult['sql']['errors'] as $sqlError): ?>
              <div><?= h((string) $sqlError) ?></div>
            <?php endforeach; ?>
          </div>
        <?php endif; ?>

        <?php if (!empty($pluginActionResult['sql']['executed'])): ?>
          <div style="margin-bottom:14px;">
            <strong>SQL ejecutados</strong>
            <?php if (!empty($pluginActionResult['sql']['connection'])): ?>
              <div style="margin-top:4px; color:var(--text-muted);">
                Modo de ejecucion: <?= h((string) $pluginActionResult['sql']['connection']) ?>
              </div>
            <?php endif; ?>
            <ul style="margin:10px 0 0 18px;">
              <?php foreach ($pluginActionResult['sql']['executed'] as $item): ?>
                <li>
                  <?= h((string) ($item['file'] ?? 'SQL')) ?>
                  (<?= h((string) ($item['stage'] ?? 'install')) ?>, <?= (int) ($item['statement_count'] ?? 0) ?> sentencia(s), <?= h((string) ($item['connection'] ?? 'usuario de aplicacion')) ?>)
                </li>
              <?php endforeach; ?>
            </ul>
          </div>
        <?php endif; ?>

        <?php if (!empty($pluginActionResult['sql']['manual'])): ?>
          <div style="display:grid; gap:12px;">
            <strong>SQL para revision o ejecucion manual</strong>
            <?php foreach ($pluginActionResult['sql']['manual'] as $item): ?>
              <div style="border:1px solid var(--border-light); border-radius:10px; padding:12px; background:var(--bg-input);">
                <div style="margin-bottom:8px;">
                  <strong><?= h((string) ($item['file'] ?? 'SQL')) ?></strong>
                  <span style="color:var(--text-muted);"> · <?= h((string) ($item['stage'] ?? 'install')) ?> · <?= h((string) ($item['mode'] ?? 'manual')) ?></span>
                </div>
                <textarea readonly style="width:100%; min-height:220px; font-family:Consolas, monospace; font-size:12px; padding:10px;"><?= h((string) ($item['sql'] ?? '')) ?></textarea>
              </div>
            <?php endforeach; ?>
          </div>
        <?php endif; ?>

        <?php if (!empty($pluginActionResult['sql_only'])): ?>
          <p class="muted-help" style="margin-top:14px;">
            El plugin no se ha registrado todavia en la intranet. Ejecuta los SQL necesarios y vuelve a instalarlo en modo automatico o administrador.
          </p>
        <?php endif; ?>
      </section>
    <?php endif; ?>

    <?php if (is_array($pluginCompareResult)): ?>
      <?php
      $rawDiffs = count((array) ($pluginCompareResult['changed'] ?? []))
        + count((array) ($pluginCompareResult['only_local'] ?? []))
        + count((array) ($pluginCompareResult['only_store'] ?? []));
      ?>
      <section class="card" style="margin-bottom:20px;">
        <div class="card-title">Comparacion con Plugin Store</div>
        <p style="margin:0 0 10px 0;">
          <strong><?= h((string) ($pluginCompareResult['plugin_name'] ?? $pluginCompareResult['plugin_id'] ?? 'Plugin')) ?></strong>
          <span style="display:block; margin-top:6px; color:var(--text-muted);"><?= h((string) ($pluginCompareResult['summary'] ?? '')) ?></span>
        </p>
        <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px;">
          <span class="badge">Local <?= h((string) ($pluginCompareResult['local_version'] ?? '-')) ?></span>
          <span class="badge">Store <?= h((string) ($pluginCompareResult['store_version'] ?? '-')) ?></span>
          <span class="badge"><?= (int) count((array) ($pluginCompareResult['same'] ?? [])) ?> iguales</span>
          <span class="badge"><?= (int) $rawDiffs ?> diferencia(s) byte a byte</span>
          <span class="badge"><?= !empty($pluginCompareResult['content_same']) ? 'Contenido equivalente' : 'Contenido distinto' ?></span>
        </div>
        <div style="font-size:.9rem; color:var(--text-muted); margin-bottom:12px;">
          ZIP: <code><?= h((string) ($pluginCompareResult['zip_url'] ?? '')) ?></code><br>
          SHA256 descargado: <code><?= h((string) ($pluginCompareResult['actual_sha256'] ?? '')) ?></code>
        </div>
        <?php if (!empty($pluginCompareResult['content_same'])): ?>
          <form method="post" style="margin:0 0 12px 0;">
            <input type="hidden" name="csrf" value="<?= h($csrf) ?>">
            <input type="hidden" name="action" value="mark_store_current">
            <input type="hidden" name="plugin_id" value="<?= h((string) ($pluginCompareResult['plugin_id'] ?? '')) ?>">
            <button class="btn primary" type="submit">Marcar como actualizado</button>
          </form>
        <?php endif; ?>
        <?php if ($rawDiffs > 0): ?>
          <details open>
            <summary style="cursor:pointer;">Ver diferencias</summary>
            <div style="display:grid; gap:8px; margin-top:10px;">
              <?php foreach ((array) ($pluginCompareResult['changed'] ?? []) as $item): ?>
                <div style="padding:8px 10px; border:1px solid var(--border-light); border-radius:8px; background:var(--bg-input);">
                  <strong><?= h((string) ($item['file'] ?? '')) ?></strong>
                  <div style="color:var(--text-muted); font-size:.85rem;">Local <?= h(substr((string) ($item['local_sha256'] ?? ''), 0, 12)) ?> · Store <?= h(substr((string) ($item['store_sha256'] ?? ''), 0, 12)) ?></div>
                </div>
              <?php endforeach; ?>
              <?php foreach ((array) ($pluginCompareResult['only_local'] ?? []) as $item): ?>
                <div style="padding:8px 10px; border:1px solid var(--border-light); border-radius:8px; background:var(--bg-input);">
                  Solo local: <strong><?= h((string) ($item['file'] ?? '')) ?></strong>
                </div>
              <?php endforeach; ?>
              <?php foreach ((array) ($pluginCompareResult['only_store'] ?? []) as $item): ?>
                <div style="padding:8px 10px; border:1px solid var(--border-light); border-radius:8px; background:var(--bg-input);">
                  Solo Store: <strong><?= h((string) ($item['file'] ?? '')) ?></strong>
                </div>
              <?php endforeach; ?>
            </div>
          </details>
        <?php endif; ?>
      </section>
    <?php endif; ?>

    <section class="card" style="margin-bottom:20px;">
      <div class="card-title">🧾 Plugins disponibles</div>

      <div class="table-tools">
        <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
          <input id="tableFilter" class="form-control" placeholder="Buscar por id o nombre…">
          <small class="muted-help" style="margin:0;">Haz click en cabeceras para ordenar.</small>
        </div>
        <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
          <a class="btn" href="index.php">Actualizar listado</a>
        </div>
      </div>

      <div class="table-responsive" style="border:1px solid var(--border-light); border-radius:8px;">
        <table id="pluginsTable" class="table-sortable">
          <thead>
            <tr>
              <th data-sort-type="text">ID</th>
              <th data-sort-type="text">Nombre</th>
              <th data-sort-type="text">Versión</th>
              <th data-sort-type="text">Publicado</th>
              <th data-sort-type="text">Estado</th>
              <th>Acciones</th>
            </tr>
          </thead>
          <tbody>
            <?php if (empty($allIds)): ?>
              <tr>
                <td colspan="6" style="padding:40px; color:var(--text-muted); font-style:italic;">No hay plugins en el
                  store.</td>
              </tr>
            <?php else: ?>
              <?php foreach ($allIds as $pid): ?>
                <?php
                $meta = $storePlugins[$pid] ?? null;
                $inst = $installed[$pid] ?? null;
                $localPluginDir = plugin_safe_join($pluginsDir, $pid);
                if (!is_dir($localPluginDir)) {
                  $localPluginDir = null;
                }
                $localManifest = $localPluginDir !== null ? pm_local_manifest($localPluginDir) : null;
                if (!is_array($meta) && is_array($localManifest)) {
                  $meta = $localManifest;
                } elseif (is_array($meta) && is_array($localManifest) && empty($meta['changelog']) && !empty($localManifest['changelog'])) {
                  $meta['changelog'] = $localManifest['changelog'];
                }
                $name = (string) ($meta['name'] ?? ($inst['name'] ?? $pid));
                $verStore = (string) ($meta['version'] ?? '');
                $verInst = (string) ($inst['version'] ?? '');
                $verLocal = is_array($localManifest) ? trim((string) ($localManifest['version'] ?? '')) : '';
                $verCurrent = $verLocal !== '' ? $verLocal : $verInst;
                $enabled = $inst ? (int) ($inst['enabled'] ?? 1) : 0;
                $isInstalled = $inst !== null;
                $hasUpdate = $isInstalled && $verStore !== '' && ($verCurrent === '' || version_compare($verStore, $verCurrent, '>'));
                $status = !$isInstalled ? 'No instalado' : ($enabled ? 'Instalado · Activo' : 'Instalado · Inactivo');
                $publishedHuman = is_array($meta) ? pm_store_published_human($meta) : '-';
                $publishedSort = is_array($meta) ? pm_store_published_sort($meta) : '-';
                $analysis = pm_plugin_ai_analysis(
                  is_array($meta) ? $meta : (is_array($inst) ? $inst : ['id' => $pid, 'name' => $name]),
                  $localPluginDir
                );
                $selectedRoles = intranet_roles_from_post(is_array($meta) ? ($meta['roles_json'] ?? []) : [], ['administradores']);
                $roleOptions = array_values(array_unique(array_merge($availableRoles, $selectedRoles)));
                sort($roleOptions, SORT_NATURAL | SORT_FLAG_CASE);
                $pluginDefaultGroup = catalogo_normalize_group_name(
                  is_array($meta) ? ($meta['default_group_name'] ?? ($meta['app']['default_group_name'] ?? 'Mis apps')) : 'Mis apps',
                  'apps'
                );
                $selectedGroupName = in_array($pluginDefaultGroup, $appGroupOptions, true)
                  ? $pluginDefaultGroup
                  : (in_array('Mis apps', $appGroupOptions, true) ? 'Mis apps' : (string) $appGroupOptions[0]);
                $latestChange = is_array($meta) ? pm_changelog_latest($meta) : null;
                $changelog = is_array($meta) ? pm_changelog_normalize((array) ($meta['changelog'] ?? [])) : [];
                ?>
                <tr>
                  <td data-sort="<?= h($pid) ?>"><code><?= h($pid) ?></code></td>
                  <td data-sort="<?= h($name) ?>">
                    <div style="display:grid; gap:6px;">
                      <strong><?= h($name) ?></strong>
                      <?php if (!empty($analysis['raw_description'])): ?>
                        <div style="color:var(--text-muted); font-size:.92rem;"><?= h((string) $analysis['raw_description']) ?></div>
                      <?php endif; ?>
                      <?php if (!empty($analysis['summary'])): ?>
                        <div style="font-size:.92rem;">
                          <span class="badge" style="margin-right:6px;">Analisis automatico</span>
                          <?= h((string) $analysis['summary']) ?>
                        </div>
                      <?php endif; ?>
                      <?php if (!empty($analysis['capabilities'])): ?>
                        <div style="display:flex; gap:6px; flex-wrap:wrap;">
                          <?php foreach ($analysis['capabilities'] as $capability): ?>
                            <span class="badge"><?= h((string) $capability) ?></span>
                          <?php endforeach; ?>
                        </div>
                      <?php endif; ?>
                      <?php if (is_array($latestChange)): ?>
                        <div style="display:grid; gap:6px; padding:10px; border:1px solid var(--border-light); border-radius:10px; background:var(--bg-input);">
                          <div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
                            <span class="badge">Cambios <?= h((string) ($latestChange['version'] ?? '')) ?></span>
                            <?php if (!empty($latestChange['date']) && strtotime((string) $latestChange['date']) !== false): ?>
                              <span style="color:var(--text-muted); font-size:.85rem;"><?= h((string) date('d/m/Y H:i', strtotime((string) $latestChange['date']))) ?></span>
                            <?php endif; ?>
                          </div>
                          <ul style="margin:0; padding-left:18px;">
                            <?php foreach (array_slice((array) ($latestChange['changes'] ?? []), 0, 4) as $change): ?>
                              <li><?= h((string) $change) ?></li>
                            <?php endforeach; ?>
                          </ul>
                          <?php if (count($changelog) > 1): ?>
                            <details>
                              <summary style="cursor:pointer;">Historico de cambios</summary>
                              <div style="display:grid; gap:10px; margin-top:8px;">
                                <?php foreach ($changelog as $entry): ?>
                                  <div style="padding:10px; border:1px solid var(--border-light); border-radius:8px;">
                                    <strong><?= h((string) ($entry['version'] ?? '')) ?></strong>
                                    <?php if (!empty($entry['date']) && strtotime((string) $entry['date']) !== false): ?>
                                      <span style="color:var(--text-muted); font-size:.85rem;"> · <?= h((string) date('d/m/Y H:i', strtotime((string) $entry['date']))) ?></span>
                                    <?php endif; ?>
                                    <ul style="margin:8px 0 0 0; padding-left:18px;">
                                      <?php foreach ((array) ($entry['changes'] ?? []) as $change): ?>
                                        <li><?= h((string) $change) ?></li>
                                      <?php endforeach; ?>
                                    </ul>
                                  </div>
                                <?php endforeach; ?>
                              </div>
                            </details>
                          <?php endif; ?>
                        </div>
                      <?php endif; ?>
                    </div>
                  </td>
                  <td data-sort="<?= h($verStore ?: $verCurrent ?: $verInst) ?>">
                    <?php if ($hasUpdate): ?>
                      <span class="badge" title="Hay una versión más nueva en el store">
                        <?= h($verCurrent ?: $verInst ?: '-') ?> -&gt;
                        <?= h($verStore) ?>
                      </span>
                    <?php else: ?>
                      <?= h($verStore ?: $verCurrent ?: $verInst ?: '-') ?>
                    <?php endif; ?>
                    <?php if ($verLocal !== '' && $verInst !== '' && $verLocal !== $verInst): ?>
                      <div style="margin-top:5px;color:var(--text-muted);font-size:.85rem;">
                        Fichero: <?= h($verLocal) ?> · BD: <?= h($verInst) ?>
                      </div>
                    <?php endif; ?>
                  </td>
                  <td data-sort="<?= h($publishedSort) ?>">
                    <?= h($publishedHuman) ?>
                  </td>
                  <td data-sort="<?= h($status) ?>">
                    <?= h($status) ?>
                  </td>
                  <td>
                    <div style="display:grid;gap:10px;">

                      <?php if (!$isInstalled || $hasUpdate): ?>
                        <form method="post" style="margin:0;display:grid;gap:8px;min-width:260px;">
                          <input type="hidden" name="csrf" value="<?= h($csrf) ?>">
                          <input type="hidden" name="action" value="<?= !$isInstalled ? 'install' : 'update' ?>">
                          <input type="hidden" name="plugin_id" value="<?= h($pid) ?>">
                          <label style="display:grid;gap:4px;">
                            <span style="font-size:.85rem;color:var(--text-muted);">Grupo</span>
                            <select name="group_name" class="form-control" required>
                              <?php foreach ($appGroupOptions as $groupName): ?>
                                <option value="<?= h($groupName) ?>" <?= $groupName === $selectedGroupName ? 'selected' : '' ?>>
                                  <?= h($groupName) ?>
                                </option>
                              <?php endforeach; ?>
                            </select>
                          </label>
                          <label style="display:grid;gap:4px;">
                            <span style="font-size:.85rem;color:var(--text-muted);">Roles</span>
                            <select name="roles_json[]" class="form-control" multiple required style="min-height:108px;">
                              <?php foreach ($roleOptions as $role): ?>
                                <option value="<?= h($role) ?>" <?= in_array($role, $selectedRoles, true) ? 'selected' : '' ?>>
                                  <?= h($role) ?>
                                </option>
                              <?php endforeach; ?>
                            </select>
                          </label>
                          <label style="display:grid;gap:4px;">
                            <span style="font-size:.85rem;color:var(--text-muted);">SQL</span>
                            <select name="sql_strategy" class="form-control">
                              <option value="auto">Auto con usuario de aplicacion</option>
                              <option value="admin">Auto con usuario administrador</option>
                              <option value="skip">Instalar sin tocar tablas</option>
                              <option value="show">Solo mostrar SQL</option>
                            </select>
                          </label>
                          <div style="display:grid;gap:6px;padding:10px;border:1px solid var(--border-light);border-radius:8px;background:var(--bg-input);">
                            <strong style="font-size:.9rem;">Credenciales admin MySQL</strong>
                            <div style="display:grid;gap:6px;grid-template-columns:repeat(2,minmax(90px,1fr));">
                              <input type="text" name="db_admin_host" class="form-control" placeholder="Host" value="<?= h(defined('DB_HOST') ? (string) DB_HOST : 'localhost') ?>">
                              <input type="number" name="db_admin_port" class="form-control" placeholder="Puerto" value="<?= h((string) (defined('DB_PORT') ? (int) DB_PORT : 3306)) ?>">
                              <input type="text" name="db_admin_user" class="form-control" placeholder="Usuario admin">
                              <input type="password" name="db_admin_pass" class="form-control" placeholder="Password admin">
                            </div>
                          </div>
                          <button class="btn primary" type="submit"><?= !$isInstalled ? 'Instalar' : 'Actualizar' ?></button>
                        </form>
                      <?php endif; ?>

                      <?php if ($isInstalled): ?>
                        <?php if (is_array($meta) && $localPluginDir !== null): ?>
                          <form method="post" style="margin:0;">
                            <input type="hidden" name="csrf" value="<?= h($csrf) ?>">
                            <input type="hidden" name="action" value="compare_store">
                            <input type="hidden" name="plugin_id" value="<?= h($pid) ?>">
                            <button class="btn" type="submit">Comparar con Store</button>
                          </form>
                        <?php endif; ?>

                        <form method="post" style="margin:0;">
                          <input type="hidden" name="csrf" value="<?= h($csrf) ?>">
                          <input type="hidden" name="action" value="toggle">
                          <input type="hidden" name="plugin_id" value="<?= h($pid) ?>">
                          <input type="hidden" name="enabled" value="<?= $enabled ? 0 : 1 ?>">
                          <button class="btn" type="submit">
                            <?= $enabled ? 'Desactivar' : 'Activar' ?>
                          </button>
                        </form>

                        <form method="post" style="margin:0;"
                          onsubmit="return confirm('¿Desinstalar el plugin <?= h($pid) ?>? (Se eliminará la carpeta del plugin)');">
                          <input type="hidden" name="csrf" value="<?= h($csrf) ?>">
                          <input type="hidden" name="action" value="uninstall">
                          <input type="hidden" name="plugin_id" value="<?= h($pid) ?>">
                          <button class="btn danger" type="submit">Desinstalar</button>
                        </form>
                      <?php endif; ?>

                    </div>
                  </td>
                </tr>
              <?php endforeach; ?>
            <?php endif; ?>
          </tbody>
        </table>
      </div>

      <p class="muted-help" style="margin-top:12px;">
        Nota: al instalar, el plugin se descarga como ZIP, se valida por SHA256 si el store lo proporciona y se
        descomprime en <code>/plugins/&lt;id&gt;</code>.
        Si el plugin incluye <code>install/install.sql</code>, <code>install/install.php</code> o
        <code>install/installer.php</code>, se ejecutan durante la instalación.
      </p>
    </section>
  </div>

  <script>
    (function () {
      const input = document.getElementById('tableFilter');
      const table = document.getElementById('pluginsTable');
      if (!input || !table) return;
      input.addEventListener('input', function () {
        const q = (this.value || '').toLowerCase().trim();
        const rows = table.querySelectorAll('tbody tr');
        rows.forEach(r => {
          const txt = (r.innerText || '').toLowerCase();
          r.style.display = (!q || txt.includes(q)) ? '' : 'none';
        });
      });
    })();
  </script>
</body>

</html>
