<template>
  <div class="shell" :class="[`theme-${activeTheme}`, { 'nav-collapsed': navCollapsed }]">
    <ParticleBackdrop />

    <LoginView
      v-if="!token"
      :login-form="loginForm"
      :error="error"
      :show-password="showPassword"
      :entra-enabled="entraLoginEnabled"
      @login="login"
      @update:show-password="showPassword = $event"
    />

    <template v-else>
      <AppSidebar
        :nav="nav"
        :user="user"
        :view="view"
        :collapsed="navCollapsed"
        @update:view="view = $event"
        @toggle-collapse="toggleNav"
      />

      <main class="workspace">
        <AppTopbar :active-title="activeTitle" :user="user" @refresh="refreshAll" @logout="logout" @open-profile="view = 'profile'" @open-help="openHelpCenter" />

        <DashboardView
          v-if="view === 'dashboard'"
          :day-part="dayPart"
          :first-name="firstName"
          :dashboard-edit-mode="dashboardEditMode"
          :dashboard-widget-library-open="dashboardWidgetLibraryOpen"
          :dragged-dashboard-widget-id="draggedDashboardWidgetId"
          :visible-dashboard-widgets="visibleDashboardWidgets"
          :hidden-dashboard-widgets="hiddenDashboardWidgets"
          :execution-trend="executionTrend"
          :jobs="jobs"
          :health-gauge="healthGauge"
          :job-status-stats="jobStatusStats"
          :dashboard-settings-preview="dashboardSettingsPreview"
          :host-transport-stats="hostTransportStats"
          :script-visibility-stats="scriptVisibilityStats"
          :run-plan-target-stats="runPlanTargetStats"
          :setting-label="settingLabel"
          @toggle-edit="dashboardEditMode = !dashboardEditMode"
          @open-library="dashboardWidgetLibraryOpen = true"
          @close-library="dashboardWidgetLibraryOpen = false"
          @reset-layout="resetDashboardLayout"
          @drag-start="dragDashboardStart"
          @drop-on="dropDashboardOn"
          @toggle-collapse="toggleWidgetCollapse"
          @resize="resizeDashboardWidget"
          @set-visible="setDashboardWidgetVisible"
          @open-job="openJob"
          @navigate="view = $event"
          @quick-new-script="view = 'scripts'; newScript(activeFolderId)"
        />

        <section v-if="view === 'scripts'" class="dashboard-workspace template-grid-workspace">
          <div class="page-header compact-header">
            <div>
              <span class="eyebrow">SCRIPT TEMPLATE WORKSPACE</span>
              <h2>PowerShell scripts</h2>
              <p>Versioned script blueprints for repeatable, governed remote execution.</p>
            </div>
            <div class="page-actions">
              <button class="ghost-button compact" type="button" @click="scriptEditMode = !scriptEditMode">
                <SlidersHorizontal :size="16" />{{ scriptEditMode ? 'Done' : 'Customize' }}
              </button>
              <button v-if="!isScriptPanelVisible('library')" class="ghost-button compact" type="button" @click="setScriptPanelVisible('library', true)">
                <Folder :size="16" />Open Script Library
              </button>
              <button v-if="!isScriptPanelVisible('inspector')" class="ghost-button compact" type="button" @click="setScriptPanelVisible('inspector', true)">
                <ShieldCheck :size="16" />Open Properties
              </button>
              <button class="ghost-button compact" type="button" @click="openVariableStudio">
                <Sparkles :size="16" />Variables
              </button>
              <button class="primary-action compact" type="button" @click="scriptPanelLibraryOpen = true">
                <ListPlus :size="16" />Panel library
              </button>
              <button v-if="scriptEditMode" class="ghost-button compact" type="button" @click="resetScriptLayout">
                <RotateCcw :size="14" />Reset
              </button>
            </div>
          </div>

          <div
            :class="['script-template-workspace glass-panel', {
              'without-library': !isScriptPanelVisible('library'),
              'without-inspector': !isScriptPanelVisible('inspector')
            }]"
          >
            <ScriptExplorer
              v-if="isScriptPanelVisible('library')"
              :folders="folders"
              :scripts="scripts"
              :selected-script-id="selectedScript?.id"
              :active-folder-id="activeFolderId"
              @select-script="selectScript"
              @select-folder="activeFolderId = $event"
              @create-folder="createFolder"
              @close="setScriptPanelVisible('library', false)"
              @context-menu="openContext($event.event, $event.item, $event.type)"
            />

            <main class="script-editor-pane">
              <header class="script-editor-header">
                <div>
                  <FileCode2 :size="16" />
                  <strong>{{ selectedScript?.name || scriptDraft.name || 'Untitled Script.ps1' }}</strong>
                  <span>PowerShell</span>
                  <span>{{ scriptDraft.visibility }}</span>
                </div>
                <div>
                  <button class="ghost-button compact" type="button" @click="openVariableStudio"><Sparkles :size="15" />Variables</button>
                  <button class="ghost-button compact" type="button" @click="newScript(activeFolderId)"><FilePlus2 :size="15" />New</button>
                  <button class="primary-action compact" type="button" @click="saveScript"><Save :size="15" />Save script</button>
                </div>
              </header>
              <div class="script-description-bar">
                <input v-model="scriptDraft.name" placeholder="Inventory Health Check.ps1" />
                <textarea v-model="scriptDraft.description" placeholder="Description"></textarea>
              </div>
              <div :ref="setEditorMount" class="monaco-host"></div>
              <footer class="script-editor-status">
                <span><i /> PowerShell IntelliSense</span>
                <span>UTF-8</span>
                <span>PowerShell</span>
                <span>{{ scriptDraft.content?.length || 0 }} chars</span>
              </footer>
            </main>

            <aside v-if="isScriptPanelVisible('inspector')" class="script-template-inspector">
              <header>
                <strong>Details</strong>
                <button title="Close Properties" type="button" @click="setScriptPanelVisible('inspector', false)"><X :size="14" /></button>
              </header>
              <label>
                <span>Name</span>
                <input v-model="scriptDraft.name" />
              </label>
              <label>
                <span>Description</span>
                <textarea v-model="scriptDraft.description" rows="4" />
              </label>
              <label>
                <span>Visibility</span>
                <select v-model="scriptDraft.visibility">
                  <option value="personal">Personal</option>
                  <option value="shared">Shared</option>
                  <option value="group">Group</option>
                </select>
              </label>
              <label v-if="scriptDraft.visibility === 'group'">
                <span>Group</span>
                <select v-model="scriptDraft.groupId">
                  <option :value="null">Choose group</option>
                  <option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option>
                </select>
              </label>
              <div class="inspector-section">
                <span>LIBRARY STATS</span>
                <div class="script-detail-row"><strong>{{ scripts.length }}</strong><small>Scripts</small></div>
                <div class="script-detail-row"><strong>{{ folders.length }}</strong><small>Folders</small></div>
                <div class="script-detail-row"><strong>{{ scriptDraft.content?.split('\n').length || 0 }}</strong><small>Lines</small></div>
              </div>
              <div class="inspector-section">
                <span>ACTIONS</span>
                <button class="primary-action compact full-width" type="button" @click="saveScript"><Save :size="15" />Save script</button>
              <button class="ghost-button compact full-width" type="button" @click="view = 'runplans'"><Rocket :size="15" />Use in RunPlan</button>
                <button v-if="selectedScript" class="ghost-button compact full-width" type="button" @click="openScriptTestRun(selectedScript)"><Play :size="15" />Test / Run</button>
                <button v-if="selectedScript" class="ghost-button compact danger-text full-width" type="button" @click="deleteScript(selectedScript)"><Trash2 :size="15" />Delete selected</button>
              </div>
            </aside>
          </div>

          <Teleport to="body">
            <div v-if="scriptPanelLibraryOpen" class="modal-backdrop" @mousedown.self="scriptPanelLibraryOpen = false">
              <section class="widget-library glass-panel">
                <header>
                  <div>
                    <span class="eyebrow">PANEL LIBRARY</span>
                    <h3>Customize script authoring</h3>
                    <p>Add hidden panels back to the script workspace, then drag and resize them on the grid.</p>
                  </div>
                  <button class="modal-close" type="button" @click="scriptPanelLibraryOpen = false">x</button>
                </header>
                <div class="widget-library-grid">
                  <article v-for="panel in hiddenScriptPanels" :key="panel.id">
                    <span><component :is="panel.icon" :size="20" /></span>
                    <div><strong>{{ panel.title }}</strong><small>{{ panel.category }} - {{ panel.description }}</small></div>
                    <button class="primary-action compact" type="button" @click="setScriptPanelVisible(panel.id, true)">Add</button>
                  </article>
                  <p v-if="!hiddenScriptPanels.length" class="widget-empty">All script panels are already visible.</p>
                </div>
              </section>
            </div>
          </Teleport>

          <VariableEditorModal
            :open="variableModalOpen"
            :catalog="variableCatalog"
            :groups="groups"
            @close="variableModalOpen = false"
            @insert="insertVariable"
            @save-custom="saveCustomVariable"
            @delete-custom="deleteCustomVariable"
          />

          <ScriptTestRunModal
            :open="scriptTestModalOpen"
            :script="scriptTestTarget"
            :hosts="hosts"
            :host-groups="hostGroups"
            :credentials="credentials"
            :job="scriptTestJob"
            :running="scriptTestRunning"
            :error="scriptTestError"
            @close="closeScriptTestRun"
            @execute="executeScriptTestRun"
          />
        </section>

        <AssetsView
          v-if="view === 'assets'"
          :folders="assetFolders"
          :assets="assets"
          :groups="groups"
          :scripts="scripts"
          :runplans="runplans"
          :psadt-profiles="psadtProfiles"
          :intune-deployments="psadtIntuneDeployments"
          :token="token"
          @refresh="refreshAssets"
          @save-folder="saveAssetFolder"
          @delete-folder="deleteAssetFolder"
          @upload-asset="uploadAsset"
          @delete-asset="deleteAsset"
          @link-asset="linkAsset"
          @insert-reference="insertAssetReference"
        />

        <PsadtView
          v-if="view === 'psadt'"
          :catalog="psadtCatalog"
          :profiles="psadtProfiles"
          :scripts="scripts"
          :intune-deployments="psadtIntuneDeployments"
          :graph-connections="graphConnections"
          :graph-status-result="graphStatusResult"
          :graph-audit-result="graphAuditResult"
          :drift-result="driftResult"
          :builder-status="builderStatus"
          :installer-analysis="installerAnalysis"
          :assets="assets"
          :applications="catalogApplications"
          :change-requests="changeRequests"
          :reporting-overview="reportingOverview"
          :validation-result="psadtValidationResult"
          :migration-plan-result="psadtMigrationPlan"
          :groups="groups"
          @refresh="refreshPsadt"
          @save-profile="savePsadtProfile"
          @delete-profile="deletePsadtProfile"
          @render-profile="previewPsadtProfile"
          @create-script="createScriptFromPsadtProfile"
          @insert-snippet="insertPsadtSnippet"
          @validate-psadt="validatePsadtTarget"
          @plan-migration="planPsadtMigration"
          @apply-migration="applyPsadtMigration"
          @save-intune-deployment="saveIntuneDeployment"
          @delete-intune-deployment="deleteIntuneDeployment"
          @save-graph-connection="saveGraphConnection"
          @delete-graph-connection="deleteGraphConnection"
          @test-graph-connection="testGraphConnection"
          @link-graph-app="linkGraphApp"
          @sync-graph-status="syncGraphStatus"
          @load-graph-status="loadGraphStatus"
          @load-graph-audit="loadGraphAudit"
          @publish-graph-app="publishGraphApp"
          @upload-graph-content="uploadGraphContent"
          @assign-graph-app="assignGraphApp"
          @set-graph-relationships="setGraphRelationships"
          @check-drift="checkDrift"
          @reconcile-graph-app="reconcileGraphApp"
          @new-version="createDeploymentVersion"
          @build-intunewin="buildIntunewin"
          @save-application="saveApplication"
          @delete-application="deleteApplication"
          @check-application-update="checkApplicationUpdate"
          @import-application="importApplication"
          @refresh-governance="refreshGovernance"
          @approve-change-request="approveChangeRequest"
          @reject-change-request="rejectChangeRequest"
          @analyze-installer="analyzeInstaller"
        />

        <section v-if="view === 'hosts'" class="resource-page">
          <div class="page-header compact-header">
            <div>
              <span class="eyebrow">HOST LIBRARY</span>
              <h2>Managed systems</h2>
              <p>Search, sort, and attach credentials to the hosts used by RunPlans.</p>
            </div>
            <div class="page-actions">
              <button class="ghost-button compact" type="button" @click="openVCenterImport">
                <Download :size="16" />Import from vCenter
              </button>
              <button class="primary-action compact" type="button" @click="openHostModal()"><Plus :size="16" />Add host</button>
            </div>
          </div>
          <ResourceTable
            :columns="hostColumns"
            :rows="pagedHosts"
            :total="filteredHosts.length"
            :search="hostFilters.search"
            :page="hostFilters.page"
            :page-count="hostPageCount"
            :page-size="hostFilters.pageSize"
            :sort="hostFilters.sort"
            :direction="hostFilters.direction"
            search-placeholder="Search hosts, addresses, tags..."
            item-label="hosts"
            empty-text="No hosts match the current search."
            @update:search="hostFilters.search = $event"
            @update:page="hostFilters.page = $event"
            @update:page-size="hostFilters.pageSize = $event"
            @sort="setSort(hostFilters, $event)"
          >
            <template #cell-name="{ row }"><strong>{{ row.name }}</strong></template>
            <template #cell-address="{ row }"><code>{{ row.address }}</code></template>
            <template #cell-transport="{ row }"><span :class="['status-pill', row.transport]">{{ row.transport }}</span></template>
            <template #cell-sourceType="{ row }"><span :class="['status-pill', row.sourceType]">{{ row.sourceType === 'vmware' ? 'VMware' : 'Manual' }}</span></template>
            <template #cell-credentialName="{ row }">{{ row.credentialName || 'No credential' }}</template>
            <template #cell-tags="{ row }"><span class="tag-row">{{ row.tags?.join(', ') || '-' }}</span></template>
            <template #cell-actions="{ row }"><button class="ghost-button compact" type="button" @click="openHostModal(row)">Edit</button></template>
          </ResourceTable>

          <HostGroupsPanel
            :host-groups="hostGroups"
            :hosts="hosts"
            :groups="groups"
            @save="saveHostGroup"
            @delete="deleteHostGroup"
            @sync="syncHostGroup"
            @sync-all="syncHostGroups"
          />
        </section>

        <section v-if="view === 'credentials'" class="resource-page">
          <div class="page-header compact-header">
            <div>
              <span class="eyebrow">CREDENTIAL VAULT</span>
              <h2>Encrypted credentials</h2>
              <p>Store reusable usernames, passwords, and API keys for host authentication.</p>
            </div>
            <div class="page-actions">
              <button class="primary-action compact" type="button" @click="openCredentialModal()"><ShieldCheck :size="16" />Add credential</button>
            </div>
          </div>
          <ResourceTable
            :columns="credentialColumns"
            :rows="pagedCredentials"
            :total="filteredCredentials.length"
            :search="credentialFilters.search"
            :page="credentialFilters.page"
            :page-count="credentialPageCount"
            :page-size="credentialFilters.pageSize"
            :sort="credentialFilters.sort"
            :direction="credentialFilters.direction"
            search-placeholder="Search credentials..."
            item-label="credentials"
            empty-text="No credentials match the current search."
            @update:search="credentialFilters.search = $event"
            @update:page="credentialFilters.page = $event"
            @update:page-size="credentialFilters.pageSize = $event"
            @sort="setSort(credentialFilters, $event)"
          >
            <template #cell-name="{ row }"><strong>{{ row.name }}</strong></template>
            <template #cell-username="{ row }"><code>{{ row.username || '-' }}</code></template>
            <template #cell-visibility="{ row }"><span :class="['status-pill', row.visibility]">{{ row.visibility }}</span></template>
            <template #cell-status><span class="vault-state"><KeyRound :size="14" />Encrypted</span></template>
            <template #cell-actions="{ row }"><button class="ghost-button compact" type="button" @click="openCredentialModal(row)">Edit</button></template>
          </ResourceTable>
        </section>

        <section v-if="view === 'runplans'" class="resource-page">
          <div class="page-header compact-header">
            <div>
              <span class="eyebrow">RUNPLAN LIBRARY</span>
              <h2>Execution plans</h2>
              <p>Pair scripts with deliberate host sets, then launch execution through the API.</p>
            </div>
            <div class="page-actions">
              <button class="primary-action compact" type="button" @click="openRunPlanModal()"><Plus :size="16" />Add RunPlan</button>
            </div>
          </div>
          <ResourceTable
            :columns="runPlanColumns"
            :rows="pagedRunPlans"
            :total="filteredRunPlans.length"
            :search="runPlanFilters.search"
            :page="runPlanFilters.page"
            :page-count="runPlanPageCount"
            :page-size="runPlanFilters.pageSize"
            :sort="runPlanFilters.sort"
            :direction="runPlanFilters.direction"
            search-placeholder="Search RunPlans, scripts, visibility..."
            item-label="RunPlans"
            empty-text="No RunPlans match the current search."
            @update:search="runPlanFilters.search = $event"
            @update:page="runPlanFilters.page = $event"
            @update:page-size="runPlanFilters.pageSize = $event"
            @sort="setSort(runPlanFilters, $event)"
          >
            <template #cell-name="{ row }"><strong>{{ row.name }}</strong></template>
            <template #cell-scriptName="{ row }">{{ row.scriptName || 'No script selected' }}</template>
            <template #cell-visibility="{ row }"><span :class="['status-pill', row.visibility]">{{ row.visibility }}</span></template>
            <template #cell-hostCount="{ row }">{{ row.hostCount || row.hostIds?.length || 0 }} host(s)<span v-if="row.hostGroupCount"> · {{ row.hostGroupCount }} group(s)</span></template>
            <template #cell-mode="{ row }">{{ row.parallel ? 'Parallel' : 'Serial' }}</template>
            <template #cell-actions="{ row }">
              <div class="table-actions">
                <button class="ghost-button compact" type="button" @click="openRunPlanModal(row)">Edit</button>
                <button class="primary-action compact" type="button" @click="executeRunPlan(row.id)"><Play :size="14" />Run</button>
              </div>
            </template>
          </ResourceTable>
        </section>

        <VCenterImportModal
          :open="vcenterImportOpen"
          :preview-rows="vcenterPreviewRows"
          :credentials="credentials"
          :groups="groups"
          :connections="vcenterConnections"
          :loading="vcenterImportLoading"
          :error="vcenterImportError"
          :status="vcenterImportStatus"
          :diagnostics="vcenterImportDiagnostics"
          :trace-count="vcenterTraceEntries.length"
          @close="vcenterImportOpen = false"
          @preview="previewVCenterImport"
          @import="importVCenterHosts"
          @open-trace="vcenterTraceOpen = true"
        />

        <VCenterTraceModal
          :open="vcenterTraceOpen"
          :trace="vcenterTraceEntries"
          @close="vcenterTraceOpen = false"
        />

        <BaseModal
          :open="hostModalOpen"
          :title="hostForm.id ? 'Update host' : 'Add host'"
          eyebrow="HOST LIBRARY"
          description="Define a target system once, attach its credential, and reuse it across RunPlans."
          @close="hostModalOpen = false"
        >
              <form @submit.prevent="saveHost" class="form-grid">
                <label><span>Display name</span><input v-model="hostForm.name" required placeholder="SQL-PROD-01" /></label>
                <label><span>Address / FQDN</span><input v-model="hostForm.address" required placeholder="sql-prod-01.contoso.local" /></label>
                <label><span>OS family</span><select v-model="hostForm.osFamily"><option value="windows">Windows</option><option value="linux">Linux</option><option value="network">Network</option><option value="api">API</option></select></label>
                <label><span>Transport</span><select v-model="hostForm.transport"><option value="winrm">WinRM</option><option value="ssh">SSH</option><option value="local">Local</option><option value="api">API</option></select></label>
                <label class="full"><span>Credential</span><select v-model="hostForm.credentialId"><option :value="null">No credential</option><option v-for="credential in credentials" :key="credential.id" :value="credential.id">{{ credential.name }}</option></select></label>
                <label class="full"><span>Tags</span><input v-model="hostForm.tagsText" placeholder="production, database, east" /></label>
                <label><span>Visibility</span><select v-model="hostForm.visibility"><option value="personal">Personal</option><option value="shared">Shared</option><option value="group">Group</option></select></label>
                <label v-if="hostForm.visibility === 'group'"><span>Group</span><select v-model="hostForm.groupId"><option :value="null">Choose group</option><option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option></select></label>
                <div class="form-actions modal-actions full">
                  <button class="ghost-button" type="button" @click="hostModalOpen = false">Cancel</button>
                  <button class="primary-action" type="submit"><Save :size="17" />{{ hostForm.id ? 'Update host' : 'Create host' }}</button>
                </div>
              </form>
        </BaseModal>

        <BaseModal
          :open="credentialModalOpen"
          :title="credentialForm.id ? 'Update credential' : 'Add credential'"
          eyebrow="CREDENTIAL VAULT"
          description="Secrets are encrypted at rest and never returned by the API after storage."
          @close="credentialModalOpen = false"
        >
              <form @submit.prevent="saveCredential" class="form-grid">
                <label><span>Name</span><input v-model="credentialForm.name" required placeholder="Domain automation account" /></label>
                <label><span>Secret type</span><select v-model="credentialForm.kind"><option value="username_password">Username + password</option><option value="api_key">API key</option></select></label>
                <label><span>Username / key label</span><input v-model="credentialForm.username" placeholder="svc-poshmanager" /></label>
                <label><span>{{ credentialForm.id ? 'Replace secret' : 'Secret' }}</span><input v-model="credentialForm.secret" :required="!credentialForm.id" type="password" placeholder="Stored encrypted after save" /></label>
                <label><span>Visibility</span><select v-model="credentialForm.visibility"><option value="personal">Personal</option><option value="shared">Shared</option><option value="group">Group</option></select></label>
                <label v-if="credentialForm.visibility === 'group'"><span>Group</span><select v-model="credentialForm.groupId"><option :value="null">Choose group</option><option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option></select></label>
                <div class="form-actions modal-actions full">
                  <button class="ghost-button" type="button" @click="credentialModalOpen = false">Cancel</button>
                  <button class="primary-action" type="submit"><ShieldCheck :size="17" />{{ credentialForm.id ? 'Update credential' : 'Encrypt and store' }}</button>
                </div>
              </form>
        </BaseModal>

        <BaseModal
          :open="runPlanModalOpen"
          :title="runPlanForm.id ? 'Update RunPlan' : 'Add RunPlan'"
          eyebrow="RUNPLAN COMPOSER"
          description="Pair one script with one or more hosts. Execution and logging remain backend API workflows."
          wide
          @close="runPlanModalOpen = false"
        >
              <form @submit.prevent="saveRunPlan" class="form-grid">
                <label><span>RunPlan name</span><input v-model="runPlanForm.name" required placeholder="Patch validation sweep" /></label>
                <label><span>Script</span><select v-model="runPlanForm.scriptId" required><option disabled value="">Choose script</option><option v-for="script in scripts" :key="script.id" :value="script.id">{{ script.name }}</option></select></label>
                <label><span>Visibility</span><select v-model="runPlanForm.visibility"><option value="personal">Personal</option><option value="shared">Shared</option><option value="group">Group</option></select></label>
                <label v-if="runPlanForm.visibility === 'group'"><span>Group</span><select v-model="runPlanForm.groupId"><option :value="null">Choose group</option><option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option></select></label>
                <label class="toggle modal-toggle"><input v-model="runPlanForm.parallel" type="checkbox" /><span><strong>Parallel execution</strong><small>Run against selected hosts concurrently.</small></span></label>
                <label class="full"><span>Description</span><textarea v-model="runPlanForm.description" rows="3" placeholder="Purpose, operator notes, approval context"></textarea></label>
                <div class="full modal-host-picker">
                  <div class="list-header"><span>Target host groups</span><strong>{{ runPlanForm.hostGroupIds.length }} selected</strong></div>
                  <div class="host-picker">
                    <label v-for="hostGroup in hostGroups" :key="hostGroup.id">
                      <input v-model="runPlanForm.hostGroupIds" type="checkbox" :value="hostGroup.id" />
                      <span><strong>{{ hostGroup.name }}</strong><small>{{ hostGroup.mode }} · {{ hostGroup.memberCount || 0 }} host(s)</small></span>
                    </label>
                  </div>
                </div>
                <div class="full modal-host-picker">
                  <div class="list-header"><span>Target hosts</span><strong>{{ runPlanForm.hostIds.length }} selected</strong></div>
                  <div class="host-picker">
                    <label v-for="host in hosts" :key="host.id">
                      <input v-model="runPlanForm.hostIds" type="checkbox" :value="host.id" />
                      <span><strong>{{ host.name }}</strong><small>{{ host.transport }} · {{ host.address }}</small></span>
                    </label>
                  </div>
                </div>
                <div class="form-actions modal-actions full">
                  <button class="ghost-button" type="button" @click="runPlanModalOpen = false">Cancel</button>
                  <button class="primary-action" type="submit"><Save :size="17" />{{ runPlanForm.id ? 'Update RunPlan' : 'Create RunPlan' }}</button>
                </div>
              </form>
        </BaseModal>

        <section v-if="view === 'logs'" class="logs-layout">
          <article :class="['glass-panel job-list', { 'panel-collapsed': panelCollapsed.jobsList }]">
            <div class="section-title"><h3>Jobs</h3><div class="section-actions"><ListFilter :size="18" /><button class="panel-toggle" type="button" @click="togglePanel('jobsList')"><component :is="panelCollapsed.jobsList ? ChevronDown : ChevronUp" :size="14" /></button></div></div>
            <button v-for="job in jobs" :key="job.id" :class="{ active: selectedJob?.id === job.id }" @click="openJob(job.id)">
              <span :class="['status-dot', job.status]"></span>
              <span><strong>{{ job.runplanName || job.id }}</strong><small>{{ job.status }} · {{ shortDate(job.createdAt) }}</small></span>
            </button>
          </article>
          <article :class="['glass-panel log-viewer', { 'panel-collapsed': panelCollapsed.logViewer }]">
            <div class="section-title log-viewer-title">
              <h3>Log Viewer</h3>
              <div class="section-actions">
                <Download :size="18" />
                <button class="panel-toggle" type="button" @click="togglePanel('logViewer')"><component :is="panelCollapsed.logViewer ? ChevronDown : ChevronUp" :size="14" /></button>
              </div>
            </div>
            <div class="tabs log-tabs">
              <button :class="{ active: logTab === 'job' }" @click="logTab = 'job'"><PlayCircle :size="15" />Job Output</button>
              <button :class="{ active: logTab === 'system' }" @click="loadSystemLogs"><Activity :size="15" />System Logs</button>
            </div>
            <div class="resource-toolbar log-toolbar">
              <label class="search-control">
                <Search :size="15" />
                <input v-model="logFilters.search" placeholder="Search source, stream, message..." />
              </label>
              <select v-model="logFilters.stream">
                <option value="">All streams</option>
                <option value="stdout">stdout</option>
                <option value="stderr">stderr</option>
                <option value="system">system</option>
                <option value="info">info</option>
                <option value="warn">warn</option>
                <option value="error">error</option>
              </select>
              <span class="toolbar-count">{{ activeLogRows.length }} entries</span>
            </div>
            <div class="data-table-wrap log-table-wrap">
              <table class="data-table log-table">
                <thead>
                  <tr><th>Time</th><th>Level</th><th>Source</th><th>Message</th></tr>
                </thead>
                <tbody>
                  <tr v-for="row in pagedLogRows" :key="row.id">
                    <td>{{ shortDate(row.time) || '-' }}</td>
                    <td><span :class="['status-pill', row.level]">{{ row.level }}</span></td>
                    <td>{{ row.source }}</td>
                    <td><code>{{ row.message }}</code></td>
                  </tr>
                </tbody>
              </table>
              <div v-if="!pagedLogRows.length" class="widget-empty">No log lines match the current filters.</div>
            </div>
            <div class="pagination">
              <button class="ghost-button compact" :disabled="logFilters.page <= 1" @click="logFilters.page--">Previous</button>
              <span>Page {{ Math.min(logFilters.page, logPageCount) }} of {{ logPageCount }}</span>
              <button class="ghost-button compact" :disabled="logFilters.page >= logPageCount" @click="logFilters.page++">Next</button>
            </div>
          </article>
        </section>

        <section v-if="view === 'admin'" class="admin-page">
          <IntuneGraphConfig
            :graph-connections="graphConnections"
            :groups="groups"
            :users="users"
            :is-admin="user?.role === 'admin'"
            @save="saveGraphConnection"
            @delete="deleteGraphConnection"
            @test="testGraphConnection"
          />

          <VCenterConnectionsConfig
            :connections="vcenterConnections"
            :groups="groups"
            @save="saveVCenterConnection"
            @delete="deleteVCenterConnection"
            @test="testVCenterConnection"
          />

          <article :class="['glass-panel form-panel settings-layout', { 'panel-collapsed': panelCollapsed.applicationConfig }]">
            <div class="section-title"><h3>Application Config</h3><div class="section-actions"><SlidersHorizontal :size="18" /><button class="panel-toggle" type="button" @click="togglePanel('applicationConfig')"><component :is="panelCollapsed.applicationConfig ? ChevronDown : ChevronUp" :size="14" /></button></div></div>
            <p class="panel-intro">Database settings are editable here; environment-sourced values are shown so Docker configuration stays visible.</p>
            <div class="settings-editor">
              <ConfigSection
                v-for="section in configSections"
                :key="section.section"
                :section="section"
                :icon="configSectionMeta(section.section).icon"
                :description="configSectionMeta(section.section).description"
                :collapsed="collapsedConfigSections[section.section]"
                :label-for="settingLabel"
                :description-for="settingDescription"
                :placeholder-for="settingPlaceholder"
                :is-secret-setting="isSecretSetting"
                :source-label="settingSourceLabel"
                :is-boolean-setting="isBooleanSetting"
                :normalized-boolean-setting="normalizedBooleanSetting"
                @toggle="toggleConfigSection(section.section)"
                @toggle-boolean="toggleBooleanSetting"
              />
            </div>
            <button class="primary-action" @click="saveSettings"><Save :size="17" />Save settings</button>
          </article>
        </section>

        <section v-if="view === 'users'" class="users-page">
          <article :class="['glass-panel form-panel admin-directory', { 'panel-collapsed': panelCollapsed.usersGroups }]">
            <div class="section-title"><h3>Users & Groups</h3><div class="section-actions"><UsersRound :size="18" /><button class="panel-toggle" type="button" @click="togglePanel('usersGroups')"><component :is="panelCollapsed.usersGroups ? ChevronDown : ChevronUp" :size="14" /></button></div></div>
            <div class="directory-command-strip">
              <div>
                <span class="eyebrow">IDENTITY DIRECTORY</span>
                <strong>{{ users.length }} users · {{ groups.length }} groups</strong>
                <small>Manage local operators and group ownership for scripts, RunPlans, and credentials.</small>
              </div>
              <div>
                <button class="ghost-button compact" type="button" @click="groupModalOpen = true"><UsersRound :size="16" />Add group</button>
                <button class="primary-action compact" type="button" @click="userModalOpen = true"><UserPlus :size="16" />Add user</button>
              </div>
            </div>
            <div class="directory-browser-grid compact-directory">
              <div class="directory-results-panel">
                <header><Mail :size="16" /><div><strong>Users</strong><span>{{ users.length }} accounts</span></div></header>
                <div class="directory-result-list">
                  <article v-for="account in users" :key="account.id" class="directory-user-result">
                    <span class="avatar">{{ account.displayName?.split(' ').map((part) => part[0]).join('').slice(0, 2) }}</span>
                    <div><strong>{{ account.displayName }}</strong><small>{{ account.email }} · {{ account.role }}</small><code>{{ account.groups?.map((group) => group.name).join(', ') || 'No groups' }}</code></div>
                    <span :class="['status-pill', account.authProvider]">{{ account.authProvider }}</span>
                  </article>
                </div>
              </div>
              <div class="directory-results-panel">
                <header><UsersRound :size="16" /><div><strong>Groups</strong><span>{{ groups.length }} groups</span></div></header>
                <div class="directory-result-list">
                  <article v-for="group in groups" :key="group.id" class="directory-user-result">
                    <span class="avatar group-avatar"><UsersRound :size="15" /></span>
                    <div><strong>{{ group.name }}</strong><small>{{ group.description || 'No description' }}</small><code>{{ group.memberCount || 0 }} members</code></div>
                    <span class="status-pill">group</span>
                  </article>
                </div>
              </div>
            </div>
          </article>
          <BaseModal
            :open="userModalOpen"
            title="Add user"
            eyebrow="USER MANAGEMENT"
            description="Create a local operator account and assign initial group memberships."
            @close="userModalOpen = false"
          >
                <form @submit.prevent="saveUser" class="form-grid">
                  <label><span>Display name</span><input v-model="userForm.displayName" required placeholder="Automation Operator" /></label>
                  <label><span>Email</span><input v-model="userForm.email" required type="email" placeholder="operator@example.com" /></label>
                  <label><span>Role</span><select v-model="userForm.role"><option value="user">User</option><option value="admin">Admin</option></select></label>
                  <label><span>Temporary password</span><input v-model="userForm.password" type="password" placeholder="Minimum 8 characters" /></label>
                  <label class="full"><span>Groups</span><select v-model="userForm.groupIds" multiple class="select-tall"><option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option></select></label>
                  <div class="form-actions modal-actions full">
                    <button class="ghost-button" type="button" @click="userModalOpen = false">Cancel</button>
                    <button class="primary-action" type="submit"><UserPlus :size="17" />Create user</button>
                  </div>
                </form>
          </BaseModal>
          <BaseModal
            :open="groupModalOpen"
            title="Add group"
            eyebrow="GROUP MANAGEMENT"
            description="Create a group for shared script, RunPlan, and credential access."
            compact
            @close="groupModalOpen = false"
          >
                <form @submit.prevent="saveGroup" class="form-grid">
                  <label><span>Group name</span><input v-model="groupForm.name" placeholder="Operations" /></label>
                  <label><span>Description</span><input v-model="groupForm.description" placeholder="Shared scripts and RunPlans" /></label>
                  <div class="form-actions modal-actions full">
                    <button class="ghost-button" type="button" @click="groupModalOpen = false">Cancel</button>
                    <button class="primary-action" type="submit"><Plus :size="17" />Create group</button>
                  </div>
                </form>
          </BaseModal>
        </section>

        <section v-if="view === 'profile'" class="profile-layout">
          <article class="glass-panel profile-identity-card">
            <div class="profile-glow"></div>
            <div class="profile-avatar">
              <img v-if="profileForm.avatarUrl" :src="profileForm.avatarUrl" alt="" />
              <span v-else>{{ userInitials }}</span>
            </div>
            <div>
              <span class="eyebrow">Operator Profile</span>
              <h3>{{ profileForm.displayName || user?.displayName }}</h3>
              <p>{{ profileForm.jobTitle || user?.role || 'PowerShell operator' }}</p>
            </div>
            <div class="identity-meta">
              <span><strong>{{ user?.authProvider || 'local' }}</strong> Authentication</span>
              <span><strong>{{ activeTheme }}</strong> Active theme</span>
              <span><strong>{{ profileForm.timezone || 'local' }}</strong> Time zone</span>
            </div>
          </article>

          <article class="glass-panel form-panel profile-editor-card">
            <div class="section-title"><h3>Profile Details</h3><UserCircle :size="18" /></div>
            <p class="panel-intro">Keep identity details attached to the session so shared scripts, RunPlans, and logs read cleanly across teams.</p>
            <form class="grid-form" @submit.prevent="saveProfile">
              <label class="field">
                <span>Display name</span>
                <input v-model="profileForm.displayName" required />
              </label>
              <label class="field">
                <span>Email address</span>
                <input v-model="profileForm.email" type="email" :readonly="isExternalAuth" />
                <small v-if="isExternalAuth">Managed by {{ user?.authProvider }} identity.</small>
              </label>
              <label class="field">
                <span>Job title</span>
                <input v-model="profileForm.jobTitle" placeholder="Automation lead" />
              </label>
              <label class="field">
                <span>Phone</span>
                <input v-model="profileForm.phone" placeholder="+1 555 0100" />
              </label>
              <label class="field">
                <span>Time zone</span>
                <input v-model="profileForm.timezone" placeholder="America/Chicago" />
              </label>
              <label class="field">
                <span>Avatar URL</span>
                <input v-model="profileForm.avatarUrl" placeholder="https://..." />
              </label>
              <div class="form-actions">
                <button class="primary-action"><Save :size="17" />Save profile</button>
              </div>
            </form>
          </article>

          <article class="glass-panel form-panel">
            <div class="section-title"><h3>Theme</h3><Palette :size="18" /></div>
            <p class="panel-intro">Theme preference is stored on your account. The current choices are placeholders for the deeper theme library we can expand later.</p>
            <div class="theme-picker">
              <button
                v-for="theme in availableThemes"
                :key="theme.id"
                type="button"
                :class="['theme-card', `theme-card-${theme.id}`, { active: activeTheme === theme.id }]"
                @click="changeTheme(theme.id)"
              >
                <span class="theme-swatch">
                  <i v-for="color in theme.colors" :key="color" :style="{ background: color }"></i>
                </span>
                <span>
                  <strong>{{ theme.name }}</strong>
                  <small>{{ theme.description }}</small>
                </span>
                <CheckCircle2 v-if="activeTheme === theme.id" :size="18" />
              </button>
            </div>
          </article>

          <article class="glass-panel form-panel">
            <div class="section-title"><h3>Security</h3><KeyRound :size="18" /></div>
            <p class="panel-intro">Local accounts can change passwords here. Entra-connected accounts are handed to the Microsoft password flow.</p>
            <form v-if="!isExternalAuth" class="grid-form" @submit.prevent="changePassword">
              <label class="field">
                <span>Current password</span>
                <input v-model="passwordForm.currentPassword" type="password" autocomplete="current-password" />
              </label>
              <label class="field">
                <span>New password</span>
                <input v-model="passwordForm.newPassword" type="password" autocomplete="new-password" />
              </label>
              <label class="field">
                <span>Confirm password</span>
                <input v-model="passwordForm.confirmPassword" type="password" autocomplete="new-password" />
              </label>
              <div class="form-actions">
                <button class="primary-action"><LockKeyhole :size="17" />Change password</button>
              </div>
            </form>
            <div v-else class="external-auth-card">
              <div>
                <strong>{{ user?.authProvider }} password management</strong>
                <span>Password changes are handled by Microsoft Entra so account policy, MFA, and conditional access stay authoritative.</span>
              </div>
              <button class="primary-action" type="button" @click="changePassword">
                <ExternalLink :size="17" />Open Entra flow
              </button>
            </div>
          </article>
        </section>

        <ErrorView
          v-if="isUnknownView"
          :title="`Unknown view: ${view}`"
          message="The requested workspace is not registered in the POSHManager navigation map."
          @go-dashboard="view = 'dashboard'"
        />
      </main>

      <div v-if="contextMenu.open" class="context-menu glass-panel" :style="{ left: `${contextMenu.x}px`, top: `${contextMenu.y}px` }">
        <button @click="newScript(contextTargetFolderId())"><FilePlus2 :size="15" />New script</button>
        <button v-if="contextMenu.type !== 'script'" @click="createFolder(contextTargetFolderId())"><FolderPlus :size="15" />New folder</button>
        <template v-if="contextMenu.type === 'script'">
          <button @click="openScriptTestRun(contextMenu.item)"><Play :size="15" />Test / Run</button>
          <button @click="openRenameScript(contextMenu.item)"><Pencil :size="15" />Rename script</button>
          <button @click="openCloneScript(contextMenu.item)"><Copy :size="15" />Clone script</button>
          <button class="danger-text" @click="deleteScript(contextMenu.item)"><Trash2 :size="15" />Delete script</button>
        </template>
        <button v-if="contextMenu.type === 'folder'" @click="deleteFolder(contextMenu.item)"><Trash2 :size="15" />Delete folder</button>
      </div>

      <BaseModal
        :open="scriptRenameModalOpen"
        title="Rename script"
        eyebrow="SCRIPT EXPLORER"
        description="Update the script library display name while preserving the script id and version history."
        compact
        @close="closeScriptActionModals"
      >
        <form class="modal-form script-action-form" @submit.prevent="renameScript">
          <label>
            <span>Script name</span>
            <input v-model.trim="scriptRenameForm.name" required placeholder="Inventory Health Check.ps1" />
          </label>
          <div v-if="scriptActionUsage?.total" class="script-impact-warning">
            <AlertTriangle :size="18" />
            <div>
              <strong>{{ scriptActionUsage.total }} linked item{{ scriptActionUsage.total === 1 ? '' : 's' }} need review</strong>
              <p>RunPlans and PSADT Intune deployments continue to reference this script id, but operator-facing names, docs, approval notes, or deployment metadata may need to be updated after rename.</p>
              <ul>
                <li v-for="item in scriptImpactItems" :key="`${item.type}-${item.id}`"><span>{{ item.type }}</span>{{ item.name }}</li>
              </ul>
            </div>
          </div>
          <div class="form-actions modal-actions full">
            <button class="ghost-button" type="button" @click="closeScriptActionModals">Cancel</button>
            <button class="primary-action" type="submit"><Save :size="16" />Rename script</button>
          </div>
        </form>
      </BaseModal>

      <BaseModal
        :open="scriptCloneModalOpen"
        title="Clone script"
        eyebrow="SCRIPT EXPLORER"
        description="Create a new independent script copy in the same folder. RunPlans and deployments are not copied."
        compact
        @close="closeScriptActionModals"
      >
        <form class="modal-form script-action-form" @submit.prevent="cloneScript">
          <label>
            <span>Clone name</span>
            <input v-model.trim="scriptCloneForm.name" required placeholder="Inventory Health Check copy.ps1" />
          </label>
          <div class="script-action-note">
            <Copy :size="17" />
            <span>The clone starts unassigned so it can be safely edited before being attached to RunPlans or Intune publishing plans.</span>
          </div>
          <div class="form-actions modal-actions full">
            <button class="ghost-button" type="button" @click="closeScriptActionModals">Cancel</button>
            <button class="primary-action" type="submit"><Copy :size="16" />Create clone</button>
          </div>
        </form>
      </BaseModal>

      <BaseModal
        :open="scriptDeleteModalOpen"
        title="Delete script"
        eyebrow="SCRIPT EXPLORER"
        description="This action is permanent and can affect execution plans."
        compact
        @close="closeScriptActionModals"
      >
        <form class="modal-form script-action-form" @submit.prevent="confirmDeleteScript">
          <div class="script-impact-warning danger">
            <AlertTriangle :size="18" />
            <div>
              <strong>Delete {{ scriptActionTarget?.name }}?</strong>
              <p v-if="scriptActionUsage?.total">This script is assigned to {{ scriptActionUsage.total }} item{{ scriptActionUsage.total === 1 ? '' : 's' }}. Linked RunPlans will be removed by the backend relationship, and PSADT Intune deployments will lose their saved script link.</p>
              <p v-else>No linked RunPlans or PSADT Intune deployments were found, but the script and version history will still be removed.</p>
              <ul v-if="scriptActionUsage?.total">
                <li v-for="item in scriptImpactItems" :key="`${item.type}-${item.id}`"><span>{{ item.type }}</span>{{ item.name }}</li>
              </ul>
            </div>
          </div>
          <div class="form-actions modal-actions full">
            <button class="ghost-button" type="button" @click="closeScriptActionModals">Cancel</button>
            <button class="primary-action danger-action" type="submit"><Trash2 :size="16" />Delete permanently</button>
          </div>
        </form>
      </BaseModal>

      <HelpCenter
        :open="helpOpen"
        :articles="helpArticles"
        :categories="helpCategories"
        :selected-article="selectedHelpArticle"
        :query="helpQuery"
        :loading="helpLoading"
        @close="helpOpen = false"
        @search="searchHelp"
        @select="selectHelpArticle"
      />
    </template>

    <div v-if="toast" class="toast glass-panel">{{ toast }}</div>
  </div>
