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 { appRouter } from '../router.js';
import {
  DeesElement,
  customElement,
  html,
  state,
  css,
  cssManager,
  type TemplateResult,
} from '@design.estate/dees-element';

@customElement('ob-view-appstore')
export class ObViewAppStore extends DeesElement {
  @state()
  accessor appStoreState: appstate.IAppStoreState = {
    apps: [],
    sourceInfo: null,
    upgradeableServices: [],
    upgradeOperations: [],
  };

  @state()
  accessor currentView: 'grid' | 'detail' = 'grid';

  @state()
  accessor selectedApp: interfaces.requests.IAppStoreApp | null = null;

  @state()
  accessor selectedAppMeta: interfaces.requests.IAppStoreAppMeta | null = null;

  @state()
  accessor selectedAppConfig: interfaces.requests.IAppStoreVersionConfig | null = null;

  @state()
  accessor selectedVersion: string = '';

  @state()
  accessor editableEnvVars: Array<{
    key: string;
    value: string;
    description: string;
    required?: boolean;
    platformInjected?: boolean;
    secret?: boolean;
    generated?: boolean;
  }> = [];

  @state()
  accessor serviceName: string = '';

  @state()
  accessor serviceDomain: string = '';

  @state()
  accessor loading: boolean = false;

  @state()
  accessor deployMode: boolean = false;

  @state()
  accessor configLoadError: string = '';

  @state()
  accessor settingsState: appstate.ISettingsState = {
    settings: null,
    backupPasswordConfigured: false,
    managedDcRouterStatus: null,
    storageTargets: [],
    storageClasses: [],
  };

  @state()
  accessor volumeStorageClasses: Record<string, string> = {};

