import { ChangeDetectionStrategy, Component, OnInit, inject } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import { HttpClient, HttpEventType, HttpHeaders } from "@angular/common/http";

type LoginResponse = {
  token: string;
  refreshToken: string;
  user: {
    id: string;
    username: string;
    role: string;
  };
};

type HealthResponse = {
  status: string;
  service: string;
  timestamp: string;
  dependencies?: {
    elasticsearch: string;
    minio: string;
  };
};

type StoredEntry = {
  id: string;
  entryType: "file" | "folder";
  userId: string;
  username: string;
  displayName: string;
  originalFileName: string;
  contentType: string;
  size: number;
  tags: string[];
  clientLastModifiedAt?: string | null;
  serverTempCreatedAt?: string | null;
  serverTempModifiedAt?: string | null;
  uploadedAt: string;
  folderPath: string;
  parentFolderPath: string;
};

type BatchImportItem = {
  file: File;
  relativePath: string;
  targetFolderPath: string;
  displayName: string;
  tags: string;
};

@Component({
  selector: "app-root",
  standalone: true,
  changeDetection: ChangeDetectionStrategy.Default,
  imports: [CommonModule, FormsModule],
  template: `
    <main class="page">
      <section class="shell">
        <header class="hero">
          <div>
            <p class="eyebrow">Search-first distributed storage</p>
            <h1>Files + Metadata Workspace</h1>
            <p class="subtitle">JWT auth with Elasticsearch metadata + MinIO object storage.</p>
          </div>
          <div class="health" [class.degraded]="health?.status !== 'ok'">
            <div class="health-title">API</div>
            <div class="health-value">{{ healthLoading ? 'Checking...' : (health?.status ?? 'unknown') }}</div>
            <div class="health-meta" *ngIf="health?.dependencies">
              ES: {{ health?.dependencies?.elasticsearch }} | MinIO: {{ health?.dependencies?.minio }}
            </div>
          </div>
        </header>

        <section class="panel" *ngIf="!token; else appArea">
          <h2>Login</h2>
          <p class="muted">Demo users: <code>alice / alice123</code> or <code>bob / bob123</code></p>

          <form class="stack" (ngSubmit)="login()">
            <label>Username <input name="username" [(ngModel)]="loginForm.username" required /></label>
            <label>Password <input name="password" type="password" [(ngModel)]="loginForm.password" required /></label>
            <button type="submit" [disabled]="authLoading">{{ authLoading ? 'Signing in...' : 'Sign in' }}</button>
          </form>
          <p class="error" *ngIf="authError">{{ authError }}</p>

          <div class="divider"></div>

          <h3>Create Account</h3>
          <form class="stack" (ngSubmit)="register()">
            <label>
              Username
              <input name="registerUsername" [(ngModel)]="registerForm.username" placeholder="letters, numbers, ., _, -" required />
            </label>
            <label>
              Password
              <input name="registerPassword" type="password" [(ngModel)]="registerForm.password" placeholder="min. 6 characters" required />
            </label>
            <button type="submit" [disabled]="registerLoading">{{ registerLoading ? 'Creating...' : 'Create account' }}</button>
          </form>
          <p class="success" *ngIf="registerSuccess">{{ registerSuccess }}</p>
          <p class="error" *ngIf="registerError">{{ registerError }}</p>
        </section>

        <ng-template #appArea>
          <section
            class="grid"
            [class.drop-target]="dragDrop.active && dragDrop.targetFolderPath === currentFolderPath"
            (dragenter)="onAppDragEnter($event)"
            (dragover)="onAppDragOver($event)"
            (dragleave)="onAppDragLeave($event)"
            (drop)="onDropToCurrentFolder($event)"
          >
            <section class="panel">
              <div class="section-head">
                <h2>Upload File</h2>
                <div class="actions">
                  <button type="button" class="ghost danger" (click)="triggerCrash()" [disabled]="crashLoading">
                    {{ crashLoading ? 'Crashing...' : 'Crash API Pod' }}
                  </button>
                  <button type="button" class="ghost" (click)="logout()">Log out ({{ currentUser?.username }})</button>
                </div>
              </div>

              <p class="muted compact">Current folder: <code>{{ currentFolderPath || '/' }}</code></p>

              <form class="stack" (ngSubmit)="uploadFile()">
                <label>File <input type="file" (change)="onFileSelected($event)" required /></label>
                <label>
                  Display filename
                  <input name="filename" [(ngModel)]="uploadForm.filename" placeholder="Optional custom name" />
                </label>
                <label>
                  Tags (comma-separated)
                  <input name="tags" [(ngModel)]="uploadForm.tags" placeholder="invoice, tax, 2026" />
                </label>
                <button type="submit" [disabled]="uploading || !selectedFile">
                  {{ uploading ? 'Uploading...' : 'Upload file' }}
                </button>
              </form>

              <div class="progress-wrap" *ngIf="uploading">
                <div class="progress-meta">
                  <span>{{ uploadProgressLabel }}</span>
                  <span>{{ uploadProgress }}%</span>
                </div>
                <div class="progress-track" aria-label="Upload progress">
                  <div class="progress-bar" [style.width.%]="uploadProgress"></div>
                </div>
              </div>

              <p class="success" *ngIf="uploadSuccess">{{ uploadSuccess }}</p>
              <p class="error" *ngIf="uploadError">{{ uploadError }}</p>
              <p class="success" *ngIf="crashMessage">{{ crashMessage }}</p>
              <p class="error" *ngIf="crashError">{{ crashError }}</p>

              <div class="divider"></div>
            </section>

            <section class="panel">
              <div class="section-head">
                <h2>Your Files</h2>
                <div class="actions">
                  <button type="button" class="ghost" (click)="openBatchImportDialog()">Batch Import</button>
                  <button type="button" class="ghost" (click)="openCreateFolderDialog()">New Folder</button>
                  <button type="button" class="ghost" (click)="loadFiles()" [disabled]="filesLoading">
                    {{ filesLoading ? 'Refreshing...' : 'Refresh' }}
                  </button>
                </div>
              </div>

              <div class="breadcrumb">
                <button type="button" class="ghost mini" (click)="goToRoot()" [disabled]="currentFolderPath === ''">Root</button>
                <button type="button" class="ghost mini" (click)="goUp()" [disabled]="currentFolderPath === ''">Up</button>
                <span *ngFor="let crumb of breadcrumbs; let i = index" class="crumb">
                  <span>/</span>
                  <button type="button" class="link-btn" (click)="goToBreadcrumb(i)">{{ crumb }}</button>
                </span>
              </div>

              <div class="filters">
                <input name="searchQuery" [(ngModel)]="search.query" placeholder="Search filename or tags" (keydown.enter)="applySearch()" />
                <input name="searchTag" [(ngModel)]="search.tag" placeholder="Filter exact tag" (keydown.enter)="applySearch()" />
                <button type="button" (click)="applySearch()">Search</button>
                <button type="button" class="ghost" (click)="clearSearch()">Clear</button>
              </div>

              <div class="filters date-filters">
                <label>
                  Created from
                  <input type="date" name="createdFrom" [(ngModel)]="searchDates.createdFrom" />
                </label>
                <label>
                  Created to
                  <input type="date" name="createdTo" [(ngModel)]="searchDates.createdTo" />
                </label>
                <label>
                  Sort by
                  <select name="sortBy" [(ngModel)]="sort.by">
                    <option value="uploadedAt">Uploaded Date</option>
                    <option value="createdAt">Created Date</option>
                    <option value="name">Name</option>
                  </select>
                </label>
                <label>
                  Direction
                  <select name="sortDir" [(ngModel)]="sort.dir">
                    <option value="desc">Newest First</option>
                    <option value="asc">Oldest First</option>
                  </select>
                </label>
                <button type="button" (click)="applySearch()">Apply Filters</button>
              </div>

              <p class="error" *ngIf="filesError">{{ filesError }}</p>
              <p class="muted" *ngIf="!filesLoading && entries.length === 0">No items found in this folder.</p>

              <div class="table-wrap" *ngIf="entries.length">
                <table>
                  <thead>
                    <tr>
                      <th>Type</th>
                      <th>Name</th>
                      <th>Tags</th>
                      <th>Source Date</th>
                      <th>Size</th>
                      <th>Uploaded</th>
                      <th></th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr
                      *ngFor="let entry of sortedEntries"
                      [class.drop-target-row]="dragDrop.active && entry.entryType === 'folder' && dragDrop.targetFolderPath === entry.folderPath"
                      (dragenter)="onFolderDragEnter($event, entry)"
                      (dragover)="onFolderDragOver($event, entry)"
                      (dragleave)="onFolderDragLeave($event)"
                      (drop)="onDropToFolder($event, entry)"
                    >
                      <td>{{ entry.entryType === 'folder' ? 'Folder' : 'File' }}</td>
                      <td>
                        <button *ngIf="entry.entryType === 'folder'" type="button" class="link-btn" (click)="openFolder(entry)">
                          {{ entry.displayName }}
                        </button>
                        <span *ngIf="entry.entryType !== 'folder'">{{ entry.displayName }}</span>
                        <div class="muted tiny" *ngIf="entry.entryType === 'file'">{{ entry.originalFileName }}</div>
                      </td>
                      <td>
                        <ng-container *ngIf="entry.tags.length; else noTags">
                          <span class="tag" *ngFor="let tag of entry.tags">{{ tag }}</span>
                        </ng-container>
                        <ng-template #noTags><span class="muted tiny">-</span></ng-template>
                      </td>
                      <td>{{ entry.entryType === 'file' ? formatSourceDate(entry) : '-' }}</td>
                      <td>{{ entry.entryType === 'folder' ? '-' : formatBytes(entry.size) }}</td>
                      <td>{{ formatDate(entry.uploadedAt) }}</td>
                      <td>
                        <div class="row-actions">
                          <button
                            *ngIf="entry.entryType === 'file'"
                            type="button"
                            class="ghost mini"
                            (click)="downloadFile(entry)"
                          >
                            Download
                          </button>
                          <button type="button" class="ghost mini danger" (click)="deleteEntry(entry)">Delete</button>
                        </div>
                      </td>
                    </tr>
                  </tbody>
                </table>
              </div>
            </section>
          </section>
        </ng-template>

        <section class="dialog-backdrop" *ngIf="folderDialogOpen" (click)="closeCreateFolderDialog()">
          <section class="dialog panel" (click)="$event.stopPropagation()">
            <div class="section-head">
              <h2>Create Folder</h2>
              <button type="button" class="ghost mini" (click)="closeCreateFolderDialog()">Close</button>
            </div>

            <p class="muted compact">Create inside <code>{{ currentFolderPath || '/' }}</code></p>

            <form class="stack" (ngSubmit)="createFolder()">
              <label>
                Folder name
                <input
                  name="folderName"
                  [(ngModel)]="folderForm.name"
                  placeholder="Folder name"
                  required
                  [disabled]="folderCreating"
                />
              </label>

              <label>
                Tags (comma-separated)
                <input
                  name="folderTags"
                  [(ngModel)]="folderForm.tags"
                  placeholder="team, project-a"
                  [disabled]="folderCreating"
                />
              </label>

              <button type="submit" [disabled]="folderCreating">
                {{ folderCreating ? 'Creating...' : 'Create folder' }}
              </button>
            </form>

            <p class="success" *ngIf="folderSuccess">{{ folderSuccess }}</p>
            <p class="error" *ngIf="folderError">{{ folderError }}</p>
          </section>
        </section>

        <section class="dialog-backdrop" *ngIf="confirmDialog.open" (click)="closeConfirmDialog()">
          <section class="dialog panel" (click)="$event.stopPropagation()">
            <div class="section-head">
              <h2>{{ confirmDialog.title }}</h2>
              <button type="button" class="ghost mini" (click)="closeConfirmDialog()" [disabled]="confirmDialog.loading">Close</button>
            </div>

            <p class="muted compact">{{ confirmDialog.message }}</p>
            <p class="error" *ngIf="confirmDialog.error">{{ confirmDialog.error }}</p>

            <div class="actions">
              <button type="button" class="ghost" (click)="closeConfirmDialog()" [disabled]="confirmDialog.loading">
                Cancel
              </button>
              <button type="button" class="ghost danger" (click)="confirmDelete()" [disabled]="confirmDialog.loading">
                {{ confirmDialog.loading ? 'Deleting...' : confirmDialog.confirmLabel }}
              </button>
            </div>
          </section>
        </section>

        <section class="dialog-backdrop" *ngIf="batchImportDialog.open" (click)="closeBatchImportDialog()">
          <section class="dialog panel wide-dialog" (click)="$event.stopPropagation()">
            <div class="section-head">
              <h2>Batch Import</h2>
              <button
                type="button"
                class="ghost mini"
                (click)="closeBatchImportDialog()"
                [disabled]="batchImportDialog.importing"
              >
                Close
              </button>
            </div>

            <p class="muted compact">Target base folder: <code>{{ currentFolderPath || '/' }}</code></p>

            <ng-container *ngIf="batchImportDialog.step === 'select'">
              <div class="stack">
                <label>
                  Select multiple files
                  <input type="file" multiple (change)="onBatchFilesSelected($event, false)" />
                </label>
                <label>
                  Or select a folder
                  <input type="file" webkitdirectory directory multiple (change)="onBatchFilesSelected($event, true)" />
                </label>
              </div>

              <p class="muted compact" *ngIf="batchImportDialog.items.length">
                {{ batchImportDialog.items.length }} item(s) selected
              </p>

              <div class="table-wrap" *ngIf="batchImportDialog.items.length">
                <table>
                  <thead>
                    <tr>
                      <th>File</th>
                      <th>Target Folder</th>
                      <th>Size</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr *ngFor="let item of batchImportDialog.items">
                      <td>{{ item.relativePath || item.displayName }}</td>
                      <td>{{ item.targetFolderPath || '/' }}</td>
                      <td>{{ formatBytes(item.file.size) }}</td>
                    </tr>
                  </tbody>
                </table>
              </div>

              <div class="actions">
                <button type="button" class="ghost" (click)="clearBatchImportSelection()" [disabled]="batchImportDialog.importing">
                  Clear
                </button>
                <button type="button" (click)="startBatchTagging()" [disabled]="!batchImportDialog.items.length">
                  Next: Tag Files
                </button>
              </div>
            </ng-container>

            <ng-container *ngIf="batchImportDialog.step === 'tagging' && currentBatchItem">
              <div class="panel subpanel">
                <p class="muted compact">
                  File {{ batchImportDialog.currentIndex + 1 }} / {{ batchImportDialog.items.length }}
                </p>
                <p><strong>{{ currentBatchItem.relativePath || currentBatchItem.displayName }}</strong></p>
                <p class="muted compact">Target folder: <code>{{ currentBatchItem.targetFolderPath || '/' }}</code></p>

                <label>
                  Tags (comma-separated)
                  <input
                    name="batchTagsCurrent"
                    [(ngModel)]="batchImportDialog.currentTagsInput"
                    placeholder="invoice, project-x"
                    [disabled]="batchImportDialog.importing"
                  />
                </label>
              </div>

              <div class="actions wrap">
                <button type="button" class="ghost" (click)="cancelBatchTagging()" [disabled]="batchImportDialog.importing">
                  Back
                </button>
                <button type="button" class="ghost" (click)="applyTagsToCurrentAndNext(false)" [disabled]="batchImportDialog.importing">
                  Save Tags & Next
                </button>
                <button type="button" (click)="applyTagsToCurrentAndNext(true)" [disabled]="batchImportDialog.importing">
                  Apply To All Following
                </button>
              </div>
            </ng-container>

            <ng-container *ngIf="batchImportDialog.step === 'review'">
              <p class="muted compact">Review {{ batchImportDialog.items.length }} file(s) before import.</p>
              <div class="table-wrap">
                <table>
                  <thead>
                    <tr>
                      <th>File</th>
                      <th>Target Folder</th>
                      <th>Tags</th>
                      <th>Size</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr *ngFor="let item of batchImportDialog.items">
                      <td>{{ item.relativePath || item.displayName }}</td>
                      <td>{{ item.targetFolderPath || '/' }}</td>
                      <td>{{ item.tags || '-' }}</td>
                      <td>{{ formatBytes(item.file.size) }}</td>
                    </tr>
                  </tbody>
                </table>
              </div>

              <div class="progress-wrap" *ngIf="batchImportDialog.importing || batchImportDialog.progressPercent > 0">
                <div class="progress-meta">
                  <span>{{ batchImportDialog.progressLabel }}</span>
                  <span>{{ batchImportDialog.progressPercent }}%</span>
                </div>
                <div class="progress-track">
                  <div class="progress-bar" [style.width.%]="batchImportDialog.progressPercent"></div>
                </div>
              </div>

              <p class="success" *ngIf="batchImportDialog.success">{{ batchImportDialog.success }}</p>
              <p class="error" *ngIf="batchImportDialog.error">{{ batchImportDialog.error }}</p>

              <div class="actions">
                <button type="button" class="ghost" (click)="batchImportDialog.step = 'select'" [disabled]="batchImportDialog.importing">
                  Change Selection
                </button>
                <button type="button" (click)="runBatchImport()" [disabled]="batchImportDialog.importing || !batchImportDialog.items.length">
                  {{ batchImportDialog.importing ? 'Importing...' : 'Start Import' }}
                </button>
              </div>
            </ng-container>
          </section>
        </section>

        <section class="drop-overlay" *ngIf="dragDrop.active">
          <div class="drop-overlay-card">
            <strong>Drop files to upload</strong>
            <span>Target: {{ dragDrop.targetFolderPath || '/' }}</span>
          </div>
        </section>
      </section>
    </main>
  `,
  styles: [`
    :host { display:block; min-height:100vh; font-family:"Segoe UI", system-ui, sans-serif; color:#132238;
      background: radial-gradient(circle at 10% 10%, rgba(72,163,255,.24), transparent 40%),
                  radial-gradient(circle at 90% 0%, rgba(0,204,153,.14), transparent 35%),
                  linear-gradient(180deg,#eef6ff,#dcefff 55%,#d5ebea); }
    .page { padding: 2rem 1rem 3rem; }
    .shell { width:min(1200px,100%); margin:0 auto; display:grid; gap:1rem; }
    .hero { display:grid; grid-template-columns:1fr auto; gap:1rem; align-items:start; }
    .panel { background:rgba(255,255,255,.92); border:1px solid #c8dcf7; border-radius:16px; padding:1.5rem; box-shadow:0 16px 40px rgba(19,34,56,.12); }
    .grid { display:grid; grid-template-columns:minmax(280px,380px) 1fr; gap:1rem; align-items:start; }
    .grid.drop-target .panel:first-child,
    .grid.drop-target .panel:last-child { outline:2px dashed rgba(15,99,216,.45); outline-offset:4px; }
    .eyebrow { margin:0; text-transform:uppercase; letter-spacing:.08em; color:#2b5fa6; font-size:.75rem; font-weight:700; }
    h1 { margin:.2rem 0 .5rem; font-size:clamp(1.6rem,2vw,2.25rem); }
    h2 { margin:0 0 .75rem; font-size:1.1rem; }
    h3 { margin:0 0 .75rem; font-size:1rem; }
    .subtitle { margin:0; color:#51647f; }
    .muted { margin:0 0 1rem; color:#51647f; font-size:.92rem; }
    .muted.compact { margin-bottom:.75rem; }
    .tiny { font-size:.78rem; margin-top:.15rem; }
    .health { min-width:260px; padding:.9rem 1rem; border-radius:14px; background:rgba(255,255,255,.92); border:1px solid #c8dcf7; box-shadow:0 12px 24px rgba(19,34,56,.08); }
    .health.degraded { border-color:#f1b474; background:rgba(255,250,245,.95); }
    .health-title { font-size:.75rem; text-transform:uppercase; letter-spacing:.08em; color:#51647f; margin-bottom:.25rem; }
    .health-value { font-weight:700; color:#0c8f4f; text-transform:capitalize; }
    .health.degraded .health-value { color:#9b5d14; }
    .health-meta { margin-top:.25rem; color:#51647f; font-size:.85rem; }
    .stack { display:grid; gap:.85rem; }
    .stack > * { min-width:0; }
    .inline-stack { grid-template-columns: 1fr auto; align-items:end; }
    .actions { display:flex; gap:.5rem; align-items:center; }
    .actions.wrap { flex-wrap:wrap; }
    label { display:grid; gap:.35rem; color:#223651; font-size:.92rem; font-weight:600; min-width:0; }
    input, select {
      border:1px solid #c8dcf7;
      border-radius:10px;
      padding:.65rem .75rem;
      font:inherit;
      color:inherit;
      background:#fff;
      width:100%;
      max-width:100%;
      min-width:0;
      box-sizing:border-box;
    }
    input:focus, select:focus { outline:2px solid rgba(15,99,216,.18); border-color:#0f63d8; }
    button { border:none; background:#0f63d8; color:#fff; border-radius:10px; padding:.65rem .9rem; cursor:pointer; font-weight:600; }
    button:hover:not(:disabled) { background:#0c56bd; }
    button:disabled { opacity:.7; cursor:wait; }
    .ghost { background:#f5f9ff; color:#1f4f93; border:1px solid #c8dcf7; }
    .ghost:hover:not(:disabled) { background:#eaf3ff; }
    .danger { color:#a53224; border-color:#efc3bd; background:#fff7f6; }
    .danger:hover:not(:disabled) { background:#ffeceb; }
    .mini { padding:.3rem .55rem; border-radius:8px; font-size:.82rem; }
    .link-btn { background:none; border:none; color:#0f63d8; padding:0; cursor:pointer; font:inherit; font-weight:600; text-align:left; }
    .link-btn:hover { text-decoration:underline; background:none; }
    .section-head { display:flex; justify-content:space-between; align-items:center; gap:.75rem; margin-bottom:.75rem; }
    .section-head h2 { margin:0; }
    .filters { display:grid; grid-template-columns:1.2fr 1fr auto auto; gap:.5rem; margin-bottom:.85rem; }
    .date-filters { grid-template-columns: repeat(4, minmax(0, 1fr)) auto; align-items:end; }
    .date-filters label { font-size:.82rem; gap:.25rem; }
    .breadcrumb { display:flex; align-items:center; gap:.35rem; flex-wrap:wrap; margin-bottom:.75rem; }
    .crumb { display:inline-flex; gap:.25rem; align-items:center; color:#51647f; }
    .table-wrap { overflow:auto; border:1px solid #d9e8fb; border-radius:12px; background:#fff; }
    table { width:100%; border-collapse:collapse; min-width:920px; }
    th, td { padding:.7rem .75rem; border-bottom:1px solid #edf3fc; text-align:left; vertical-align:top; font-size:.9rem; }
    th { color:#51647f; background:#f8fbff; font-weight:700; position:sticky; top:0; }
    .drop-target-row { background: rgba(15,99,216,.06); }
    .tag { display:inline-block; margin:0 .35rem .35rem 0; padding:.2rem .5rem; border-radius:999px; background:#eaf3ff; color:#1f4f93; border:1px solid #d3e4fb; font-size:.8rem; }
    .error { color:#b13621; margin:.75rem 0 0; font-size:.9rem; }
    .success { color:#0c8f4f; margin:.75rem 0 0; font-size:.9rem; }
    .divider { margin:1rem 0; height:1px; background:linear-gradient(90deg,transparent,#d9e8fb,transparent); }
    code { background:#f5f9ff; border:1px solid #d9e8fb; border-radius:6px; padding:.1rem .35rem; font-size:.85em; }
    .progress-wrap { margin-top:.8rem; }
    .progress-meta { display:flex; justify-content:space-between; font-size:.84rem; color:#51647f; margin-bottom:.35rem; }
    .progress-track { height:10px; background:#eaf1fb; border:1px solid #d4e3f8; border-radius:999px; overflow:hidden; }
    .progress-bar { height:100%; background:linear-gradient(90deg,#0f63d8,#00a7a0); transition: width .15s linear; }
    .row-actions { display:flex; gap:.4rem; align-items:center; }
    .dialog-backdrop { position:fixed; inset:0; background:rgba(19,34,56,.35); display:grid; place-items:center; padding:1rem; z-index:50; }
    .dialog { width:min(520px,100%); margin:0; max-height:calc(100vh - 2rem); overflow:auto; }
    .wide-dialog { width:min(900px,100%); }
    .wide-dialog .table-wrap { max-height: 320px; }
    .drop-overlay { position:fixed; inset:0; pointer-events:none; display:grid; place-items:start center; padding-top:1rem; z-index:40; }
    .drop-overlay-card {
      background: rgba(19,34,56,.92);
      color: #fff;
      border-radius: 12px;
      padding: .7rem .9rem;
      display:grid;
      gap:.15rem;
      box-shadow:0 14px 28px rgba(19,34,56,.25);
      min-width: 260px;
      text-align:center;
    }
    .drop-overlay-card span { color: rgba(255,255,255,.85); font-size:.85rem; }
    .subpanel { padding:1rem; margin-bottom:.85rem; box-shadow:none; }
    @media (max-width: 980px) { .hero { grid-template-columns:1fr; } .grid { grid-template-columns:1fr; } .filters { grid-template-columns:1fr; } .inline-stack { grid-template-columns:1fr; } }
  `]
})
export class AppComponent implements OnInit {
  private readonly http = inject(HttpClient);