</template>

<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/browser/bracketMatching.js';
import 'monaco-editor/esm/vs/editor/contrib/find/browser/findController.js';
import 'monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js';
import 'monaco-editor/esm/vs/editor/contrib/hover/browser/hoverContribution.js';
import 'monaco-editor/esm/vs/editor/contrib/parameterHints/browser/parameterHints.js';
import 'monaco-editor/esm/vs/editor/contrib/snippet/browser/snippetController2.js';
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import {
  Activity, AlertTriangle, BarChart3, BookOpenText, CheckCircle2, ChevronDown, ChevronRight, ChevronUp, Copy, Database, Download, ExternalLink,
  FileCode2, FilePlus2, Folder, FolderPlus, Gauge, KeyRound, ListFilter, ListPlus, LockKeyhole, Mail,
  PackageOpen, Pencil, PieChart, Play, PlayCircle, Plus, Palette, Rocket, RotateCcw, Save, Search, Server, Settings, ShieldCheck, SlidersHorizontal, Sparkles, Trash2, UserPlus,
  UserCircle, UsersRound, X
} from '@lucide/vue';
import { createApi } from './api';
import { configurePowerShellMonaco } from './editor/powershellMonaco';
import AppSidebar from './components/AppSidebar.vue';
import AppTopbar from './components/AppTopbar.vue';
import ConfigSection from './components/settings/ConfigSection.vue';
import IntuneGraphConfig from './components/settings/IntuneGraphConfig.vue';
import VCenterConnectionsConfig from './components/settings/VCenterConnectionsConfig.vue';
import HelpCenter from './components/help/HelpCenter.vue';
import ParticleBackdrop from './components/ParticleBackdrop.vue';
import BaseModal from './components/ui/BaseModal.vue';
import ResourceTable from './components/ui/ResourceTable.vue';
import ScriptExplorer from './components/scripts/ScriptExplorer.vue';
import ScriptTestRunModal from './components/scripts/ScriptTestRunModal.vue';
import VariableEditorModal from './components/scripts/VariableEditorModal.vue';
import HostGroupsPanel from './components/hosts/HostGroupsPanel.vue';
import VCenterImportModal from './components/hosts/VCenterImportModal.vue';
import VCenterTraceModal from './components/hosts/VCenterTraceModal.vue';
import DashboardView from './views/DashboardView.vue';
import ErrorView from './views/ErrorView.vue';
import LoginView from './views/LoginView.vue';
import PsadtView from './views/PsadtView.vue';
import AssetsView from './views/AssetsView.vue';