  public static styles = [
    cssManager.defaultStyles,
    shared.viewHostCss,
    css`
      .detail-card {
        background: var(--ci-shade-1, #09090b);
        border: 1px solid var(--ci-shade-2, #27272a);
        border-radius: 8px;
        padding: 24px;
        margin-bottom: 16px;
      }

      .detail-header {
        display: flex;
        align-items: flex-start;
        gap: 16px;
        margin-bottom: 24px;
      }

      .detail-icon {
        width: 64px;
        height: 64px;
        border-radius: 12px;
        background: var(--ci-shade-2, #27272a);
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 28px;
        font-weight: 700;
        color: var(--ci-shade-5, #a1a1aa);
        flex-shrink: 0;
      }

      .detail-title {
        font-size: 24px;
        font-weight: 700;
        color: var(--ci-shade-7, #e4e4e7);
        margin: 0 0 4px 0;
      }

      .detail-category {
        display: inline-block;
        padding: 2px 10px;
        border-radius: 9999px;
        font-size: 12px;
        font-weight: 500;
        background: var(--ci-shade-2, #27272a);
        color: var(--ci-shade-5, #a1a1aa);
        margin-bottom: 8px;
      }

      .detail-description {
        font-size: 14px;
        color: var(--ci-shade-5, #a1a1aa);
        line-height: 1.6;
        margin: 0;
      }

      .detail-meta {
        display: flex;
        gap: 16px;
        margin-top: 8px;
        font-size: 13px;
        color: var(--ci-shade-4, #71717a);
      }

      .detail-meta a {
        color: var(--ci-shade-5, #a1a1aa);
        text-decoration: none;
      }

      .detail-meta a:hover {
        text-decoration: underline;
      }

      .section-label {
        font-size: 13px;
        font-weight: 600;
        color: var(--ci-shade-5, #a1a1aa);
        text-transform: uppercase;
        letter-spacing: 0.05em;
        margin-bottom: 10px;
      }

      .badge {
        display: inline-flex;
        align-items: center;
        gap: 4px;
        padding: 4px 10px;
        border-radius: 6px;
        font-size: 12px;
        font-weight: 500;
        background: rgba(59, 130, 246, 0.15);
        color: #60a5fa;
        margin-right: 6px;
        margin-bottom: 6px;
      }

      .version-row {
        display: flex;
        align-items: center;
        gap: 16px;
      }

      .version-select {
        background: var(--ci-shade-2, #27272a);
        border: 1px solid var(--ci-shade-3, #3f3f46);
        border-radius: 6px;
        padding: 8px 12px;
        color: var(--ci-shade-7, #e4e4e7);
        font-size: 14px;
        cursor: pointer;
      }

      .image-tag {
        font-family: monospace;
        font-size: 13px;
        color: var(--ci-shade-5, #a1a1aa);
        background: var(--ci-shade-2, #27272a);
        padding: 4px 8px;
        border-radius: 4px;
      }

      .source-card {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 16px;
        background: var(--ci-shade-1, #09090b);
        border: 1px solid var(--ci-shade-2, #27272a);
        border-radius: 8px;
        padding: 12px 16px;
        margin-bottom: 16px;
        color: var(--ci-shade-5, #a1a1aa);
        font-size: 13px;
      }

      .source-card strong {
        color: var(--ci-shade-7, #e4e4e7);
      }

      .env-table {
        width: 100%;
        border-collapse: collapse;
      }

      .env-table th {
        text-align: left;
        font-size: 12px;
        font-weight: 500;
        color: var(--ci-shade-4, #71717a);
        padding: 8px 8px 8px 0;
        border-bottom: 1px solid var(--ci-shade-2, #27272a);
      }

      .env-table td {
        padding: 6px 8px 6px 0;
        vertical-align: middle;
      }

      .env-input {
        width: 100%;
        background: var(--ci-shade-2, #27272a);
        border: 1px solid var(--ci-shade-3, #3f3f46);
        border-radius: 4px;
        padding: 6px 8px;
        color: var(--ci-shade-7, #e4e4e7);
        font-size: 13px;
        font-family: monospace;
        box-sizing: border-box;
      }

      .env-input:disabled {
        opacity: 0.5;
        cursor: not-allowed;
      }

      .env-key {
        font-family: monospace;
        font-size: 13px;
        color: var(--ci-shade-6, #d4d4d8);
        white-space: nowrap;
      }

      .env-desc {
        font-size: 12px;
        color: var(--ci-shade-4, #71717a);
      }

      .env-badge {
        font-size: 10px;
        padding: 1px 6px;
        border-radius: 3px;
        margin-left: 6px;
      }

      .env-badge.required {
        background: rgba(239, 68, 68, 0.15);
        color: #f87171;
      }

      .env-badge.auto {
        background: rgba(34, 197, 94, 0.15);
        color: #4ade80;
      }

      .name-input {
        background: var(--ci-shade-2, #27272a);
        border: 1px solid var(--ci-shade-3, #3f3f46);
        border-radius: 6px;
        padding: 10px 14px;
        color: var(--ci-shade-7, #e4e4e7);
        font-size: 14px;
        width: 300px;
        box-sizing: border-box;
      }

      .actions-row {
        display: flex;
        justify-content: flex-end;
        gap: 12px;
        margin-top: 24px;
      }

      .btn {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        padding: 10px 20px;
        border: none;
        border-radius: 6px;
        font-size: 14px;
        font-weight: 500;
        cursor: pointer;
        transition: opacity 200ms ease;
      }

      .btn:hover { opacity: 0.9; }

      .btn-primary {
        background: var(--ci-shade-7, #e4e4e7);
        color: var(--ci-shade-0, #09090b);
      }

      .btn-secondary {
        background: transparent;
        border: 1px solid var(--ci-shade-2, #27272a);
        color: var(--ci-shade-6, #d4d4d8);
      }

      .loading-spinner {
        padding: 32px;
        text-align: center;
        color: var(--ci-shade-4, #71717a);
      }

      .footprint-list {
        display: grid;
        gap: 8px;
      }

      .footprint-item {
        display: flex;
        justify-content: space-between;
        gap: 12px;
        padding: 10px 12px;
        border: 1px solid var(--ci-shade-2, #27272a);
        border-radius: 6px;
        font-size: 13px;
        color: var(--ci-shade-6, #d4d4d8);
      }

      .footprint-meta {
        color: var(--ci-shade-4, #71717a);
        font-family: monospace;
      }

      .storage-select {
        background: var(--ci-shade-2, #27272a);
        border: 1px solid var(--ci-shade-3, #3f3f46);
        border-radius: 6px;
        padding: 6px 8px;
        color: var(--ci-shade-7, #e4e4e7);
        font-size: 12px;
      }

      .exposure-warning {
        margin-top: 10px;
        color: #fbbf24;
        font-size: 12px;
        line-height: 1.5;
      }

      .error-message {
        padding: 12px 14px;
        border-radius: 6px;
        background: rgba(239, 68, 68, 0.12);
        color: #f87171;
        font-size: 13px;
        line-height: 1.5;
      }
    `,
  ];

