import * as plugins from '../plugins.js';
import * as shared from './shared/index.js';
import * as appstate from '../appstate.js';
import * as interfaces from '../../ts_interfaces/index.js';
import {
  DeesElement,
  customElement,
  html,
  state,
  css,
  cssManager,
  type TemplateResult,
} from '@design.estate/dees-element';

type TStorageTarget = interfaces.data.IStorageTarget;
type TStorageClass = interfaces.data.IStorageClass;
type TStorageTargetKind = interfaces.data.TStorageTargetKind;
type TStorageClassPurpose = interfaces.data.TStorageClassPurpose;

@customElement('ob-view-settings')
export class ObViewSettings extends DeesElement {
  @state()
  accessor settingsState: appstate.ISettingsState = {
    settings: null,
    backupPasswordConfigured: false,
    managedDcRouterStatus: null,
    storageTargets: [],
    storageClasses: [],
  };

  @state()
  accessor loginState: appstate.ILoginState = {
    identity: null,
    phase: 'anonymous',
    generation: 0,
  };

  constructor() {
    super();

    const settingsSub = appstate.settingsStatePart
      .select((s) => s)
      .subscribe((newState) => {
        this.settingsState = newState;
      });
    this.rxSubscriptions.push(settingsSub);

    const loginSub = appstate.loginStatePart
      .select((s) => s)
      .subscribe((newState) => {
        this.loginState = newState;
      });
    this.rxSubscriptions.push(loginSub);
  }

  public static styles = [
    cssManager.defaultStyles,
    shared.viewHostCss,
    css`
      dees-tile {
        display: block;
        margin-bottom: 24px;
      }

      .gateway-header {
        height: 36px;
        display: flex;
        align-items: center;
        padding: 0 16px;
        width: 100%;
        box-sizing: border-box;
      }

      .gateway-heading {
        flex: 1;
        display: flex;
        align-items: baseline;
        gap: 8px;
        min-width: 0;
      }

      .gateway-title {
        font-size: 13px;
        font-weight: 500;
        letter-spacing: -0.01em;
        color: var(--dees-color-text-secondary);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      .gateway-subtitle {
        font-size: 12px;
        color: var(--dees-color-text-muted);
        letter-spacing: -0.01em;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      .gateway-content {
        padding: 20px;
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 16px;
      }

      .gateway-mode-row,
      .gateway-status-row {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 12px;
        padding: 16px 20px;
        border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
      }

      .gateway-mode-row {
        justify-content: flex-start;
      }

      .gateway-mode-button {
        border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
        border-radius: 999px;
        background: ${cssManager.bdTheme('#ffffff', '#18181b')};
        color: ${cssManager.bdTheme('#3f3f46', '#d4d4d8')};
        padding: 8px 12px;
        font: inherit;
        cursor: pointer;
      }

      .gateway-mode-button.active {
        border-color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
        background: ${cssManager.bdTheme('#eff6ff', '#172554')};
        color: ${cssManager.bdTheme('#1d4ed8', '#bfdbfe')};
      }

      .gateway-status-label {
        font-size: 12px;
        font-weight: 600;
        text-transform: uppercase;
        color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
      }

      .gateway-status-value {
        margin-top: 4px;
        font-size: 14px;
        color: ${cssManager.bdTheme('#18181b', '#fafafa')};
      }

      .gateway-status-error,
      .gateway-disabled {
        color: ${cssManager.bdTheme('#b91c1c', '#fca5a5')};
        font-size: 13px;
      }

      .gateway-disabled {
        grid-column: 1 / -1;
      }

      .gateway-actions {
        display: flex;
        gap: 8px;
      }

      .gateway-field.full {
        grid-column: 1 / -1;
      }

      .gateway-readonly {
        padding: 10px 12px;
        border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
        border-radius: 8px;
        background: ${cssManager.bdTheme('#fafafa', '#18181b')};
      }

      .gateway-readonly-label {
        font-size: 12px;
        font-weight: 600;
        color: ${cssManager.bdTheme('#52525b', '#d4d4d8')};
      }

      .gateway-readonly-value {
        margin-top: 4px;
        font-size: 13px;
        color: ${cssManager.bdTheme('#18181b', '#fafafa')};
        word-break: break-all;
      }

      .gateway-readonly-hint {
        margin-top: 4px;
        font-size: 12px;
        color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
      }

      dees-input-text {
        width: 100%;
      }

      .gateway-footer {
        display: flex;
        flex-direction: row;
        justify-content: flex-end;
        align-items: center;
        gap: 0;
        height: 36px;
        width: 100%;
        box-sizing: border-box;
      }

      .tile-button {
        padding: 0 16px;
        height: 100%;
        text-align: center;
        font-size: 12px;
        font-weight: 500;
        cursor: pointer;
        user-select: none;
        transition: all 0.15s ease;
        background: transparent;
        border: none;
        border-left: 1px solid var(--dees-color-border-subtle);
        color: var(--dees-color-text-muted);
        white-space: nowrap;
        display: flex;
        align-items: center;
        gap: 6px;
      }

      .storage-content {
        padding: 20px;
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 16px;
      }

      .storage-panel {
        border: 1px solid var(--dees-color-border-subtle);
        border-radius: 10px;
        padding: 14px;
        background: ${cssManager.bdTheme('#fafafa', '#18181b')};
      }

      .storage-panel.full {
        grid-column: 1 / -1;
      }

      .storage-panel-title {
        font-size: 13px;
        font-weight: 600;
        color: var(--dees-color-text-primary);
        margin-bottom: 8px;
      }

      .storage-row {
        display: flex;
        justify-content: space-between;
        gap: 12px;
        padding: 8px 0;
        border-top: 1px solid var(--dees-color-border-subtle);
        font-size: 13px;
      }

      .storage-row:first-of-type {
        border-top: none;
      }

      .storage-meta {
        color: var(--dees-color-text-muted);
        text-align: right;
      }

      .storage-panel dees-table {
        display: block;
        margin-top: 8px;
      }

      .storage-name {
        font-weight: 600;
        color: var(--dees-color-text-primary);
      }

      .storage-description {
        margin-top: 4px;
        color: var(--dees-color-text-muted);
        font-size: 12px;
      }

      .storage-badge {
        display: inline-flex;
        align-items: center;
        border-radius: 999px;
        padding: 3px 8px;
        background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
        color: var(--dees-color-text-secondary);
        font-size: 12px;
      }

      .storage-config {
        font-family: monospace;
        color: var(--dees-color-text-muted);
        overflow-wrap: anywhere;
      }

      .storage-empty {
        padding: 24px;
        text-align: center;
        color: var(--dees-color-text-muted);
      }

      .storage-select {
        width: 100%;
        margin-top: 8px;
        padding: 10px 12px;
        border: 1px solid var(--dees-color-border-subtle);
        border-radius: 8px;
        background: ${cssManager.bdTheme('#ffffff', '#09090b')};
        color: var(--dees-color-text-primary);
      }

      .tile-button:first-child {
        border-left: none;
      }

      .tile-button:hover {
        background: var(--dees-color-hover);
        color: var(--dees-color-text-primary);
      }

      .tile-button.primary {
        color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
        font-weight: 600;
      }

      .tile-button.primary:hover {
        background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
        color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
      }

      @media (max-width: 700px) {
        .gateway-content {
          grid-template-columns: 1fr;
        }

        .gateway-status-row {
          align-items: flex-start;
          flex-direction: column;
        }

        .storage-content {
          grid-template-columns: 1fr;
        }
      }
    `,
  ];