self.MonacoEnvironment = {
  getWorker() {
    return new editorWorker();
  }
};

const token = ref(localStorage.getItem('poshmanager.token') || '');
const user = ref(JSON.parse(localStorage.getItem('poshmanager.user') || 'null'));
const api = createApi(() => token.value, logout);

const view = ref('dashboard');
const error = ref('');
const toast = ref('');
const showPassword = ref(false);
const entraLoginEnabled = ref(false);
const navCollapsed = ref(localStorage.getItem('poshmanager.navCollapsed') === 'true');
const dashboardEditMode = ref(false);
const dashboardWidgetLibraryOpen = ref(false);
const draggedDashboardWidgetId = ref('');
const scriptEditMode = ref(false);
const scriptPanelLibraryOpen = ref(false);
const scriptRenameModalOpen = ref(false);
const scriptCloneModalOpen = ref(false);
const scriptDeleteModalOpen = ref(false);
const scriptActionTarget = ref(null);
const scriptActionUsage = ref(null);
const scriptTestModalOpen = ref(false);
const scriptTestTarget = ref(null);
const scriptTestJob = ref(null);
const scriptTestRunning = ref(false);
const scriptTestError = ref('');
const userModalOpen = ref(false);
const groupModalOpen = ref(false);
const hostModalOpen = ref(false);
const vcenterImportOpen = ref(false);
const vcenterImportLoading = ref(false);
const vcenterImportError = ref('');
const vcenterPreviewRows = ref([]);
const vcenterImportStatus = ref(null);
const vcenterImportDiagnostics = ref(null);
const vcenterTraceEntries = ref([]);
const vcenterTraceOpen = ref(false);
const credentialModalOpen = ref(false);
const runPlanModalOpen = ref(false);
const variableModalOpen = ref(false);
const helpOpen = ref(false);
const helpLoading = ref(false);
const helpQuery = ref('');
const helpArticles = ref([]);
const helpCategories = ref([]);
const selectedHelpArticle = ref(null);
const summary = ref({});
const settings = ref({});
const settingsDraft = reactive({});
const settingMetadata = {
  server_fqdn: {
    label: 'Server FQDN',
    description: 'External hostname or URL used for generated links and reverse-proxy aware app behavior.',
    placeholder: 'https://poshmanager.example.com',
    section: 'Deployment'
  },
  trusted_origins: {
    label: 'Trusted Origins',
    description: 'Comma-separated browser origins allowed to call the API when deployed behind a proxy.',
    placeholder: 'https://poshmanager.example.com,https://ops.example.com',
    section: 'Deployment'
  },
  client_origin: {
    label: 'Client Origin',
    description: 'Primary UI origin used by auth callbacks and generated links.',
    placeholder: 'https://poshmanager.example.com',
    section: 'Deployment'
  },
  api_port: {
    label: 'API Port',
    description: 'Internal Express API port used behind the Vite/reverse-proxy surface.',
    placeholder: '5174',
    section: 'Runtime'
  },
  database_path: {
    label: 'Database Path',
    description: 'SQLite database location inside the container or mounted volume.',
    placeholder: '/app/data/poshmanager.sqlite',
    section: 'Runtime'
  },
  log_dir: {
    label: 'Log Directory',
    description: 'Directory where Winston request and system logs are written.',
    placeholder: '/app/data/logs',
    section: 'Runtime'
  },
  file_locker_dir: {
    label: 'FileLocker Directory',
    description: 'Filesystem root where uploaded package assets, images, scripts, and support files are stored.',
    placeholder: '/app/FileLocker',
    section: 'Runtime'
  },
  asset_dir: {
    label: 'Legacy Asset Directory',
    description: 'Compatibility alias for older deployments. New uploads use FileLocker.',
    placeholder: '/app/FileLocker',
    section: 'Runtime'
  },
  max_asset_bytes: {
    label: 'Max Asset Bytes',
    description: 'Maximum allowed size for one uploaded asset.',
    placeholder: '52428800',
    section: 'Runtime'
  },
  json_limit: {
    label: 'JSON Body Limit',
    description: 'Express request body limit used by API-first browser uploads.',
    placeholder: '60mb',
    section: 'Runtime'
  },
  node_env: {
    label: 'Node Environment',
    description: 'Runtime mode for the API process.',
    placeholder: 'production',
    section: 'Runtime'
  },
  entra_enabled: {
    label: 'Microsoft Entra ID',
    description: 'Turns on Entra-backed authentication while keeping local login available when disabled.',
    section: 'Authentication'
  },
  entra_tenant_id: {
    label: 'Entra Tenant ID',
    description: 'Tenant identifier used for Microsoft identity flows.',
    placeholder: '00000000-0000-0000-0000-000000000000',
    section: 'Authentication'
  },
  entra_client_id: {
    label: 'Entra Client ID',
    description: 'Application registration client ID used by the Entra integration.',
    placeholder: '00000000-0000-0000-0000-000000000000',
    section: 'Authentication'
  },
  entra_callback_url: {
    label: 'Entra Callback URL',
    description: 'Redirect URL registered for the POSHManager sign-in flow.',
    placeholder: 'https://poshmanager.example.com/api/auth/entra/callback',
    section: 'Authentication'
  },
  entra_password_change_url: {
    label: 'Entra Password Flow',
    description: 'Microsoft URL opened when Entra-backed users change passwords.',
    placeholder: 'https://mysignins.microsoft.com/security-info/password/change',
    section: 'Authentication'
  },
  powershell_bin: {
    label: 'PowerShell Binary',
    description: 'Executable used by the API worker inside the Linux container when RunPlans execute.',
    placeholder: 'pwsh',
    section: 'Execution'
  },
  allow_script_execution: {
    label: 'Script Execution',
    description: 'Master safety switch that controls whether RunPlans can execute scripts on target hosts.',
    section: 'Execution'
  },
  host_group_sync_interval_minutes: {
    label: 'Host Group Sync Interval',
    description: 'Minutes between background sync runs for dynamic host groups. Set 0 to disable the scheduler.',
    placeholder: '60',
    section: 'Execution'
  },
  vcenter_enabled: {
    label: 'VMware vCenter',
    description: 'Enable VM discovery and import through the VMware vCenter REST API.',
    section: 'Integrations'
  },
  vcenter_base_url: {
    label: 'vCenter Base URL',
    description: 'Root URL of the vCenter API endpoint used for VM inventory discovery.',
    placeholder: 'https://vcenter.contoso.local',
    section: 'Integrations'
  },
  vcenter_username: {
    label: 'vCenter Username',
    description: 'Account used to create vCenter API sessions for host import previews.',
    placeholder: 'administrator@vsphere.local',
    section: 'Integrations'
  },
  vcenter_password: {
    label: 'vCenter Password',
    description: 'Password used only by the backend service when authenticating to vCenter.',
    placeholder: 'Stored in app settings or pinned by VCENTER_PASSWORD',
    section: 'Integrations',
    secret: true
  },
  vcenter_api_mode: {
    label: 'vCenter API Mode',
    description: 'Use auto for modern /api with fallback to legacy /rest. Set api or rest to force one contract.',
    placeholder: 'auto',
    section: 'Integrations'
  },
  vcenter_request_timeout_ms: {
    label: 'vCenter Request Timeout',
    description: 'Maximum time the backend waits for one vCenter HTTP request before returning a traceable timeout.',
    placeholder: '20000',
    section: 'Integrations'
  },
  vcenter_allow_untrusted_tls: {
    label: 'Allow Untrusted vCenter TLS',
    description: 'Permit self-signed or private CA vCenter certificates for lab and internal deployments.',
    section: 'Integrations'
  }
};
const users = ref([]);
const groups = ref([]);
const credentials = ref([]);
const hosts = ref([]);
const hostGroups = ref([]);
const folders = ref([]);
const scripts = ref([]);
const assetFolders = ref([]);
const assets = ref([]);
const runplans = ref([]);
const jobs = ref([]);
const variableCatalog = ref({ builtIns: [], psadt: [], custom: [] });
const psadtCatalog = ref({ module: {}, sources: [], supportMatrix: [], structure: [], invokeParameters: [], functions: [], admxPolicies: [], launchCommands: [], snippets: [] });
const psadtProfiles = ref([]);
const psadtIntuneDeployments = ref([]);
const catalogApplications = ref([]);
const changeRequests = ref([]);
const reportingOverview = ref(null);
const graphConnections = ref([]);
const vcenterConnections = ref([]);
const graphStatusResult = ref(null);
const graphAuditResult = ref([]);
const driftResult = ref(null);
const builderStatus = ref(null);
const installerAnalysis = ref(null);
const psadtValidationResult = ref(null);
const psadtMigrationPlan = ref(null);
const selectedJob = ref(null);
const systemLogs = ref([]);
const logTab = ref('job');
const logFilters = reactive({ search: '', stream: '', page: 1, pageSize: 14 });
const hostFilters = reactive({ search: '', sort: 'name', direction: 'asc', page: 1, pageSize: 8 });
const credentialFilters = reactive({ search: '', sort: 'name', direction: 'asc', page: 1, pageSize: 8 });
const runPlanFilters = reactive({ search: '', sort: 'name', direction: 'asc', page: 1, pageSize: 8 });
const collapsedConfigSections = reactive(loadCollapsedConfigSections());
const activeFolderId = ref(null);
const selectedScript = ref(null);
const editorMount = ref(null);
let editor;
let scriptTestPollTimer = null;
let scriptTestPollFailures = 0;

