import { LitElement, html, css, nothing } from 'lit';
import { customElement, state, query } from 'lit/decorators.js';
import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '@awesome.me/webawesome/dist/components/popup/popup.js';
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';

import {
    CreatePlaylist,
    CreatePlaylistWithTracks,
    CreateSmartPlaylist,
    AddTracksToPlaylist,
    DeletePlaylist,
    RenamePlaylist,
    ImportPlaylists,
    FindDuplicateTracksInPlaylist,
} from '@go/playlist/service.js';
import { PlaylistFilePicker } from '@go/frontendutil/frontendutil.js';
import type * as playlist from '@go/playlist/models.js';
import { PlaylistController } from '@store/controllers/playlist-controller';
import { SearchController } from '@store/controllers/search-controller';
import {
    hasTrackPayload,
    getDragPayload,
    getActiveDragSource,
    getActiveDragPlaylistId,
} from '@utils/drag-controller';
import {
    MenuKeyboard,
    contextMenuStyles,
    isContextMenuKey,
} from '@utils/context-menu-controller.js';
import { describeError } from '@utils/describe-error';
import { notificationStore } from '@store/notification-store';
import { confirmAction } from '@components/confirm-dialog/confirm-dialog';
import { ViewLifecycleMixin } from '@utils/view-lifecycle';
import { FavoritesController } from '@store/controllers/favorites-controller';
import '@components/duplicate-tracks-dialog/duplicate-tracks-dialog.js';
import type { DuplicateTracksDialog } from '@components/duplicate-tracks-dialog/duplicate-tracks-dialog.js';
import {
    ICON_NEW,
    ICON_PLAYLIST,
} from '@utils/icon-language';

const SCROLL_DEBOUNCE_MS = 100;

type PlaylistSortField = 'name' | 'created' | 'modified' | 'tracks';
type SortDirection = 'asc' | 'desc';

const PLAYLIST_SORT_KEY = 'playlist-view-sort-field';
const PLAYLIST_SORT_DIR_KEY = 'playlist-view-sort-direction';

const SORT_OPTIONS: { id: PlaylistSortField; label: string }[] = [
    { id: 'modified', label: 'Recent' },
    { id: 'name', label: 'Name' },
    { id: 'created', label: 'Date Created' },
    { id: 'tracks', label: 'Track Count' },
];

interface PlaylistEntry {
    summary: playlist.Summary;
    tracks: playlist.Track[];
}

@customElement('playlist-view')
export class PlaylistView extends ViewLifecycleMixin(LitElement) {
    private playlistCtrl = new PlaylistController(this);
    private searchCtrl = new SearchController(this);
    private favCtrl = new FavoritesController(this);

    /** Tracks the store's cached array reference to detect refreshes. */
    private lastPlaylistsRef:
        | playlist.WithTracks[]
        | null = null;

    private scrollDebounceTimer: ReturnType<
        typeof setTimeout
    > | null = null;
    private lastSearchTerm = '';

    // =================================================================
    // Filtered entries (search — playlist name only)
    // =================================================================

    private get filteredEntries(): PlaylistEntry[] {
        const term =
            this.searchCtrl.term.toLowerCase();

        if (!term) return this.entries;

        return this.entries.filter((e) =>
            e.summary.Name.toLowerCase().includes(
                term,
            ),
        );
    }

    @state() private entries: PlaylistEntry[] = [];
    @state() private loading = true;
    @state() private refreshing = false;
    @state() private creating = false;
    @state() private creatingSmart = false;
    @state() private newPlaylistName = '';
    @state() private playlistContextMenuOpen = false;
    @state() private playlistContextMenuIndex = -1;
    @state() private renamingPlaylistIndex = -1;
    @state() private renameValue = '';

    /** Indices of playlists selected via Ctrl/Shift+Click. */
    @state() private selectedPlaylists: Set<number> = new Set();

    /** Anchor index for Shift+Click range selection on playlists. */
    private lastSelectedPlaylistIndex: number | null = null;

    /** Index of the playlist currently hovered during a drag. */
    @state() private dragOverPlaylistIndex = -1;

    /** True when dragging over empty space in the playlist list. */
    @state() private dragOverEmptyZone = false;

    /** True when dragging over the "New Playlist" button. */
    @state() private dragOverNewButton = false;

    /** Error message from the last failed import, auto-clears. */
    @state() private importError = '';

    /** Active sort field for playlists. */
    @state() private sortField: PlaylistSortField = 'modified';

    /** Sort direction. */
    @state() private sortDirection: SortDirection = 'desc';

    /**
     * File paths from a drop that landed outside any playlist.
     * When non-empty the create form is in "create-and-add" mode.
     */
    private pendingDropPaths: string[] = [];

    @query('#playlist-context-menu')
    private playlistContextMenuPopup!: WaPopup;

    @query('duplicate-tracks-dialog')
    private duplicateDialog!: DuplicateTracksDialog;

    /** This view renders its own context menu rather than using
     *  `ContextMenuController`, so it borrows just the keyboard model —
     *  which is the part that must not exist twice. */
    private menuKeyboard = new MenuKeyboard(() =>
        this.closePlaylistContextMenu(),
    );

    private closePlaylistCtxMenuHandler =
        () => this.closePlaylistContextMenu();

    private playlistCtxMenuMousedownHandler =
        (e: MouseEvent) => {
            const plPopup =
                this.playlistContextMenuPopup;

            if (
                plPopup &&
                e.composedPath().includes(plPopup)
            ) {
                return;
            }

            this.closePlaylistContextMenu();
        };