  async connectedCallback() {
    super.connectedCallback();
    await appstate.dispatchProtectedAction(appstate.fetchSettingsAction, null);
  }

  public render(): TemplateResult {
    return html`
      <ob-sectionheading>Settings</ob-sectionheading>
      ${this.renderAdminUiSettings()}
      ${this.renderExternalGatewaySettings()}
      ${this.renderStorageSettings()}
      <sz-settings-view
        .settings=${this.settingsState.settings || {
          darkMode: true,
          cloudflareToken: '',
          cloudflareZoneId: '',
          adminUiDomain: '',
          dcrouterMode: 'managed',
          dcrouterManagedImage: 'code.foss.global/serve.zone/dcrouter:latest',
          dcrouterManagedOpsPort: 3300,
          dcrouterManagedHttpPort: 80,
          dcrouterManagedHttpsPort: 443,
          dcrouterManagedDataDir: './.nogit/dcrouter-data',
          dcrouterGatewayClientId: '',
          platformStorageClass: 'local-hot',
          autoRenewCerts: false,
          renewalThreshold: 30,
          acmeEmail: '',
          httpPort: 80,
          httpsPort: 443,
          forceHttps: false,
        }}
        .currentUser=${this.loginState.identity?.username || 'admin'}
        @setting-change=${(e: CustomEvent) => {
          const { key, value } = e.detail;
          appstate.dispatchProtectedAction(appstate.updateSettingsAction, {
            settings: { [key]: value },
          });
        }}
        @save=${(e: CustomEvent) => {
          appstate.dispatchProtectedAction(appstate.updateSettingsAction, {
            settings: e.detail,
          });
        }}
        @change-password=${(e: CustomEvent) => {
          console.log('Change password requested:', e.detail);
        }}
        @reset=${() => {
          appstate.dispatchProtectedAction(appstate.fetchSettingsAction, null);
        }}
      ></sz-settings-view>
    `;
  }

  private renderStorageSettings(): TemplateResult {
    const settings = this.settingsState.settings;
    const storageClasses: TStorageClass[] = this.settingsState.storageClasses;
    const databaseClasses = storageClasses.filter((storageClassArg) => storageClassArg.purpose === 'database');
    const selectedPlatformClass = settings?.platformStorageClass ||
      databaseClasses.find((storageClassArg) => storageClassArg.isDefault)?.name ||
      'local-hot';

    return html`
      <dees-tile>
        <div slot="header" class="gateway-header">
          <div class="gateway-heading">
            <span class="gateway-title">Storage Classes</span>
            <span class="gateway-subtitle">Route service and platform volumes to local, network, backup, or custom Docker storage</span>
          </div>
        </div>
        <div class="storage-content">
          <div class="storage-panel full">
            <div class="storage-panel-title">Platform Data Storage</div>
            <div class="gateway-readonly-hint">
              MongoDB, SmartStorage, Valkey, ClickHouse, and MariaDB use this class unless a provider-specific setting overrides it.
            </div>
            ${databaseClasses.length ? html`
              <select
                class="storage-select"
                .value=${selectedPlatformClass}
                @change=${(event: Event) => this.updateGatewayDraft('platformStorageClass', (event.target as HTMLSelectElement).value)}
              >
                ${databaseClasses.map((storageClassArg) => html`
                  <option value=${storageClassArg.name} ?selected=${storageClassArg.name === selectedPlatformClass}>
                    ${storageClassArg.displayName} (${storageClassArg.name})
                  </option>
                `)}
              </select>
            ` : html`<div class="storage-empty">No database storage classes configured.</div>`}
          </div>
          <div class="storage-panel">
            ${this.renderStorageClassTable(storageClasses)}
          </div>
          <div class="storage-panel">
            ${this.renderStorageTargetTable(this.settingsState.storageTargets)}
          </div>
        </div>
        <div slot="footer" class="gateway-footer">
          <button class="tile-button primary" type="button" @click=${() => this.saveStorageSettings()}>
            Save Storage Settings
          </button>
        </div>
      </dees-tile>
    `;
  }