  token = localStorage.getItem("sfds_token") ?? "";
  refreshToken = localStorage.getItem("sfds_refresh_token") ?? "";
  currentUser = this.readStoredUser();

  health: HealthResponse | null = null;
  healthLoading = false;

  loginForm = { username: this.currentUser?.username ?? "alice", password: "" };
  authLoading = false;
  authError = "";

  registerForm = { username: "", password: "" };
  registerLoading = false;
  registerError = "";
  registerSuccess = "";

  selectedFile: File | null = null;
  uploadForm = { filename: "", tags: "" };
  uploading = false;
  uploadError = "";
  uploadSuccess = "";
  uploadProgress = 0;
  uploadProgressLabel = "Preparing upload";
  crashLoading = false;
  crashMessage = "";
  crashError = "";

  folderDialogOpen = false;
  folderForm = { name: "", tags: "" };
  folderCreating = false;
  folderError = "";
  folderSuccess = "";
  confirmDialog = {
    open: false,
    loading: false,
    title: "",
    message: "",
    confirmLabel: "Delete",
    error: "",
    entry: null as StoredEntry | null,
    force: false
  };
  dragDrop = {
    active: false,
    hoverDepth: 0,
    targetFolderPath: ""
  };
  batchImportDialog = {
    open: false,
    baseFolderPath: "",
    step: "select" as "select" | "tagging" | "review",
    items: [] as BatchImportItem[],
    currentIndex: 0,
    currentTagsInput: "",
    importing: false,
    progressPercent: 0,
    progressLabel: "",
    error: "",
    success: ""
  };

