components_export_panel.js


import { format } from '../utilities/export.js';
import { SCRIPT_DIALOGUE_INDENT } from '../utilities/constants.js';

/**
 * Modal dialog for configuring and initiating a transcript export.
 * Presents file type, export style, save path controls, and a live format preview
 * generated from the provided Transcript via the shared export formatters.
 */
export class ExportPanel {
    /**
     * @param {Transcript} transcript - The transcript to preview and export.
     * @param {Object.<string, Speaker>} speakers - Speaker id → Speaker map.
     * @param {object} callbacks
     * @param {function} [callbacks.onExport] - Called with { fileType, style, filename, path, text }
     *   when Export is clicked. `text` is the fully formatted output string. `path` is the full
     *   filesystem path if the user used the browse dialog, otherwise null.
     * @param {function} [callbacks.onDismiss] - Called when the dialog is cancelled or closed.
     */
    #transcript    = null;
    #speakers      = {};
    #filePath      = null;
    #defaultDir    = null;
    #defaultTitle  = '';

    /**
     * @param {Transcript} transcript - The transcript to preview and export.
     * @param {Object.<string, Speaker>} speakers - Speaker id → Speaker map.
     * @param {object} [options] - Optional callbacks and defaults.
     * @param {function} [options.onExport] - Called with export details when Export is clicked.
     * @param {function} [options.onDismiss] - Called when the dialog is cancelled or closed.
     * @param {string} [options.defaultTitle=''] - Pre-populated value for the title field.
     */
    constructor(transcript, speakers, { onExport, onDismiss, defaultTitle = '' } = {}) {
        this._onExport  = onExport  ?? (() => {});
        this._onDismiss = onDismiss ?? (() => {});
        this.#transcript   = transcript;
        this.#speakers     = speakers ?? {};
        this.#defaultTitle = defaultTitle;
        this.#buildDialog();
    }

    /** Builds and appends the modal overlay and its contents to the document body. */
    #buildDialog() {
        const overlay = document.createElement('div');
        overlay.className = 'export-panel-overlay';

        const modal = document.createElement('div');
        modal.className = 'export-panel-modal';