  private renderStorageClassTable(storageClassesArg: TStorageClass[]): TemplateResult {
    return html`
      <dees-table
        .heading1=${'Classes'}
        .heading2=${'Logical classes selected by services, app templates, and platform providers'}
        .data=${storageClassesArg}
        .dataName=${'storage class'}
        .rowKey=${'name'}
        .showColumnFilters=${true}
        .displayFunction=${(storageClassArg: TStorageClass) => ({
          Name: html`
            <div>
              <div class="storage-name">${storageClassArg.displayName}</div>
              <div class="storage-description">${storageClassArg.name}</div>
              ${storageClassArg.description ? html`<div class="storage-description">${storageClassArg.description}</div>` : null}
            </div>
          `,
          Purpose: html`<span class="storage-badge">${storageClassArg.purpose}</span>`,
          Target: storageClassArg.targetName,
          Driver: storageClassArg.driver || '-',
          Default: storageClassArg.isDefault ? html`<span class="storage-badge">Default</span>` : '-',
        })}
        .dataActions=${[
          {
            name: 'New class',
            iconName: 'lucide:Plus',
            type: ['header'],
            actionFunc: async () => {
              await this.openStorageClassDialog();
            },
          },
          {
            name: 'Edit class',
            iconName: 'lucide:Pencil',
            type: ['inRow', 'contextmenu'],
            actionFunc: async (actionDataArg: plugins.deesCatalog.ITableActionDataArg<TStorageClass>) => {
              await this.openStorageClassDialog(actionDataArg.item);
            },
          },
          {
            name: 'Delete class',
            iconName: 'lucide:Trash2',
            type: ['inRow', 'contextmenu'],
            actionRelevancyCheckFunc: (storageClassArg: TStorageClass) => !storageClassArg.isDefault,
            actionFunc: async (actionDataArg: plugins.deesCatalog.ITableActionDataArg<TStorageClass>) => {
              await this.deleteStorageClass(actionDataArg.item);
            },
          },
        ] as plugins.deesCatalog.ITableAction<TStorageClass>[]}
      ></dees-table>
    `;
  }

  private renderStorageTargetTable(storageTargetsArg: TStorageTarget[]): TemplateResult {
    const storageClasses: TStorageClass[] = this.settingsState.storageClasses;
    return html`
      <dees-table
        .heading1=${'Targets'}
        .heading2=${'Physical Docker volume backends for local, network, backup, and custom storage'}
        .data=${storageTargetsArg}
        .dataName=${'storage target'}
        .rowKey=${'name'}
        .showColumnFilters=${true}
        .displayFunction=${(targetArg: TStorageTarget) => ({
          Name: html`
            <div>
              <div class="storage-name">${targetArg.displayName}</div>
              <div class="storage-description">${targetArg.name}</div>
            </div>
          `,
          Kind: html`<span class="storage-badge">${targetArg.kind}</span>`,
          Config: html`<span class="storage-config">${this.describeStorageTargetConfig(targetArg)}</span>`,
          Classes: storageClasses.filter((storageClassArg) => storageClassArg.targetName === targetArg.name).length,
        })}
        .dataActions=${[
          {
            name: 'New target',
            iconName: 'lucide:Plus',
            type: ['header'],
            actionFunc: async () => {
              await this.openStorageTargetDialog();
            },
          },
          {
            name: 'Edit target',
            iconName: 'lucide:Pencil',
            type: ['inRow', 'contextmenu'],
            actionFunc: async (actionDataArg: plugins.deesCatalog.ITableActionDataArg<TStorageTarget>) => {
              await this.openStorageTargetDialog(actionDataArg.item);
            },
          },
          {
            name: 'Delete target',
            iconName: 'lucide:Trash2',
            type: ['inRow', 'contextmenu'],
            actionRelevancyCheckFunc: (targetArg: TStorageTarget) => {
              return !storageClasses.some((storageClassArg) => storageClassArg.targetName === targetArg.name);
            },
            actionFunc: async (actionDataArg: plugins.deesCatalog.ITableActionDataArg<TStorageTarget>) => {
              await this.deleteStorageTarget(actionDataArg.item);
            },
          },
        ] as plugins.deesCatalog.ITableAction<TStorageTarget>[]}
      ></dees-table>
    `;
  }