    private clearSelectionHandler = (e: MouseEvent) => {
        const path = e.composedPath();
        const isPlaylistHeaderClick = path.some(
            (el) =>
                el instanceof HTMLElement &&
                el.classList.contains('playlist-header') &&
                this.shadowRoot?.contains(el),
        );

        if (!isPlaylistHeaderClick) {
            this.selectedPlaylists = new Set();
            this.lastSelectedPlaylistIndex = null;
        }
    };

    static override styles = [
        contextMenuStyles,
        css`
        :host {
            display: flex;
            flex-direction: column;
            overflow: hidden;
            height: 100%;
            position: relative;
            contain: layout style;
        }

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

        .header-spinner {
            display: inline-block;
            width: 14px;
            height: 14px;
            border: 2px solid var(--yj-border-subtle, #555);
            border-top-color: var(--yj-text-primary, #fff);
            border-radius: 50%;
            animation: spin 0.6s linear infinite;
        }

        .new-playlist-button {
            background: none;
            border: 1px solid var(--yj-border-subtle, #555);
            border-radius: 4px;
            color: var(--yj-text-primary, #fff);
            padding: 6px 12px;
            font-size: 13px;
            cursor: pointer;
            display: flex;
            align-items: center;
            gap: 6px;
            font-family: inherit;
        }

        .new-playlist-button:hover,
        .new-playlist-button.drag-over {
            border-color: var(--yj-accent, #ffd43b);
            color: var(--yj-accent-text, #ffd43b);
        }

        .new-playlist-button.drag-over {
            background-color: var(
                --yj-accent-bg-strong,
                rgba(255, 212, 59, 0.15)
            );
        }

        .create-form {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 12px 16px;
            border-bottom: 1px solid var(--yj-border-subtle, #333);
            flex-shrink: 0;
        }

        .create-form input {
            flex: 1;
            background: var(--yj-bg-surface, #2b3035);
            border: 1px solid var(--yj-border-subtle, #555);
            border-radius: 4px;
            color: var(--yj-text-primary, #fff);
            padding: 6px 10px;
            font-size: 13px;
            outline: none;
            font-family: inherit;
        }

        .create-form input:focus {
            border-color: var(--yj-accent, #ffd43b);
        }

        .create-form input::placeholder {
            color: var(--yj-text-tertiary, #888);
        }

        .create-form button {
            background: var(--yj-bg-overlay, #495057);
            border: none;
            border-radius: 4px;
            color: var(--yj-text-primary, #fff);
            padding: 6px 12px;
            font-size: 13px;
            cursor: pointer;
            font-family: inherit;
        }

        .create-form button:hover {
            background: var(--yj-bg-overlay, #5a6268);
        }

        .create-form button.primary {
            background: var(--yj-accent, #ffd43b);
            color: var(--yj-accent-fg, #000);
        }

        .create-form button.primary:hover {
            background: var(--yj-accent-hover, #ffe066);
        }

        .create-form button.primary:disabled {
            background: var(--yj-accent-muted, #665a1e);
            color: var(--yj-text-tertiary, #888);
            cursor: not-allowed;
        }

        .playlist-list {
            flex: 1;
            overflow-y: auto;
            padding: 0;
            margin: 0;
            list-style: none;
            display: flex;
            flex-direction: column;
            contain: paint;
        }

        .playlist-item {
            border-bottom: 1px solid
                var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
        }

        .playlist-header {
            display: flex;
            align-items: center;
            padding: 12px 16px;
            gap: 10px;
            cursor: pointer;
            user-select: none;
        }

        .playlist-header:hover {
            background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
        }

        .playlist-header.selected {
            background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15));
        }

        .playlist-item.drag-over > .playlist-header {
            background-color: var(--yj-accent-bg-strong, rgba(255, 212, 59, 0.15));
            outline: 1px dashed var(--yj-accent, #ffd43b);
            outline-offset: -1px;
        }

        .playlist-icon {
            font-size: 18px;
            color: var(--yj-text-tertiary, #888);
            flex-shrink: 0;
        }

        .playlist-name {
            font-size: 14px;
            color: var(--yj-text-primary, #fff);
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            flex: 1;
        }

        .track-count {
            font-size: 11px;
            color: var(--yj-text-tertiary, #666);
            flex-shrink: 0;
        }

        .loading {
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 32px;
            color: var(--yj-text-secondary, #b3b3b3);
        }

        .empty-state {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 48px 20px;
            color: var(--yj-text-secondary, #b3b3b3);
            text-align: center;
            gap: 8px;
        }

        .empty-state wa-icon {
            font-size: 32px;
        }

        .empty-state p {
            margin: 4px 0;
        }

        .drop-zone-icon {
            display: none;
            align-items: center;
            justify-content: center;
            width: 56px;
            height: 56px;
            border-radius: 12px;
            background: var(
                --yj-accent-bg-strong,
                rgba(255, 212, 59, 0.18)
            );
            color: var(--yj-accent-text, #ffd43b);
            font-size: 28px;
            pointer-events: none;
        }

        .empty-state.drag-over {
            background-color: var(
                --yj-accent-bg-strong,
                rgba(255, 212, 59, 0.15)
            );
            outline: 2px dashed
                var(--yj-accent, #ffd43b);
            outline-offset: -4px;
        }

        .empty-state.drag-over .drop-zone-icon {
            display: flex;
        }

        .empty-state.drag-over > :not(.drop-zone-icon) {
            display: none;
        }

        .drop-zone {
            flex: 1;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 80px;
        }

        .drop-zone.drag-over {
            background-color: var(
                --yj-accent-bg-strong,
                rgba(255, 212, 59, 0.15)
            );
            outline: 2px dashed
                var(--yj-accent, #ffd43b);
            outline-offset: -4px;
        }

        .drop-zone.drag-over .drop-zone-icon {
            display: flex;
        }

        #playlist-context-menu {
            z-index: 200;
        }

        .rename-input {
            flex: 1;
            background: var(--yj-bg-surface, #2b3035);
            border: 1px solid var(--yj-accent, #ffd43b);
            border-radius: 4px;
            color: var(--yj-text-primary, #fff);
            padding: 4px 8px;
            font-size: 14px;
            outline: none;
            font-family: inherit;
            min-width: 0;
        }

        .import-button {
            background: none;
            border: 1px solid var(--yj-border-subtle, #555);
            border-radius: 4px;
            color: var(--yj-text-primary, #fff);
            padding: 6px 12px;
            font-size: 13px;
            cursor: pointer;
            display: flex;
            align-items: center;
            gap: 6px;
            font-family: inherit;
        }

        .import-button:hover {
            border-color: var(--yj-accent, #ffd43b);
            color: var(--yj-accent-text, #ffd43b);
        }

        .import-error {
            padding: 0.5em 0.75em;
            margin: 0.5em 16px 0;
            font-size: 0.8em;
            color: var(--yj-error-text, #ff8787);
            background: color-mix(
                in srgb,
                var(--yj-error, #e03131) 10%,
                var(--yj-bg-elevated, #343a40)
            );
            border-radius: 4px;
            border-left: 3px solid
                var(--yj-error, #e03131);
        }


        #sort-dropdown {
          z-index: 200;
        }

    `];