  entries: StoredEntry[] = [];
  filesLoading = false;
  filesError = "";
  currentFolderPath = "";
  search = { query: "", tag: "" };
  sort = { by: "uploadedAt", dir: "desc" as "asc" | "desc" };
  searchDates = { createdFrom: "", createdTo: "" };

  get breadcrumbs(): string[] {
    return this.currentFolderPath.split("/").filter(Boolean);
  }

  get sortedEntries(): StoredEntry[] {
    return [...this.entries].sort((a, b) => {
      if (a.entryType !== b.entryType) {
        return a.entryType === "folder" ? -1 : 1;
      }
      return a.displayName.localeCompare(b.displayName);
    });
  }

  get currentBatchItem(): BatchImportItem | null {
    return this.batchImportDialog.items[this.batchImportDialog.currentIndex] ?? null;
  }

  ngOnInit(): void {
    this.loadHealth();
    if (this.token) {
      this.loadFiles();
    }
  }

  loadHealth(): void {
    this.healthLoading = true;
    this.http.get<HealthResponse>("/api/health").subscribe({
      next: (response) => {
        this.health = response;
        this.healthLoading = false;
      },
      error: () => {
        this.healthLoading = false;
      }
    });
  }

  login(): void {
    this.authLoading = true;
    this.authError = "";
    this.registerError = "";
    this.registerSuccess = "";

    this.http.post<LoginResponse>("/api/auth/login", this.loginForm).subscribe({
      next: (response) => {
        this.applyAuthSuccess(response);
        this.authLoading = false;
        this.loginForm.password = "";
        this.currentFolderPath = "";
        this.loadFiles();
      },
      error: (error) => {
        this.authError = error?.error?.error ?? "Login failed";
        this.authLoading = false;
      }
    });
  }