  private describeStorageTargetConfig(targetArg: TStorageTarget): string {
    const config = targetArg.config || {};
    if (targetArg.kind === 'local' || targetArg.kind === 'backup') {
      return config.path || 'Docker managed volumes';
    }
    if (targetArg.kind === 'nfs') {
      return `${config.server || 'server'}:${config.exportPath || 'exportPath'}`;
    }
    if (targetArg.kind === 'smb') {
      return `//${config.server || 'server'}/${config.share || 'share'}`;
    }
    return [config.driver, config.device].filter(Boolean).join(' ') || 'Custom driver options';
  }

  private async openStorageTargetDialog(targetArg?: TStorageTarget): Promise<void> {
    const isEdit = !!targetArg;
    const config = targetArg?.config || {};
    const kindOptions: TStorageTargetKind[] = ['local', 'nfs', 'smb', 'backup', 'custom'];
    await plugins.deesCatalog.DeesModal.createAndShow({
      heading: targetArg ? `Edit storage target: ${targetArg.name}` : 'Create storage target',
      width: 'large',
      mobileFullscreen: true,
      content: html`
        ${this.renderStorageDialogStyles()}
        <div class="storage-dialog">
          ${targetArg
            ? this.renderStorageDialogReadonly('Name', targetArg.name, 'Storage target names are stable references and cannot be renamed here.')
            : this.renderStorageDialogTextField('name', 'Name', '', 'Lowercase stable name, for example nfs-apps or backup-nas.')}
          ${this.renderStorageDialogTextField('displayName', 'Display Name', targetArg?.displayName || '', 'Human-readable label shown in the settings UI.')}
          <label class="storage-dialog-field">
            <span>Kind</span>
            <select data-field="kind">
              ${kindOptions.map((kindArg) => html`
                <option value=${kindArg} ?selected=${kindArg === (targetArg?.kind || 'local')}>${kindArg}</option>
              `)}
            </select>
          </label>
          ${this.renderStorageDialogTextField('path', 'Local Path', config.path || '', 'Optional bind root for local and backup targets.')}
          ${this.renderStorageDialogTextField('server', 'Server', config.server || '', 'NFS or SMB server hostname/IP.')}
          ${this.renderStorageDialogTextField('exportPath', 'NFS Export Path', config.exportPath || '', 'NFS export path, for example /exports/onebox.')}
          ${this.renderStorageDialogTextField('share', 'SMB Share', config.share || '', 'SMB share name without leading slashes.')}
          ${this.renderStorageDialogTextField('username', 'SMB Username', config.username || '', 'Optional SMB username.')}
          ${this.renderStorageDialogTextField('password', 'SMB Password', config.password || '', 'Optional SMB password.', true)}
          ${this.renderStorageDialogTextField('domain', 'SMB Domain', config.domain || '', 'Optional SMB workgroup/domain.')}
          ${this.renderStorageDialogTextField('version', 'SMB Version', config.version || '', 'Optional CIFS version, for example 3.0.')}
          ${this.renderStorageDialogTextField('driver', 'Custom Driver', config.driver || '', 'Docker volume driver for custom targets.')}
          ${this.renderStorageDialogTextField('device', 'Custom Device', config.device || '', 'Docker local driver device override.')}
          <label class="storage-dialog-field full">
            <span>Driver Options JSON</span>
            <textarea data-field="options" .value=${this.stringifyOptions(config.options)}></textarea>
            <small>Optional JSON object merged into Docker volume options.</small>
          </label>
        </div>
      `,
      menuOptions: [
        {
          name: 'Cancel',
          action: async (modalRef?: plugins.deesCatalog.DeesModal) => {
            await modalRef?.destroy();
          },
        },
        {
          name: isEdit ? 'Save target' : 'Create target',
          action: async (modalRef?: plugins.deesCatalog.DeesModal) => {
            if (!modalRef) return;
            try {
              const storageTarget = this.readStorageTargetFromModal(modalRef, targetArg);
              await appstate.dispatchProtectedAction(appstate.upsertStorageTargetAction, {
                storageTarget,
              });
              await modalRef.destroy();
              await plugins.deesCatalog.DeesToast.success(`Storage target ${storageTarget.name} saved`);
            } catch (err) {
              await plugins.deesCatalog.DeesToast.error(this.getErrorMessage(err));
            }
          },
        },
      ],
    });
  }

