var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { LitElement, html, nothing } from 'lit';
import { property, state } from 'lit/decorators.js';
import { announce } from '../../util/aria.js';
import { EventEmitterMixin } from '../../util/EventEmitterMixin.js';
import { l10n } from '../../util/i18n.js';
import { ReadyMixin } from '../../util/ReadyMixin.js';
import { formatFileSize, matchesAccept, validateFileSize } from '../../util/upload.js';
import '../icon/Icon.js';
import './UploadItem.js';
/**
 * A file upload component with drop zone and file list.
 *
 * The component is presentation + file-selection only. It handles file browsing,
 * drag-and-drop reception, client-side validation, and rendering the file list
 * with progress bars. It does not perform network requests — the consumer listens
 * for events, performs their own upload, and drives state back via imperative methods.
 *
 * @element btu-upload
 *
 * @fires {CustomEvent<unknown>} btu-upload-ready - Fired when the component has completed first render
 * @fires {CustomEvent<{id: string, file: File}>} btu-upload-file-added - Fired when a file passes validation and is added
 * @fires {CustomEvent<{id: string, file: File}>} btu-upload-file-removed - Fired when a file is removed (user action, programmatic, or on disconnect for in-flight uploads)
 * @fires {CustomEvent<{id: string, file: File}>} btu-upload-file-retry - Fired when user clicks "Try again" on a failed upload
 * @fires {CustomEvent<{file: File, reason: 'type' | 'size' | 'count'}>} btu-upload-file-rejected - Fired when a file fails client-side validation or the queue is full
 * @fires {CustomEvent<{id: string, file: File}>} btu-upload-file-completed - Fired when a file's status transitions into 'complete'
 * @fires {CustomEvent<{method: string, id: string, reason: 'unknown-id' | 'invalid-transition', message: string}>} btu-upload-api-error - Fired when an imperative API call references an unknown id or an illegal status transition
 *
 * @cssprop --upload-dropzone-bg - Drop zone background color (default: gray-25)
 * @cssprop --upload-dropzone-border-color - Drop zone border color (default: gray-300; gray-200 when disabled, gray-400 in high-contrast)
 * @cssprop --upload-dropzone-border-radius - Drop zone border radius (default: borderRadius.lg)
 * @cssprop --upload-dropzone-hover-bg - Drop zone hover background (hover default: gray-50; drag-over default: primary-25)
 * @cssprop --upload-dropzone-hover-border-color - Drop zone hover/focus border color (default: primary-300; drag-over uses primary-400)
 * @cssprop --upload-dropzone-focus-ring-color - Drop zone focus ring color (default: primary-100)
 * @cssprop --upload-item-bg - Item background color (default: white)
 * @cssprop --upload-item-border-color - Item border color (default: gray-200)
 * @cssprop --upload-item-active-border-color - Item active/in-progress border (default: gray-300)
 * @cssprop --upload-item-complete-border-color - Item completed border color (default: primary-500)
 * @cssprop --upload-item-error-bg - Item error background (default: error-25)
 * @cssprop --upload-item-error-border-color - Item error border color (default: error-300)
 */