    // =================================================================
    // Sort controls
    // =================================================================

    private restoreSortPreferences() {
        try {
            const field =
                localStorage.getItem(PLAYLIST_SORT_KEY);

            if (
                field &&
                SORT_OPTIONS.some(
                    (o) => o.id === field,
                )
            ) {
                this.sortField =
                    field as PlaylistSortField;
            }

            const dir = localStorage.getItem(
                PLAYLIST_SORT_DIR_KEY,
            );

            if (dir === 'asc' || dir === 'desc') {
                this.sortDirection = dir;
            }
        } catch {
            /* localStorage unavailable */
        }
    }

    private saveSortPreferences() {
        try {
            localStorage.setItem(
                PLAYLIST_SORT_KEY,
                this.sortField,
            );
            localStorage.setItem(
                PLAYLIST_SORT_DIR_KEY,
                this.sortDirection,
            );
        } catch {
            /* localStorage unavailable */
        }
    }

    private get sortedEntries(): PlaylistEntry[] {
        const entries = this.filteredEntries;
        const dir =
            this.sortDirection === 'asc' ? 1 : -1;

        return [...entries].sort((a, b) => {
            // Pin default playlist to top when enabled.
            if (this.favCtrl.pinDefault) {
                const aIsDefault =
                    a.summary.ID ===
                    this.favCtrl.playlistId;
                const bIsDefault =
                    b.summary.ID ===
                    this.favCtrl.playlistId;

                if (aIsDefault && !bIsDefault)
                    return -1;

                if (!aIsDefault && bIsDefault)
                    return 1;
            }

            let cmp = 0;

            switch (this.sortField) {
                case 'name':
                    cmp = a.summary.Name.localeCompare(
                        b.summary.Name,
                    );
                    break;
                case 'created':
                    cmp = (
                        a.summary.CreatedAt || ''
                    ).localeCompare(
                        b.summary.CreatedAt || '',
                    );
                    break;
                case 'modified':
                    cmp = (
                        a.summary.UpdatedAt || ''
                    ).localeCompare(
                        b.summary.UpdatedAt || '',
                    );
                    break;
                case 'tracks':
                    cmp =
                        a.tracks.length -
                        b.tracks.length;
                    break;
            }

            return cmp * dir;
        });
    }

    override connectedCallback() {
        super.connectedCallback();
        this.restoreSortPreferences();
        this.loadPlaylists();
    }

    protected override onViewActivate(): void {
        this.listenWhileActive(
            document,
            'click',
            this.closePlaylistCtxMenuHandler,
        );
        this.listenWhileActive(
            document,
            'contextmenu',
            this.closePlaylistCtxMenuHandler,
        );
        this.listenWhileActive(
            document,
            'mousedown',
            this.playlistCtxMenuMousedownHandler,
        );
        this.listenWhileActive(
            document,
            'click',
            this.clearSelectionHandler,
        );
    }

    override disconnectedCallback() {
        super.disconnectedCallback();

        if (this.scrollDebounceTimer !== null) {
            clearTimeout(this.scrollDebounceTimer);
            this.scrollDebounceTimer = null;
        }
    }

    override updated() {
        const currentTerm = this.searchCtrl.term;

        if (currentTerm !== this.lastSearchTerm) {
            this.lastSearchTerm = currentTerm;
            this.selectedPlaylists = new Set();
            this.lastSelectedPlaylistIndex = null;
        }

        // Re-fetch when the store delivers fresh
        // data after eager refetch on invalidation.
        const cached =
            this.playlistCtrl.cachedPlaylists;

        if (
            cached !== null &&
            cached !== this.lastPlaylistsRef
        ) {
            this.lastPlaylistsRef = cached;
            this.loadPlaylists();
        }
    }

    private get scrollContainer(): HTMLElement | null {
        return (
            this.shadowRoot?.querySelector(
                '.playlist-list',
            ) ?? null
        );
    }