  private async openStorageClassDialog(storageClassArg?: TStorageClass): Promise<void> {
    const isEdit = !!storageClassArg;
    const purposeOptions: TStorageClassPurpose[] = ['runtime', 'database', 'backup'];
    const targetOptions: TStorageTarget[] = this.settingsState.storageTargets;
    await plugins.deesCatalog.DeesModal.createAndShow({
      heading: storageClassArg ? `Edit storage class: ${storageClassArg.name}` : 'Create storage class',
      width: 'large',
      mobileFullscreen: true,
      content: html`
        ${this.renderStorageDialogStyles()}
        <div class="storage-dialog">
          ${storageClassArg
            ? this.renderStorageDialogReadonly('Name', storageClassArg.name, 'Storage class names are stable references and cannot be renamed here.')
            : this.renderStorageDialogTextField('name', 'Name', '', 'Lowercase stable name, for example nfs-runtime or backup-cold.')}
          ${this.renderStorageDialogTextField('displayName', 'Display Name', storageClassArg?.displayName || '', 'Human-readable label shown in forms and tables.')}
          <label class="storage-dialog-field full">
            <span>Description</span>
            <textarea data-field="description" .value=${storageClassArg?.description || ''}></textarea>
          </label>
          <label class="storage-dialog-field">
            <span>Target</span>
            <select data-field="targetName">
              ${targetOptions.map((targetArg: TStorageTarget) => html`
                <option value=${targetArg.name} ?selected=${targetArg.name === (storageClassArg?.targetName || targetOptions[0]?.name)}>
                  ${targetArg.displayName} (${targetArg.name})
                </option>
              `)}
            </select>
          </label>
          <label class="storage-dialog-field">
            <span>Purpose</span>
            <select data-field="purpose">
              ${purposeOptions.map((purposeArg) => html`
                <option value=${purposeArg} ?selected=${purposeArg === (storageClassArg?.purpose || 'runtime')}>${purposeArg}</option>
              `)}
            </select>
          </label>
          ${this.renderStorageDialogTextField('driver', 'Driver Override', storageClassArg?.driver || '', 'Optional Docker volume driver override.')}
          <label class="storage-dialog-field checkbox-field">
            <input data-field="isDefault" type="checkbox" .checked=${!!storageClassArg?.isDefault} />
            <span>Use as default for this purpose</span>
          </label>
          <label class="storage-dialog-field full">
            <span>Class Options JSON</span>
            <textarea data-field="options" .value=${this.stringifyOptions(storageClassArg?.options)}></textarea>
            <small>Optional JSON object merged into Docker volume options after target options.</small>
          </label>
        </div>
      `,
      menuOptions: [
        {
          name: 'Cancel',
          action: async (modalRef?: plugins.deesCatalog.DeesModal) => {
            await modalRef?.destroy();
          },
        },
        {
          name: isEdit ? 'Save class' : 'Create class',
          action: async (modalRef?: plugins.deesCatalog.DeesModal) => {
            if (!modalRef) return;
            try {
              const storageClass = this.readStorageClassFromModal(modalRef, storageClassArg);
              await appstate.dispatchProtectedAction(appstate.upsertStorageClassAction, {
                storageClass,
              });
              await modalRef.destroy();
              await plugins.deesCatalog.DeesToast.success(`Storage class ${storageClass.name} saved`);
            } catch (err) {
              await plugins.deesCatalog.DeesToast.error(this.getErrorMessage(err));
            }
          },
        },
      ],
    });
  }

  private renderStorageDialogStyles(): TemplateResult {
    return html`
      <style>
        .storage-dialog {
          display: grid;
          grid-template-columns: repeat(2, minmax(0, 1fr));
          gap: 14px;
        }

        .storage-dialog-field {
          display: flex;
          flex-direction: column;
          gap: 6px;
          min-width: 0;
          font-size: 13px;
          color: var(--dees-color-text-secondary);
        }

        .storage-dialog-field.full,
        .storage-dialog-readonly.full {
          grid-column: 1 / -1;
        }

        .storage-dialog-field > span,
        .storage-dialog-readonly-label {
          font-weight: 600;
          color: var(--dees-color-text-primary);
        }

        .storage-dialog-field select,
        .storage-dialog-field textarea,
        .storage-dialog-readonly-value {
          width: 100%;
          box-sizing: border-box;
          border: 1px solid var(--dees-color-border-subtle);
          border-radius: 8px;
          background: ${cssManager.bdTheme('#ffffff', '#09090b')};
          color: var(--dees-color-text-primary);
          padding: 10px 12px;
          font: inherit;
        }

        .storage-dialog-field textarea {
          min-height: 92px;
          resize: vertical;
          font-family: monospace;
        }

        .storage-dialog-field dees-input-text {
          width: 100%;
        }

        .storage-dialog-field small,
        .storage-dialog-readonly-hint {
          color: var(--dees-color-text-muted);
          font-size: 12px;
        }

        .storage-dialog-readonly {
          display: flex;
          flex-direction: column;
          gap: 6px;
        }

        .checkbox-field {
          flex-direction: row;
          align-items: center;
          padding-top: 26px;
        }

        .checkbox-field input {
          width: 16px;
          height: 16px;
        }

        @media (max-width: 700px) {
          .storage-dialog {
            grid-template-columns: 1fr;
          }
        }
      </style>
    `;
  }

  private renderStorageDialogTextField(
    keyArg: string,
    labelArg: string,
    valueArg: string,
    descriptionArg: string,
    isPasswordArg = false,
  ): TemplateResult {
    return html`
      <label class="storage-dialog-field">
        <dees-input-text
          data-field=${keyArg}
          .key=${keyArg}
          .label=${labelArg}
          .value=${valueArg}
          .description=${descriptionArg}
          .isPasswordBool=${isPasswordArg}
        ></dees-input-text>
      </label>
    `;
  }

  private renderStorageDialogReadonly(labelArg: string, valueArg: string, hintArg: string): TemplateResult {
    return html`
      <div class="storage-dialog-readonly">
        <div class="storage-dialog-readonly-label">${labelArg}</div>
        <div class="storage-dialog-readonly-value">${valueArg}</div>
        <div class="storage-dialog-readonly-hint">${hintArg}</div>
      </div>
    `;
  }