  register(): void {
    this.registerLoading = true;
    this.registerError = "";
    this.registerSuccess = "";
    this.authError = "";

    this.http.post<LoginResponse>("/api/auth/register", this.registerForm).subscribe({
      next: (response) => {
        this.applyAuthSuccess(response);
        this.registerLoading = false;
        this.registerSuccess = "Account created and signed in";
        this.loginForm.username = response.user.username;
        this.loginForm.password = "";
        this.registerForm = { username: "", password: "" };
        this.currentFolderPath = "";
        this.loadFiles();
      },
      error: (error) => {
        this.registerError = error?.error?.error ?? "Registration failed";
        this.registerLoading = false;
      }
    });
  }

  logout(): void {
    this.token = "";
    this.refreshToken = "";
    this.currentUser = null;
    this.entries = [];
    this.currentFolderPath = "";
    this.selectedFile = null;
    this.uploadProgress = 0;
    this.uploading = false;
    this.crashLoading = false;
    this.crashMessage = "";
    this.crashError = "";
    localStorage.removeItem("sfds_token");
    localStorage.removeItem("sfds_refresh_token");
    localStorage.removeItem("sfds_user");
  }

  triggerCrash(): void {
    this.crashLoading = true;
    this.crashMessage = "";
    this.crashError = "";

    this.http.post<{ status?: string; message?: string }>("/api/crash", {}).subscribe({
      next: (response) => {
        this.crashLoading = false;
        this.crashMessage = response?.message || "Crash request accepted.";
      },
      error: (error) => {
        this.crashLoading = false;
        this.crashError = error?.error?.error ?? "Crash request failed";
      }
    });
  }