  constructor() {
    super();
    const sub = appstate.appStoreStatePart
      .select((s) => s)
      .subscribe((newState) => {
        this.appStoreState = newState;
      });
    this.rxSubscriptions.push(sub);

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

  async connectedCallback() {
    super.connectedCallback();
    await Promise.all([
      appstate.dispatchProtectedAction(appstate.fetchAppStoreTemplatesAction, null),
      appstate.dispatchProtectedAction(appstate.fetchAppStoreUpgradeOperationsAction, null),
      appstate.dispatchProtectedAction(appstate.fetchSettingsAction, null),
    ]);
  }

  public render(): TemplateResult {
    switch (this.currentView) {
      case 'detail':
        return this.renderDetailView();
      default:
        return this.renderGridView();
    }
  }

  private renderGridView(): TemplateResult {
    const appTemplates = this.appStoreState.apps.map((app) => ({
      id: app.id,
      name: app.name,
      description: app.description,
      category: app.category,
      iconName: app.iconName,
      iconUrl: app.iconUrl,
      image: '',
      port: 0,
    }));

    return html`
      <ob-sectionheading>App Store</ob-sectionheading>
      ${this.renderSourceInfo()}
      ${this.renderUpgradeOperations()}
      ${appTemplates.length === 0
        ? html`<div class="loading-spinner">Loading app templates...</div>`
        : html`
          <sz-app-store-view
            .apps=${appTemplates}
            @view-app=${(e: CustomEvent) => this.handleViewDetails(e)}
            @deploy-app=${(e: CustomEvent) => this.handleAppClick(e)}
          ></sz-app-store-view>
        `}
    `;
  }

  private renderSourceInfo(): TemplateResult | '' {
    const sourceInfo = this.appStoreState.sourceInfo;
    if (!sourceInfo) return '';
    return html`
      <div class="source-card">
        <span>
          App Store source: <strong>${sourceInfo.packageName}@${sourceInfo.packageVersion}</strong>
          ${sourceInfo.source === 'package' ? ' packaged release' : ' remote'}
        </span>
        ${sourceInfo.baseUrl ? html`<span class="image-tag">${sourceInfo.baseUrl}</span>` : ''}
      </div>
    `;
  }

  private renderUpgradeOperations(): TemplateResult | '' {
    const visibleOperations = this.appStoreState.upgradeOperations
      .filter((operation) => operation.status === 'running' || operation.status === 'failed')
      .slice(0, 3);
    if (visibleOperations.length === 0) return '';

    return html`
      <div class="detail-card">
        <div class="section-label">Recent Upgrade Operations</div>
        <div class="footprint-list">
          ${visibleOperations.map((operation) => html`
            <div class="footprint-item">
              <span>${operation.serviceName}: ${operation.fromVersion} &rarr; ${operation.targetVersion}</span>
              <span class="footprint-meta">${operation.status} / ${operation.step}</span>
            </div>
          `)}
        </div>
      </div>
    `;
  }

  private renderDetailView(): TemplateResult {
    if (this.loading) {
      return html`
        <ob-sectionheading>App Store</ob-sectionheading>
        <div class="loading-spinner">Loading app details...</div>
      `;
    }

    const app = this.selectedApp;
    const meta = this.selectedAppMeta;
    const config = this.selectedAppConfig;

    if (this.configLoadError) {
      return html`
        <ob-sectionheading>App Store</ob-sectionheading>
        <button class="btn btn-secondary" style="margin-bottom: 16px;" @click=${() => { this.currentView = 'grid'; }}>
          &larr; Back to App Store
        </button>
        <div class="detail-card">
          <div class="section-label">Could not load app details</div>
          <div class="error-message">${this.configLoadError}</div>
          <div class="actions-row">
            <button class="btn btn-secondary" @click=${() => { this.currentView = 'grid'; }}>Back</button>
            ${app ? html`
              <button class="btn btn-primary" @click=${async () => {
                this.loading = true;
                await this.fetchVersionConfig(app.id, this.selectedVersion || app.latestVersion);
                this.loading = false;
              }}>Retry</button>
            ` : ''}
          </div>
        </div>
      `;
    }

    if (!app) {
      return html`
        <ob-sectionheading>App Store</ob-sectionheading>
        <div class="loading-spinner">App not found.</div>
      `;
    }

    if (!config) {
      return html`
        <ob-sectionheading>App Store</ob-sectionheading>
        <div class="loading-spinner">App details are not available.</div>
      `;
    }

    const platformReqs = config.platformRequirements || {};
    const hasPlatformReqs = Object.values(platformReqs).some(Boolean);
    const platformLabels: Record<string, string> = {
      mongodb: 'MongoDB',
      s3: 'S3 Object Storage (SmartStorage)',
      clickhouse: 'ClickHouse',
      valkey: 'Valkey',
      mariadb: 'MariaDB',
    };

    return html`
      <ob-sectionheading>App Store</ob-sectionheading>
      <button class="btn btn-secondary" style="margin-bottom: 16px;" @click=${() => { this.currentView = 'grid'; }}>
        &larr; Back to App Store
      </button>

      <!-- Header -->
      <div class="detail-card">
        <div class="detail-header">
          <div class="detail-icon">${(app.name || '?')[0].toUpperCase()}</div>
          <div style="flex: 1;">
            <h2 class="detail-title">${app.name}</h2>
            <span class="detail-category">${app.category}</span>
            <p class="detail-description">${app.description}</p>
            <div class="detail-meta">
              ${meta?.maintainer ? html`<span>Maintainer: <strong>${meta.maintainer}</strong></span>` : ''}
              ${meta?.links ? Object.entries(meta.links).map(([label, url]) =>
                html`<a href="${url}" target="_blank" rel="noopener">${label}</a>`
              ) : ''}
              ${app.tags?.length ? html`<span>Tags: ${app.tags.join(', ')}</span>` : ''}
            </div>
          </div>
        </div>
      </div>

      <!-- Platform Services -->
      ${hasPlatformReqs ? html`
        <div class="detail-card">
          <div class="section-label">Platform Services</div>
          <div>
            ${Object.entries(platformReqs)
              .filter(([_, enabled]) => enabled)
              .map(([key]) => html`<span class="badge">${platformLabels[key] || key}</span>`)}
          </div>
          <div style="font-size: 12px; color: var(--ci-shade-4, #71717a); margin-top: 8px;">
            These platform services will be automatically provisioned when you deploy.
          </div>
        </div>
      ` : ''}

      ${this.renderDeploymentFootprint(config)}
      ${this.deployMode ? this.renderVolumeStorageSelection(config) : ''}

      <!-- Version & Image -->
      <div class="detail-card">
        <div class="section-label">Version</div>
        <div class="version-row">
          <select class="version-select" @change=${(e: Event) => this.handleVersionChange((e.target as HTMLSelectElement).value)}>
            ${(meta?.versions || [this.selectedVersion]).map((v) =>
              html`<option value="${v}" ?selected=${v === this.selectedVersion}>${v}${v === app.latestVersion ? ' (latest)' : ''}</option>`
            )}
          </select>
          <span class="image-tag">${config.image}</span>
          ${config.minOneboxVersion ? html`<span style="font-size: 12px; color: var(--ci-shade-4, #71717a);">Requires onebox &ge; ${config.minOneboxVersion}</span>` : ''}
        </div>
      </div>

      <!-- Environment Variables -->
      ${this.editableEnvVars.length > 0 ? html`
        <div class="detail-card">
          <div class="section-label">Environment Variables</div>
          <table class="env-table">
            <thead>
              <tr>
                <th style="width: 30%;">Variable</th>
                <th style="width: 40%;">Value</th>
                <th>Description</th>
              </tr>
            </thead>
            <tbody>
              ${this.editableEnvVars.map((ev, index) => html`
                <tr>
                  <td>
                    <span class="env-key">${ev.key}</span>
                    ${ev.required ? html`<span class="env-badge required">required</span>` : ''}
                    ${ev.platformInjected ? html`<span class="env-badge auto">auto</span>` : ''}
                    ${ev.secret ? html`<span class="env-badge secret">secret</span>` : ''}
                    ${ev.generated ? html`<span class="env-badge auto">generated</span>` : ''}
                  </td>
                  <td>
                    <input
                      class="env-input"
                      type=${ev.secret ? 'password' : 'text'}
                      .value=${ev.value}
                      ?disabled=${ev.platformInjected || !this.deployMode}
                      placeholder=${ev.platformInjected
                        ? 'Auto-injected by platform'
                        : ev.generated
                          ? 'Generated securely on deploy'
                          : 'Enter value...'}
                      @input=${(e: Event) => this.handleEnvVarChange(index, (e.target as HTMLInputElement).value)}
                    />
                  </td>
                  <td><span class="env-desc">${ev.description || ''}</span></td>
                </tr>
              `)}
            </tbody>
          </table>
        </div>
      ` : ''}

      <!-- Deploy section (only in deploy mode) or action button (view mode) -->
      ${this.deployMode ? html`
        <div class="detail-card">
          <div class="section-label">Service Name</div>
          <input
            class="name-input"
            type="text"
            .value=${this.serviceName}
            placeholder="e.g. my-ghost-blog"
            @input=${(e: Event) => { this.serviceName = (e.target as HTMLInputElement).value; }}
          />
          <div style="font-size: 12px; color: var(--ci-shade-4, #71717a); margin-top: 6px;">
            Lowercase letters, numbers, and hyphens only.
          </div>

          <div class="section-label" style="margin-top: 18px;">Domain</div>
          <input
            class="name-input"
            type="text"
            .value=${this.serviceDomain}
            placeholder="e.g. cloudly.example.com"
            @input=${(e: Event) => this.handleServiceDomainChange((e.target as HTMLInputElement).value)}
          />
          <div style="font-size: 12px; color: var(--ci-shade-4, #71717a); margin-top: 6px;">
            Onebox routes this domain to the deployed app. Required when the app uses SERVICE_DOMAIN.
          </div>

          ${this.renderDeployConfirmation(config)}

          <div class="actions-row">
            <button class="btn btn-secondary" @click=${() => { this.currentView = 'grid'; }}>Cancel</button>
            <button class="btn btn-primary" @click=${() => this.handleDeploy()}>
              Deploy v${this.selectedVersion}
            </button>
          </div>
        </div>
      ` : html`
        <div class="actions-row" style="margin-top: 8px;">
          <button class="btn btn-secondary" @click=${() => { this.currentView = 'grid'; }}>
            &larr; Back
          </button>
          <button class="btn btn-primary" @click=${() => { this.deployMode = true; }}>
            Deploy this App
          </button>
        </div>
      `}
    `;
  }

  private renderDeploymentFootprint(config: interfaces.requests.IAppStoreVersionConfig): TemplateResult | '' {
    const volumes = this.getConfigVolumes(config);
    const publishedPorts = config.publishedPorts || [];

    if (volumes.length === 0 && publishedPorts.length === 0) {
      return '';
    }

    return html`
      <div class="detail-card">
        <div class="section-label">Deployment Footprint</div>
        <div class="footprint-list">
          ${volumes.map((volume) => html`
            <div class="footprint-item">
              <span>Volume mount</span>
              <span class="footprint-meta">
                ${volume.source || volume.name || 'managed volume'}:${volume.mountPath}${volume.readOnly ? ':ro' : ''}
              </span>
            </div>
          `)}
          ${publishedPorts.map((port) => html`
            <div class="footprint-item">
              <span>Published host port</span>
              <span class="footprint-meta">${this.formatPublishedPort(port)}</span>
            </div>
          `)}
        </div>
        ${publishedPorts.length > 0 ? html`
          <div class="exposure-warning">
            This app publishes raw host ports outside the HTTP proxy. Confirm firewall and network policy before deploying.
          </div>
        ` : ''}
      </div>
    `;
  }

  private renderDeployConfirmation(config: interfaces.requests.IAppStoreVersionConfig): TemplateResult | '' {
    const volumes = this.getConfigVolumes(config);
    const publishedPorts = config.publishedPorts || [];
    if (volumes.length === 0 && publishedPorts.length === 0) return '';

    return html`
      <div class="exposure-warning">
        Deploying this app will create ${volumes.length} persistent volume(s)
        ${publishedPorts.length > 0 ? html`and expose ${publishedPorts.length} host port declaration(s)` : ''}.
      </div>
    `;
  }

  private renderVolumeStorageSelection(config: interfaces.requests.IAppStoreVersionConfig): TemplateResult | '' {
    const volumes = this.getConfigVolumes(config);
    if (volumes.length === 0) return '';
    const runtimeClasses = this.settingsState.storageClasses.filter((storageClassArg) => storageClassArg.purpose === 'runtime');
    if (runtimeClasses.length === 0) return '';

    return html`
      <div class="detail-card">
        <div class="section-label">Volume Storage</div>
        <div class="footprint-list">
          ${volumes.map((volumeArg) => {
            const key = this.getVolumeStorageKey(volumeArg);
            const selectedClass = this.volumeStorageClasses[key] || this.getDefaultStorageClassName('runtime');
            return html`
              <div class="footprint-item">
                <span>${volumeArg.mountPath}</span>
                <select
                  class="storage-select"
                  .value=${selectedClass}
                  @change=${(event: Event) => this.updateVolumeStorageClass(key, (event.target as HTMLSelectElement).value)}
                >
                  ${runtimeClasses.map((storageClassArg) => html`
                    <option value=${storageClassArg.name} ?selected=${storageClassArg.name === selectedClass}>
                      ${storageClassArg.displayName}
                    </option>
                  `)}
                </select>
              </div>
            `;
          })}
        </div>
      </div>
    `;
  }

  private getConfigVolumes(config: interfaces.requests.IAppStoreVersionConfig): interfaces.data.IServiceVolume[] {
    return (config.volumes || []).map((volume) => {
      if (typeof volume === 'string') {
        return { mountPath: volume };
      }
      return volume;
    }).filter((volume) => Boolean(volume.mountPath));
  }

  private getDefaultStorageClassName(purposeArg: interfaces.data.TStorageClassPurpose): string {
    return this.settingsState.storageClasses.find((storageClassArg) => {
      return storageClassArg.purpose === purposeArg && storageClassArg.isDefault;
    })?.name || (purposeArg === 'database' ? 'local-hot' : purposeArg === 'backup' ? 'local-backup' : 'local-runtime');
  }

  private getVolumeStorageKey(volumeArg: interfaces.data.IServiceVolume): string {
    return volumeArg.source || volumeArg.name || volumeArg.mountPath;
  }

  private updateVolumeStorageClass(keyArg: string, storageClassArg: string): void {
    this.volumeStorageClasses = {
      ...this.volumeStorageClasses,
      [keyArg]: storageClassArg,
    };
  }

  private formatPublishedPort(port: interfaces.data.IServicePublishedPort): string {
    const protocol = port.protocol || 'tcp';
    const target = port.targetPortEnd ? `${port.targetPort}-${port.targetPortEnd}` : String(port.targetPort);
    const publishedStart = port.publishedPort || port.targetPort;
    const publishedEnd = port.publishedPortEnd || (port.targetPortEnd ? publishedStart + (port.targetPortEnd - port.targetPort) : undefined);
    const published = publishedEnd ? `${publishedStart}-${publishedEnd}` : String(publishedStart);
    return `${port.hostIp || '0.0.0.0'}:${published}/${protocol} -> ${target}/${protocol}`;
  }

  private async handleViewDetails(e: CustomEvent) {
    const app = e.detail?.app;
    if (!app) return;

    const catalogApp = this.appStoreState.apps.find((a) => a.id === app.id);
    if (!catalogApp) return;

    this.deployMode = false;
    this.selectedApp = catalogApp;
    this.selectedAppConfig = null;
    this.configLoadError = '';
    this.selectedVersion = catalogApp.latestVersion;
    this.serviceName = catalogApp.id;
    this.loading = true;
    this.currentView = 'detail';

    await this.fetchVersionConfig(catalogApp.id, catalogApp.latestVersion);
    if (
      this.selectedApp?.id === catalogApp.id
      && this.selectedVersion === catalogApp.latestVersion
    ) {
      this.loading = false;
    }
  }

  private async handleAppClick(e: CustomEvent) {
    const app = e.detail?.app;
    if (!app) return;

    const catalogApp = this.appStoreState.apps.find((a) => a.id === app.id);
    if (!catalogApp) return;

    this.deployMode = true;
    this.selectedApp = catalogApp;
    this.selectedAppConfig = null;
    this.configLoadError = '';
    this.selectedVersion = catalogApp.latestVersion;
    this.serviceName = catalogApp.id;
    this.loading = true;
    this.currentView = 'detail';

    await this.fetchVersionConfig(catalogApp.id, catalogApp.latestVersion);
    if (
      this.selectedApp?.id === catalogApp.id
      && this.selectedVersion === catalogApp.latestVersion
    ) {
      this.loading = false;
    }
  }

  private async handleVersionChange(version: string) {
    if (!this.selectedApp || version === this.selectedVersion) return;
    this.selectedVersion = version;
    this.selectedAppConfig = null;
    this.configLoadError = '';
    this.loading = true;
    const requestedAppId = this.selectedApp.id;
    await this.fetchVersionConfig(requestedAppId, version);
    if (
      this.selectedApp?.id === requestedAppId
      && this.selectedVersion === version
    ) {
      this.loading = false;
    }
  }

  private async fetchVersionConfig(appId: string, version: string): Promise<boolean> {
    try {
      this.configLoadError = '';
      this.selectedAppConfig = null;
      const response = await appstate.runProtectedRequest(
        'appstore.versionConfig',
        { appId, version },
        async (identity) => await new plugins.domtools.plugins.typedrequest.TypedRequest<
          interfaces.requests.IReq_GetAppStoreConfig
        >('/typedrequest', 'getAppStoreConfig').fire({ identity, appId, version }),
        { mode: 'latest' },
      );
      if (!response) return false;

      if (!response?.appMeta || !response?.config) {
        throw new Error('The App Store returned an empty response. Please retry.');
      }

      this.selectedAppMeta = response.appMeta;
      this.selectedAppConfig = response.config;
      this.volumeStorageClasses = Object.fromEntries(
        this.getConfigVolumes(response.config).map((volumeArg) => [
          this.getVolumeStorageKey(volumeArg),
          volumeArg.storageClass || this.getDefaultStorageClassName('runtime'),
        ]),
      );

      // Build editable env vars
      this.editableEnvVars = (response.config.envVars || []).map((ev) => ({
        key: ev.key,
        value: ev.value || '',
        description: ev.description || '',
        required: ev.required,
        platformInjected: ev.value?.includes('${') || false,
        secret: ev.secret,
        generated: Boolean(ev.generate),
      }));
      this.serviceDomain = '';
      return true;
    } catch (err) {
      const message = this.getErrorMessage(err);
      this.configLoadError = message;
      this.editableEnvVars = [];
      console.error('Failed to fetch app config:', err);
      return false;
    }
  }

  private getErrorMessage(errorArg: unknown): string {
    if (!errorArg) return 'Unknown error';
    if (typeof errorArg === 'string') return errorArg;
    const errorLike = errorArg as { errorText?: string; message?: string; text?: string };
    return errorLike.errorText || errorLike.message || errorLike.text || 'Unknown error';
  }

  private handleEnvVarChange(index: number, value: string) {
    const updated = [...this.editableEnvVars];
    updated[index] = { ...updated[index], value };
    this.editableEnvVars = updated;
  }

  private handleServiceDomainChange(valueArg: string) {
    this.serviceDomain = this.normalizeDomain(valueArg);
  }

  private normalizeDomain(valueArg: string) {
    return valueArg.trim().replace(/^https?:\/\//, '').replace(/\/$/, '');
  }

  private async handleDeploy() {
    const app = this.selectedApp;
    const config = this.selectedAppConfig;
    if (!app || !config) return;
    const missingRequiredEnvVars = this.editableEnvVars.filter((envVarArg) => {
      return envVarArg.required
        && !envVarArg.platformInjected
        && !envVarArg.generated
        && !envVarArg.value.trim();
    });
    if (missingRequiredEnvVars.length > 0) {
      console.error(
        `Missing required environment variables: ${missingRequiredEnvVars
          .map((envVarArg) => envVarArg.key)
          .join(', ')}`,
      );
      return;
    }
    const needsServiceDomain = (config.envVars || []).some((envVarArg) => {
      return envVarArg.value?.includes('${SERVICE_DOMAIN}');
    });
    if (needsServiceDomain && !this.serviceDomain) {
      console.error('A domain is required for this app.');
      return;
    }

    const envVars: Record<string, string> = {};
    for (const ev of this.editableEnvVars) {
      if (ev.key && ev.value) {
        envVars[ev.key] = ev.value;
      }
    }

    try {
      const install = {
          appId: app.id,
          version: this.selectedVersion,
          serviceName: this.serviceName || app.id,
          domain: this.serviceDomain || undefined,
          envVars,
          volumeStorageClasses: this.volumeStorageClasses,
      };
      const response = await appstate.runProtectedRequest(
        'appstore.install',
        install,
        async (identity) => await new plugins.domtools.plugins.typedrequest.TypedRequest<
          interfaces.requests.IReq_InstallAppStoreApp
        >('/typedrequest', 'installAppStoreApp').fire({ identity, install }),
        { mode: 'mutation', domain: 'appstore' },
      );
      if (!response) return;
      appRouter.navigateToView('services');
    } catch (err) {
      const message = this.getErrorMessage(err);
      console.error('Failed to deploy from App Store:', err);
      await plugins.deesCatalog.DeesToast.error(message);
    }
  }
}