    private restoreScrollPosition() {
        const saved =
            this.playlistCtrl.getScrollPosition();

        if (saved > 0 && this.scrollContainer) {
            this.scrollContainer.scrollTop = saved;
        }
    }

    private onScroll = () => {
        if (this.scrollDebounceTimer !== null) {
            clearTimeout(this.scrollDebounceTimer);
        }

        this.scrollDebounceTimer = setTimeout(() => {
            if (this.scrollContainer) {
                this.playlistCtrl.setScrollPosition(
                    this.scrollContainer.scrollTop,
                );
            }
        }, SCROLL_DEBOUNCE_MS);
    };

    private async loadPlaylists() {
        try {
            this.loading = true;

            const playlists =
                await this.playlistCtrl.getPlaylists();

            this.entries = playlists.map((p) => ({
                summary: p.Summary,
                tracks: p.Tracks ?? [],
            }));
        } catch (err) {
            console.error(
                'Failed to load playlists:',
                err,
            );
            this.entries = [];
        } finally {
            this.loading = false;
        }

        await this.updateComplete;
        this.restoreScrollPosition();
    }

    /**
     * Re-fetches playlists without clearing the current view.
     * Shows a spinner in the header while the fetch is in-flight.
     */
    private async refreshPlaylists() {
        this.refreshing = true;

        try {
            const playlists =
                await this.playlistCtrl.refetch();

            this.entries = playlists.map((p) => ({
                summary: p.Summary,
                tracks: p.Tracks ?? [],
            }));
        } catch (err) {
            console.error(
                'Failed to refresh playlists:',
                err,
            );
        } finally {
            this.refreshing = false;
        }
    }

    private handlePlaylistHeaderClick = (
        e: MouseEvent,
        index: number,
    ) => {
        const entry = this.entries[index];

        if (!entry) return;

        const isCtrl = e.ctrlKey || e.metaKey;
        const isShift = e.shiftKey;

        if (isCtrl) {
            // Ctrl/Cmd+Click: toggle playlist in selection
            const next = new Set(this.selectedPlaylists);

            if (next.has(index)) {
                next.delete(index);
            } else {
                next.add(index);
            }

            this.selectedPlaylists = next;
            this.lastSelectedPlaylistIndex = index;
            return;
        }

        if (isShift && this.lastSelectedPlaylistIndex !== null) {
            // Shift+Click: range-select playlists
            const start = Math.min(this.lastSelectedPlaylistIndex, index);
            const end = Math.max(this.lastSelectedPlaylistIndex, index);
            const next = new Set(this.selectedPlaylists);

            for (let i = start; i <= end; i++) {
                next.add(i);
            }

            this.selectedPlaylists = next;
            return;
        }

        // Plain click: navigate to playlist details
        this.selectedPlaylists = new Set();
        this.lastSelectedPlaylistIndex = null;

        this.dispatchEvent(
            new CustomEvent('navigate', {
                bubbles: true,
                composed: true,
                detail: {
                    view: entry.summary.IsSmart
                        ? 'smart-playlist-details'
                        : 'playlist-details',
                    playlistId: entry.summary.ID,
                    playlistName: entry.summary.Name,
                },
            }),
        );
    };

    // =================================================================
    // Drop target (tracks dropped onto a specific playlist)
    // =================================================================

    private onPlaylistDragOver = (
        e: DragEvent,
        index: number,
    ) => {
        if (!hasTrackPayload(e)) return;

        // Don't allow dropping tracks onto smart
        // playlists — they have no playlist_tracks rows.
        const entry = this.entries[index];

        if (entry?.summary.IsSmart) return;

        // Don't allow dropping tracks back onto
        // the same playlist.
        if (
            entry &&
            getActiveDragSource() === 'playlist' &&
            getActiveDragPlaylistId() ===
                entry.summary.ID
        ) {
            return;
        }

        e.preventDefault();

        if (e.dataTransfer) {
            e.dataTransfer.dropEffect = 'copy';
        }

        if (this.dragOverPlaylistIndex !== index) {
            this.dragOverPlaylistIndex = index;
        }

        // A specific playlist is targeted — hide the
        // "new playlist" drop zone highlights.
        if (this.dragOverEmptyZone) {
            this.dragOverEmptyZone = false;
        }

        if (this.dragOverNewButton) {
            this.dragOverNewButton = false;
        }
    };

    private onPlaylistDragLeave = (
        e: DragEvent,
        index: number,
    ) => {
        // Only clear if we're actually leaving this
        // playlist item (not entering a child).
        const related = e.relatedTarget as Node | null;
        const items =
            this.shadowRoot?.querySelectorAll(
                '.playlist-item',
            );
        const item = items?.[index];

        if (item && !item.contains(related)) {
            if (this.dragOverPlaylistIndex === index) {
                this.dragOverPlaylistIndex = -1;
            }
        }
    };

    private onPlaylistDrop = async (
        e: DragEvent,
        index: number,
    ) => {
        e.preventDefault();
        e.stopPropagation();
        this.dragOverPlaylistIndex = -1;

        const payload = getDragPayload(e);

        if (
            !payload ||
            payload.filePaths.length === 0
        ) {
            return;
        }

        const entry = this.entries[index];

        if (!entry) return;

        // Don't allow dropping tracks back onto
        // the same playlist.
        if (
            payload.source === 'playlist' &&
            payload.sourcePlaylistId ===
                entry.summary.ID
        ) {
            return;
        }

        try {
            const result = await FindDuplicateTracksInPlaylist(
                entry.summary.ID,
                payload.filePaths,
            );
            const duplicates = result.Duplicates ?? [];
            const unique = result.Unique ?? [];

            if (duplicates.length > 0) {
                await this.updateComplete;
                this.duplicateDialog.show(
                    entry.summary.ID,
                    duplicates,
                    unique,
                );

                return;
            }

            await AddTracksToPlaylist(
                entry.summary.ID,
                payload.filePaths,
            );
            await this.refreshPlaylists();
        } catch (err) {
            console.error(
                'Failed to add tracks to playlist:',
                err,
            );
        }
    };