  onFileSelected(event: Event): void {
    const input = event.target as HTMLInputElement;
    this.selectedFile = input.files?.[0] ?? null;
    this.uploadError = "";
    this.uploadSuccess = "";
    this.uploadProgress = 0;

    if (this.selectedFile && !this.uploadForm.filename) {
      this.uploadForm.filename = this.selectedFile.name;
    }
  }

  uploadFile(): void {
    if (!this.selectedFile) {
      this.uploadError = "Select a file first";
      return;
    }

    const formData = new FormData();
    formData.append("file", this.selectedFile);
    formData.append("filename", this.uploadForm.filename.trim());
    formData.append("tags", this.uploadForm.tags.trim());
    formData.append("folderPath", this.currentFolderPath);
    if (this.selectedFile.lastModified) {
      formData.append("clientLastModifiedAt", new Date(this.selectedFile.lastModified).toISOString());
    }

    this.uploading = true;
    this.uploadError = "";
    this.uploadSuccess = "";
    this.uploadProgress = 0;
    this.uploadProgressLabel = "Uploading file";

    this.uploadSingleFileRequest(formData).subscribe({
      next: (event) => {
        if (event.type === HttpEventType.Sent) {
          this.uploadProgressLabel = "Upload started";
        }

        if (event.type === HttpEventType.UploadProgress) {
          const total = event.total ?? this.selectedFile?.size ?? 0;
          this.uploadProgress = total > 0 ? Math.min(99, Math.round((event.loaded / total) * 100)) : 0;
          this.uploadProgressLabel = `Uploaded ${this.formatBytes(event.loaded)} of ${this.formatBytes(total)}`;
        }

        if (event.type === HttpEventType.Response) {
          this.uploading = false;
          this.uploadProgress = 100;
          this.uploadProgressLabel = "Upload complete";
          this.uploadSuccess = "File uploaded";
          this.uploadForm = { filename: "", tags: "" };
          this.selectedFile = null;
          this.loadFiles();
        }
      },
      error: (error) => {
        this.uploading = false;
        this.uploadError = error?.error?.error ?? "Upload failed";
      }
    });
  }

  createFolder(): void {
    const name = this.folderForm.name.trim();
    if (!name) {
      this.folderError = "Folder name is required";
      return;
    }

    this.folderCreating = true;
    this.folderError = "";
    this.folderSuccess = "";

    this.http.post<{ folder: StoredEntry }>("/api/folders", {
      name,
      tags: this.folderForm.tags.trim(),
      parentFolderPath: this.currentFolderPath
    }, { headers: this.authHeaders() }).subscribe({
      next: () => {
        this.folderCreating = false;
        this.folderSuccess = "Folder created";
        this.folderForm = { name: "", tags: "" };
        this.folderDialogOpen = false;
        this.loadFiles();
      },
      error: (error) => {
        this.folderCreating = false;
        this.folderError = error?.error?.error ?? "Failed to create folder";
      }
    });
  }