export default class Upload extends EventEmitterMixin(ReadyMixin(LitElement)) {
    constructor() {
        super(...arguments);
        /**
         * Accepted file types, matching the HTML `<input accept>` attribute format.
         * Client-side filter only — the browser's file picker uses this as a hint,
         * and drag-drop is enforced by this component. Server-side validation is
         * still required.
         *
         * Supported forms (comma-separated, any combination):
         * - `image/*` — MIME wildcard (any image)
         * - `application/pdf` — exact MIME type
         * - `.pdf`, `.docx` — file extension (case-insensitive)
         *
         * Examples:
         * - `accept="image/*"` — any image
         * - `accept="image/*,application/pdf"` — images and PDFs
         * - `accept=".jpg,.jpeg,.png,.heic"` — specific extensions
         * - `accept=""` (or omitted) — allow all
         *
         * Note: users can bypass the file picker's filter (most browsers offer an
         * "All Files" option), so this attribute is a UX filter, not a security
         * boundary.
         *
         * @attr
         */
        this.accept = '';
        /**
         * Maximum file size in bytes. Files exceeding this are rejected. 0 means unlimited.
         * @attr max-file-size
         */
        this.maxFileSize = 0;
        /**
         * Maximum number of files allowed. When omitted, uploads are unlimited.
         * `max-files="1"` enters single-file mode (selecting a new file replaces the existing one).
         * `max-files="0"` allows zero files. Values above 1 cap the queue at that many files.
         * @attr max-files
         */
        this.maxFiles = Infinity;
        /**
         * Disables all interaction — file selection, drag-and-drop, and action buttons.
         * @attr
         */
        this.disabled = false;
        /**
         * Disables drag-and-drop. File selection via click still works.
         * @attr no-drag
         */
        this.noDrag = false;
        this._files = new Map();
        this._items = new Map();
        this._milestones = new Map();
        this._dragOver = false;
        this._dragCounter = 0;
    }
    get _inputEl() {
        return this.querySelector('input[type="file"]');
    }
    createRenderRoot() {
        return this;
    }
    connectedCallback() {
        super.connectedCallback();
        this.classList.add('btu-upload');
    }
    disconnectedCallback() {
        super.disconnectedCallback();
        // Notify consumers about files still in flight so they can abort their XHRs
        // and reconcile their own state. Completed files are terminal; don't re-emit.
        for (const state of this._files.values()) {
            if (state.status !== 'complete') {
                this.emit('btu-upload-file-removed', { id: state.id, file: state.file });
            }
        }
        this._files.clear();
        this._items.clear();
        this._milestones.clear();
        this._dragCounter = 0;
        this._dragOver = false;
    }
    firstUpdated() {
        // Contain UploadItem's internal bubbling signals at this boundary so they
        // don't leak to ancestor listeners. Consumers only see the public events.
        this.addEventListener('btu-upload-item-remove', e => {
            e.stopPropagation();
            const { id } = e.detail;
            this.removeFile(id);
        });
        this.addEventListener('btu-upload-item-retry', e => {
            e.stopPropagation();
            const { id } = e.detail;
            const fileState = this._files.get(id);
            if (fileState) {
                this.emit('btu-upload-file-retry', { id: fileState.id, file: fileState.file });
            }
        });
        this.emit('btu-upload-ready');
    }
    _handleInputChange(e) {
        const input = e.target;
        if (!input.files)
            return;
        this._processFiles(Array.from(input.files));
        input.value = '';
    }
    _handleSelectClick(e) {
        // Stop the click from bubbling to the dropzone, which would re-invoke the picker.
        e.stopPropagation();
        if (this._interactionBlocked)
            return;
        this._inputEl?.click();
    }
    _handleDropzoneClick() {
        if (this._interactionBlocked)
            return;
        this._inputEl?.click();
    }
    _handleDragEnter(e) {
        if (this._interactionBlocked || this.noDrag)
            return;
        e.preventDefault();
        this._dragCounter++;
        this._dragOver = true;
    }
    _handleDragOver(e) {
        if (this._interactionBlocked || this.noDrag)
            return;
        e.preventDefault();
    }
    _handleDragLeave(e) {
        if (this._interactionBlocked || this.noDrag)
            return;
        this._dragCounter--;
        if (this._dragCounter <= 0) {
            this._dragCounter = 0;
            this._dragOver = false;
        }
    }
    _handleDrop(e) {
        if (this._interactionBlocked || this.noDrag)
            return;
        e.preventDefault();
        this._dragCounter = 0;
        this._dragOver = false;
        if (!e.dataTransfer?.files)
            return;
        this._processFiles(Array.from(e.dataTransfer.files));
    }
    _processFiles(files) {
        if (this._interactionBlocked)
            return;
        if (this.maxFiles === 1) {
            const file = files[0];
            if (!file)
                return;
            if (this._validateFile(file)) {
                // Replace existing file (fires btu-upload-file-removed, then btu-upload-file-added)
                if (this._files.size > 0) {
                    const existingId = this._files.keys().next().value;
                    this.removeFile(existingId);
                }
                this._addFile(file);
            }
            // Reject any additional files the user dropped at once — single-file mode
            // only accepts one; without this, files[1..n] would disappear silently.
            for (const extra of files.slice(1)) {
                this._rejectCount(extra);
            }
            return;
        }
        for (const file of files) {
            if (this._files.size >= this.maxFiles) {
                this._rejectCount(file);
                continue;
            }
            if (!this._validateFile(file))
                continue;
            this._addFile(file);
        }
    }
    _rejectCount(file) {
        this.emit('btu-upload-file-rejected', { file, reason: 'count' });
        announce(l10n('upload', 'announceRejectedCount', 'File rejected: ___. Maximum file count reached.', [file.name]));
    }
    _validateFile(file) {
        if (this.accept && !matchesAccept(file, this.accept)) {
            this.emit('btu-upload-file-rejected', { file, reason: 'type' });
            announce(l10n('upload', 'announceRejectedType', 'File rejected: ___. File type not accepted.', [file.name]));
            return false;
        }
        if (this.maxFileSize > 0 && !validateFileSize(file, this.maxFileSize)) {
            this.emit('btu-upload-file-rejected', { file, reason: 'size' });
            announce(l10n('upload', 'announceRejectedSize', 'File rejected: ___. File exceeds size limit.', [file.name]));
            return false;
        }
        return true;
    }
    _addFile(file) {
        const id = crypto.randomUUID();
        const fileState = {
            id,
            file,
            status: 'pending',
            progress: 0,
        };
        this._files.set(id, fileState);
        this._milestones.set(id, new Set());
        this._renderItem(fileState);
        this.requestUpdate();
        this.emit('btu-upload-file-added', { id, file });
        const sizeStr = formatFileSize(file.size);
        announce(l10n('upload', 'announceAdded', 'File added: ___, ___', [file.name, sizeStr]));
    }
    _reportUnknownId(method, id) {
        const message = `no file found with id "${id}"`;
        this.emit('btu-upload-api-error', { method, id, reason: 'unknown-id', message });
        console.warn(`[btu-upload] ${method}: ${message}`);
    }
    _getOrReport(method, id) {
        const current = this._files.get(id);
        if (!current) {
            this._reportUnknownId(method, id);
            return null;
        }
        return current;
    }
    _renderItem(fileState) {
        const list = this.querySelector('.btu-upload-list');
        if (!list)
            return;
        const item = document.createElement('btu-upload-item');
        item.setAttribute('role', 'listitem');
        item._configure(fileState);
        list.appendChild(item);
        this._items.set(fileState.id, item);
    }
    /* ─── Imperative API ─── */
    /**
     * Update the progress of a file upload.
     * Clamps value to 0-100 and coerces non-finite inputs to 0. Announces milestones
     * (25%, 50%, 75%) to screen readers; 100% completion is announced by setFileStatus('complete').
     *
     * Status transitions:
     * - `pending` → `uploading` (auto — first progress report starts the upload)
     * - `uploading` → `uploading` (progress updates)
     * - `error` → `error` (progress updates only; status is preserved so a stale late
     *   callback can't wipe the error UI. To recover from error, call
     *   `setFileStatus(id, 'uploading')` first, then report progress.)
     * - `complete` → ignored (terminal state)
     */
    setFileProgress(id, percent) {
        const current = this._getOrReport('setFileProgress', id);
        if (!current)
            return;
        if (current.status === 'complete')
            return;
        const safe = Number.isFinite(percent) ? Math.round(percent) : 0;
        const clamped = Math.min(Math.max(safe, 0), 100);
        // No-op when progress hasn't changed and no status transition is needed.
        // Upload callbacks can fire hundreds of times per file; skipping these
        // avoids a per-byte state copy + child re-render.
        if (current.status !== 'pending' && clamped === current.progress)
            return;
        // Pending auto-transitions to uploading once progress is reported.
        const next = current.status === 'error'
            ? { ...current, progress: clamped }
            : { id: current.id, file: current.file, symbol: current.symbol, status: 'uploading', progress: clamped };
        this._files.set(id, next);
        this._items.get(id)?._configure(next);
        const milestones = this._milestones.get(id);
        if (milestones && milestones.size < 3) {
            for (const milestone of [25, 50, 75]) {
                if (clamped >= milestone && !milestones.has(milestone)) {
                    milestones.add(milestone);
                    announce(l10n('upload', 'announceProgress', '___ ___% uploaded', [current.file.name, String(milestone)]));
                }
            }
        }
    }
    /**
     * Set the status of a file.
     * Valid statuses: 'uploading', 'complete', 'error'. For 'error' status, an
     * optional message can be provided.
     *
     * Transitions from 'complete' are rejected (terminal state) — a late-resolving
     * XHR cannot overwrite a prior success. Rejected calls emit `btu-upload-api-error`
     * with reason: 'invalid-transition'.
     */
    setFileStatus(id, status, message) {
        const current = this._getOrReport('setFileStatus', id);
        if (!current)
            return;
        if (current.status === 'complete' && status !== 'complete') {
            const msg = `cannot transition from 'complete' to '${status}' for id "${id}"`;
            this.emit('btu-upload-api-error', { method: 'setFileStatus', id, reason: 'invalid-transition', message: msg });
            console.warn(`[btu-upload] setFileStatus: ${msg}`);
            return;
        }
        const { id: _id, file, symbol, progress } = current;
        let next;
        switch (status) {
            case 'uploading':
                next = { id: _id, file, symbol, status: 'uploading', progress };
                break;
            case 'complete':
                next = { id: _id, file, symbol, status: 'complete', progress: 100 };
                break;
            case 'error':
                next = { id: _id, file, symbol, status: 'error', progress, errorMessage: message ?? '' };
                break;
        }
        this._files.set(id, next);
        this._items.get(id)?._configure(next);
        if (status === 'complete') {
            announce(l10n('upload', 'announceComplete', '___ upload complete', [file.name]));
        }
        else if (status === 'error') {
            // Include the detail message in the announcement so screen-reader users get
            // the same context the visible error text shows; the global announce() live
            // region is the only path that reaches AT, since the error text itself is
            // not in a live region (would otherwise duplicate the announcement).
            const detail = message?.trim();
            announce(detail
                ? l10n('upload', 'announceErrorWithDetail', '___ upload failed: ___', [file.name, detail])
                : l10n('upload', 'announceError', '___ upload failed', [file.name]));
        }
        if (current.status !== 'complete' && status === 'complete') {
            this.emit('btu-upload-file-completed', { id: next.id, file: next.file });
        }
    }
    /**
     * Override the icon symbol for a file.
     */
    setFileSymbol(id, symbol) {
        const current = this._getOrReport('setFileSymbol', id);
        if (!current)
            return;
        if (current.symbol === symbol)
            return;
        const next = { ...current, symbol };
        this._files.set(id, next);
        this._items.get(id)?._configure(next);
    }
    /**
     * Remove a file programmatically. Fires `btu-upload-file-removed`.
     */
    removeFile(id) {
        const current = this._getOrReport('removeFile', id);
        if (!current)
            return;
        this._files.delete(id);
        this._milestones.delete(id);
        this._items.get(id)?.remove();
        this._items.delete(id);
        this.requestUpdate();
        this.emit('btu-upload-file-removed', { id, file: current.file });
        announce(l10n('upload', 'announceRemoved', 'File removed: ___', [current.file.name]));
    }
    /**
     * Get a snapshot of all current files and their states.
     * States are readonly; mutations on the returned array's elements are ineffective.
     */
    getFiles() {
        return Array.from(this._files.values());
    }
    get _dropzoneFull() {
        // Single-file mode is replace-on-select, not capped — never treat as full,
        // otherwise _interactionBlocked gates every handler once the first file lands.
        if (this.maxFiles === 1)
            return false;
        return this._files.size >= this.maxFiles;
    }
    get _interactionBlocked() {
        return this.disabled || this._dropzoneFull;
    }
    get _showCountPill() {
        return Number.isFinite(this.maxFiles) && this.maxFiles !== 1;
    }
    _renderSelectButton(text) {
        return html `<button
      type="button"
      class="btu-upload-dropzone-action"
      ?disabled="${this._interactionBlocked}"
      @click="${this._handleSelectClick}"
    >
      ${text}
    </button>`;
    }
    render() {
        const isFull = this._dropzoneFull;
        const isDisabled = this.disabled;
        const dropzoneClasses = [
            'btu-upload-dropzone',
            this._dragOver ? 'btu-upload-dropzone-dragover' : '',
            isDisabled || isFull ? 'btu-upload-dropzone-disabled' : '',
        ]
            .filter(Boolean)
            .join(' ');
        const dropzoneLabel = !isDisabled && isFull
            ? l10n('upload', 'dropzoneLabelFull', 'File upload, maximum reached')
            : l10n('upload', 'dropzoneLabel', 'File upload');
        return html `
      <input
        type="file"
        class="sr-only"
        tabindex="-1"
        aria-hidden="true"
        accept="${this.accept || nothing}"
        ?multiple="${this.maxFiles !== 1}"
        ?disabled="${isDisabled}"
        @change="${this._handleInputChange}"
      />

      <div
        class="${dropzoneClasses}"
        aria-label="${dropzoneLabel}"
        aria-disabled="${isDisabled || isFull ? 'true' : 'false'}"
        @click="${this._handleDropzoneClick}"
        @dragenter="${this._handleDragEnter}"
        @dragover="${this._handleDragOver}"
        @dragleave="${this._handleDragLeave}"
        @drop="${this._handleDrop}"
      >
        ${this._showCountPill
            ? html `<span
              class="btu-upload-dropzone-count"
              aria-label="${l10n('upload', 'countOfMaxLabel', '___ of ___ files', [this._files.size, this.maxFiles])}"
              >${l10n('upload', 'countOfMax', '___ of ___', [this._files.size, this.maxFiles])}</span
            >`
            : nothing}
        <div class="btu-upload-dropzone-icon">
          <btu-icon symbol="upload-cloud" size="md"></btu-icon>
        </div>
        <div class="btu-upload-dropzone-prompt">
          <span class="btu-upload-dropzone-text" aria-hidden="${this._dragOver ? 'true' : 'false'}">
            ${this.noDrag
            ? this._renderSelectButton(l10n('upload', 'dropzoneActionNoDrag', 'Select files'))
            : html `${l10n('upload', 'dropzonePrefix', 'Drag and drop or')}
                ${this._renderSelectButton(l10n('upload', 'dropzoneAction', 'select'))}
                ${l10n('upload', 'dropzoneSuffix', 'files')}`}
          </span>
          ${this.noDrag
            ? nothing
            : html `<span class="btu-upload-dropzone-drag-message" aria-hidden="${this._dragOver ? 'false' : 'true'}"
                >${l10n('upload', 'dragOver', 'Drop Files Here')}</span
              >`}
        </div>
      </div>

      <div class="btu-upload-list" role="list" aria-label="${l10n('upload', 'listLabel', 'Uploaded files')}"></div>
    `;
    }
}
__decorate([
    property({ type: String })
], Upload.prototype, "accept", void 0);
__decorate([
    property({ type: Number, attribute: 'max-file-size' })
], Upload.prototype, "maxFileSize", void 0);
__decorate([
    property({
        attribute: 'max-files',
        converter: {
            fromAttribute: value => {
                if (value === null || value === '')
                    return Infinity;
                const parsed = Number(value);
                if (!Number.isFinite(parsed))
                    return Infinity;
                return Math.max(0, Math.floor(parsed));
            },
            toAttribute: value => {
                if (typeof value !== 'number' || !Number.isFinite(value))
                    return null;
                return String(value);
            },
        },
    })
], Upload.prototype, "maxFiles", void 0);
__decorate([
    property({ type: Boolean, reflect: true })
], Upload.prototype, "disabled", void 0);
__decorate([
    property({ type: Boolean, attribute: 'no-drag' })
], Upload.prototype, "noDrag", void 0);
__decorate([
    state()
], Upload.prototype, "_dragOver", void 0);
if (!customElements.get('btu-upload')) {
    customElements.define('btu-upload', Upload);
}
//# sourceMappingURL=Upload.js.map