  private readStorageTargetFromModal(
    modalRef: plugins.deesCatalog.DeesModal,
    existingTargetArg?: TStorageTarget,
  ): Omit<TStorageTarget, 'createdAt' | 'updatedAt'> {
    const contentEl = this.getStorageModalContent(modalRef);
    const name = existingTargetArg?.name || this.normalizeStorageName(this.getStorageModalFieldValue(contentEl, 'name'));
    const displayName = this.getStorageModalFieldValue(contentEl, 'displayName') || name;
    const kind = (this.getStorageModalFieldValue(contentEl, 'kind') || existingTargetArg?.kind || 'local') as TStorageTargetKind;
    const config: interfaces.data.IStorageTargetConfig = {};

    for (const key of ['path', 'server', 'exportPath', 'share', 'username', 'password', 'domain', 'version', 'driver', 'device'] as const) {
      const value = this.getStorageModalFieldValue(contentEl, key);
      if (value) {
        config[key] = value;
      }
    }

    const options = this.parseOptionsJson(this.getStorageModalFieldValue(contentEl, 'options'), 'Driver Options JSON');
    if (options) {
      config.options = options;
    }

    return {
      id: existingTargetArg?.id,
      name,
      displayName,
      kind,
      config,
    };
  }

  private readStorageClassFromModal(
    modalRef: plugins.deesCatalog.DeesModal,
    existingClassArg?: TStorageClass,
  ): Omit<TStorageClass, 'createdAt' | 'updatedAt'> {
    const contentEl = this.getStorageModalContent(modalRef);
    const name = existingClassArg?.name || this.normalizeStorageName(this.getStorageModalFieldValue(contentEl, 'name'));
    const displayName = this.getStorageModalFieldValue(contentEl, 'displayName') || name;
    const targetName = this.getStorageModalFieldValue(contentEl, 'targetName') || existingClassArg?.targetName || this.settingsState.storageTargets[0]?.name || '';
    if (!targetName) {
      throw new Error('Create a storage target before creating a storage class.');
    }
    const purpose = (this.getStorageModalFieldValue(contentEl, 'purpose') || existingClassArg?.purpose || 'runtime') as TStorageClassPurpose;
    const driver = this.getStorageModalFieldValue(contentEl, 'driver') || undefined;
    const description = this.getStorageModalFieldValue(contentEl, 'description') || undefined;
    const options = this.parseOptionsJson(this.getStorageModalFieldValue(contentEl, 'options'), 'Class Options JSON');

    return {
      id: existingClassArg?.id,
      name,
      displayName,
      description,
      targetName,
      purpose,
      isDefault: this.getStorageModalCheckboxValue(contentEl, 'isDefault'),
      driver,
      options,
    };
  }

  private getStorageModalContent(modalRef: plugins.deesCatalog.DeesModal): Element {
    const contentEl = modalRef.shadowRoot?.querySelector('.content');
    if (!contentEl) {
      throw new Error('Unable to read storage form content.');
    }
    return contentEl;
  }

  private getStorageModalFieldValue(contentEl: Element, fieldArg: string): string {
    const field = contentEl.querySelector(`[data-field="${fieldArg}"]`) as (HTMLElement & { value?: string }) | null;
    return field?.value?.trim() || '';
  }

  private getStorageModalCheckboxValue(contentEl: Element, fieldArg: string): boolean {
    const field = contentEl.querySelector(`[data-field="${fieldArg}"]`) as HTMLInputElement | null;
    return !!field?.checked;
  }