    // =================================================================
    // Drop target (empty space → create new playlist)
    // =================================================================

    private onEmptyZoneDragOver = (e: DragEvent) => {
        if (!hasTrackPayload(e)) return;

        e.preventDefault();

        if (e.dataTransfer) {
            e.dataTransfer.dropEffect = 'copy';
        }

        // Only show the "new playlist" drop zone when
        // not hovering a specific playlist item.
        if (
            this.dragOverPlaylistIndex === -1 &&
            !this.dragOverEmptyZone
        ) {
            this.dragOverEmptyZone = true;
        }

        if (this.dragOverNewButton) {
            this.dragOverNewButton = false;
        }
    };

    private onEmptyZoneDragLeave = (e: DragEvent) => {
        const related =
            e.relatedTarget as Node | null;

        if (!related || !this.contains(related)) {
            this.dragOverEmptyZone = false;
        }
    };

    private onEmptyZoneDrop = (e: DragEvent) => {
        e.preventDefault();
        this.dragOverEmptyZone = false;

        const payload = getDragPayload(e);

        if (
            !payload ||
            payload.filePaths.length === 0
        ) {
            return;
        }

        this.pendingDropPaths = payload.filePaths;
        this.creating = true;
        this.newPlaylistName = '';

        void this.updateComplete.then(() => {
            const input =
                this.shadowRoot?.querySelector<HTMLInputElement>(
                    '.create-form input',
                );

            input?.focus();
        });
    };

    // =================================================================
    // Drop target ("New Playlist" button)
    // =================================================================

    private onNewButtonDragOver = (e: DragEvent) => {
        if (!hasTrackPayload(e)) return;

        e.preventDefault();
        e.stopPropagation();

        if (e.dataTransfer) {
            e.dataTransfer.dropEffect = 'copy';
        }

        if (!this.dragOverNewButton) {
            this.dragOverNewButton = true;
        }

        // Hide the empty-zone highlight while
        // hovering the button.
        if (this.dragOverEmptyZone) {
            this.dragOverEmptyZone = false;
        }
    };

    private onNewButtonDragLeave = (
        e: DragEvent,
    ) => {
        const related =
            e.relatedTarget as Node | null;
        const btn =
            this.shadowRoot?.querySelector(
                '.new-playlist-button',
            );

        if (btn && !btn.contains(related)) {
            this.dragOverNewButton = false;
        }
    };

    private onNewButtonDrop = (e: DragEvent) => {
        e.preventDefault();
        e.stopPropagation();
        this.dragOverNewButton = false;
        this.onEmptyZoneDrop(e);
    };

    // =================================================================
    // Playlist-level context menu (rename, delete)
    // =================================================================

    private handlePlaylistContextMenu = (
        e: MouseEvent,
        index: number,
        opener?: HTMLElement,
    ) => {
        e.preventDefault();
        e.stopPropagation();

        // If the right-clicked playlist is NOT in the current
        // multi-selection, replace the selection with just that one.
        if (!this.selectedPlaylists.has(index)) {
            this.selectedPlaylists = new Set([index]);
            this.lastSelectedPlaylistIndex = index;
        }

        this.playlistContextMenuIndex = index;
        this.playlistContextMenuOpen = true;

        this.updateComplete.then(() => {
            const popup =
                this.playlistContextMenuPopup;

            if (popup) {
                popup.anchor = {
                    getBoundingClientRect() {
                        return new DOMRect(
                            e.clientX,
                            e.clientY,
                            0,
                            0,
                        );
                    },
                };
                popup.active = true;
                this.menuKeyboard.open(
                    popup.querySelector('.context-menu-panel'),
                    opener,
                );
            }
        });
    };

    /** Shift+F10 / ContextMenu on a focused playlist header. */
    private handlePlaylistMenuKey(
        e: KeyboardEvent,
        index: number,
    ): void {
        const header = e.currentTarget as HTMLElement | null;

        if (!header) return;

        const rect = header.getBoundingClientRect();

        this.handlePlaylistContextMenu(
            new MouseEvent('contextmenu', {
                clientX: rect.left + 16,
                clientY: rect.top + rect.height / 2,
            }),
            index,
            header,
        );
    }

    private closePlaylistContextMenu() {
        if (!this.playlistContextMenuOpen) return;

        this.menuKeyboard.close();
        this.playlistContextMenuOpen = false;
        this.playlistContextMenuIndex = -1;

        const popup =
            this.playlistContextMenuPopup;

        if (popup) {
            popup.active = false;
        }
    }