const loginForm = reactive({ email: 'admin@posh.local', password: 'change-me-now' });
const scriptDraft = reactive({ id: null, folderId: null, name: '', description: '', content: '', visibility: 'personal', groupId: null });
const scriptRenameForm = reactive({ name: '' });
const scriptCloneForm = reactive({ name: '' });
const hostForm = reactive({ id: null, name: '', address: '', fqdn: '', osFamily: 'windows', transport: 'winrm', credentialId: null, tagsText: '', visibility: 'shared', groupId: null, sourceType: 'manual', sourceConnectionId: null, sourceRef: '' });
const credentialForm = reactive({ id: null, name: '', kind: 'username_password', username: '', secret: '', visibility: 'personal', groupId: null });
const runPlanForm = reactive({ id: null, name: '', description: '', scriptId: '', visibility: 'personal', groupId: null, parallel: true, hostIds: [], hostGroupIds: [] });
const groupForm = reactive({ name: '', description: '' });
const userForm = reactive({ email: '', displayName: '', password: '', role: 'user', groupIds: [] });
const profileForm = reactive({ displayName: '', email: '', jobTitle: '', phone: '', timezone: '', avatarUrl: '' });
const passwordForm = reactive({ currentPassword: '', newPassword: '', confirmPassword: '' });
const contextMenu = reactive({ open: false, x: 0, y: 0, item: null, type: null });
const panelCollapsed = reactive(loadPanelCollapsed());
const dashboardWidgetCatalog = [
  { id: 'scripts', title: 'Scripts', category: 'Library', icon: FileCode2, minW: 2, minH: 2, defaultW: 3, defaultH: 2, description: 'Script count and library state.' },
  { id: 'hosts', title: 'Hosts', category: 'Inventory', icon: Server, minW: 2, minH: 2, defaultW: 3, defaultH: 2, description: 'Registered target systems.' },
  { id: 'runplans', title: 'RunPlans', category: 'Execution', icon: Rocket, minW: 2, minH: 2, defaultW: 3, defaultH: 2, description: 'Saved execution plans.' },
  { id: 'jobs', title: 'Jobs', category: 'Telemetry', icon: Activity, minW: 2, minH: 2, defaultW: 3, defaultH: 2, description: 'Execution history.' },
  { id: 'execution-trend', title: 'Execution Trend', category: 'Charts', icon: BarChart3, minW: 4, minH: 3, defaultW: 6, defaultH: 3, description: 'RunPlan job activity over the latest executions.' },
  { id: 'operations-health', title: 'Operations Health', category: 'Charts', icon: PieChart, minW: 4, minH: 3, defaultW: 6, defaultH: 3, description: 'Execution status distribution and platform mix.' },
  { id: 'recent-jobs', title: 'Recent Jobs', category: 'Execution logs', icon: PlayCircle, minW: 4, minH: 3, defaultW: 6, defaultH: 3, description: 'Latest RunPlan execution state.' },
  { id: 'configuration', title: 'Configuration Snapshot', category: 'Control plane', icon: Settings, minW: 4, minH: 3, defaultW: 6, defaultH: 3, description: 'Runtime app settings.' },
  { id: 'quick-actions', title: 'Quick Actions', category: 'Operations', icon: Sparkles, minW: 3, minH: 2, defaultW: 4, defaultH: 3, description: 'Fast create and navigation controls.' },
  { id: 'host-mix', title: 'Host Transport Mix', category: 'Inventory', icon: Server, minW: 3, minH: 2, defaultW: 4, defaultH: 3, description: 'Transport breakdown for host targets.' },
  { id: 'script-visibility', title: 'Script Visibility', category: 'Governance', icon: ShieldCheck, minW: 3, minH: 2, defaultW: 4, defaultH: 3, description: 'Personal, shared, and group script spread.' },
  { id: 'runplan-targets', title: 'RunPlan Targets', category: 'Execution', icon: ListFilter, minW: 3, minH: 2, defaultW: 4, defaultH: 3, description: 'Host count distribution across RunPlans.' }
];
const defaultDashboardLayout = [
  { id: 'scripts', w: 3, h: 2, visible: true, collapsed: false },
  { id: 'hosts', w: 3, h: 2, visible: true, collapsed: false },
  { id: 'runplans', w: 3, h: 2, visible: true, collapsed: false },
  { id: 'jobs', w: 3, h: 2, visible: true, collapsed: false },
  { id: 'execution-trend', w: 6, h: 3, visible: true, collapsed: false },
  { id: 'operations-health', w: 6, h: 3, visible: true, collapsed: false },
  { id: 'recent-jobs', w: 6, h: 3, visible: true, collapsed: false },
  { id: 'configuration', w: 6, h: 3, visible: true, collapsed: false },
  { id: 'quick-actions', w: 4, h: 3, visible: false, collapsed: false },
  { id: 'host-mix', w: 4, h: 3, visible: false, collapsed: false },
  { id: 'script-visibility', w: 4, h: 3, visible: false, collapsed: false },
  { id: 'runplan-targets', w: 4, h: 3, visible: false, collapsed: false }
];
const dashboardLayout = reactive(loadDashboardLayout());
const scriptPanelCatalog = [
  { id: 'library', title: 'Script Library', category: 'Storage', icon: Folder, minW: 2, minH: 5, defaultW: 2, defaultH: 7, description: 'Folder tree and script selection.' },
  { id: 'editor', title: 'PowerShell Editor', category: 'Authoring', icon: FileCode2, minW: 6, minH: 5, defaultW: 8, defaultH: 7, description: 'Monaco terminal editor and script metadata.' },
  { id: 'inspector', title: 'Properties', category: 'Governance', icon: ShieldCheck, minW: 2, minH: 4, defaultW: 2, defaultH: 7, description: 'Selection details, visibility, and operational actions.' }
];
const defaultScriptLayout = [
  { id: 'library', w: 2, h: 7, visible: true, collapsed: false },
  { id: 'editor', w: 8, h: 7, visible: true, collapsed: false },
  { id: 'inspector', w: 2, h: 7, visible: true, collapsed: false }
];
const scriptLayout = reactive(loadScriptLayout());

const nav = [
  { id: 'dashboard', label: 'Dashboard', icon: Gauge },
  { id: 'scripts', label: 'Scripts', icon: BookOpenText },
  { id: 'assets', label: 'Assets', icon: PackageOpen },
  { id: 'psadt', label: 'PSADT', icon: Sparkles },
  { id: 'hosts', label: 'Hosts', icon: Server },
  { id: 'credentials', label: 'Credential Vault', icon: KeyRound },
  { id: 'runplans', label: 'RunPlans', icon: Rocket },
  { id: 'logs', label: 'Logs', icon: Activity },
  { id: 'users', label: 'Users & Groups', icon: UsersRound },
  { id: 'admin', label: 'Config', icon: Settings }
];

const availableThemes = [
  {
    id: 'dashtreme',
    name: 'Dashtreme Pulse',
    description: 'Bright magenta, cyan, and amber glass controls.',
    colors: ['#ffe38e', '#ff73b0', '#74ece7']
  },
  {
    id: 'terminal',
    name: 'Green Screen',
    description: 'A darker operations console with phosphor edges.',
    colors: ['#0b2d12', '#41ff64', '#d7ff72']
  },
  {
    id: 'aurora',
    name: 'Aurora Glass',
    description: 'Cool blue and violet gradients with quiet contrast.',
    colors: ['#5de7ff', '#7c5cff', '#f5f7ff']
  }
];