  loadFiles(): void {
    if (!this.token) {
      return;
    }

    this.filesLoading = true;
    this.filesError = "";
    this.folderError = "";
    this.folderSuccess = "";

    const params = new URLSearchParams();
    params.set("folderPath", this.currentFolderPath);
    if (this.search.query.trim()) {
      params.set("query", this.search.query.trim());
    }
    if (this.search.tag.trim()) {
      params.set("tag", this.search.tag.trim());
    }
    if (this.searchDates.createdFrom) {
      params.set("createdFrom", new Date(`${this.searchDates.createdFrom}T00:00:00`).toISOString());
    }
    if (this.searchDates.createdTo) {
      params.set("createdTo", new Date(`${this.searchDates.createdTo}T23:59:59.999`).toISOString());
    }
    params.set("sortBy", this.sort.by);
    params.set("sortDir", this.sort.dir);

    this.http.get<{ files: StoredEntry[] }>(`/api/files?${params.toString()}`, { headers: this.authHeaders() }).subscribe({
      next: (response) => {
        this.entries = response.files ?? [];
        this.filesLoading = false;
      },
      error: (error) => {
        this.filesError = error?.error?.error ?? "Failed to load files";
        this.filesLoading = false;
        if (error?.status === 401) {
          this.logout();
        }
      }
    });
  }

  openFolder(entry: StoredEntry): void {
    if (entry.entryType !== "folder") {
      return;
    }
    this.currentFolderPath = entry.folderPath || "";
    this.loadFiles();
  }

  openCreateFolderDialog(): void {
    this.folderError = "";
    this.folderSuccess = "";
    this.folderDialogOpen = true;
  }

  openBatchImportDialog(baseFolderPath = this.currentFolderPath): void {
    this.resetBatchImportDialogState(baseFolderPath);
    this.batchImportDialog.open = true;
  }

  closeBatchImportDialog(): void {
    if (this.batchImportDialog.importing) {
      return;
    }
    this.batchImportDialog.open = false;
    this.resetBatchImportDialogState(this.currentFolderPath);
  }

  clearBatchImportSelection(): void {
    if (this.batchImportDialog.importing) {
      return;
    }
    this.batchImportDialog.items = [];
    this.batchImportDialog.currentIndex = 0;
    this.batchImportDialog.currentTagsInput = "";
    this.batchImportDialog.error = "";
    this.batchImportDialog.success = "";
    this.batchImportDialog.progressPercent = 0;
    this.batchImportDialog.progressLabel = "";
  }

  onBatchFilesSelected(event: Event, fromFolder: boolean): void {
    const input = event.target as HTMLInputElement;
    const selected = Array.from(input.files ?? []);
    if (!selected.length) {
      return;
    }

    const items = selected.map((file) => this.buildBatchImportItem(file, fromFolder, this.batchImportDialog.baseFolderPath));
    this.batchImportDialog.items = items;
    this.batchImportDialog.step = "select";
    this.batchImportDialog.currentIndex = 0;
    this.batchImportDialog.currentTagsInput = "";
    this.batchImportDialog.error = "";
    this.batchImportDialog.success = "";
    this.batchImportDialog.progressPercent = 0;
    this.batchImportDialog.progressLabel = "";

    input.value = "";
  }

  startBatchTagging(): void {
    if (!this.batchImportDialog.items.length) {
      return;
    }
    this.batchImportDialog.step = "tagging";
    this.batchImportDialog.currentIndex = 0;
    this.batchImportDialog.currentTagsInput = this.batchImportDialog.items[0]?.tags ?? "";
    this.batchImportDialog.error = "";
    this.batchImportDialog.success = "";
  }

  cancelBatchTagging(): void {
    if (this.batchImportDialog.importing) {
      return;
    }
    this.batchImportDialog.step = "select";
  }

  applyTagsToCurrentAndNext(applyToAllFollowing: boolean): void {
    const current = this.currentBatchItem;
    if (!current) {
      return;
    }

    const tagValue = this.batchImportDialog.currentTagsInput.trim();
    current.tags = tagValue;

    if (applyToAllFollowing) {
      for (let i = this.batchImportDialog.currentIndex + 1; i < this.batchImportDialog.items.length; i++) {
        this.batchImportDialog.items[i].tags = tagValue;
      }
      this.batchImportDialog.step = "review";
      return;
    }

    const nextIndex = this.batchImportDialog.currentIndex + 1;
    if (nextIndex >= this.batchImportDialog.items.length) {
      this.batchImportDialog.step = "review";
      return;
    }

    this.batchImportDialog.currentIndex = nextIndex;
    this.batchImportDialog.currentTagsInput = this.batchImportDialog.items[nextIndex]?.tags ?? "";
  }

  async runBatchImport(): Promise<void> {
    if (!this.batchImportDialog.items.length || this.batchImportDialog.importing) {
      return;
    }

    this.batchImportDialog.importing = true;
    this.batchImportDialog.error = "";
    this.batchImportDialog.success = "";
    this.batchImportDialog.progressPercent = 0;
    this.batchImportDialog.progressLabel = "Preparing import";

    try {
      await this.ensureBatchFoldersExist(this.batchImportDialog.items);

      const items = this.batchImportDialog.items;
      const totalFiles = items.length;
      const concurrency = 20;

      let nextIndex = 0;

      const workers = Array.from({ length: concurrency }, async () => {
        while (nextIndex < totalFiles) {
          const index = nextIndex++;
          const item = items[index];

          await this.uploadBatchItem(item, index, totalFiles);
        }
      });

      await Promise.all(workers);

      this.batchImportDialog.progressPercent = 100;
      this.batchImportDialog.progressLabel = `Imported ${totalFiles} file(s)`;
      this.batchImportDialog.success = `Imported ${totalFiles} file(s) successfully`;
      this.loadFiles();
    } catch (error: any) {
      this.batchImportDialog.error = error?.message || "Batch import failed";
    } finally {
      this.batchImportDialog.importing = false;
    }
  }

  onAppDragEnter(event: DragEvent): void {
    this.activateDragTarget(event, this.currentFolderPath);
  }

  onAppDragOver(event: DragEvent): void {
    this.activateDragTarget(event, this.currentFolderPath);
  }

  onAppDragLeave(event: DragEvent): void {
    this.deactivateDragTarget(event);
  }

  onDropToCurrentFolder(event: DragEvent): void {
    this.handleDrop(event, this.currentFolderPath);
  }

  onFolderDragEnter(event: DragEvent, entry: StoredEntry): void {
    if (entry.entryType !== "folder") {
      return;
    }
    this.activateDragTarget(event, entry.folderPath || "");
  }

  onFolderDragOver(event: DragEvent, entry: StoredEntry): void {
    if (entry.entryType !== "folder") {
      return;
    }
    this.activateDragTarget(event, entry.folderPath || "");
  }

  onFolderDragLeave(event: DragEvent): void {
    this.deactivateDragTarget(event);
  }

  onDropToFolder(event: DragEvent, entry: StoredEntry): void {
    if (entry.entryType !== "folder") {
      return;
    }
    this.handleDrop(event, entry.folderPath || "");
  }

  closeCreateFolderDialog(): void {
    if (this.folderCreating) {
      return;
    }
    this.folderDialogOpen = false;
    this.folderError = "";
    this.folderSuccess = "";
  }

  goToRoot(): void {
    this.currentFolderPath = "";
    this.loadFiles();
  }