    private async onPlaylistContextAction(
        action: string,
    ) {
        const index =
            this.playlistContextMenuIndex;
        const entry = this.entries[index];

        if (!entry) return;

        switch (action) {
            case 'rename':
                this.renamingPlaylistIndex = index;
                this.renameValue =
                    entry.summary.Name;

                void this.updateComplete.then(
                    () => {
                        const input =
                            this.shadowRoot?.querySelector<HTMLInputElement>(
                                '.rename-input',
                            );

                        input?.focus();
                        input?.select();
                    },
                );
                break;
            case 'set-default':
                void this.favCtrl
                    .setDefaultPlaylist(entry.summary.ID)
                    .catch((err: unknown) => {
                        console.error(
                            'Failed to set default playlist:',
                            err,
                        );
                    });
                break;
            case 'delete': {
                if (this.selectedPlaylists.size > 1) {
                    const entries = [...this.selectedPlaylists]
                        .map(i => this.entries[i])
                        .filter((e): e is PlaylistEntry => e !== undefined);
                    const tracks = entries.reduce(
                        (sum, e) => sum + e.tracks.length,
                        0,
                    );

                    // The loop used to delete every selected playlist
                    // with no prompt at all (errors.M6).
                    const ok = await confirmAction({
                        title: `Delete ${entries.length} playlists?`,
                        message: entries
                            .map((e) => e.summary.Name)
                            .join(', '),
                        impact: `${tracks.toLocaleString()} track entries will be removed. The audio files are not touched.`,
                        confirmLabel: 'Delete playlists',
                        danger: true,
                    });

                    if (!ok) break;

                    for (const e of entries) {
                        await this.deletePlaylist(
                            e.summary.ID,
                            e.summary.Name,
                        );
                    }

                    this.selectedPlaylists = new Set();
                    this.lastSelectedPlaylistIndex = null;
                    await this.refreshPlaylists();
                } else {
                    await this.handleDeletePlaylist(
                        entry.summary.ID,
                        entry.summary.Name,
                        entry.tracks.length,
                    );
                }

                break;
            }
        }

        this.selectedPlaylists = new Set();
        this.lastSelectedPlaylistIndex = null;
        this.closePlaylistContextMenu();
    }

    private async handleDeletePlaylist(
        playlistID: number,
        name = 'this playlist',
        trackCount = 0,
    ) {
        const ok = await confirmAction({
            title: `Delete “${name}”?`,
            message: 'The playlist is deleted; the audio files are not.',
            impact:
                trackCount > 0
                    ? `${trackCount.toLocaleString()} track entries will be removed.`
                    : undefined,
            confirmLabel: 'Delete playlist',
            danger: true,
        });

        if (!ok) return;

        await this.deletePlaylist(playlistID, name);
        await this.refreshPlaylists();
    }

    /**
     * The delete itself, already confirmed. A partial failure used to
     * look exactly like a success until the refresh put the playlist
     * back (errors.M6), so it is Persistent: the thing the user asked
     * for did not happen and retrying means something.
     */
    private async deletePlaylist(
        playlistID: number,
        name: string,
    ): Promise<void> {
        try {
            await DeletePlaylist(playlistID);
        } catch (err) {
            console.error('Failed to delete playlist:', err);
            notificationStore.persistent({
                key: 'playlist-delete',
                text: `Could not delete “${name}”. ${describeError(err)}`,
                detail: String(err),
                coalescedText: (count) =>
                    `Could not delete ${count} playlists.`,
                action: {
                    label: 'Try again',
                    run: () =>
                        void this.deletePlaylist(playlistID, name).then(() =>
                            this.refreshPlaylists(),
                        ),
                },
            });
        }
    }

    private handleRenameKeydown = async (
        e: KeyboardEvent,
    ) => {
        if (e.key === 'Enter') {
            await this.submitRename();
        } else if (e.key === 'Escape') {
            this.renamingPlaylistIndex = -1;
            this.renameValue = '';
        }
    };

    private handleRenameBlur = async () => {
        await this.submitRename();
    };

    private handleRenameInput = (e: Event) => {
        const input = e.target as HTMLInputElement;
        this.renameValue = input.value;
    };

    private async submitRename() {
        const index = this.renamingPlaylistIndex;

        if (index < 0) return;

        const entry = this.entries[index];

        if (!entry) return;

        const trimmed = this.renameValue.trim();

        this.renamingPlaylistIndex = -1;
        this.renameValue = '';

        if (
            !trimmed ||
            trimmed === entry.summary.Name
        ) {
            return;
        }

        try {
            await RenamePlaylist(
                entry.summary.ID,
                trimmed,
            );
            await this.refreshPlaylists();
        } catch (err) {
            console.error(
                'Failed to rename playlist:',
                err,
            );
        }
    }

    // =================================================================
    // Import playlist
    // =================================================================

    private handleImportPlaylist = async () => {
        try {
            const filePaths =
                await PlaylistFilePicker();

            if (!filePaths || filePaths.length === 0)
                return;

            this.importError = '';
            await ImportPlaylists(filePaths);
        } catch (err) {
            console.error(
                'Failed to import playlist:',
                err,
            );
            this.importError = describeError(
                err,
                'That playlist file could not be imported.',
            );
            setTimeout(() => {
                this.importError = '';
            }, 6000);
        }
    };

    // =================================================================
    // Create playlist
    // =================================================================

    private handleNewPlaylistClick = () => {
        this.creating = true;
        this.creatingSmart = false;
        this.newPlaylistName = '';

        void this.updateComplete.then(() => {
            const input =
                this.shadowRoot?.querySelector<HTMLInputElement>(
                    '.create-form input',
                );

            input?.focus();
        });
    };

    private handleNewSmartPlaylistClick = () => {
        this.creatingSmart = true;
        this.creating = false;
        this.newPlaylistName = '';

        void this.updateComplete.then(() => {
            const input =
                this.shadowRoot?.querySelector<HTMLInputElement>(
                    '.create-form input',
                );

            input?.focus();
        });
    };

    private handleCancelCreate = () => {
        this.creating = false;
        this.creatingSmart = false;
        this.newPlaylistName = '';
        this.pendingDropPaths = [];
    };