const hostColumns = [
  { key: 'name', label: 'Name' },
  { key: 'address', label: 'Address' },
  { key: 'transport', label: 'Transport' },
  { key: 'sourceType', label: 'Source' },
  { key: 'credentialName', label: 'Credential' },
  { key: 'tags', label: 'Tags', sortable: false },
  { key: 'actions', label: 'Actions', sortable: false }
];

const credentialColumns = [
  { key: 'name', label: 'Name' },
  { key: 'kind', label: 'Type' },
  { key: 'username', label: 'Username / label' },
  { key: 'visibility', label: 'Visibility' },
  { key: 'status', label: 'Status', sortable: false },
  { key: 'actions', label: 'Actions', sortable: false }
];

const runPlanColumns = [
  { key: 'name', label: 'Name' },
  { key: 'scriptName', label: 'Script' },
  { key: 'visibility', label: 'Visibility' },
  { key: 'hostCount', label: 'Targets' },
  { key: 'mode', label: 'Mode', sortable: false },
  { key: 'actions', label: 'Actions', sortable: false, class: 'table-actions-cell' }
];

const activeTitle = computed(() => (view.value === 'profile' ? 'My Profile' : nav.find((item) => item.id === view.value)?.label || 'Dashboard'));
const routedViewIds = computed(() => new Set([...nav.map((item) => item.id), 'profile']));
const isUnknownView = computed(() => token.value && !routedViewIds.value.has(view.value));
const activeTheme = computed(() => user.value?.theme || 'dashtreme');
const isExternalAuth = computed(() => Boolean(user.value?.authProvider && user.value.authProvider !== 'local'));
const userInitials = computed(() => (user.value?.displayName || 'PS').split(/\s+/).map((part) => part[0]).join('').slice(0, 2).toUpperCase());
const firstName = computed(() => (user.value?.displayName || 'Operator').split(/\s+/)[0] || 'Operator');
const dayPart = computed(() => {
  const hour = new Date().getHours();
  if (hour < 12) return 'morning';
  if (hour < 17) return 'afternoon';
  return 'evening';
});
const dashboardMetricMap = computed(() => ({
  scripts: { metric: true, label: 'Scripts', value: summary.value.scripts || scripts.value.length, icon: FileCode2, tone: 'violet', meta: 'library objects indexed' },
  hosts: { metric: true, label: 'Hosts', value: summary.value.hosts || hosts.value.length, icon: Server, tone: 'cyan', meta: 'systems available for targeting' },
  runplans: { metric: true, label: 'RunPlans', value: summary.value.runplans || runplans.value.length, icon: Rocket, tone: 'mint', meta: 'execution plans ready' },
  jobs: { metric: true, label: 'Jobs', value: summary.value.jobs || jobs.value.length, icon: Activity, tone: 'orange', meta: 'execution records captured' }
}));
const dashboardCatalogById = computed(() => new Map(dashboardWidgetCatalog.map((widget) => [widget.id, widget])));
const visibleDashboardWidgets = computed(() => dashboardLayout
  .filter((widget) => widget.visible !== false)
  .map((widget) => ({ ...dashboardCatalogById.value.get(widget.id), ...widget, ...(dashboardMetricMap.value[widget.id] || {}) })));
const hiddenDashboardWidgets = computed(() => dashboardLayout
  .filter((widget) => widget.visible === false)
  .map((widget) => ({ ...dashboardCatalogById.value.get(widget.id), ...widget })));
const scriptCatalogById = computed(() => new Map(scriptPanelCatalog.map((panel) => [panel.id, panel])));
const hiddenScriptPanels = computed(() => scriptLayout
  .filter((panel) => panel.visible === false)
  .map((panel) => ({ ...scriptCatalogById.value.get(panel.id), ...panel })));
const hostTransportStats = computed(() => ['winrm', 'ssh', 'local', 'api'].map((transport) => ({
  transport,
  count: hosts.value.filter((host) => host.transport === transport).length
})));
const scriptVisibilityStats = computed(() => ['personal', 'shared', 'group'].map((visibility) => ({
  visibility,
  count: scripts.value.filter((script) => script.visibility === visibility).length
})));
const runPlanTargetStats = computed(() => runplans.value.slice(0, 5).map((runplan) => ({
  name: runplan.name,
  count: runplan.hostCount || runplan.hostIds?.length || 0
})));
const scriptImpactItems = computed(() => [
  ...(scriptActionUsage.value?.runplans || []).map((item) => ({ id: item.id, name: item.name, type: 'RunPlan' })),
  ...(scriptActionUsage.value?.intuneDeployments || []).map((item) => ({ id: item.id, name: item.name, type: 'PSADT Intune' }))
].slice(0, 6));
const dashboardSettingsPreview = computed(() => ['server_fqdn', 'trusted_origins', 'allow_script_execution', 'entra_enabled']
  .filter((key) => settings.value[key])
  .map((key) => ({ key, ...settings.value[key] })));
const configSections = computed(() => {
  const sections = ['Deployment', 'Authentication', 'Integrations', 'Execution', 'Runtime', 'Other'];
  return sections
    .map((section) => ({
      section,
      rows: Object.entries(settingsDraft)
        .filter(([key]) => (settingMetadata[key]?.section || 'Other') === section)
        .map(([key, row]) => ({ key, row }))
    }))
    .filter((section) => section.rows.length);
});
const executionTrend = computed(() => {
  const latest = jobs.value.slice(0, 8).reverse();
  const padded = [
    ...Array.from({ length: Math.max(0, 8 - latest.length) }, (_, index) => ({
      label: `Pending ${index + 1}`,
      status: 'queued',
      value: 18 + (index % 4) * 10
    })),
    ...latest
  ];
  const max = Math.max(padded.length, 1);
  return padded.map((job, index) => ({
    label: job.runplanName || job.label || `Job ${index + 1}`,
    status: job.status,
    value: Math.round(((index + 1) / max) * 100)
  }));
});
const jobStatusStats = computed(() => ['queued', 'running', 'completed', 'failed'].map((status) => ({
  status,
  count: jobs.value.filter((job) => job.status === status).length
})));
const healthGauge = computed(() => {
  const total = jobs.value.length || 1;
  const completed = jobs.value.filter((job) => job.status === 'completed').length;
  return Math.round((completed / total) * 100);
});
const activeLogRows = computed(() => {
  const rows = logTab.value === 'job'
    ? (selectedJob.value?.logs || []).map((log) => ({
      id: log.id,
      time: log.createdAt,
      level: log.stream,
      source: log.hostName || 'system',
      message: log.line
    }))
    : systemLogs.value.map((log, index) => ({
      id: log.requestId || log.request_id || index,
      time: log.timestamp,
      level: log.level,
      source: log.method ? `${log.method} ${log.path}` : (log.service || 'api'),
      message: log.message || log.path || JSON.stringify(log)
    }));
  const query = logFilters.search.trim().toLowerCase();
  return rows.filter((row) => {
    const matchesStream = !logFilters.stream || row.level === logFilters.stream;
    const haystack = `${row.level || ''} ${row.source || ''} ${row.message || ''}`.toLowerCase();
    return matchesStream && (!query || haystack.includes(query));
  });
});
const logPageCount = computed(() => Math.max(1, Math.ceil(activeLogRows.value.length / logFilters.pageSize)));
const pagedLogRows = computed(() => {
  const start = (Math.min(logFilters.page, logPageCount.value) - 1) * logFilters.pageSize;
  return activeLogRows.value.slice(start, start + logFilters.pageSize);
});
const filteredHosts = computed(() => filterRows(hosts.value, hostFilters, (host) => [
  host.name,
  host.address,
  host.fqdn,
  host.transport,
  host.sourceType,
  host.sourceConnectionName,
  host.credentialName,
  host.tags?.join(' ')
]));
const hostPageCount = computed(() => pageCount(filteredHosts.value, hostFilters));
const pagedHosts = computed(() => pageRows(filteredHosts.value, hostFilters));
const filteredCredentials = computed(() => filterRows(credentials.value, credentialFilters, (credential) => [
  credential.name,
  credential.kind,
  credential.username,
  credential.visibility
]));
const credentialPageCount = computed(() => pageCount(filteredCredentials.value, credentialFilters));
const pagedCredentials = computed(() => pageRows(filteredCredentials.value, credentialFilters));
const filteredRunPlans = computed(() => filterRows(runplans.value, runPlanFilters, (runplan) => [
  runplan.name,
  runplan.scriptName,
  runplan.visibility,
  runplan.description
]));
const runPlanPageCount = computed(() => pageCount(filteredRunPlans.value, runPlanFilters));
const pagedRunPlans = computed(() => pageRows(filteredRunPlans.value, runPlanFilters));

watch(view, async (nextView, previousView) => {
  contextMenu.open = false;
  if (previousView === 'scripts' && nextView !== 'scripts') disposeEditor();
  if (nextView === 'scripts') await ensureEditorReady();
  if (nextView === 'profile') hydrateProfileForm();
});

watch(() => scriptDraft.content, (value) => {
  if (editor && editor.getValue() !== value) editor.setValue(value || '');
});

watch(user, () => hydrateProfileForm(), { immediate: true });

watch(() => [logFilters.search, logFilters.stream, logTab.value], () => {
  logFilters.page = 1;
});

watch(() => [hostFilters.search, hostFilters.sort, hostFilters.direction, hostFilters.pageSize], () => {
  hostFilters.page = 1;
});

watch(() => [credentialFilters.search, credentialFilters.sort, credentialFilters.direction, credentialFilters.pageSize], () => {
  credentialFilters.page = 1;
});

watch(() => [runPlanFilters.search, runPlanFilters.sort, runPlanFilters.direction, runPlanFilters.pageSize], () => {
  runPlanFilters.page = 1;
});

watch(logPageCount, () => {
  if (logFilters.page > logPageCount.value) logFilters.page = logPageCount.value;
});

watch(hostPageCount, () => {
  if (hostFilters.page > hostPageCount.value) hostFilters.page = hostPageCount.value;
});

watch(credentialPageCount, () => {
  if (credentialFilters.page > credentialPageCount.value) credentialFilters.page = credentialPageCount.value;
});

watch(runPlanPageCount, () => {
  if (runPlanFilters.page > runPlanPageCount.value) runPlanFilters.page = runPlanPageCount.value;
});

onMounted(async () => {
  window.addEventListener('click', closeContext);
  await consumeEntraRedirect();
  await probeEntraLogin();
  if (token.value) await refreshAll();
  if (view.value === 'scripts') await ensureEditorReady();
});

// The Entra callback redirects back to the SPA with a one-time token (or an
// error) in the query string. Consume it, persist the session, and scrub the URL.
async function consumeEntraRedirect() {
  const params = new URLSearchParams(window.location.search);
  const entraToken = params.get('entraToken');
  const entraError = params.get('entraError');
  if (entraError) {
    error.value = entraError;
  } else if (entraToken) {
    token.value = entraToken;
    localStorage.setItem('poshmanager.token', entraToken);
  }
  if (entraToken || entraError) {
    window.history.replaceState({}, document.title, window.location.pathname);
  }
}

async function probeEntraLogin() {
  if (token.value) return;
  try {
    const health = await api.get('/api/health');
    entraLoginEnabled.value = Boolean(health?.entraLoginEnabled);
  } catch {
    entraLoginEnabled.value = false;
  }
}

onBeforeUnmount(() => {
  window.removeEventListener('click', closeContext);
  stopScriptTestPolling();
  disposeEditor();
});

function filterRows(rows, filters, fields) {
  // Shared table filtering keeps Hosts, Credential Vault, and RunPlans behavior consistent.
  const query = filters.search.trim().toLowerCase();
  return [...rows]
    .filter((row) => {
      if (!query) return true;
      return fields(row).filter(Boolean).join(' ').toLowerCase().includes(query);
    })
    .sort((a, b) => compareRows(a, b, filters.sort, filters.direction));
}

function compareRows(a, b, key, direction = 'asc') {
  const left = a?.[key] ?? '';
  const right = b?.[key] ?? '';
  const result = typeof left === 'number' || typeof right === 'number'
    ? Number(left || 0) - Number(right || 0)
    : String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' });
  return direction === 'desc' ? result * -1 : result;
}

function pageCount(rows, filters) {
  return Math.max(1, Math.ceil(rows.length / Number(filters.pageSize || 8)));
}

function pageRows(rows, filters) {
  const page = Math.min(filters.page, pageCount(rows, filters));
  const start = (page - 1) * Number(filters.pageSize || 8);
  return rows.slice(start, start + Number(filters.pageSize || 8));
}

function setSort(filters, key) {
  if (filters.sort === key) filters.direction = filters.direction === 'asc' ? 'desc' : 'asc';
  else {
    filters.sort = key;
    filters.direction = 'asc';
  }
}

async function login() {
  try {
    error.value = '';
    const result = await api.post('/api/auth/login', loginForm);
    token.value = result.token;
    localStorage.setItem('poshmanager.token', result.token);
    syncUser(result.user);
    await refreshAll();
  } catch (err) {
    error.value = err.message;
  }
}

function logout() {
  token.value = '';
  user.value = null;
  localStorage.removeItem('poshmanager.token');
  localStorage.removeItem('poshmanager.user');
}

function toggleNav() {
  navCollapsed.value = !navCollapsed.value;
  localStorage.setItem('poshmanager.navCollapsed', String(navCollapsed.value));
  requestAnimationFrame(() => editor?.layout());
}

function setEditorMount(el) {
  if (!el) {
    editorMount.value = null;
    disposeEditor();
    return;
  }
  if (editorMount.value && editorMount.value !== el) {
    disposeEditor();
  }
  editorMount.value = el;
  if (view.value === 'scripts') void ensureEditorReady();
}

async function safeRefresh(label, promise, fallback, failures, quiet = false) {
  try {
    return await promise;
  } catch (err) {
    console.warn(`POSHManager refresh failed for ${label}`, err);
    if (!quiet) failures.push(normalizeRefreshFailure(label, err));
    return typeof fallback === 'function' ? fallback() : fallback;
  }
}

function normalizeRefreshFailure(label, err) {
  const message = err?.message || 'request failed';
  return {
    label,
    message,
    path: err?.path || '',
    status: err?.status || null,
    network: !err?.status
  };
}

function formatRefreshFailures(failures) {
  const visibleFailures = failures.filter(Boolean);
  if (!visibleFailures.length) return '';
  const networkFailures = visibleFailures.filter((failure) => failure.network);
  if (networkFailures.length === visibleFailures.length) {
    const first = networkFailures[0];
    return `API is unreachable (${first.message}). Using last known data where available.`;
  }
  return `Some data did not refresh: ${visibleFailures.slice(0, 2).map((failure) => `${failure.label}: ${failure.message}`).join('; ')}`;
}