  goUp(): void {
    if (!this.currentFolderPath) {
      return;
    }
    const parts = this.currentFolderPath.split("/").filter(Boolean);
    parts.pop();
    this.currentFolderPath = parts.length ? `/${parts.join("/")}` : "";
    this.loadFiles();
  }

  goToBreadcrumb(index: number): void {
    const parts = this.breadcrumbs.slice(0, index + 1);
    this.currentFolderPath = parts.length ? `/${parts.join("/")}` : "";
    this.loadFiles();
  }

  applySearch(): void {
    this.loadFiles();
  }

  clearSearch(): void {
    this.search = { query: "", tag: "" };
    this.searchDates = { createdFrom: "", createdTo: "" };
    this.sort = { by: "uploadedAt", dir: "desc" };
    this.loadFiles();
  }

  downloadFile(entry: StoredEntry): void {
    if (entry.entryType !== "file") {
      return;
    }

    this.http.get(`/api/files/${encodeURIComponent(entry.id)}/download`, {
      headers: this.authHeaders(),
      responseType: "blob"
    }).subscribe({
      next: (blob) => {
        const url = URL.createObjectURL(blob);
        const anchor = document.createElement("a");
        anchor.href = url;
        anchor.download = entry.originalFileName || entry.displayName;
        anchor.click();
        URL.revokeObjectURL(url);
      },
      error: () => {
        this.filesError = `Failed to download ${entry.displayName}`;
      }
    });
  }

  deleteEntry(entry: StoredEntry): void {
    this.openDeleteDialog(
      entry,
      entry.entryType === "folder" ? "Delete Folder" : "Delete File",
      entry.entryType === "folder"
        ? `Delete folder "${entry.displayName}"?`
        : `Delete file "${entry.displayName}"?`,
      "Delete",
      false
    );
  }

  private deleteEntryRequest(entry: StoredEntry, force: boolean): void {
    const suffix = force ? "?force=true" : "";
    this.confirmDialog.loading = true;
    this.confirmDialog.error = "";
    this.http.delete(`/api/entries/${encodeURIComponent(entry.id)}${suffix}`, {
      headers: this.authHeaders()
    }).subscribe({
      next: () => {
        this.confirmDialog.loading = false;
        this.closeConfirmDialog(true);
        this.filesError = "";
        this.loadFiles();
      },
      error: (error) => {
        this.confirmDialog.loading = false;
        if (entry.entryType === "folder" && error?.status === 409 && error?.error?.requiresConfirmation) {
          const fileCount = Number(error?.error?.fileCount ?? 0);
          const itemCount = Number(error?.error?.itemCount ?? 0);
          this.openDeleteDialog(
            entry,
            "Delete Non-Empty Folder",
            `Folder "${entry.displayName}" contains ${fileCount} file(s) and ${itemCount} item(s). Delete everything inside?`,
            "Delete Folder + Contents",
            true
          );
          return;
        }

        const message = error?.error?.error ?? `Failed to delete ${entry.displayName}`;
        this.confirmDialog.error = message;
        this.filesError = message;
      }
    });
  }

  confirmDelete(): void {
    if (!this.confirmDialog.entry || this.confirmDialog.loading) {
      return;
    }
    this.deleteEntryRequest(this.confirmDialog.entry, this.confirmDialog.force);
  }

  private openDeleteDialog(
    entry: StoredEntry,
    title: string,
    message: string,
    confirmLabel: string,
    force: boolean
  ): void {
    this.confirmDialog = {
      ...this.confirmDialog,
      open: true,
      loading: false,
      title,
      message,
      confirmLabel,
      error: "",
      entry,
      force
    };
  }

  closeConfirmDialog(skipReset = false): void {
    if (this.confirmDialog.loading) {
      return;
    }
    if (skipReset) {
      this.confirmDialog = {
        ...this.confirmDialog,
        open: false,
        error: "",
        entry: null,
        force: false,
        loading: false
      };
      return;
    }
    this.confirmDialog = {
      ...this.confirmDialog,
      open: false,
      error: "",
      entry: null,
      force: false,
      loading: false
    };
  }

  formatBytes(bytes: number): string {
    if (!Number.isFinite(bytes) || bytes <= 0) {
      return "0 B";
    }
    if (bytes < 1024) {
      return `${bytes} B`;
    }
    const units = ["KB", "MB", "GB", "TB"];
    let value = bytes;
    let unit = "B";
    for (const nextUnit of units) {
      value /= 1024;
      unit = nextUnit;
      if (value < 1024) {
        break;
      }
    }
    return `${value.toFixed(value >= 10 ? 0 : 1)} ${unit}`;
  }

  formatDate(value: string): string {
    return value ? new Date(value).toLocaleString() : "";
  }

  formatSourceDate(entry: StoredEntry): string {
    return this.formatDate(entry.clientLastModifiedAt || entry.serverTempCreatedAt || "");
  }

  private uploadSingleFileRequest(formData: FormData) {
    return this.http.post<{ file: StoredEntry }>("/api/files", formData, {
      headers: this.authHeaders(),
      observe: "events",
      reportProgress: true
    });
  }

  private resetBatchImportDialogState(baseFolderPath = this.currentFolderPath): void {
    this.batchImportDialog.baseFolderPath = baseFolderPath;
    this.batchImportDialog.step = "select";
    this.batchImportDialog.items = [];
    this.batchImportDialog.currentIndex = 0;
    this.batchImportDialog.currentTagsInput = "";
    this.batchImportDialog.error = "";
    this.batchImportDialog.success = "";
    this.batchImportDialog.progressPercent = 0;
    this.batchImportDialog.progressLabel = "";
  }

