<%- include('top') %>

<div class="container mt-4">
  <div class="row">
    <div class="col-12">
      <div class="card shadow">
        <div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
          <div>
            <i class="fa-solid fa-plug"></i> Plugins
          </div>
          <div class="d-flex gap-2 align-items-center">
            <button class="btn btn-sm btn-primary shadow-sm" onclick="openNewPluginModal()">
              <i class="fas fa-plus"></i> New Plugin
            </button>
          </div>
        </div>
        <div class="p-3 pb-0 text-muted small border-bottom">
          <i class="fa-solid fa-circle-info"></i> Configured plugin instances. Each is a loadable, scheduled copy of a
          plugin type (e.g. Proxmox, UniFi, Nmap) — you can run several of the same type with different settings.
          Secrets are stored in OpenBao and shown masked. <a href="/docs/plugins">Learn more</a>.
        </div>
        <div class="table-responsive">
          <table class="card-body table table-hover mb-0 align-middle">
            <thead class="table-light">
              <tr>
                <th class="ps-3">Name</th>
                <th>Type</th>
                <th>Schedule</th>
                <th>State</th>
                <th>Last Run</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody id="plugins-list" jq-repeat="plugins">
              <tr id="plugin-row-{{id}}">
                <td class="ps-3">
                  <strong>{{name}}</strong>
                  <div class="small text-muted font-monospace">{{slug}}</div>
                </td>
                <td><span class="badge bg-secondary">{{pluginType}}</span></td>
                <td><code class="text-dark">{{cron}}</code></td>
                <td>
                  {{#enabled}}<span class="badge bg-success">Loaded</span>{{/enabled}}
                  {{^enabled}}<span class="badge bg-secondary">Unloaded</span>{{/enabled}}
                </td>
                <td class="small">
                  {{#lastRunAt}}<span title="{{lastRunAt}}">{{lastRunFmt}}</span>{{/lastRunAt}}
                  {{^lastRunAt}}<span class="text-muted">never</span>{{/lastRunAt}}
                  {{#lastStatus}}
                    {{#isOk}}<span class="badge bg-success-subtle text-success-emphasis ms-1">ok</span>{{/isOk}}
                    {{#isError}}<span class="badge bg-danger-subtle text-danger-emphasis ms-1" title="{{lastError}}">error</span>{{/isError}}
                    {{#isRunning}}<span class="badge bg-info-subtle text-info-emphasis ms-1">running</span>{{/isRunning}}
                  {{/lastStatus}}
                </td>
                <td>
                  <button class="btn btn-sm btn-primary" title="Edit" onclick="openEditModal('{{id}}')"><i class="fa-solid fa-pen"></i></button>
                  <button class="btn btn-sm btn-warning" title="Edit Secrets" onclick="openSecretsModal('{{id}}')"><i class="fa-solid fa-key"></i></button>
                  <button class="btn btn-sm btn-info" title="Test" onclick="testPlugin('{{id}}')"><i class="fa-solid fa-vial"></i></button>
                  <button class="btn btn-sm btn-success" title="Run now" onclick="runNow('{{id}}')"><i class="fa-solid fa-play"></i></button>
                  {{#lastRunAt}}<button class="btn btn-sm btn-secondary" title="View Logs" onclick="showLogs('{{id}}')"><i class="fa-solid fa-file-lines"></i></button>{{/lastRunAt}}
                  {{#enabled}}<button class="btn btn-sm btn-outline-danger" title="Unload" onclick="togglePlugin('{{id}}', false)">Unload</button>{{/enabled}}
                  {{^enabled}}<button class="btn btn-sm btn-outline-success" title="Load" onclick="togglePlugin('{{id}}', true)">Load</button>{{/enabled}}
                  <button class="btn btn-sm btn-outline-danger" title="Delete" onclick="deletePlugin('{{id}}')"><i class="fa-solid fa-trash"></i></button>
                </td>
              </tr>
            </tbody>
            <tbody id="plugins-empty-state" style="display: none;">
              <tr>
                <td colspan="6" class="text-center py-5 text-muted">
                  <i class="fa-solid fa-plug fs-2 mb-3 text-black-50"></i>
                  <h5>No plugin instances</h5>
                  <p>Click <strong>New Plugin</strong> to configure one.</p>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>
</div>

<script>
  app.auth.forceLogin(['app_sso_admin', 'app_sso_directory_admin', 'admin']);

  // type -> manifest (configSchema etc.), loaded once for the New-plugin form.
  var pluginTypes = {};
  // id -> instance (plain), kept current after each load so modals can resolve a row.
  var pluginsById = {};

  $(document).ready(function() {
    app.api.get('plugins/types', function(err, res) {
      if (err) { app.messages.toast('Error loading plugin types: ' + (err.message || err), 'danger'); return; }
      (res.results || []).forEach(function(t) { pluginTypes[t.type] = t; });
    });
    loadPlugins();
  });

  function fmtRun(ms) {
    if (!ms) return '';
    var d = new Date(Number(ms));
    return moment(d).fromNow();
  }

  function loadPlugins() {
    app.api.get('plugins', function(err, res) {
      if (err) { app.messages.toast('Error loading plugins: ' + (err.message || err), 'danger'); return; }
      var list = res.results || [];
      pluginsById = {};
      $.scope.plugins.empty();
      if (!list.length) {
        $('#plugins-list').hide();
        $('#plugins-empty-state').show();
      } else {
        list.forEach(function(p) {
          pluginsById[p.id] = p;
          p.lastRunFmt = fmtRun(p.lastRunAt);
          p.isOk = p.lastStatus === 'ok';
          p.isError = p.lastStatus === 'error';
          p.isRunning = p.lastStatus === 'running';
          $.scope.plugins.push(p);
        });
        $('#plugins-list').show();
        $('#plugins-empty-state').hide();
      }
    });
  }

  // Build an HTML form fragment for a type's configSchema. `prefix` namespaces
  // the field ids so the New and Edit modals don't collide. `values` (optional)
  // pre-fills fields (masked secrets stay masked; non-secret values are shown).
  // `includeSecrets` (default true) — the Edit (non-secret) modal passes false so
  // secret fields are never shown there (secrets have their own modal); the New
  // modal passes true so initial secrets can be set at create time.
  function configFormHtml(type, prefix, values, includeSecrets) {
    var schema = pluginTypes[type] && pluginTypes[type].configSchema;
    if (!schema || !schema.length) return '<p class="text-muted">No configuration fields for this plugin.</p>';
    if (includeSecrets === undefined) includeSecrets = true;
    var v = values || {};
    var html = '';
    schema.forEach(function(f) {
      if (!includeSecrets && f.secret) return;
      var val = v[f.key];
      if (f.secret) val = '';
      var label = f.label + (f.secret ? ' <span class="text-warning" title="stored in OpenBao"><i class="fa-solid fa-key"></i></span>' : '') + (f.required ? ' <span class="text-danger">*</span>' : '');

      if (f.type === 'boolean' || f.type === 'checkbox' || f.key === 'autoPromote') {
        var isChecked = val === true || val === 'true' || val === 1 || val === '1' || (val === undefined && f.default !== false);
        html += '<div class="mb-3 form-check">' +
          '<input type="checkbox" class="form-check-input" id="' + prefix + f.key + '"' + (isChecked ? ' checked' : '') + '>' +
          '<label class="form-check-label fw-bold" for="' + prefix + f.key + '">' + label + '</label>' +
        '</div>';
      } else if (f.type === 'site_select' || f.key === 'location') {
        var selectedVal = String(val || f.default || '').trim();
        var sites = window.availableSites || [];
        var siteOpts = '<option value="">(Default Site)</option>';
        sites.forEach(function(s) {
          var sel = (s.name === selectedVal || s.slug === selectedVal || (!selectedVal && s.slug === 'site-default')) ? ' selected' : '';
          siteOpts += '<option value="' + app.util.escapeHtml(s.name) + '"' + sel + '>' + app.util.escapeHtml(s.name) + ' (' + app.util.escapeHtml(s.slug) + ')</option>';
        });
        html += '<div class="mb-3">' +
          '<label class="form-label fw-bold">' + label + '</label>' +
          '<select class="form-select" id="' + prefix + f.key + '">' + siteOpts + '</select>' +
        '</div>';
      } else {
        if (val === undefined || val === null) val = '';
        var inputType = f.type === 'password' ? 'password' : (f.type === 'url' ? 'url' : 'text');
        var req = f.required ? ' required' : '';
        var ph = f.placeholder ? (' placeholder="' + f.placeholder + '"') : '';
        html += '<div class="mb-3">' +
          '<label class="form-label fw-bold">' + label + '</label>' +
          '<input type="' + inputType + '" class="form-control" id="' + prefix + f.key + '" value="' + String(val).replace(/"/g, '&quot;') + '"' + req + ph + '>';
        if (f.secret) html += '<div class="form-text">Leave blank to keep the current secret.</div>';
        html += '</div>';
      }
    });
    return html;
  }

  // ── Schedule picker (Hourly / Daily / Weekly / Custom) ───────────────────
  // The stored value is always a 5-field cron string. A `<select>` picks a
  // preset; "Custom" reveals the raw cron text input. `prefix` namespaces the
  // element ids (np-/ed-) so the two modals don't collide.
  var CRON_PRESETS = [
    { key: 'hourly', label: 'Hourly',           cron: '0 * * * *' },
    { key: 'daily',  label: 'Daily (midnight)',  cron: '0 0 * * *' },
    { key: 'weekly', label: 'Weekly (Sun)',      cron: '0 0 * * 0' },
    { key: 'custom', label: 'Custom…',          cron: null },
  ];
  function cronKeyFor(cron) {
    var m = CRON_PRESETS.filter(function(p){ return p.cron === cron; })[0];
    return m ? m.key : 'custom';
  }
  function cronSelectHtml(prefix, current) {
    current = current || '0 * * * *';
    var key = cronKeyFor(current);
    var opts = CRON_PRESETS.map(function(p){
      return '<option value="' + p.key + '"' + (p.key === key ? ' selected' : '') + '>' + p.label + '</option>';
    }).join('');
    var rawStyle = key === 'custom' ? '' : ' style="display:none"';
    var rawVal = key === 'custom' ? current : current;
    return '<select class="form-select" id="' + prefix + 'cron-select" onchange="onCronChange(\'' + prefix + '\')">' + opts + '</select>' +
      '<input type="text" class="form-control font-monospace mt-2" id="' + prefix + 'cron" value="' + rawVal + '"' + rawStyle + '>';
  }
  function onCronChange(prefix) {
    var sel = document.getElementById(prefix + 'cron-select');
    var raw = document.getElementById(prefix + 'cron');
    if (!sel || !raw) return;
    if (sel.value === 'custom') {
      raw.style.display = '';
    } else {
      raw.style.display = 'none';
      var preset = CRON_PRESETS.filter(function(p){ return p.key === sel.value; })[0];
      if (preset) raw.value = preset.cron;
    }
  }
  function cronFromForm(prefix) {
    var sel = document.getElementById(prefix + 'cron-select');
    if (sel && sel.value !== 'custom') {
      var preset = CRON_PRESETS.filter(function(p){ return p.key === sel.value; })[0];
      if (preset) return preset.cron;
    }
    var raw = document.getElementById(prefix + 'cron');
    return (raw && raw.value.trim()) || '0 * * * *';
  }

  // Collect a flat {field: value} object from the rendered config form.
  function collectConfig(type, prefix) {
    var schema = pluginTypes[type] && pluginTypes[type].configSchema;
    var out = {};
    if (!schema) return out;
    schema.forEach(function(f) {
      var el = document.getElementById(prefix + f.key);
      if (el) {
        if (f.type === 'boolean' || f.type === 'checkbox' || el.type === 'checkbox') {
          out[f.key] = el.checked;
        } else {
          out[f.key] = el.value;
        }
      }
    });
    return out;
  }

  function typeOptionsHtml(selected) {
    var opts = '<option value="">Select a plugin type…</option>';
    Object.keys(pluginTypes).sort().forEach(function(t) {
      opts += '<option value="' + t + '"' + (t === selected ? ' selected' : '') + '>' + pluginTypes[t].name + ' (' + t + ')</option>';
    });
    return opts;
  }

  // --- New Plugin modal ---
  function openNewPluginModal() {
    app.modal.open({
      title: 'New Plugin',
      bodyHtml:
        '<div class="actionMessage mb-3" style="display:none"></div>' +
        '<div class="mb-3"><label class="form-label">Plugin Type <span class="text-danger">*</span></label>' +
          '<select class="form-select" id="np-type" onchange="renderNewPluginFields()">' + typeOptionsHtml('') + '</select></div>' +
        '<div class="mb-3"><label class="form-label">Name <span class="text-danger">*</span></label>' +
          '<input type="text" class="form-control" id="np-name" placeholder="Proxmox — Home Lab"></div>' +
        '<div class="mb-3"><label class="form-label">Schedule</label>' +
          cronSelectHtml('np-', '0 * * * *') +
          '<div class="form-text">A slug is derived automatically from the name.</div></div>' +
        '<hr><h6>Configuration</h6><div id="np-config-fields"><p class="text-muted">Select a plugin type first.</p></div>',
      footer: { buttonsHtml: app.modal.footerButtons({ onSave: 'saveNewPlugin()', saveLabel: 'Create Plugin' }) }
    });
  }

  function renderNewPluginFields() {
    var type = document.getElementById('np-type').value;
    document.getElementById('np-config-fields').innerHTML = configFormHtml(type, 'np-');
  }

  async function saveNewPlugin() {
    var type = document.getElementById('np-type').value;
    if (!type) return app.messages.action('Select a plugin type.', app.modal.body(), 'danger');
    var name = document.getElementById('np-name').value.trim();
    var cron = cronFromForm('np-');
    if (!name) return app.messages.action('Name is required.', app.modal.body(), 'danger');
    var config = collectConfig(type, 'np-');
    try {
      await app.api.post('plugins', { pluginType: type, name: name, cron: cron, config: config });
      app.modal.close();
      app.messages.toast('Plugin created and scheduled.', 'success');
      loadPlugins();
    } catch (err) {
      app.messages.action(err.message || 'Failed to create plugin', app.modal.body(), 'danger');
    }
  }

  // --- Edit (non-secret) modal ---
  function openEditModal(id) {
    var p = pluginsById[id];
    if (!p) return;
    app.modal.open({
      title: 'Edit — ' + p.name,
      bodyHtml:
        '<div class="actionMessage mb-3" style="display:none"></div>' +
        '<div class="mb-3"><label class="form-label">Name <span class="text-danger">*</span></label>' +
          '<input type="text" class="form-control" id="ed-name" value="' + String(p.name).replace(/"/g, '&quot;') + '"></div>' +
        '<div class="mb-3"><label class="form-label">Slug (read-only)</label>' +
          '<input type="text" class="form-control font-monospace" id="ed-slug" value="' + p.slug + '" readonly></div>' +
        '<div class="mb-3"><label class="form-label">Schedule</label>' +
          cronSelectHtml('ed-', p.cron || '0 * * * *') + '</div>' +
        '<hr><h6>Configuration</h6><div id="ed-config-fields">' + configFormHtml(p.pluginType, 'ed-', p.config, false) + '</div>' +
        '<div class="form-text">Secret fields are edited separately with the <i class="fa-solid fa-key"></i> button.</div>',
      footer: {
        metaHtml: app.modal.formatAudit ? app.modal.formatAudit(p, { formatDate: function(ms){ return moment(ms).format('YYYY-MM-DD HH:mm'); } }) : '',
        buttonsHtml: app.modal.footerButtons({ onSave: 'saveEdit(\'' + id + '\')', saveLabel: 'Save' })
      }
    });
  }

  async function saveEdit(id) {
    var p = pluginsById[id];
    if (!p) return;
    var name = document.getElementById('ed-name').value.trim();
    var cron = cronFromForm('ed-');
    if (!name) return app.messages.action('Name is required.', app.modal.body(), 'danger');
    var config = collectConfig(p.pluginType, 'ed-');
    try {
      await app.api.put('plugins/' + id, { name: name, cron: cron, config: config });
      app.modal.close();
      app.messages.toast('Plugin saved.', 'success');
      loadPlugins();
    } catch (err) {
      app.messages.action(err.message || 'Failed to save', app.modal.body(), 'danger');
    }
  }

  // --- Edit Secrets modal ---
  function openSecretsModal(id) {
    var p = pluginsById[id];
    if (!p) return;
    var masked = p.secrets || {};
    // Render only the secret fields, prefilled with the masked values.
    var schema = (pluginTypes[p.pluginType] && pluginTypes[p.pluginType].configSchema) || [];
    var secretFields = schema.filter(function(f) { return f.secret; });
    var html = '<div class="actionMessage mb-3" style="display:none"></div>' +
      '<p class="text-muted small">Stored in OpenBao. Leave a field blank to keep its current value.</p>';
    if (!secretFields.length) {
      html += '<p class="text-muted">This plugin has no secret fields.</p>';
    } else {
      secretFields.forEach(function(f) {
        var val = masked[f.key] || '';
        html += '<div class="mb-3"><label class="form-label">' + f.label + '</label>' +
          '<input type="password" class="form-control" id="sec-' + f.key + '" value="' + String(val).replace(/"/g, '&quot;') + '" placeholder="' + (val ? '******** (unchanged)' : 'new value') + '"></div>';
      });
    }
    app.modal.open({
      title: 'Edit Secrets — ' + p.name,
      bodyHtml: html,
      footer: { buttonsHtml: app.modal.footerButtons({ onSave: 'saveSecrets(\'' + id + '\')', saveLabel: 'Save Secrets' }) }
    });
  }

  async function saveSecrets(id) {
    var p = pluginsById[id];
    if (!p) return;
    var schema = pluginTypes[p.pluginType] && pluginTypes[p.pluginType].configSchema;
    var secrets = {};
    if (schema) {
      schema.forEach(function(f) {
        if (!f.secret) return;
        var el = document.getElementById('sec-' + f.key);
        if (el) secrets[f.key] = el.value;
      });
    }
    try {
      await app.api.put('plugins/' + id + '/secrets', secrets);
      app.modal.close();
      app.messages.toast('Secrets saved.', 'success');
      loadPlugins();
    } catch (err) {
      app.messages.action(err.message || 'Failed to save secrets', app.modal.body(), 'danger');
    }
  }

  async function testPlugin(id) {
    try {
      var res = await app.api.post('plugins/' + id + '/test', {});
      app.messages.toast('Test passed.', 'success');
    } catch (err) {
      app.messages.toast('Test failed: ' + (err.message || 'validation failed'), 'danger');
    }
  }

  async function runNow(id) {
    try {
      await app.api.post('plugins/' + id + '/run', {});
      app.messages.toast('Run enqueued. Refresh shortly for status.', 'info');
      setTimeout(loadPlugins, 3000);
    } catch (err) {
      app.messages.toast('Failed to run: ' + (err.message || err), 'danger');
    }
  }

  async function showLogs(id) {
    var p = pluginsById[id];
    if (!p) return;
    try {
      const res = await app.api.get('plugins/' + id + '/runs');
      const logText = (res.results && res.results.lastLog) || (res.results && res.results.lastError) || 'No logs available.';
      app.modal.open({
        title: 'Logs — ' + p.name,
        bodyHtml: '<pre class="bg-dark text-white p-3 rounded" style="white-space: pre-wrap; font-size: 0.85em;">' + String(logText).replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</pre>',
        footer: { buttonsHtml: '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' }
      });
    } catch (err) {
      app.messages.toast('Failed to load logs: ' + (err.message || err), 'danger');
    }
  }

  async function togglePlugin(id, enable) {
    try {
      await app.api.post('plugins/' + id + (enable ? '/load' : '/unload'), {});
      app.messages.toast(enable ? 'Plugin loaded.' : 'Plugin unloaded.', 'success');
      loadPlugins();
    } catch (err) {
      app.messages.toast('Failed: ' + (err.message || err), 'danger');
    }
  }

  async function deletePlugin(id) {
    var p = pluginsById[id];
    if (!p) return;
    var ok = await app.messages.confirm('Delete plugin "' + p.name + '"? Its schedule and OpenBao secrets will be removed.', app.modal.body ? app.modal.body() : null, 'danger');
    if (!ok) return;
    try {
      await app.api.delete('plugins/' + id);
      app.messages.toast('Plugin deleted.', 'success');
      loadPlugins();
    } catch (err) {
      app.messages.toast('Failed to delete: ' + (err.message || err), 'danger');
    }
  }
</script>

<%- include('bottom') %>