async function refreshAll() {
  // Bootstrap all API-backed view state together so navigation does not trigger partial reload races.
  try {
    await api.get('/api/health');
  } catch (err) {
    console.warn('POSHManager refresh skipped because the API health probe failed', err);
    notify(`API is unreachable (${err.message || 'health probe failed'}). Using last known data where available.`);
    return;
  }
  const refreshFailures = [];
  const [meResult, boot, userRows, groupRows, credentialRows, hostRows, hostGroupRows, folderRows, scriptRows, assetFolderRows, assetRows, variableRows, psadtCatalogRows, psadtProfileRows, psadtIntuneRows, graphRows, vcenterRows, runplanRows, jobRows] = await Promise.all([
    safeRefresh('session', api.get('/api/auth/me'), () => ({ user: user.value }), refreshFailures),
    safeRefresh('bootstrap', api.get('/api/bootstrap'), () => ({ summary: summary.value, settings: settings.value }), refreshFailures),
    safeRefresh('users', api.get('/api/users'), () => users.value, refreshFailures, true),
    safeRefresh('groups', api.get('/api/groups'), () => groups.value, refreshFailures),
    safeRefresh('credentials', api.get('/api/credentials'), () => credentials.value, refreshFailures),
    safeRefresh('hosts', api.get('/api/hosts'), () => hosts.value, refreshFailures),
    safeRefresh('host groups', api.get('/api/hosts/groups'), () => hostGroups.value, refreshFailures),
    safeRefresh('folders', api.get('/api/folders'), () => folders.value, refreshFailures),
    safeRefresh('scripts', api.get('/api/scripts'), () => scripts.value, refreshFailures),
    safeRefresh('asset folders', api.get('/api/assets/folders'), () => assetFolders.value, refreshFailures),
    safeRefresh('assets', api.get('/api/assets'), () => assets.value, refreshFailures),
    safeRefresh('variables', api.get('/api/variables/catalog'), () => variableCatalog.value, refreshFailures),
    safeRefresh('PSADT catalog', api.get('/api/psadt/catalog'), () => psadtCatalog.value, refreshFailures),
    safeRefresh('PSADT profiles', api.get('/api/psadt/profiles'), () => psadtProfiles.value, refreshFailures),
    safeRefresh('Intune deployments', api.get('/api/psadt/intune/deployments'), () => psadtIntuneDeployments.value, refreshFailures),
    safeRefresh('Graph connections', api.get('/api/graph/connections'), () => graphConnections.value, refreshFailures),
    safeRefresh('vCenter connections', api.get('/api/hosts/import/vcenter/connections'), () => vcenterConnections.value, refreshFailures),
    safeRefresh('RunPlans', api.get('/api/runplans'), () => runplans.value, refreshFailures),
    safeRefresh('jobs', api.get('/api/jobs'), () => jobs.value, refreshFailures)
  ]);
  syncUser(meResult.user);
  summary.value = boot.summary;
  settings.value = boot.settings;
  Object.keys(settingsDraft).forEach((key) => delete settingsDraft[key]);
  Object.entries(settings.value).forEach(([key, row]) => { settingsDraft[key] = { ...row }; });
  users.value = userRows;
  groups.value = groupRows;
  credentials.value = credentialRows;
  hosts.value = hostRows;
  hostGroups.value = hostGroupRows;
  folders.value = folderRows;
  scripts.value = scriptRows;
  assetFolders.value = assetFolderRows;
  assets.value = assetRows;
  variableCatalog.value = variableRows;
  psadtCatalog.value = psadtCatalogRows;
  psadtProfiles.value = psadtProfileRows;
  psadtIntuneDeployments.value = psadtIntuneRows;
  graphConnections.value = graphRows;
  vcenterConnections.value = vcenterRows;
  runplans.value = runplanRows;
  jobs.value = jobRows;
  if (selectedScript.value) {
    const refreshedSelection = scriptRows.find((script) => script.id === selectedScript.value.id);
    if (refreshedSelection) selectScript(refreshedSelection);
  } else if (scriptRows.length) {
    selectScript(scriptRows[0]);
  }
  const refreshMessage = formatRefreshFailures(refreshFailures);
  if (refreshMessage) notify(refreshMessage);
}

async function openHelpCenter() {
  helpOpen.value = true;
  if (!helpArticles.value.length) await searchHelp(helpQuery.value);
}

async function searchHelp(query = '', category = '') {
  helpLoading.value = true;
  helpQuery.value = query;
  try {
    const params = new URLSearchParams();
    if (query) params.set('q', query);
    if (category) params.set('category', category);
    const result = await api.get(`/api/help${params.toString() ? `?${params}` : ''}`);
    helpArticles.value = result.articles || [];
    helpCategories.value = result.categories || [];
    if (!selectedHelpArticle.value && helpArticles.value.length) {
      await selectHelpArticle(helpArticles.value[0].id);
    } else if (selectedHelpArticle.value && !helpArticles.value.some((article) => article.id === selectedHelpArticle.value.id)) {
      selectedHelpArticle.value = null;
      if (helpArticles.value.length) await selectHelpArticle(helpArticles.value[0].id);
    }
  } finally {
    helpLoading.value = false;
  }
}

async function selectHelpArticle(articleId) {
  helpLoading.value = true;
  try {
    selectedHelpArticle.value = await api.get(`/api/help/${articleId}`);
  } finally {
    helpLoading.value = false;
  }
}

function syncUser(nextUser) {
  user.value = nextUser;
  if (nextUser) localStorage.setItem('poshmanager.user', JSON.stringify(nextUser));
  hydrateProfileForm();
}

function hydrateProfileForm() {
  if (!user.value) return;
  Object.assign(profileForm, {
    displayName: user.value.displayName || '',
    email: user.value.email || '',
    jobTitle: user.value.jobTitle || '',
    phone: user.value.phone || '',
    timezone: user.value.timezone || '',
    avatarUrl: user.value.avatarUrl || ''
  });
}

function selectScript(script) {
  selectedScript.value = script;
  Object.assign(scriptDraft, { ...script });
  activeFolderId.value = script.folderId;
  if (editor) editor.setValue(script.content || '');
  else if (view.value === 'scripts') void ensureEditorReady();
}

function newScript(folderId = null) {
  selectedScript.value = null;
  Object.assign(scriptDraft, {
    id: null,
    folderId,
    name: 'Untitled Script.ps1',
    description: '',
    content: 'Write-Output "Hello from POSHManager"',
    visibility: 'personal',
    groupId: null
  });
  editor?.setValue(scriptDraft.content);
  contextMenu.open = false;
}

async function saveScript() {
  // Monaco is the source of truth for editor content; sync it before sending the API payload.
  scriptDraft.content = editor?.getValue() ?? scriptDraft.content;
  const payload = { ...scriptDraft };
  const saved = scriptDraft.id
    ? await api.put(`/api/scripts/${scriptDraft.id}`, payload)
    : await api.post('/api/scripts', payload);
  await refreshAll();
  selectScript(saved);
  notify('Script saved');
}

async function deleteScript(script) {
  if (!script) return;
  closeContext();
  scriptActionTarget.value = script;
  scriptActionUsage.value = await fetchScriptUsage(script);
  scriptDeleteModalOpen.value = true;
}

async function confirmDeleteScript() {
  if (!scriptActionTarget.value) return;
  await api.delete(`/api/scripts/${scriptActionTarget.value.id}?force=true`);
  selectedScript.value = null;
  await refreshAll();
  closeScriptActionModals();
  notify('Script deleted');
}

async function openRenameScript(script) {
  if (!script) return;
  closeContext();
  scriptActionTarget.value = script;
  scriptRenameForm.name = script.name;
  scriptActionUsage.value = await fetchScriptUsage(script);
  scriptRenameModalOpen.value = true;
}

async function renameScript() {
  if (!scriptActionTarget.value) return;
  const name = scriptRenameForm.name.trim();
  if (!name) return;
  const saved = await api.put(`/api/scripts/${scriptActionTarget.value.id}`, { ...scriptActionTarget.value, name });
  await refreshAll();
  selectScript(saved);
  closeScriptActionModals();
  notify('Script renamed');
}

function openCloneScript(script) {
  if (!script) return;
  closeContext();
  scriptActionTarget.value = script;
  scriptActionUsage.value = null;
  scriptCloneForm.name = cloneScriptName(script.name);
  scriptCloneModalOpen.value = true;
}

async function cloneScript() {
  if (!scriptActionTarget.value) return;
  const name = scriptCloneForm.name.trim();
  if (!name) return;
  const saved = await api.post(`/api/scripts/${scriptActionTarget.value.id}/clone`, { name });
  await refreshAll();
  selectScript(saved);
  closeScriptActionModals();
  notify('Script cloned');
}

async function fetchScriptUsage(script) {
  return api.get(`/api/scripts/${script.id}/usage`).catch(() => ({ runplans: [], intuneDeployments: [], total: 0 }));
}

function closeScriptActionModals() {
  scriptRenameModalOpen.value = false;
  scriptCloneModalOpen.value = false;
  scriptDeleteModalOpen.value = false;
  scriptActionTarget.value = null;
  scriptActionUsage.value = null;
  Object.assign(scriptRenameForm, { name: '' });
  Object.assign(scriptCloneForm, { name: '' });
}

function cloneScriptName(name = 'Untitled Script.ps1') {
  const match = String(name).match(/^(.*?)(\.[^.]+)?$/);
  return `${match?.[1] || name} copy${match?.[2] || ''}`;
}

function openScriptTestRun(script) {
  if (!script) return;
  closeContext();
  scriptTestTarget.value = script;
  scriptTestJob.value = null;
  scriptTestRunning.value = false;
  scriptTestError.value = '';
  scriptTestPollFailures = 0;
  scriptTestModalOpen.value = true;
}

function closeScriptTestRun() {
  scriptTestModalOpen.value = false;
  scriptTestTarget.value = null;
  scriptTestJob.value = null;
  scriptTestRunning.value = false;
  scriptTestError.value = '';
  scriptTestPollFailures = 0;
  stopScriptTestPolling();
}

async function executeScriptTestRun(payload) {
  if (!scriptTestTarget.value) return;
  scriptTestRunning.value = true;
  scriptTestJob.value = null;
  scriptTestError.value = '';
  scriptTestPollFailures = 0;
  stopScriptTestPolling();
  try {
    const job = await api.post(`/api/scripts/${scriptTestTarget.value.id}/test-run`, payload);
    scriptTestJob.value = await api.get(`/api/jobs/${job.id}`).catch(() => job);
    startScriptTestPolling(job.id);
    notify('Script test run started');
  } catch (err) {
    scriptTestRunning.value = false;
    scriptTestError.value = err.message;
    scriptTestJob.value = {
      id: 'not-started',
      scriptName: scriptTestTarget.value?.name,
      status: 'failed',
      logs: [{ stream: 'stderr', line: err.message, createdAt: new Date().toISOString() }]
    };
    notify(err.message);
  }
}

function startScriptTestPolling(jobId) {
  stopScriptTestPolling();
  scriptTestPollTimer = window.setInterval(async () => {
    try {
      const job = await api.get(`/api/jobs/${jobId}`);
      scriptTestPollFailures = 0;
      scriptTestError.value = '';
      scriptTestJob.value = job;
      if (!['queued', 'running'].includes(job.status)) {
        scriptTestRunning.value = false;
        stopScriptTestPolling();
      }
    } catch (err) {
      scriptTestPollFailures += 1;
      scriptTestError.value = `Could not refresh test output (${scriptTestPollFailures}/3): ${err.message}`;
      if (scriptTestPollFailures >= 3) {
        scriptTestRunning.value = false;
        stopScriptTestPolling();
      }
    }
  }, 1000);
}

function stopScriptTestPolling() {
  if (scriptTestPollTimer) window.clearInterval(scriptTestPollTimer);
  scriptTestPollTimer = null;
}

async function createFolder(parentId = null) {
  const name = window.prompt('Folder name');
  if (!name) return;
  await api.post('/api/folders', { name, parentId, visibility: 'personal' });
  await refreshAll();
  closeContext();
}

async function deleteFolder(folder) {
  if (!window.confirm(`Delete folder "${folder.name}" and its subfolders? Scripts inside the folder will move to Unfiled.`)) return;
  await api.delete(`/api/folders/${folder.id}`);
  await refreshAll();
  closeContext();
}

async function refreshAssets() {
  const [folderRows, assetRows] = await Promise.all([
    api.get('/api/assets/folders'),
    api.get('/api/assets')
  ]);
  assetFolders.value = folderRows;
  assets.value = assetRows;
  notify('Asset library refreshed');
}

async function saveAssetFolder(payload) {
  const body = { ...payload };
  const folderId = body.id;
  delete body.id;
  if (folderId) await api.put(`/api/assets/folders/${folderId}`, body);
  else await api.post('/api/assets/folders', body);
  await refreshAssets();
  notify('Asset folder saved');
}

async function deleteAssetFolder(folder) {
  if (!window.confirm(`Delete asset folder "${folder.name}"? Assets in this folder will become unfiled.`)) return;
  await api.delete(`/api/assets/folders/${folder.id}`);
  await refreshAssets();
  notify('Asset folder deleted');
}

async function uploadAsset(payload) {
  const body = new FormData();
  body.append('file', payload.file);
  for (const [key, value] of Object.entries(payload)) {
    if (key === 'file') continue;
    if (value != null) body.append(key, String(value));
  }
  await api.postForm('/api/assets', body);
  await refreshAssets();
  notify('Asset uploaded');
}

async function deleteAsset(asset) {
  if (!window.confirm(`Delete asset "${asset.name}"?`)) return;
  await api.delete(`/api/assets/${asset.id}`);
  await refreshAssets();
  notify('Asset deleted');
}

async function linkAsset(payload) {
  const body = { ...payload };
  const assetId = body.assetId;
  delete body.assetId;
  await api.post(`/api/assets/${assetId}/links`, body);
  notify('Asset link created');
}

async function insertAssetReference(asset) {
  view.value = 'scripts';
  const packagePath = asset.packagePath || asset.originalName;
  await insertVariable(`\n# POSHManager asset: ${asset.name}\n$AssetReference = ${JSON.stringify(asset.referenceToken)}\n$AssetPackagePath = ${JSON.stringify(packagePath)}\n`);
  notify('Asset reference inserted');
}

async function openVariableStudio() {
  variableCatalog.value = await api.get('/api/variables/catalog');
  variableModalOpen.value = true;
}

async function insertVariable(value) {
  await ensureEditorReady();
  const text = String(value || '');
  if (!editor) {
    scriptDraft.content = `${scriptDraft.content || ''}${text}`;
    return;
  }
  const selection = editor.getSelection();
  editor.executeEdits('variable-studio', [{ range: selection, text, forceMoveMarkers: true }]);
  editor.focus();
}

async function saveCustomVariable(payload) {
  const body = { ...payload };
  const id = body.id;
  delete body.id;
  if (id) await api.put(`/api/variables/custom/${id}`, body);
  else await api.post('/api/variables/custom', body);
  variableCatalog.value = await api.get('/api/variables/catalog');
  notify('Variable saved');
}

async function deleteCustomVariable(id) {
  await api.delete(`/api/variables/custom/${id}`);
  variableCatalog.value = await api.get('/api/variables/catalog');
  notify('Variable deleted');
}

async function refreshPsadt() {
  const [catalogRows, profileRows, intuneRows, graphRows, applicationRows] = await Promise.all([
    api.get('/api/psadt/catalog'),
    api.get('/api/psadt/profiles'),
    api.get('/api/psadt/intune/deployments'),
    api.get('/api/graph/connections'),
    api.get('/api/intune/applications')
  ]);
  psadtCatalog.value = catalogRows;
  psadtProfiles.value = profileRows;
  psadtIntuneDeployments.value = intuneRows;
  graphConnections.value = graphRows;
  catalogApplications.value = applicationRows;
  await refreshGovernance(true);
  builderStatus.value = await api.get('/api/psadt/intune/builder/status').catch(() => null);
  notify('PSADT Workbench refreshed');
}