  private buildBatchImportItem(file: File, fromFolder: boolean, baseFolderPath = this.currentFolderPath): BatchImportItem {
    const rawRelative = fromFolder ? ((file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name) : file.name;
    const normalizedRelative = rawRelative.replace(/\\/g, "/");
    const parts = normalizedRelative.split("/").filter(Boolean);
    const displayName = parts[parts.length - 1] || file.name;
    const folderParts = parts.slice(0, -1);
    const targetFolderPath = folderParts.length
      ? `${baseFolderPath}/${folderParts.join("/")}`.replace(/\/+/g, "/").replace(/\/$/, "")
      : baseFolderPath;

    return {
      file,
      relativePath: normalizedRelative,
      targetFolderPath,
      displayName,
      tags: ""
    };
  }

  private async ensureBatchFoldersExist(items: BatchImportItem[]): Promise<void> {
    const uniqueFolders = Array.from(
      new Set(
        items
          .map((item) => item.targetFolderPath)
          .filter((path) => !!path)
      )
    ).sort((a, b) => a.length - b.length);

    for (const folderPath of uniqueFolders) {
      const parts = folderPath.split("/").filter(Boolean);
      let parent = "";
      for (const part of parts) {
        const next = `${parent}/${part}`.replace(/\/+/g, "/");
        await this.createFolderIfMissing(part, parent);
        parent = next;
      }
    }
  }

  private activateDragTarget(event: DragEvent, targetFolderPath: string): void {
    if (!this.hasFileDragData(event)) {
      return;
    }
    event.preventDefault();
    event.stopPropagation();
    if (event.type === "dragenter") {
      this.dragDrop.hoverDepth += 1;
    }
    this.dragDrop.active = true;
    this.dragDrop.targetFolderPath = targetFolderPath;
    if (event.dataTransfer) {
      event.dataTransfer.dropEffect = "copy";
    }
  }

  private deactivateDragTarget(event: DragEvent): void {
    if (!this.hasFileDragData(event)) {
      return;
    }
    event.preventDefault();
    event.stopPropagation();
    this.dragDrop.hoverDepth = Math.max(0, this.dragDrop.hoverDepth - 1);
    if (this.dragDrop.hoverDepth === 0) {
      this.dragDrop.active = false;
      this.dragDrop.targetFolderPath = "";
    }
  }

  private handleDrop(event: DragEvent, targetFolderPath: string): void {
    if (!this.hasFileDragData(event)) {
      return;
    }
    event.preventDefault();
    event.stopPropagation();

    const files = Array.from(event.dataTransfer?.files ?? []);
    this.resetDragDropState();

    if (!files.length) {
      return;
    }

    if (files.length === 1) {
      void this.uploadDroppedSingleFile(files[0], targetFolderPath);
      return;
    }

    this.batchImportDialog.baseFolderPath = targetFolderPath;
    this.batchImportDialog.items = files.map((file) => this.buildBatchImportItem(file, false, targetFolderPath));
    this.batchImportDialog.currentIndex = 0;
    this.batchImportDialog.currentTagsInput = "";
    this.batchImportDialog.progressPercent = 0;
    this.batchImportDialog.progressLabel = "";
    this.batchImportDialog.error = "";
    this.batchImportDialog.success = "";
    this.batchImportDialog.step = "tagging";
    this.batchImportDialog.open = true;
    this.batchImportDialog.currentTagsInput = this.batchImportDialog.items[0]?.tags ?? "";
  }

  private hasFileDragData(event: DragEvent): boolean {
    const types = Array.from(event.dataTransfer?.types ?? []);
    return types.includes("Files");
  }

  private resetDragDropState(): void {
    this.dragDrop.active = false;
    this.dragDrop.hoverDepth = 0;
    this.dragDrop.targetFolderPath = "";
  }

  private async uploadDroppedSingleFile(file: File, targetFolderPath: string): Promise<void> {
    this.selectedFile = file;
    this.uploadForm.filename = file.name;
    this.uploadForm.tags = "";
    this.uploading = true;
    this.uploadError = "";
    this.uploadSuccess = "";
    this.uploadProgress = 0;
    this.uploadProgressLabel = `Uploading ${file.name}`;

    const formData = new FormData();
    formData.append("file", file);
    formData.append("filename", file.name);
    formData.append("tags", "");
    formData.append("folderPath", targetFolderPath);
    if (file.lastModified) {
      formData.append("clientLastModifiedAt", new Date(file.lastModified).toISOString());
    }

    await new Promise<void>((resolve, reject) => {
      this.uploadSingleFileRequest(formData).subscribe({
        next: (event) => {
          if (event.type === HttpEventType.UploadProgress) {
            const total = event.total ?? file.size ?? 0;
            this.uploadProgress = total > 0 ? Math.min(99, Math.round((event.loaded / total) * 100)) : 0;
            this.uploadProgressLabel = `Uploaded ${this.formatBytes(event.loaded)} of ${this.formatBytes(total)}`;
          }
          if (event.type === HttpEventType.Response) {
            this.uploadProgress = 100;
            this.uploadProgressLabel = "Upload complete";
            resolve();
          }
        },
        error: (error) => reject(new Error(error?.error?.error || `Failed to upload ${file.name}`))
      });
    }).then(() => {
      this.uploadSuccess = `Uploaded ${file.name}`;
      this.loadFiles();
    }).catch((error: Error) => {
      this.uploadError = error.message;
    }).finally(() => {
      this.uploading = false;
      this.selectedFile = null;
    });
  }

  private async createFolderIfMissing(name: string, parentFolderPath: string): Promise<void> {
    await new Promise<void>((resolve, reject) => {
      this.http.post("/api/folders", { name, tags: "", parentFolderPath }, { headers: this.authHeaders() }).subscribe({
        next: () => resolve(),
        error: (error) => {
          if (error?.status === 409) {
            resolve();
            return;
          }
          reject(new Error(error?.error?.error || "Failed to create folder during batch import"));
        }
      });
    });
  }

  private async uploadBatchItem(item: BatchImportItem, index: number, totalFiles: number): Promise<void> {
    const formData = new FormData();
    formData.append("file", item.file);
    formData.append("filename", item.displayName);
    formData.append("tags", item.tags);
    formData.append("folderPath", item.targetFolderPath);
    if (item.file.lastModified) {
      formData.append("clientLastModifiedAt", new Date(item.file.lastModified).toISOString());
    }

    await new Promise<void>((resolve, reject) => {
      this.uploadSingleFileRequest(formData).subscribe({
        next: (event) => {
          if (event.type === HttpEventType.UploadProgress) {
            const loaded = event.loaded ?? 0;
            const fileTotal = event.total ?? item.file.size ?? 0;
            const filePct = fileTotal > 0 ? loaded / fileTotal : 0;
            const overall = ((index + filePct) / totalFiles) * 100;
            this.batchImportDialog.progressPercent = Math.min(99, Math.max(0, Math.round(overall)));
            this.batchImportDialog.progressLabel = `Uploading ${index + 1}/${totalFiles}: ${item.relativePath || item.displayName}`;
          }

          if (event.type === HttpEventType.Response) {
            this.batchImportDialog.progressPercent = Math.min(99, Math.round(((index + 1) / totalFiles) * 100));
            resolve();
          }
        },
        error: (error) => {
          reject(new Error(error?.error?.error || `Failed to upload ${item.displayName}`));
        }
      });
    });
  }

  private authHeaders(): HttpHeaders {
    return new HttpHeaders({ Authorization: `Bearer ${this.token}` });
  }

  private applyAuthSuccess(response: LoginResponse): void {
    this.token = response.token;
    this.refreshToken = response.refreshToken;
    this.currentUser = response.user;
    localStorage.setItem("sfds_token", response.token);
    localStorage.setItem("sfds_refresh_token", response.refreshToken);
    localStorage.setItem("sfds_user", JSON.stringify(response.user));
  }

  private readStoredUser(): LoginResponse["user"] | null {
    const raw = localStorage.getItem("sfds_user");
    if (!raw) {
      return null;
    }
    try {
      return JSON.parse(raw) as LoginResponse["user"];
    } catch {
      return null;
    }
  }
}