        modal.appendChild(this.#buildHeader(overlay));
        const { body, fileTypeSelect, styleSelect, pathInput, speakersCheck, currentOptions, effectiveStyle } = this.#buildBody();
        modal.appendChild(body);
        modal.appendChild(this.#buildActions(overlay, fileTypeSelect, styleSelect, pathInput, speakersCheck, currentOptions, effectiveStyle));

        let _mouseDownOnOverlay = false;
        overlay.addEventListener('mousedown', (e) => { _mouseDownOnOverlay = e.target === overlay; });
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay && _mouseDownOnOverlay) { overlay.remove(); this._onDismiss(); }
        });

        overlay.appendChild(modal);
        document.body.appendChild(overlay);
    }

    /**
     * Builds the modal header bar with a title and close button.
     *
     * @param {HTMLElement} overlay - The overlay element, used to dismiss the dialog on close.
     * @returns {HTMLElement}
     */
    #buildHeader(overlay) {
        const header = document.createElement('div');
        header.className = 'export-panel-header';
        header.innerHTML = '<span>Export Transcript</span>';

        const closeBtn = document.createElement('button');
        closeBtn.className = 'export-panel-close';
        closeBtn.innerHTML = '<span class="icon icon-close" style="width:12px;height:12px;"></span>';
        closeBtn.addEventListener('click', () => { overlay.remove(); this._onDismiss(); });
        header.appendChild(closeBtn);
        return header;
    }

    /**
     * Builds the modal body containing the export form and live preview pane.
     *
     * @returns {{ body: HTMLElement, fileTypeSelect: HTMLSelectElement, styleSelect: HTMLSelectElement,
     *   pathInput: HTMLInputElement, speakersCheck: HTMLInputElement,
     *   currentOptions: function(): object, effectiveStyle: function(): string }}
     */
    #buildBody() {
        const body = document.createElement('div');
        body.className = 'export-panel-body';

        // ── Form column ──
        const form = document.createElement('div');
        form.className = 'export-panel-form';

        const fileTypeSelect = this.#makeSelect('exportFileType',
            [['PDF', 'pdf'], ['DOCX', 'docx'], ['TXT', 'txt'], ['Markdown', 'md'], ['CSV', 'csv']]
        );
        form.appendChild(this.#makeRow('File Type', fileTypeSelect));

        const styleSelect = this.#makeSelect('exportStyle',
            [['Script', 'script'], ['Professional', 'professional'], ['Transcript', 'transcript']]
        );
        const styleRow = this.#makeRow('Export Style', styleSelect);
        form.appendChild(styleRow);

        const titleInput = document.createElement('input');
        titleInput.type = 'text';
        titleInput.className = 'export-panel-path-input';
        titleInput.placeholder = 'Untitled';
        titleInput.value = this.#defaultTitle;
        titleInput.addEventListener('keydown', e => e.stopPropagation());
        const titleRow = this.#makeRow('Title', titleInput);
        form.appendChild(titleRow);

        const descInput = document.createElement('textarea');
        descInput.className = 'export-panel-textarea';
        descInput.rows = 3;
        descInput.placeholder = 'Optional description…';
        descInput.addEventListener('keydown', e => e.stopPropagation());
        const descRow = this.#makeRow('Description', descInput);
        form.appendChild(descRow);

        const recordingDateInput = document.createElement('input');
        recordingDateInput.type = 'text';
        recordingDateInput.className = 'export-panel-path-input';
        recordingDateInput.placeholder = 'e.g. January 1, 2025';
        recordingDateInput.addEventListener('keydown', e => e.stopPropagation());
        const recordingDateRow = this.#makeRow('Recording Date', recordingDateInput);
        form.appendChild(recordingDateRow);

        const optionsSection = document.createElement('div');
        optionsSection.className = 'export-panel-options-section';

        const optionsLabel = document.createElement('div');
        optionsLabel.className = 'export-panel-label';
        optionsLabel.textContent = 'Options';
        optionsSection.appendChild(optionsLabel);

        const speakersCheck = document.createElement('input');
        speakersCheck.type = 'checkbox';
        speakersCheck.id = 'exportIncludeSpeakers';
        speakersCheck.className = 'export-panel-checkbox';
        const speakersCheckRow = this.#makeCheckboxRow('Include Speaker List', speakersCheck);
        optionsSection.appendChild(speakersCheckRow);

        const timestampsCheck = document.createElement('input');
        timestampsCheck.type = 'checkbox';
        timestampsCheck.id = 'exportIncludeTimestamps';
        timestampsCheck.className = 'export-panel-checkbox';
        timestampsCheck.checked = true;
        const timestampsCheckRow = this.#makeCheckboxRow('Include Timestamps', timestampsCheck);
        optionsSection.appendChild(timestampsCheckRow);

        const recordingDateCheck = document.createElement('input');
        recordingDateCheck.type = 'checkbox';
        recordingDateCheck.id = 'exportRecordingDate';
        recordingDateCheck.className = 'export-panel-checkbox';
        const recordingDateCheckRow = this.#makeCheckboxRow('Display Recording Date', recordingDateCheck);
        optionsSection.appendChild(recordingDateCheckRow);

        const exportDateCheck = document.createElement('input');
        exportDateCheck.type = 'checkbox';
        exportDateCheck.id = 'exportExportDate';
        exportDateCheck.className = 'export-panel-checkbox';
        const exportDateCheckRow = this.#makeCheckboxRow('Display Export Date', exportDateCheck);
        optionsSection.appendChild(exportDateCheckRow);

        form.appendChild(optionsSection);

        const locationDisplay = document.createElement('div');
        locationDisplay.className = 'export-panel-location-display';
        fetch('/api/dialog/default-dir')
            .then(r => r.json())
            .then(({ dir }) => {
                this.#defaultDir = dir;
                if (!this.#filePath) locationDisplay.textContent = dir;
            });


        const pathWrap = document.createElement('div');
        pathWrap.className = 'export-panel-path-wrap';

        const pathInput = document.createElement('input');
        pathInput.type = 'text';
        pathInput.className = 'export-panel-path-input';
        pathInput.value = 'transcript.pdf';

        const browseBtn = document.createElement('button');
        browseBtn.className = 'export-panel-browse-btn';
        browseBtn.textContent = '…';
        browseBtn.title = 'Choose save location';
        browseBtn.addEventListener('click', async () => {
            const ext    = fileTypeSelect.value;
            const params = new URLSearchParams({ filename: pathInput.value, ext });
            const resp   = await fetch(`/api/dialog/save?${params}`);
            const data   = await resp.json();
            if (data.cancelled) return;
            this.#filePath = data.path;
            pathInput.value = data.filename;
            locationDisplay.textContent = data.dir;
        });

        pathWrap.appendChild(pathInput);
        pathWrap.appendChild(browseBtn);

        const pathRow = this.#makeRow('Save Path', locationDisplay);
        pathRow.appendChild(pathWrap);
        pathRow.style.marginTop = 'auto';
        form.appendChild(pathRow);

        body.appendChild(form);

        // ── Preview column ──
        const previewSection = document.createElement('div');
        previewSection.className = 'export-panel-preview-section';

        const previewLabel = document.createElement('div');
        previewLabel.className = 'export-panel-preview-label';
        previewLabel.textContent = 'Preview';
        previewSection.appendChild(previewLabel);

        const previewBox = document.createElement('pre');
        previewBox.className = 'export-panel-preview';
        previewSection.appendChild(previewBox);

        body.appendChild(previewSection);

        // Wire up live updates — CSV file type overrides the style select
        const effectiveStyle = () => {
            const ft = fileTypeSelect.value;
            if (ft === 'csv') return 'csv';
            if (ft === 'txt') return 'transcript';
            if (ft === 'md')  return 'md';
            return styleSelect.value;
        };

        const currentOptions = () => ({
            includeSpeakers:     speakersCheck.checked,
            includeTimestamps:   timestampsCheck.checked,
            displayRecordingDate: recordingDateCheck.checked,
            displayExportDate:   exportDateCheck.checked,
            recordingDate:       recordingDateInput.value.trim(),
            title:               titleInput.value.trim(),
            description:         descInput.value.trim(),
        });

        const setHidden = (row, hidden) => {
            if (row) row.style.display = hidden ? 'none' : '';
        };

        const updateVisibility = () => {
            const ft           = fileTypeSelect.value;
            const isCsv        = ft === 'csv';
            const isMd         = ft === 'md';
            const noTimestamps = !isCsv && effectiveStyle() === 'professional';

            // Hide Script option for Markdown; restore it otherwise
            const scriptOpt = styleSelect.querySelector('option[value="script"]');
            if (scriptOpt) {
                scriptOpt.hidden = isMd;
                if (isMd && styleSelect.value === 'script') styleSelect.value = 'professional';
            }

            setHidden(styleRow,        isCsv || ft === 'txt');
            setHidden(titleRow,        isCsv);
            setHidden(descRow,         isCsv);
            setHidden(recordingDateRow, isCsv);
            setHidden(optionsSection,  isCsv);
            if (!isCsv) setHidden(timestampsCheckRow, noTimestamps);

            styleSelect.disabled        = isCsv;
            speakersCheck.disabled      = isCsv;
            timestampsCheck.disabled    = isCsv || noTimestamps;
            recordingDateCheck.disabled = isCsv;
            exportDateCheck.disabled    = isCsv;
        };

        const refresh = () => { updateVisibility(); this.#updatePreview(previewBox, effectiveStyle(), currentOptions()); };

        styleSelect.addEventListener('change', refresh);
        speakersCheck.addEventListener('change', refresh);
        timestampsCheck.addEventListener('change', refresh);
        recordingDateCheck.addEventListener('change', refresh);
        exportDateCheck.addEventListener('change', refresh);
        titleInput.addEventListener('input', refresh);
        descInput.addEventListener('input', refresh);
        recordingDateInput.addEventListener('input', refresh);
        fileTypeSelect.addEventListener('change', () => {
            const ext = fileTypeSelect.value;
            pathInput.value = pathInput.value.replace(/\.[^.]+$/, `.${ext}`);
            this.#filePath = null;
            locationDisplay.textContent = this.#defaultDir ?? '';
            refresh();
        });

        refresh();
        return { body, fileTypeSelect, styleSelect, pathInput, speakersCheck, currentOptions, effectiveStyle };
    }

    /**
     * Builds the Cancel / Export action bar.
     *
     * @param {HTMLElement} overlay - The overlay element to remove on action.
     * @param {HTMLSelectElement} fileTypeSelect - The file type selector.
     * @param {HTMLSelectElement} styleSelect - The export style selector.
     * @param {HTMLInputElement} pathInput - The filename input.
     * @param {HTMLInputElement} speakersCheck - The include-speakers checkbox.
     * @param {function(): object} currentOptions - Returns the current form options.
     * @param {function(): string} effectiveStyle - Returns the resolved export style key.
     * @returns {HTMLElement}
     */
    #buildActions(overlay, fileTypeSelect, styleSelect, pathInput, speakersCheck, currentOptions, effectiveStyle) {
        const actions = document.createElement('div');
        actions.className = 'export-panel-actions';

        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'sample-btn';
        cancelBtn.textContent = 'Cancel';
        cancelBtn.style.cssText = 'padding:0.35rem 0.75rem;font-size:var(--fs-label);';
        cancelBtn.addEventListener('click', () => { overlay.remove(); this._onDismiss(); });

        const exportBtn = document.createElement('button');
        exportBtn.className = 'sample-btn export-panel-export-btn';
        exportBtn.textContent = 'Export';
        exportBtn.style.cssText = 'padding:0.35rem 0.75rem;font-size:var(--fs-label);';
        exportBtn.addEventListener('click', () => {
            const style   = effectiveStyle();
            const options = currentOptions();
            const text    = this.#transcript
                ? format(style, this.#transcript, this.#speakers, options)
                : '';
            overlay.remove();
            this._onExport({
                fileType: fileTypeSelect.value,
                style,
                filename: pathInput.value,
                path:     this.#filePath,
                text,
                ...options,
            });
        });

        actions.appendChild(cancelBtn);
        actions.appendChild(exportBtn);
        return actions;
    }

    /**
     * Creates a labelled checkbox row element.
     *
     * @param {string} labelText - The label to display beside the checkbox.
     * @param {HTMLInputElement} checkbox - The checkbox input element.
     * @returns {HTMLLabelElement}
     */
    #makeCheckboxRow(labelText, checkbox) {
        const row = document.createElement('label');
        row.className = 'export-panel-checkbox-row';
        row.appendChild(checkbox);
        const span = document.createElement('span');
        span.textContent = labelText;
        row.appendChild(span);
        return row;
    }

    /**
     * Creates a labelled form row element containing the given control.
     *
     * @param {string} labelText - The label to display above the control.
     * @param {HTMLElement} control - The form control to include in the row.
     * @returns {HTMLDivElement}
     */
    #makeRow(labelText, control) {
        const row = document.createElement('div');
        row.className = 'export-panel-row';
        const label = document.createElement('label');
        label.className = 'export-panel-label';
        label.textContent = labelText;
        row.appendChild(label);
        row.appendChild(control);
        return row;
    }

    /**
     * Creates a styled select element populated with the given options.
     *
     * @param {string} id - The id attribute for the select element.
     * @param {Array.<string[]>} options - Array of [label, value] pairs.
     * @returns {HTMLSelectElement}
     */
    #makeSelect(id, options) {
        const select = document.createElement('select');
        select.className = 'export-panel-select';
        select.id = id;
        options.forEach(([label, value]) => {
            const opt = document.createElement('option');
            opt.value = value;
            opt.textContent = label;
            select.appendChild(opt);
        });
        return select;
    }

    /**
     * Refreshes the preview pane with freshly formatted transcript text.
     *
     * @param {HTMLPreElement} previewBox - The element to render the preview into.
     * @param {string} style - The export style key ('script' | 'professional' | 'transcript' | 'csv' | 'md').
     * @param {object} [options={}] - Options passed through to the formatter.
     */
    #updatePreview(previewBox, style, options = {}) {
        const fonts = {
            script:       `'Courier New', Courier, monospace`,
            professional: `Georgia, 'Palatino Linotype', Palatino, serif`,
            transcript:   `'IBM Plex Sans', sans-serif`,
            csv:          `var(--font-mono)`,
        };

        previewBox.style.fontFamily = fonts[style] ?? `var(--font-mono)`;
        previewBox.style.textAlign  = (style === 'professional' || style === 'script') ? 'justify' : 'left';
        previewBox.style.paddingRight = '';

        if (!this.#transcript) {
            previewBox.textContent = '';
            return;
        }

        const text = format(style, this.#transcript, this.#speakers, options);
        const PREVIEW_LINES = 50;
        const truncated = text.split('\n').slice(0, PREVIEW_LINES).join('\n');

        if (style === 'professional') {
            // Bold any line that exactly matches a speaker's display name
            const names = new Set(Object.values(this.#speakers).map(s => s.name));
            const escaped = truncated.replace(/&/g, '&amp;').replace(/</g, '&lt;');
            previewBox.innerHTML = escaped.replace(/^(.+)$/gm,
                (_, line) => names.has(line) ? `<strong>${line}</strong>` : line
            );
        } else if (style === 'script') {
            // Wrap content lines in display:block spans with right margin, but leave
            // timecodes and HR lines unindented so they span the full width
            const escaped = truncated.replace(/&/g, '&amp;').replace(/</g, '&lt;');
            const pr = `${SCRIPT_DIALOGUE_INDENT}ch`;
            previewBox.innerHTML = escaped.replace(/^(.+)$/gm, (_, line) => {
                const isTimecode = /^\(\d{2}:\d{2}:\d{2}\)/.test(line);
                const isHr       = /^─+$/.test(line.trim());
                return (isTimecode || isHr)
                    ? line
                    : `<span style="display:block;padding-right:${pr}">${line}</span>`;
            });
        } else {
            previewBox.textContent = truncated;
        }
    }
}