  private parseOptionsJson(valueArg: string, labelArg: string): Record<string, string> | undefined {
    if (!valueArg) return undefined;
    let parsed: unknown;
    try {
      parsed = JSON.parse(valueArg);
    } catch {
      throw new Error(`${labelArg} must be valid JSON.`);
    }
    if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
      throw new Error(`${labelArg} must be a JSON object.`);
    }
    const options: Record<string, string> = {};
    for (const [key, value] of Object.entries(parsed)) {
      if (value === undefined || value === null) continue;
      options[key] = String(value);
    }
    return Object.keys(options).length ? options : undefined;
  }

  private stringifyOptions(optionsArg?: Record<string, string>): string {
    return optionsArg && Object.keys(optionsArg).length ? JSON.stringify(optionsArg, null, 2) : '';
  }

  private normalizeStorageName(nameArg: string): string {
    const name = nameArg.trim().toLowerCase();
    if (!/^[a-z0-9][a-z0-9_.-]{1,80}$/.test(name)) {
      throw new Error('Storage names must be 2-81 lowercase characters using letters, numbers, dot, dash, or underscore.');
    }
    return name;
  }

  private async deleteStorageClass(storageClassArg: TStorageClass): Promise<void> {
    if (storageClassArg.isDefault) {
      await plugins.deesCatalog.DeesToast.warning('Default storage classes cannot be deleted.');
      return;
    }
    if (!globalThis.confirm(`Delete storage class ${storageClassArg.name}?`)) return;
    try {
      await appstate.dispatchProtectedAction(appstate.deleteStorageClassAction, {
        name: storageClassArg.name,
      });
      await plugins.deesCatalog.DeesToast.success(`Storage class ${storageClassArg.name} deleted`);
    } catch (err) {
      await plugins.deesCatalog.DeesToast.error(this.getErrorMessage(err));
    }
  }

  private async deleteStorageTarget(targetArg: TStorageTarget): Promise<void> {
    const usedByClasses = this.settingsState.storageClasses.filter((storageClassArg: TStorageClass) => storageClassArg.targetName === targetArg.name);
    if (usedByClasses.length) {
      await plugins.deesCatalog.DeesToast.warning(`Target ${targetArg.name} is still used by ${usedByClasses.length} storage class(es).`);
      return;
    }
    if (!globalThis.confirm(`Delete storage target ${targetArg.name}?`)) return;
    try {
      await appstate.dispatchProtectedAction(appstate.deleteStorageTargetAction, {
        name: targetArg.name,
      });
      await plugins.deesCatalog.DeesToast.success(`Storage target ${targetArg.name} deleted`);
    } catch (err) {
      await plugins.deesCatalog.DeesToast.error(this.getErrorMessage(err));
    }
  }

  private getErrorMessage(errArg: unknown): string {
    return errArg instanceof Error ? errArg.message : String(errArg);
  }

  private renderAdminUiSettings(): TemplateResult {
    const settings = this.settingsState.settings;
    return html`
      <dees-tile>
        <div slot="header" class="gateway-header">
          <div class="gateway-heading">
            <span class="gateway-title">Onebox Admin UI</span>
            <span class="gateway-subtitle">Configure the required WebAuthn hostname for this Onebox dashboard</span>
          </div>
        </div>
        <div class="gateway-content">
          ${this.renderGatewayInput('adminUiDomain', 'Admin UI Domain', settings?.adminUiDomain || '', 'Required, for example onebox.example.com. This HTTPS hostname becomes the passkey RP ID and is pinned after enrollment; route visibility is configured separately.')}
          ${this.renderGatewayReadonly('Local Target', 'Onebox OpsServer on port 3000', 'The external gateway forwards to CoreTraffic, which forwards this hostname to the Onebox Admin UI.')}
        </div>
        <div slot="footer" class="gateway-footer">
          <button class="tile-button primary" type="button" @click=${() => this.saveAdminUiSettings()}>
            Save Admin UI Domain
          </button>
        </div>
      </dees-tile>
    `;
  }

  private renderExternalGatewaySettings(): TemplateResult {
    const settings = this.settingsState.settings;
    const mode = settings?.dcrouterMode || 'managed';
    return html`
      <dees-tile>
        <div slot="header" class="gateway-header">
          <div class="gateway-heading">
            <span class="gateway-title">dcrouter Gateway</span>
            <span class="gateway-subtitle">Run a local managed dcrouter or delegate routing to an external dcrouter</span>
          </div>
        </div>
        <div class="gateway-mode-row">
          ${this.renderModeButton('managed', 'Managed Local', mode)}
          ${this.renderModeButton('external', 'External dcrouter', mode)}
          ${this.renderModeButton('disabled', 'Disabled', mode)}
        </div>
        ${mode === 'managed' ? this.renderManagedGatewayStatus() : null}
        <div class="gateway-content">
          ${mode === 'managed' ? html`
            ${this.renderGatewayInput('dcrouterManagedImage', 'dcrouter Image', settings?.dcrouterManagedImage || 'code.foss.global/serve.zone/dcrouter:latest', 'OCI image used for the managed local gateway.')}
            ${this.renderGatewayInput('dcrouterManagedDataDir', 'Data Directory', settings?.dcrouterManagedDataDir || './.nogit/dcrouter-data', 'Host directory mounted into the dcrouter container.')}
            ${this.renderGatewayInput('dcrouterManagedOpsPort', 'Local Ops Port', String(settings?.dcrouterManagedOpsPort || 3300), 'Bound to 127.0.0.1 for Onebox to call dcrouter APIs.')}
            ${this.renderGatewayInput('dcrouterManagedHttpPort', 'Public HTTP Port', String(settings?.dcrouterManagedHttpPort || 80), 'Host port owned by dcrouter for HTTP ingress.')}
            ${this.renderGatewayInput('dcrouterManagedHttpsPort', 'Public HTTPS Port', String(settings?.dcrouterManagedHttpsPort || 443), 'Host port owned by dcrouter for HTTPS ingress.')}
            ${this.renderGatewayReadonly('Gateway Client ID', settings?.dcrouterGatewayClientId || 'Created when managed dcrouter starts', 'Diagnostic only. Onebox manages this local client automatically.')}
          ` : mode === 'external' ? html`
            ${this.renderGatewayInput('dcrouterGatewayUrl', 'Gateway URL', settings?.dcrouterGatewayUrl || '', 'Base URL of the dcrouter OpsServer.')}
            ${this.renderGatewayInput('dcrouterGatewayApiToken', 'API Token', settings?.dcrouterGatewayApiToken || '', 'Requires gateway-client access in dcrouter.', true)}
            ${this.renderGatewayReadonly('Gateway Client ID', settings?.dcrouterGatewayClientId || 'Derived from token', 'Configure this in dcrouter Gateway Clients, not in Onebox.')}
            ${this.renderGatewayInput('dcrouterTargetHost', 'Target Host', settings?.dcrouterTargetHost || '', 'Defaults to the configured server IP when empty.')}
            ${this.renderGatewayInput('dcrouterTargetPort', 'Target Port', String(settings?.dcrouterTargetPort || 80), 'Internal HTTP port dcrouter forwards to.')}
          ` : html`
            <div class="gateway-disabled">dcrouter route delegation is disabled. Onebox will keep using its local CoreTraffic directly.</div>
          `}
        </div>
        <div slot="footer" class="gateway-footer">
          <button class="tile-button primary" type="button" @click=${() => this.saveExternalGatewaySettings()}>
            Save dcrouter Settings
          </button>
        </div>
      </dees-tile>
    `;
  }

  private renderModeButton(
    mode: 'managed' | 'external' | 'disabled',
    label: string,
    activeMode: string,
  ): TemplateResult {
    return html`
      <button
        class="gateway-mode-button ${activeMode === mode ? 'active' : ''}"
        @click=${() => this.updateGatewayDraft('dcrouterMode', mode)}
      >${label}</button>
    `;
  }

  private renderManagedGatewayStatus(): TemplateResult {
    const status = this.settingsState.managedDcRouterStatus;
    const stateText = status?.running ? (status.healthy ? 'Running' : 'Starting') : 'Stopped';
    return html`
      <div class="gateway-status-row">
        <div>
          <div class="gateway-status-label">Managed dcrouter</div>
          <div class="gateway-status-value">${stateText}${status?.gatewayUrl ? ` at ${status.gatewayUrl}` : ''}</div>
          ${status?.message ? html`<div class="gateway-status-error">${status.message}</div>` : null}
        </div>
        <div class="gateway-actions">
          <dees-button .text=${'Start'} .type=${'default'} @click=${() => appstate.dispatchProtectedAction(appstate.startManagedDcRouterAction, null)}></dees-button>
          <dees-button .text=${'Restart'} .type=${'default'} @click=${() => appstate.dispatchProtectedAction(appstate.restartManagedDcRouterAction, null)}></dees-button>
          <dees-button .text=${'Stop'} .type=${'default'} @click=${() => appstate.dispatchProtectedAction(appstate.stopManagedDcRouterAction, null)}></dees-button>
        </div>
      </div>
    `;
  }

  private renderGatewayInput(
    key: keyof NonNullable<appstate.ISettingsState['settings']>,
    label: string,
    value: string,
    hint: string,
    isPassword = false,
  ): TemplateResult {
    return html`
      <div class="gateway-field ${key === 'dcrouterGatewayUrl' || key === 'adminUiDomain' ? 'full' : ''}">
        <dees-input-text
          .key=${key}
          .label=${label}
          .value=${value}
          .description=${hint}
          .isPasswordBool=${isPassword}
          @input=${(event: Event) => this.updateGatewayDraft(key, (event.target as HTMLInputElement).value)}
        ></dees-input-text>
      </div>
    `;
  }

  private renderGatewayReadonly(label: string, value: string, hint: string): TemplateResult {
    return html`
      <div class="gateway-readonly">
        <div class="gateway-readonly-label">${label}</div>
        <div class="gateway-readonly-value">${value}</div>
        <div class="gateway-readonly-hint">${hint}</div>
      </div>
    `;
  }

  private updateGatewayDraft(
    key: keyof NonNullable<appstate.ISettingsState['settings']>,
    value: string,
  ): void {
    const currentSettings = this.settingsState.settings || {} as NonNullable<appstate.ISettingsState['settings']>;
    const numberKeys = new Set([
      'dcrouterTargetPort',
      'dcrouterManagedOpsPort',
      'dcrouterManagedHttpPort',
      'dcrouterManagedHttpsPort',
    ]);
    const nextValue = numberKeys.has(key as string) ? Number(value) || 0 : value;
    this.settingsState = {
      ...this.settingsState,
      settings: {
        ...currentSettings,
        [key]: nextValue,
      },
    };
  }

  private async saveExternalGatewaySettings(): Promise<void> {
    const settings = this.settingsState.settings;
    if (!settings) return;

    await appstate.dispatchProtectedAction(appstate.updateSettingsAction, {
      settings: {
        dcrouterMode: settings.dcrouterMode || 'managed',
        dcrouterManagedImage: settings.dcrouterManagedImage || 'code.foss.global/serve.zone/dcrouter:latest',
        dcrouterManagedOpsPort: Number(settings.dcrouterManagedOpsPort) || 3300,
        dcrouterManagedHttpPort: Number(settings.dcrouterManagedHttpPort) || 80,
        dcrouterManagedHttpsPort: Number(settings.dcrouterManagedHttpsPort) || 443,
        dcrouterManagedDataDir: settings.dcrouterManagedDataDir || './.nogit/dcrouter-data',
        dcrouterGatewayUrl: settings.dcrouterGatewayUrl || '',
        dcrouterGatewayApiToken: settings.dcrouterGatewayApiToken || '',
        dcrouterTargetHost: settings.dcrouterTargetHost || '',
        dcrouterTargetPort: Number(settings.dcrouterTargetPort) || 80,
      },
    });
    await appstate.dispatchProtectedAction(appstate.fetchManagedDcRouterStatusAction, null);
  }

  private async saveAdminUiSettings(): Promise<void> {
    const settings = this.settingsState.settings;
    if (!settings) return;

    await appstate.dispatchProtectedAction(appstate.updateSettingsAction, {
      settings: {
        adminUiDomain: settings.adminUiDomain || '',
      },
    });
  }

  private async saveStorageSettings(): Promise<void> {
    const settings = this.settingsState.settings;
    if (!settings) return;

    await appstate.dispatchProtectedAction(appstate.updateSettingsAction, {
      settings: {
        platformStorageClass: settings.platformStorageClass || 'local-hot',
      },
    });
  }
}