    private handleCreatePlaylist = async () => {
        const name = this.newPlaylistName.trim();
        if (!name) return;

        if (this.creatingSmart) {
            try {
                const summary = await CreateSmartPlaylist(
                    name,
                    '{"rules":[],"limit":0,"sort_field":"","sort_dir":""}',
                );

                this.creatingSmart = false;
                this.newPlaylistName = '';
                this.dispatchEvent(
                    new CustomEvent('navigate', {
                        bubbles: true,
                        composed: true,
                        detail: {
                            view: 'smart-playlist-details',
                            playlistId: summary.ID,
                            playlistName: summary.Name,
                            autoEdit: true,
                        },
                    }),
                );
            } catch (err) {
                console.error(
                    'Failed to create smart playlist:',
                    err,
                );
            }
            return;
        }

        const paths = this.pendingDropPaths;

        try {
            if (paths.length > 0) {
                await CreatePlaylistWithTracks(
                    name,
                    paths,
                );
            } else {
                await CreatePlaylist(name);
            }

            this.creating = false;
            this.newPlaylistName = '';
            this.pendingDropPaths = [];
            await this.refreshPlaylists();
        } catch (err) {
            console.error(
                'Failed to create playlist:',
                err,
            );
        }
    };

    private handleInputChange = (e: Event) => {
        const input = e.target as HTMLInputElement;
        this.newPlaylistName = input.value;
    };

    private handleInputKeydown = (e: KeyboardEvent) => {
        if (e.key === 'Enter') {
            void this.handleCreatePlaylist();
        } else if (e.key === 'Escape') {
            this.handleCancelCreate();
        }
    };

    // =================================================================
    // Render
    // =================================================================

    private onPageHeaderSort = (
        e: CustomEvent<{ field: string; direction: 'asc' | 'desc' }>,
    ) => {
        this.sortField = e.detail.field as PlaylistSortField;
        this.sortDirection = e.detail.direction;
        this.saveSortPreferences();
    };

    override render() {
        return html`
            <page-header
                heading="Playlists"
                .count=${this.loading && this.entries.length === 0
                    ? null
                    : this.sortedEntries.length}
                count-noun="playlist"
                .sortOptions=${SORT_OPTIONS}
                sort-field=${this.sortField}
                sort-direction=${this.sortDirection}
                search-term=${this.searchCtrl.term}
                ?busy=${this.refreshing}
                @sort-change=${this.onPageHeaderSort}
            >
                <div slot="actions" class="header-actions">
                    <button
                        class="import-button"
                        @click=${this.handleImportPlaylist}
                    >
                        <wa-icon name="file-import"></wa-icon>
                        Import
                    </button>
                    <button
                        class="new-playlist-button ${this.dragOverNewButton ? 'drag-over' : ''}"
                        @click=${this.handleNewPlaylistClick}
                        @dragover=${this.onNewButtonDragOver}
                        @dragleave=${this.onNewButtonDragLeave}
                        @drop=${this.onNewButtonDrop}
                    >
                        <wa-icon name=${ICON_NEW}></wa-icon>
                        New Playlist
                    </button>
                    <button
                        class="new-playlist-button"
                        @click=${this.handleNewSmartPlaylistClick}
                    >
                        <wa-icon name="filter"></wa-icon>
                        New Smart Playlist
                    </button>
                </div>
            </page-header>

            ${this.importError
                ? html`<div class="import-error">
                      ${this.importError}
                  </div>`
                : nothing}

            ${this.creating || this.creatingSmart
                ? this.renderCreateForm()
                : nothing}
            ${this.loading &&
            this.entries.length === 0
                ? html`<div class="loading">
                      Loading playlists...
                  </div>`
                : this.renderPlaylistList()}

            <wa-popup
                id="playlist-context-menu"
                placement="bottom-start"
                flip
                shift
                .active=${this
                    .playlistContextMenuOpen}
            >
                ${this.playlistContextMenuOpen
                    ? html`
                          <div
                              class="context-menu-panel"
                              role="menu"
                              aria-label="Playlist actions"
                          >
                              ${this.selectedPlaylists.size <= 1
                                  ? html`
                                        <wa-dropdown-item
                                            @click=${() =>
                                                void this.onPlaylistContextAction(
                                                    'rename',
                                                )}
                                        >
                                            <wa-icon
                                                slot="icon"
                                                name="pen"
                                            ></wa-icon>
                                            Rename
                                        </wa-dropdown-item>
                                        ${this.entries[this.playlistContextMenuIndex]?.summary.IsSmart
                                            ? nothing
                                            : html`
                                                <wa-dropdown-item
                                                    @click=${() =>
                                                        void this.onPlaylistContextAction(
                                                            'set-default',
                                                        )}
                                                >
                                                    <wa-icon
                                                        slot="icon"
                                                        name="star"
                                                    ></wa-icon>
                                                    Set as Default Playlist
                                                </wa-dropdown-item>
                                            `}
                                    `
                                  : nothing}
                              <wa-dropdown-item
                                  @click=${() =>
                                      void this.onPlaylistContextAction(
                                          'delete',
                                      )}
                              >
                                  <wa-icon
                                      slot="icon"
                                      name="trash"
                                  ></wa-icon>
                                  ${this.selectedPlaylists.size > 1
                                      ? `Delete ${this.selectedPlaylists.size} Playlists`
                                      : 'Delete Playlist'}
                              </wa-dropdown-item>
                          </div>
                      `
                    : nothing}
            </wa-popup>

            <duplicate-tracks-dialog
                @playlist-action-complete=${() =>
                    this.refreshPlaylists()}
            ></duplicate-tracks-dialog>
        `;
    }