async function analyzeInstaller(payload) {
  try {
    installerAnalysis.value = await api.post('/api/packaging/analyze', payload);
    notify(installerAnalysis.value.primary ? `Detected ${installerAnalysis.value.primary.label}` : (installerAnalysis.value.message || 'No match'));
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function buildIntunewin(payload) {
  if (!payload.setupFile) return notify('Enter the setup file to build', 'error');
  try {
    notify('Building .intunewin package...');
    const result = await api.post(`/api/psadt/intune/deployments/${payload.deploymentId}/build`, { setupFile: payload.setupFile });
    await Promise.all([refreshPsadt(), refreshAssets?.()]);
    notify(`Built ${result.asset.originalName}`);
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function refreshGovernance(silent = false) {
  const [requests, overview] = await Promise.all([
    api.get('/api/intune/change-requests'),
    api.get('/api/intune/reporting/overview')
  ]);
  changeRequests.value = requests;
  reportingOverview.value = overview;
  if (!silent) notify('Governance refreshed');
}

async function approveChangeRequest(id) {
  try {
    await api.post(`/api/intune/change-requests/${id}/approve`, {});
    await refreshGovernance(true);
    notify('Change request approved');
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function rejectChangeRequest(id) {
  try {
    await api.post(`/api/intune/change-requests/${id}/reject`, {});
    await refreshGovernance(true);
    notify('Change request rejected');
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function saveApplication(payload) {
  const body = { ...payload };
  const appId = body.id;
  delete body.id;
  if (appId) await api.put(`/api/intune/applications/${appId}`, body);
  else await api.post('/api/intune/applications', body);
  catalogApplications.value = await api.get('/api/intune/applications');
  notify('Application saved');
}

async function deleteApplication(id) {
  await api.delete(`/api/intune/applications/${id}`);
  catalogApplications.value = await api.get('/api/intune/applications');
  notify('Application deleted');
}

async function checkApplicationUpdate(id) {
  try {
    const result = await api.post(`/api/intune/applications/${id}/check-update`, {});
    catalogApplications.value = await api.get('/api/intune/applications');
    notify(result.application.updateState?.updateAvailable ? `Update available: ${result.application.latestKnownVersion}` : 'Up to date');
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function importApplication(payload) {
  try {
    await api.post('/api/intune/applications/import', payload);
    catalogApplications.value = await api.get('/api/intune/applications');
    notify('Application imported from tenant');
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function savePsadtProfile(payload) {
  const body = { ...payload };
  const profileId = body.id;
  delete body.id;
  if (profileId) await api.put(`/api/psadt/profiles/${profileId}`, body);
  else await api.post('/api/psadt/profiles', body);
  await refreshPsadt();
  notify('PSADT profile saved');
}

async function deletePsadtProfile(id) {
  await api.delete(`/api/psadt/profiles/${id}`);
  await refreshPsadt();
  notify('PSADT profile deleted');
}

async function previewPsadtProfile(id) {
  const rendered = await api.get(`/api/psadt/profiles/${id}/render`);
  view.value = 'scripts';
  await nextTick();
  newScript(activeFolderId.value);
  Object.assign(scriptDraft, {
    name: rendered.scriptName,
    description: `Generated from PSADT profile ${rendered.profile.name}.`,
    content: rendered.script,
    visibility: rendered.profile.visibility,
    groupId: rendered.profile.groupId || null
  });
  editor?.setValue(rendered.script);
  notify('PSADT profile rendered into the editor');
}

async function createScriptFromPsadtProfile(id) {
  const rendered = await api.get(`/api/psadt/profiles/${id}/render`);
  const saved = await api.post('/api/scripts', {
    folderId: activeFolderId.value,
    name: rendered.scriptName,
    description: `Generated from PSADT profile ${rendered.profile.name}. Command: ${rendered.intuneInstallCommand}`,
    content: rendered.script,
    visibility: rendered.profile.visibility,
    groupId: rendered.profile.groupId || null
  });
  await refreshAll();
  selectScript(saved);
  view.value = 'scripts';
  notify('PSADT script created');
}

async function insertPsadtSnippet(body) {
  view.value = 'scripts';
  await insertVariable(`\n${body}\n`);
  notify('PSADT snippet inserted');
}

async function validatePsadtTarget(payload) {
  psadtValidationResult.value = await api.post('/api/psadt/validate', payload);
  notify(`PSADT verifier completed: score ${psadtValidationResult.value.score}`);
}

async function planPsadtMigration(payload) {
  psadtMigrationPlan.value = await api.post('/api/psadt/migration/plan', payload);
  notify(`Migration plan ready: ${psadtMigrationPlan.value.summary.steps} steps`);
}

async function applyPsadtMigration(payload) {
  psadtMigrationPlan.value = await api.post('/api/psadt/migration/apply', { ...payload, createScript: true });
  if (psadtMigrationPlan.value.migratedScript) {
    await refreshAll();
    selectScript(psadtMigrationPlan.value.migratedScript);
  }
  notify('Migrated PSADT script copy created');
}

async function saveIntuneDeployment(payload) {
  const body = { ...payload };
  const deploymentId = body.id;
  delete body.id;
  if (deploymentId) await api.put(`/api/psadt/intune/deployments/${deploymentId}`, body);
  else await api.post('/api/psadt/intune/deployments', body);
  await refreshPsadt();
  notify('Intune deployment plan saved');
}

async function deleteIntuneDeployment(id) {
  await api.delete(`/api/psadt/intune/deployments/${id}`);
  await refreshPsadt();
  notify('Intune deployment plan deleted');
}

async function saveGraphConnection(payload) {
  const body = { ...payload };
  const connectionId = body.id;
  delete body.id;
  if (connectionId) await api.put(`/api/graph/connections/${connectionId}`, body);
  else await api.post('/api/graph/connections', body);
  graphConnections.value = await api.get('/api/graph/connections');
  notify('Graph environment saved');
}

async function deleteGraphConnection(id) {
  await api.delete(`/api/graph/connections/${id}`);
  graphConnections.value = await api.get('/api/graph/connections');
  notify('Graph environment deleted');
}

async function testGraphConnection(id) {
  const result = await api.post(`/api/graph/connections/${id}/test`, {});
  graphConnections.value = await api.get('/api/graph/connections');
  notify(result.message || 'Graph connection test completed');
}

async function saveVCenterConnection(payload) {
  const body = { ...payload };
  const connectionId = body.id;
  delete body.id;
  if (connectionId) await api.put(`/api/hosts/import/vcenter/connections/${connectionId}`, body);
  else await api.post('/api/hosts/import/vcenter/connections', body);
  vcenterConnections.value = await api.get('/api/hosts/import/vcenter/connections');
  notify('vCenter system saved');
}

async function deleteVCenterConnection(id) {
  await api.delete(`/api/hosts/import/vcenter/connections/${id}`);
  vcenterConnections.value = await api.get('/api/hosts/import/vcenter/connections');
  notify('vCenter system deleted');
}

async function testVCenterConnection(id) {
  const result = await api.post(`/api/hosts/import/vcenter/connections/${id}/test`, {});
  vcenterConnections.value = await api.get('/api/hosts/import/vcenter/connections');
  notify(result.message || 'vCenter connection test completed');
}

async function linkGraphApp(payload) {
  const result = await api.post(`/api/psadt/intune/deployments/${payload.deploymentId}/graph/link`, {
    connectionId: payload.connectionId,
    graphAppId: payload.graphAppId
  });
  await refreshPsadt();
  notify(`Linked ${result.name} to Intune app ${result.graphAppId}`);
}

async function syncGraphStatus(payload) {
  graphStatusResult.value = await api.post(`/api/psadt/intune/deployments/${payload.deploymentId}/graph/sync`, {
    connectionId: payload.connectionId,
    graphAppId: payload.graphAppId,
    mode: payload.mode || 'reports'
  });
  await refreshPsadt();
  notify(`Graph sync completed: ${graphStatusResult.value.summary.total} rows`);
}

async function loadGraphStatus(deploymentId) {
  graphStatusResult.value = await api.get(`/api/psadt/intune/deployments/${deploymentId}/graph/status`);
  notify(`Loaded ${graphStatusResult.value.summary.total} Intune status rows`);
}

async function loadGraphAudit(deploymentId) {
  graphAuditResult.value = await api.get(`/api/psadt/intune/deployments/${deploymentId}/graph/audit`);
  notify(`Loaded ${graphAuditResult.value.length} audit entr${graphAuditResult.value.length === 1 ? 'y' : 'ies'}`);
}

function notifyWarnings(warnings) {
  if (warnings?.length) notify(`Note: ${warnings[0]}${warnings.length > 1 ? ` (+${warnings.length - 1} more)` : ''}`);
}

function handledApproval(result, deploymentId) {
  if (result?.status === 'approval-required') {
    notify('Approval required — a change request was created for an admin to approve.');
    refreshGovernance(true);
    loadGraphAudit(deploymentId);
    return true;
  }
  return false;
}

async function publishGraphApp(payload) {
  try {
    const result = await api.post(`/api/psadt/intune/deployments/${payload.deploymentId}/graph/publish`, { connectionId: payload.connectionId });
    if (handledApproval(result, payload.deploymentId)) return;
    notify(`Published to Intune app ${result.graphAppId}`);
    notifyWarnings(result.warnings);
    await refreshPsadt();
    await loadGraphAudit(payload.deploymentId);
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function uploadGraphContent(payload) {
  if (!payload.assetId) return notify('Choose a .intunewin content asset first', 'error');
  try {
    notify('Uploading .intunewin content to Intune...');
    const result = await api.post(`/api/psadt/intune/deployments/${payload.deploymentId}/graph/upload-content`, { connectionId: payload.connectionId, assetId: payload.assetId });
    if (handledApproval(result, payload.deploymentId)) return;
    notify(`Committed content version ${result.content.contentVersionId}`);
    await refreshPsadt();
    await loadGraphAudit(payload.deploymentId);
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function assignGraphApp(payload) {
  try {
    const result = await api.post(`/api/psadt/intune/deployments/${payload.deploymentId}/graph/assign`, { connectionId: payload.connectionId });
    if (handledApproval(result, payload.deploymentId)) return;
    notify(`Assigned ${result.assigned} target(s)`);
    notifyWarnings(result.warnings);
    await loadGraphAudit(payload.deploymentId);
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function checkDrift(payload) {
  try {
    driftResult.value = await api.post(`/api/psadt/intune/deployments/${payload.deploymentId}/graph/drift`, { connectionId: payload.connectionId });
    notify(driftResult.value.inSync ? 'In sync with tenant' : 'Drift detected against tenant');
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function reconcileGraphApp(payload) {
  try {
    const result = await api.post(`/api/psadt/intune/deployments/${payload.deploymentId}/graph/reconcile`, { connectionId: payload.connectionId });
    if (handledApproval(result, payload.deploymentId)) return;
    notify('Re-applied plan to tenant');
    driftResult.value = null;
    await loadGraphAudit(payload.deploymentId);
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function createDeploymentVersion(deploymentId) {
  try {
    const created = await api.post(`/api/psadt/intune/deployments/${deploymentId}/new-version`, {});
    await refreshPsadt();
    notify(`Created ${created.name}`);
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function setGraphRelationships(payload) {
  try {
    const result = await api.post(`/api/psadt/intune/deployments/${payload.deploymentId}/graph/relationships`, { connectionId: payload.connectionId });
    if (handledApproval(result, payload.deploymentId)) return;
    notify(`Set ${result.relationships} relationship(s)`);
    await loadGraphAudit(payload.deploymentId);
  } catch (err) {
    notify(err.message, 'error');
  }
}

async function saveHost() {
  const payload = { ...hostForm, tags: hostForm.tagsText.split(',').map((tag) => tag.trim()).filter(Boolean) };
  if (hostForm.id) await api.put(`/api/hosts/${hostForm.id}`, payload);
  else await api.post('/api/hosts', payload);
  resetHostForm();
  hostModalOpen.value = false;
  await refreshAll();
  notify('Host saved');
}

async function saveHostGroup(payload) {
  const body = { ...payload };
  const groupId = body.id;
  delete body.id;
  if (groupId) await api.put(`/api/hosts/groups/${groupId}`, body);
  else await api.post('/api/hosts/groups', body);
  hostGroups.value = await api.get('/api/hosts/groups');
  notify('Host group saved');
}

async function deleteHostGroup(groupId) {
  await api.delete(`/api/hosts/groups/${groupId}`);
  hostGroups.value = await api.get('/api/hosts/groups');
  await refreshAll();
  notify('Host group deleted');
}

async function syncHostGroup(groupId) {
  await api.post(`/api/hosts/groups/${groupId}/sync`, {});
  hostGroups.value = await api.get('/api/hosts/groups');
  notify('Host group synced');
}

async function syncHostGroups() {
  const result = await api.post('/api/hosts/groups/sync', {});
  hostGroups.value = await api.get('/api/hosts/groups');
  notify(`Dynamic host groups synced: ${result.synced || 0}`);
}

async function openVCenterImport() {
  vcenterImportOpen.value = true;
  vcenterImportError.value = '';
  vcenterImportDiagnostics.value = null;
  vcenterTraceEntries.value = [];
  vcenterTraceOpen.value = false;
  try {
    vcenterImportStatus.value = await api.get('/api/hosts/import/vcenter/status');
    vcenterConnections.value = vcenterImportStatus.value.connections || vcenterConnections.value;
  } catch (err) {
    vcenterImportError.value = err.message;
  }
}

async function previewVCenterImport(payload) {
  vcenterImportLoading.value = true;
  vcenterImportError.value = '';
  vcenterTraceEntries.value = [{
    info: true,
    message: 'vCenter preview request is running.',
    details: {
      filter: payload.filter,
      limit: payload.limit,
      startedAt: new Date().toISOString()
    }
  }];
  vcenterTraceOpen.value = true;
  const watchdogs = [
    setTimeout(() => {
      vcenterTraceEntries.value = [
        ...vcenterTraceEntries.value,
        {
          info: true,
          message: 'Still waiting for the POSHManager API preview response.',
          details: {
            route: '/api/hosts/import/vcenter/preview',
            elapsedMs: 8000,
            note: 'If this remains here, the backend is still waiting on vCenter DNS, TCP, TLS, authentication, or VM inventory response.'
          }
        }
      ];
    }, 8000),
    setTimeout(() => {
      vcenterTraceEntries.value = [
        ...vcenterTraceEntries.value,
        {
          info: true,
          message: 'The preview request is still pending.',
          details: {
            route: '/api/hosts/import/vcenter/preview',
            elapsedMs: 18000,
            note: 'The backend vCenter request timeout should return a traceable timeout shortly unless the API process is blocked before dispatch.'
          }
        }
      ];
    }, 18000)
  ];
  try {
    const result = await api.post('/api/hosts/import/vcenter/preview', payload);
    vcenterImportStatus.value = result.status;
    vcenterImportDiagnostics.value = result.diagnostics || null;
    vcenterTraceEntries.value = result.trace?.length ? result.trace : [{
      info: true,
      message: 'Preview completed, but the API returned no trace entries.',
      details: { count: result.count || 0, diagnostics: result.diagnostics || null }
    }];
    vcenterPreviewRows.value = result.candidates || [];
    vcenterTraceOpen.value = true;
    notify(`Found ${result.count || 0} vCenter host candidate(s) from ${result.diagnostics?.totalFromVCenter ?? 'unknown'} VM(s)`);
  } catch (err) {
    vcenterTraceEntries.value = err.payload?.trace?.length ? err.payload.trace : [{
      info: true,
      message: 'Preview failed before a vCenter HTTP trace could be captured.',
      details: { error: err.message, status: err.status || null }
    }];
    vcenterTraceOpen.value = true;
    vcenterImportError.value = err.message;
    notify(err.message, 'error');
  } finally {
    watchdogs.forEach((timer) => clearTimeout(timer));
    vcenterImportLoading.value = false;
  }
}

async function importVCenterHosts(payload) {
  vcenterImportLoading.value = true;
  vcenterImportError.value = '';
  vcenterTraceEntries.value = [{
    info: true,
    message: 'vCenter import request is running.',
    details: { vmIds: payload.vmIds, startedAt: new Date().toISOString() }
  }];
  vcenterTraceOpen.value = true;
  try {
    const result = await api.post('/api/hosts/import/vcenter', payload);
    vcenterTraceEntries.value = result.trace?.length ? result.trace : vcenterTraceEntries.value;
    await refreshAll();
    vcenterPreviewRows.value = [];
    vcenterImportDiagnostics.value = null;
    vcenterTraceOpen.value = false;
    vcenterImportOpen.value = false;
    const skipped = result.skipped?.length || 0;
    notify(`Imported ${result.imported?.length || 0} host(s)${skipped ? `, skipped ${skipped} duplicate(s)` : ''}`);
  } catch (err) {
    vcenterTraceEntries.value = err.payload?.trace?.length ? err.payload.trace : [{
      info: true,
      message: 'Import failed before a vCenter HTTP trace could be captured.',
      details: { error: err.message, status: err.status || null }
    }];
    vcenterTraceOpen.value = true;
    vcenterImportError.value = err.message;
    notify(err.message, 'error');
  } finally {
    vcenterImportLoading.value = false;
  }
}

function resetHostForm() {
  Object.assign(hostForm, { id: null, name: '', address: '', fqdn: '', osFamily: 'windows', transport: 'winrm', credentialId: null, tagsText: '', visibility: 'shared', groupId: null, sourceType: 'manual', sourceConnectionId: null, sourceRef: '' });
}

function openHostModal(host = null) {
  if (host) Object.assign(hostForm, { ...host, tagsText: host.tags?.join(', ') || '' });
  else resetHostForm();
  hostModalOpen.value = true;
}

async function saveCredential() {
  const payload = { ...credentialForm };
  if (credentialForm.id && !payload.secret) delete payload.secret;
  if (credentialForm.id) await api.put(`/api/credentials/${credentialForm.id}`, payload);
  else await api.post('/api/credentials', payload);
  resetCredentialForm();
  credentialModalOpen.value = false;
  await refreshAll();
  notify('Credential encrypted and stored');
}

function resetCredentialForm() {
  Object.assign(credentialForm, { id: null, name: '', kind: 'username_password', username: '', secret: '', visibility: 'personal', groupId: null });
}

function openCredentialModal(credential = null) {
  if (credential) Object.assign(credentialForm, { ...credential, secret: '', groupId: credential.groupId || null });
  else resetCredentialForm();
  credentialModalOpen.value = true;
}

async function saveRunPlan() {
  const payload = { ...runPlanForm };
  if (runPlanForm.id) await api.put(`/api/runplans/${runPlanForm.id}`, payload);
  else await api.post('/api/runplans', payload);
  resetRunPlanForm();
  runPlanModalOpen.value = false;
  await refreshAll();
  notify('RunPlan saved');
}

function resetRunPlanForm() {
  Object.assign(runPlanForm, { id: null, name: '', description: '', scriptId: '', visibility: 'personal', groupId: null, parallel: true, hostIds: [], hostGroupIds: [] });
}

function openRunPlanModal(runplan = null) {
  if (runplan) Object.assign(runPlanForm, { ...runplan, hostIds: runplan.hostIds || [], hostGroupIds: runplan.hostGroupIds || [] });
  else resetRunPlanForm();
  runPlanModalOpen.value = true;
}

async function executeRunPlan(id) {
  const job = await api.post(`/api/runplans/${id}/execute`, {});
  await refreshAll();
  await openJob(job.id);
  view.value = 'logs';
  notify('RunPlan queued');
}

async function openJob(id) {
  selectedJob.value = await api.get(`/api/jobs/${id}`);
  view.value = 'logs';
  logTab.value = 'job';
  logFilters.page = 1;
}

async function loadSystemLogs() {
  logTab.value = 'system';
  logFilters.page = 1;
  systemLogs.value = await api.get('/api/logs/system?limit=300');
}

async function saveSettings() {
  const payload = {};
  Object.entries(settingsDraft).forEach(([key, row]) => { payload[key] = row.value; });
  settings.value = await api.put('/api/settings', payload);
  notify('Settings saved');
}

async function saveGroup() {
  await api.post('/api/groups', groupForm);
  Object.assign(groupForm, { name: '', description: '' });
  groupModalOpen.value = false;
  await refreshAll();
  notify('Group created');
}

async function saveUser() {
  const payload = { ...userForm };
  if (!payload.password) delete payload.password;
  await api.post('/api/users', payload);
  Object.assign(userForm, { email: '', displayName: '', password: '', role: 'user', groupIds: [] });
  userModalOpen.value = false;
  await refreshAll();
  notify('User created');
}

async function saveProfile() {
  try {
    const result = await api.put('/api/auth/profile', profileForm);
    syncUser(result.user);
    notify('Profile updated');
  } catch (err) {
    notify(err.message);
  }
}

async function changeTheme(theme) {
  if (activeTheme.value === theme) return;
  try {
    const result = await api.put('/api/auth/theme', { theme });
    syncUser(result.user);
    notify('Theme updated');
  } catch (err) {
    notify(err.message);
  }
}

async function changePassword() {
  if (!isExternalAuth.value && passwordForm.newPassword !== passwordForm.confirmPassword) {
    notify('New password confirmation does not match');
    return;
  }

  try {
    const result = await api.post('/api/auth/change-password', {
      currentPassword: passwordForm.currentPassword,
      newPassword: passwordForm.newPassword
    });

    if (result.external && result.redirectUrl) {
      window.open(result.redirectUrl, '_blank', 'noopener,noreferrer');
      notify('Opening Entra password flow');
      return;
    }

    Object.assign(passwordForm, { currentPassword: '', newPassword: '', confirmPassword: '' });
    notify('Password changed');
  } catch (err) {
    notify(err.message);
  }
}

async function ensureEditorReady(attempt = 0) {
  // Monaco needs a mounted, measurable container; retry briefly when switching into Scripts.
  if (view.value !== 'scripts') return;
  await nextTick();
  await new Promise((resolve) => requestAnimationFrame(resolve));
  if (!editorMount.value) {
    if (attempt < 4) setTimeout(() => void ensureEditorReady(attempt + 1), 60);
    return;
  }
  const box = editorMount.value.getBoundingClientRect();
  if ((box.width < 80 || box.height < 80) && attempt < 6) {
    setTimeout(() => void ensureEditorReady(attempt + 1), 80);
    return;
  }
  if (!editor) mountEditor();
  if (editor) {
    editor.setValue(scriptDraft.content || '');
    editor.layout({ width: Math.floor(box.width), height: Math.floor(box.height) });
  }
}

function disposeEditor() {
  if (editor) {
    scriptDraft.content = editor.getValue();
    editor.dispose();
    editor = null;
  }
}

function mountEditor() {
  if (!editorMount.value) return;
  configurePowerShellMonaco(monaco, () => ({
    variableCatalog: variableCatalog.value,
    psadtCatalog: psadtCatalog.value,
    assets: assets.value
  }));
  editor = monaco.editor.create(editorMount.value, {
    value: scriptDraft.content || '',
    language: 'powershell',
    theme: 'posh-vscode-dark',
    automaticLayout: true,
    minimap: { enabled: true, side: 'right', renderCharacters: false, scale: 1 },
    fontFamily: '"Cascadia Code", "Cascadia Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace',
    fontLigatures: false,
    fontSize: 13,
    lineHeight: 21,
    cursorBlinking: 'smooth',
    cursorStyle: 'line',
    cursorWidth: 2,
    renderLineHighlight: 'all',
    renderWhitespace: 'selection',
    glyphMargin: true,
    folding: true,
    foldingHighlight: true,
    links: true,
    mouseWheelZoom: true,
    quickSuggestions: { other: true, comments: false, strings: true },
    suggestOnTriggerCharacters: true,
    acceptSuggestionOnEnter: 'on',
    tabCompletion: 'on',
    wordBasedSuggestions: 'matchingDocuments',
    snippetSuggestions: 'top',
    parameterHints: { enabled: true, cycle: true },
    overviewRulerBorder: false,
    hideCursorInOverviewRuler: true,
    bracketPairColorization: { enabled: true },
    guides: { bracketPairs: true, indentation: true },
    roundedSelection: true,
    scrollBeyondLastLine: false
  });
  editor.onDidChangeModelContent(() => {
    scriptDraft.content = editor.getValue();
    triggerPowerShellSuggest();
  });
  editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Space, () => {
    editor.trigger('poshmanager', 'editor.action.triggerSuggest', {});
  });
}

function triggerPowerShellSuggest() {
  if (!editor) return;
  const position = editor.getPosition();
  const line = editor.getModel()?.getLineContent(position.lineNumber).slice(0, position.column - 1) || '';
  if (!/(^|[\s;|])([A-Za-z]+-[A-Za-z]*|\$[A-Za-z_][\w]*|\{\{[A-Za-z_:.-]*)$/.test(line)) return;
  window.clearTimeout(triggerPowerShellSuggest.timer);
  triggerPowerShellSuggest.timer = window.setTimeout(() => {
    if (view.value === 'scripts') editor?.trigger('poshmanager', 'editor.action.triggerSuggest', {});
  }, 120);
}

function openContext(event, item, type) {
  contextMenu.open = true;
  contextMenu.x = event.clientX;
  contextMenu.y = event.clientY;
  contextMenu.item = item;
  contextMenu.type = type;
}

function closeContext() {
  contextMenu.open = false;
}

function contextTargetFolderId() {
  if (contextMenu.type === 'folder') return contextMenu.item?.id || null;
  if (contextMenu.type === 'script') return contextMenu.item?.folderId || activeFolderId.value || null;
  return activeFolderId.value || null;
}

function notify(message) {
  toast.value = message;
  setTimeout(() => {
    if (toast.value === message) toast.value = '';
  }, 2400);
}

function loadDashboardLayout() {
  try {
    const stored = JSON.parse(localStorage.getItem('poshmanager.dashboardLayout') || '[]');
    if (!Array.isArray(stored) || !stored.length) return defaultDashboardLayout.map((widget) => ({ ...widget }));
    return normalizeGridLayout(stored, defaultDashboardLayout, dashboardWidgetCatalog);
  } catch {
    return defaultDashboardLayout.map((widget) => ({ ...widget }));
  }
}

function saveDashboardLayout() {
  localStorage.setItem('poshmanager.dashboardLayout', JSON.stringify(dashboardLayout));
}

function resetDashboardLayout() {
  dashboardLayout.splice(0, dashboardLayout.length, ...defaultDashboardLayout.map((widget) => ({ ...widget })));
  saveDashboardLayout();
  notify('Dashboard layout reset');
}

function normalizeGridLayout(stored, defaults, catalog) {
  // LocalStorage layouts are merged with the catalog so new widgets appear without breaking old users.
  const catalogById = new Map(catalog.map((item) => [item.id, item]));
  const defaultsById = new Map(defaults.map((item) => [item.id, item]));
  const normalized = [];
  const seen = new Set();
  for (const item of stored) {
    if (!catalogById.has(item.id) || seen.has(item.id)) continue;
    const definition = catalogById.get(item.id);
    const fallback = defaultsById.get(item.id) || {};
    seen.add(item.id);
    normalized.push({
      id: item.id,
      w: Math.min(12, Math.max(definition.minW || 2, Number(item.w || fallback.w || definition.defaultW || 3))),
      h: Math.max(definition.minH || 1, Number(item.h || fallback.h || definition.defaultH || 2)),
      visible: item.visible !== false,
      collapsed: Boolean(item.collapsed)
    });
  }
  for (const item of defaults) {
    if (!seen.has(item.id)) normalized.push({ ...item });
  }
  return normalized;
}

function toggleWidgetCollapse(id) {
  const widget = dashboardLayout.find((item) => item.id === id);
  if (!widget) return;
  widget.collapsed = !widget.collapsed;
  saveDashboardLayout();
}

function resizeDashboardWidget(id, axis, delta) {
  const widget = dashboardLayout.find((item) => item.id === id);
  const definition = dashboardCatalogById.value.get(id) || {};
  if (!widget) return;
  const min = axis === 'w' ? definition.minW || 2 : definition.minH || 1;
  const max = axis === 'w' ? 12 : 5;
  widget[axis] = Math.min(max, Math.max(min, Number(widget[axis] || min) + delta));
  saveDashboardLayout();
}

function setDashboardWidgetVisible(id, visible) {
  const widget = dashboardLayout.find((item) => item.id === id);
  if (!widget) return;
  widget.visible = visible;
  if (visible) dashboardWidgetLibraryOpen.value = false;
  saveDashboardLayout();
}

function dragDashboardStart(id) {
  if (!dashboardEditMode.value) return;
  draggedDashboardWidgetId.value = id;
}

function dropDashboardOn(targetId) {
  if (!dashboardEditMode.value || !draggedDashboardWidgetId.value || draggedDashboardWidgetId.value === targetId) return;
  moveGridItem(dashboardLayout, draggedDashboardWidgetId.value, targetId);
  draggedDashboardWidgetId.value = '';
  saveDashboardLayout();
}

function moveGridItem(layout, sourceId, targetId) {
  const sourceIndex = layout.findIndex((item) => item.id === sourceId);
  const targetIndex = layout.findIndex((item) => item.id === targetId);
  if (sourceIndex < 0 || targetIndex < 0) return;
  const [item] = layout.splice(sourceIndex, 1);
  layout.splice(targetIndex, 0, item);
}

function loadScriptLayout() {
  try {
    const stored = JSON.parse(localStorage.getItem('poshmanager.scriptLayout.v2') || '[]');
    if (!Array.isArray(stored) || !stored.length) return defaultScriptLayout.map((panel) => ({ ...panel }));
    return normalizeGridLayout(stored, defaultScriptLayout, scriptPanelCatalog);
  } catch {
    return defaultScriptLayout.map((panel) => ({ ...panel }));
  }
}

function saveScriptLayout() {
  localStorage.setItem('poshmanager.scriptLayout.v2', JSON.stringify(scriptLayout));
  requestAnimationFrame(() => editor?.layout());
}

function resetScriptLayout() {
  scriptLayout.splice(0, scriptLayout.length, ...defaultScriptLayout.map((panel) => ({ ...panel })));
  saveScriptLayout();
  notify('Script workspace layout reset');
}

function setScriptPanelVisible(id, visible) {
  const panel = scriptLayout.find((item) => item.id === id);
  if (!panel) return;
  panel.visible = visible;
  if (visible) panel.collapsed = false;
  if (visible) scriptPanelLibraryOpen.value = false;
  saveScriptLayout();
}

function isScriptPanelVisible(id) {
  return scriptLayout.find((item) => item.id === id)?.visible !== false;
}

function loadPanelCollapsed() {
  try {
    return JSON.parse(localStorage.getItem('poshmanager.panelCollapsed') || '{}');
  } catch {
    return {};
  }
}

function togglePanel(id) {
  panelCollapsed[id] = !panelCollapsed[id];
  localStorage.setItem('poshmanager.panelCollapsed', JSON.stringify(panelCollapsed));
}

function shortDate(value) {
  return value ? new Date(value).toLocaleString() : '';
}

function settingLabel(key) {
  return settingMetadata[key]?.label || key
    .split('_')
    .filter(Boolean)
    .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
    .join(' ');
}

function settingDescription(key) {
  return settingMetadata[key]?.description || 'Application setting managed by the POSHManager API.';
}

function settingPlaceholder(key) {
  return settingMetadata[key]?.placeholder || '';
}

function isSecretSetting(key) {
  return Boolean(settingMetadata[key]?.secret || /(?:password|secret|token|key)$/i.test(key));
}

function settingSourceLabel(source) {
  return source === 'env' ? 'ENV' : 'DB';
}

function configSectionMeta(section) {
  const meta = {
    Deployment: { icon: Server, description: 'Reverse proxy, trusted origins, and public URLs.' },
    Authentication: { icon: ShieldCheck, description: 'Identity provider and password-flow configuration.' },
    Integrations: { icon: Download, description: 'External systems used for inventory, publishing, and governance workflows.' },
    Execution: { icon: PlayCircle, description: 'PowerShell execution guardrails.' },
    Runtime: { icon: Database, description: 'Container runtime paths and API process settings.' },
    Other: { icon: Settings, description: 'Additional app settings exposed by the API.' }
  };
  return meta[section] || meta.Other;
}

function loadCollapsedConfigSections() {
  try {
    return JSON.parse(localStorage.getItem('poshmanager.collapsedConfigSections') || '{}');
  } catch {
    return {};
  }
}

function toggleConfigSection(section) {
  collapsedConfigSections[section] = !collapsedConfigSections[section];
  localStorage.setItem('poshmanager.collapsedConfigSections', JSON.stringify(collapsedConfigSections));
}

function normalizedBooleanSetting(value) {
  return String(value).toLowerCase() === 'true';
}

function toggleBooleanSetting(row) {
  row.value = normalizedBooleanSetting(row.value) ? 'false' : 'true';
}

function isBooleanSetting(value) {
  return ['true', 'false'].includes(String(value).toLowerCase());
}

</script>