    private renderCreateForm() {
        const canCreate =
            this.newPlaylistName.trim().length > 0;
        const placeholder = this.creatingSmart
            ? 'Smart playlist name'
            : 'Playlist name';

        return html`
            <div class="create-form">
                <input
                    type="text"
                    placeholder=${placeholder}
                    .value=${this.newPlaylistName}
                    @input=${this.handleInputChange}
                    @keydown=${this.handleInputKeydown}
                />
                <button
                    @click=${this.handleCancelCreate}
                >
                    Cancel
                </button>
                <button
                    class="primary"
                    ?disabled=${!canCreate}
                    @click=${this.handleCreatePlaylist}
                >
                    Create
                </button>
            </div>
        `;
    }

    private renderPlaylistList() {
        if (this.entries.length === 0) {
            return html`
                <div
                    class="empty-state ${this.dragOverEmptyZone ? 'drag-over' : ''}"
                    @dragover=${this.onEmptyZoneDragOver}
                    @dragleave=${this.onEmptyZoneDragLeave}
                    @drop=${this.onEmptyZoneDrop}
                >
                    <div class="drop-zone-icon">
                        <wa-icon
                            name=${ICON_NEW}
                        ></wa-icon>
                    </div>
                    <wa-icon name=${ICON_PLAYLIST}></wa-icon>
                    <p>No playlists yet</p>
                    <p style="font-size: 12px;">
                        Create a playlist or drop
                        tracks here.
                    </p>
                </div>
            `;
        }

        const visible = this.sortedEntries;

        if (visible.length === 0) {
            return html`
                <div
                    class="empty-state ${this.dragOverEmptyZone ? 'drag-over' : ''}"
                    @dragover=${this.onEmptyZoneDragOver}
                    @dragleave=${this.onEmptyZoneDragLeave}
                    @drop=${this.onEmptyZoneDrop}
                >
                    <div class="drop-zone-icon">
                        <wa-icon
                            name=${ICON_NEW}
                        ></wa-icon>
                    </div>
                    <p>
                        No playlists match your
                        search.
                    </p>
                </div>
            `;
        }

        return html`
            <ul
                class="playlist-list"
                @scroll=${this.onScroll}
            >
                ${visible.map((entry) => {
                    const originalIndex =
                        this.entries.indexOf(entry);

                    return this.renderPlaylistItem(
                        entry,
                        originalIndex,
                    );
                })}
                <li
                    class="drop-zone ${this.dragOverEmptyZone ? 'drag-over' : ''}"
                    @dragover=${this
                        .onEmptyZoneDragOver}
                    @dragleave=${this
                        .onEmptyZoneDragLeave}
                    @drop=${this.onEmptyZoneDrop}
                >
                    <div class="drop-zone-icon">
                        <wa-icon
                            name=${ICON_NEW}
                        ></wa-icon>
                    </div>
                </li>
            </ul>
        `;
    }

    private renderPlaylistItem(
        entry: PlaylistEntry,
        index: number,
    ) {
        const trackCount = entry.tracks.length;
        const countLabel = `${trackCount} track${trackCount !== 1 ? 's' : ''}`;
        const isDragOver =
            this.dragOverPlaylistIndex === index;

        const isRenaming =
            this.renamingPlaylistIndex === index;

        return html`
            <li
                class="playlist-item ${isDragOver
                    ? 'drag-over'
                    : ''}"
                @dragover=${(e: DragEvent) =>
                    this.onPlaylistDragOver(e, index)}
                @dragleave=${(e: DragEvent) =>
                    this.onPlaylistDragLeave(e, index)}
                @drop=${(e: DragEvent) =>
                    this.onPlaylistDrop(e, index)}
            >
                <div
                    class="playlist-header ${this.selectedPlaylists.has(index) ? 'selected' : ''}"
                    role="button"
                    tabindex="0"
                    aria-label=${`Playlist ${entry.summary.Name}`}
                    @click=${(e: MouseEvent) =>
                        this.handlePlaylistHeaderClick(e, index)}
                    @keydown=${(e: KeyboardEvent) => {
                        if (isContextMenuKey(e)) {
                            e.preventDefault();
                            e.stopPropagation();
                            this.handlePlaylistMenuKey(e, index);
                        }
                    }}
                    @contextmenu=${(e: MouseEvent) =>
                        this.handlePlaylistContextMenu(
                            e,
                            index,
                        )}
                >
                    ${entry.summary.ID === this.favCtrl.playlistId
                        ? html`<wa-icon
                              class="playlist-icon"
                              name=${this.favCtrl.iconFor(true)}
                          ></wa-icon>`
                        : entry.summary.IsSmart
                          ? html`<wa-icon
                                class="playlist-icon"
                                name="filter"
                            ></wa-icon>`
                          : nothing}
                    ${isRenaming
                        ? html`
                              <input
                                  class="rename-input"
                                  type="text"
                                  .value=${this
                                      .renameValue}
                                  @input=${this
                                      .handleRenameInput}
                                  @keydown=${this
                                      .handleRenameKeydown}
                                  @blur=${this
                                      .handleRenameBlur}
                                  @click=${(
                                      e: Event,
                                  ) =>
                                      e.stopPropagation()}
                              />
                          `
                        : html`
                              <span
                                  class="playlist-name"
                                  title=${entry.summary.Name}
                              >
                                  ${entry.summary
                                      .Name}
                              </span>
                          `}
                    <span class="track-count">
                        ${countLabel}
                    </span>
                </div>
            </li>
        `;
    }
}

declare global {
    interface HTMLElementTagNameMap {
        'playlist-view': PlaylistView;
    }
}
