import axios from 'axios';

/**
 * Responsive overlay (vanilla front-end copy).
 *
 * Replaces the previous mtcOverlay plugin on `$.fn`. Same surface area:
 *
 *   import Overlay from '@site_scripts/mtc_overlay.js';
 *   Overlay.attach('.someTrigger', { content_class: 'standardOverlay' });
 *
 *   // or via the legacy window helper used during the transition:
 *   window.mtcOverlay('.someTrigger', { … });
 *
 * Content modes are picked from the trigger's attributes:
 *   - image — `href` ends in .jpg / .jpeg / .png / .gif
 *   - video — `video: true` option AND href matches youtube / vimeo
 *   - inline — `data-no-ajax` attribute → copies/moves content from `href`'s
 *     target element on the same page
 *   - gallery — `rel` attribute matches multiple triggers AND
 *     `gallery: true` option
 *   - ajax — default; POSTs to `href` and renders the response (optionally
 *     filtered by `data-filter` selector)
 *
 * Front-end only. The admin's legacy copy stays put until the admin spec.
 */

const DEFAULTS = {
    button_html: '<div class="overlayCloseButton"><a href="#"><i class="fal fa-times"></i></a></div>',
    content_class: '',
    gallery: false,
    gallery_prev_html: '<button class="js_galleryPrev galleryPrev"><i class="fal fa-angle-left"></i></button>',
    gallery_next_html: '<button class="js_galleryNext galleryNext"><i class="fal fa-angle-right"></i></button>',
    video: false,
    video_settings: { autoplay: false, width: 500, height: 284 },
    fullscreen_iframe: false,
    margin_vertical: 20,
    margin_horizontal: 40,
    max_width: 1024,
    fade_time: 300,
    site_wrapper: '.siteWrapper',
    arrow_controls: true,
    close_on_esc: true,
    swipe: true,
    inline: true,
    responsive: [{ breakpoint: 640, settings: { margin_vertical: 10, margin_horizontal: 10 } }],
    beforeOpen() {},
    onOpen() {},
    onClose() {},
};

const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif'];

function deepMerge(target, source) {
    const out = { ...target };
    for (const key of Object.keys(source)) {
        const sv = source[key];
        const tv = out[key];
        if (sv && typeof sv === 'object' && !Array.isArray(sv) && tv && typeof tv === 'object' && !Array.isArray(tv)) {
            out[key] = deepMerge(tv, sv);
        } else {
            out[key] = sv;
        }
    }
    return out;
}

function fadeOpacityTo(el, duration, from, to, onDone) {
    el.style.opacity = from;
    el.style.transition = `opacity ${duration}ms ease`;
    requestAnimationFrame(() => {
        requestAnimationFrame(() => {
            el.style.opacity = to;
        });
    });

    // transitionend can be skipped (e.g. zero-duration, display:none mid-flight,
    // or a CSS override). Pair it with a timeout fallback so the cleanup always
    // runs — guard with a fired flag so we never call onDone twice.
    let fired = false;
    let timeoutId = null;
    const finalize = () => {
        if (fired) return;
        fired = true;
        el.style.transition = '';
        el.removeEventListener('transitionend', cleanup);
        if (timeoutId !== null) clearTimeout(timeoutId);
        if (onDone) onDone();
    };
    const cleanup = (e) => {
        if (e && e.propertyName !== 'opacity') return;
        finalize();
    };
    el.addEventListener('transitionend', cleanup);
    timeoutId = setTimeout(finalize, duration + 50);
}

function fadeOpacityIn(el, duration, onDone) {
    fadeOpacityTo(el, duration, '0', '1', onDone);
}

function fadeOpacityOut(el, duration, onDone) {
    fadeOpacityTo(el, duration, '1', '0', onDone);
}

// Tracks which DOM elements have already been wired up by Overlay.attach,
// avoiding double-binding without polluting the element with custom props.
const overlayBoundElements = new WeakMap();

export default class Overlay {

    constructor(trigger, options = {}) {
        this.trigger = trigger;
        this.allSettings = deepMerge(DEFAULTS, options);
        this.settings = this.allSettings;
        this.galleryLoading = false;
        this.activeElement = null;
        this.galleryImages = null;
    }

    // ---- responsive ----

    readResponsive() {
        let prev = this.allSettings;
        if (Array.isArray(this.allSettings.responsive)) {
            this.allSettings.responsive.forEach((entry) => {
                entry.settings = deepMerge(prev, entry.settings || {});
                delete entry.settings.responsive;
                prev = entry.settings;
            });
        }
    }

    checkResponsive() {
        this.settings = this.allSettings;
        if (!Array.isArray(this.allSettings.responsive)) {
            return;
        }
        this.allSettings.responsive.forEach((entry) => {
            if (window.matchMedia(`(max-width: ${entry.breakpoint}px)`).matches) {
                this.settings = entry.settings;
            }
        });
    }

    // ---- DOM helpers ----

    get outer() { return document.querySelector('.overlayBoxOuter'); }
    get box() { return document.querySelector('.overlayBox'); }
    get content() { return document.querySelector('.overlayBox .overlayContent'); }
    get inner() { return document.querySelector('.overlayBox .overlayContentInner'); }

    // ---- layout / sizing ----

    calculateDimensions() {
        const outer = this.outer;
        const box = this.box;
        const inner = this.inner;
        if (!outer || !box || !inner) return;

        const windowHeight = outer.offsetHeight;
        const contentWidth = inner.offsetWidth;
        const contentHeight = inner.offsetHeight;
        const outerHeight = box.offsetHeight;
        const desiredHeight = windowHeight - 2 * this.settings.margin_vertical - (outerHeight - contentHeight);

        box.style.width = 'auto';
        if (contentHeight > 0) {
            const desiredWidth = Math.floor(contentWidth / contentHeight * desiredHeight);
            box.style.width = `${desiredWidth}px`;
        }
    }

    resize() {
        const outer = this.outer;
        const box = this.box;
        const content = this.content;
        if (!outer || !box || !content) return;

        const marginV = this.settings.margin_vertical;
        const marginH = this.settings.margin_horizontal;
        const windowHeight = window.innerHeight;

        // Reset.
        content.style.width = '100%';
        box.style.width = 'auto';
        box.style.top = `${marginV}px`;
        box.style.left = `${marginH}px`;
        box.style.right = `${marginH}px`;
        box.style.bottom = 'auto';

        // Width: image-aware vs ajax content.
        let contentWidth;
        const imageEl = box.querySelector('.overlayImage img');
        if (imageEl) {
            contentWidth = Math.min(this.settings.max_width, imageEl.naturalWidth || imageEl.width);
        } else {
            contentWidth = Math.min(outer.offsetWidth - 2 * marginH, this.settings.max_width);
        }
        box.style.width = `${contentWidth}px`;

        // Vertical centering.
        let overlayBoxTop = marginV;
        if (windowHeight - 2 * marginV > box.offsetHeight) {
            overlayBoxTop = (outer.offsetHeight - box.offsetHeight) / 2;
        } else if (box.querySelector('.overlayVideo') || box.querySelector('.overlayImage')) {
            this.calculateDimensions();
        }

        let overlayBoxLeft = marginH;
        if (outer.offsetWidth - 2 * marginH >= box.offsetWidth) {
            overlayBoxLeft = Math.floor((outer.offsetWidth - box.offsetWidth) / 2);
        }

        // If content is taller than the viewport (or fullscreen iframe), stick
        // to top + bottom margins instead of centring.
        if (box.offsetHeight > windowHeight - 2 * marginV || this.settings.fullscreen_iframe) {
            box.style.bottom = `${marginV}px`;
            box.style.top = `${marginV}px`;
            box.style.left = `${overlayBoxLeft}px`;
            box.style.right = `${marginH}px`;
        } else {
            box.style.left = `${overlayBoxLeft}px`;
            box.style.top = `${overlayBoxTop}px`;
            box.style.right = `${marginH}px`;
        }
    }

    // ---- mode: image ----

    imageMode(callback) {
        this.inner.classList.add('overlayImage');
        const img = document.createElement('img');
        img.onload = () => {
            const wrap = this.box.querySelector('.overlayImage');
            wrap?.appendChild(img);
            callback();
            this.resize();
            this.imageUrl();
        };
        img.src = this.activeElement.getAttribute('href');
        this.imageTitle();
    }

    // ---- mode: ajax ----

    async ajaxResponse(callback) {
        const dataFilter = this.trigger.getAttribute('data-filter');
        const url = this.activeElement.getAttribute('href');
        try {
            // Match the legacy POST-with-no-body behaviour the jQuery impl used.
            const { data: response } = await axios.post(url, null, { responseType: 'text' });
            let html = response;
            if (dataFilter) {
                // Parse the response and pick the filtered subtree.
                const doc = new DOMParser().parseFromString(response, 'text/html');
                const filtered = doc.querySelector(dataFilter);
                html = filtered ? filtered.innerHTML : '';
            }

            const inner = this.inner;
            inner.classList.add('overlayAjax');
            inner.innerHTML = '<div></div>';
            inner.querySelector(':scope > div').innerHTML = html;

            if (this.settings.fullscreen_iframe) {
                this.box.classList.add('overlayFullscreen');
            }

            this.ajaxImagesLoaded(() => {
                callback();
                this.resize();
            });
        } catch (err) {
            const status = err.response?.status;
            const message = err.response?.data?.message ?? err.message;
            console.error(`[Overlay.ajaxResponse] ${status ?? 'network error'}: ${message}`);
        }

        this.imageTitle();
    }

    ajaxImagesLoaded(callback) {
        const images = this.inner?.querySelectorAll('img') || [];
        if (!images.length) {
            callback();
            return;
        }
        let pending = images.length;
        const done = () => {
            pending -= 1;
            if (pending <= 0) callback();
        };
        images.forEach((el) => {
            const probe = new Image();
            probe.onload = done;
            probe.onerror = done;
            probe.src = el.src;
        });
    }

    // ---- mode: inline (showBlock) ----

    showBlock(callback) {
        const url = this.activeElement.getAttribute('href');
        const target = document.querySelector(url);
        let html = null;
        if (target) {
            html = [...target.children];
        }

        const inner = this.inner;
        inner.classList.add('overlayAjax');
        inner.innerHTML = '<div></div>';
        const wrap = inner.querySelector(':scope > div');

        if (html) {
            if (this.settings.inline) {
                html.forEach((el) => wrap.appendChild(el));
                target.classList.add('js_restoreOverlayContent');
            } else {
                html.forEach((el) => wrap.appendChild(el.cloneNode(true)));
            }
        }

        if (this.settings.fullscreen_iframe) {
            this.box.classList.add('overlayFullscreen');
        }

        this.imageTitle();
        this.ajaxImagesLoaded(() => {
            callback();
            this.resize();
        });
    }

    restoreOverlayContent() {
        if (!this.settings.inline) return;
        const wrap = document.querySelector('.overlayAjax > div');
        const place = document.querySelector('.js_restoreOverlayContent');
        if (wrap && place) {
            [...wrap.children].forEach((c) => place.appendChild(c));
            place.classList.remove('js_restoreOverlayContent');
        }
    }

    // ---- title / link ----

    imageTitle() {
        const title = this.activeElement.dataset.title;
        const box = this.box;
        box?.classList.add('hasTitle');

        if (title) {
            let titleEl = box.querySelector('.overlayBoxTitle');
            if (!titleEl) {
                titleEl = document.createElement('span');
                titleEl.className = 'overlayBoxTitle';
                box.appendChild(titleEl);
            }
            titleEl.textContent = title;
        } else {
            box?.querySelector('.overlayBoxTitle')?.remove();
            box?.classList.remove('hasTitle');
        }
    }

    imageUrl() {
        const url = this.activeElement.dataset.url;
        const box = this.box;
        box?.classList.add('hasLink');

        if (url) {
            let linkEl = box.querySelector('.overlayBoxLink');
            if (!linkEl) {
                linkEl = document.createElement('a');
                linkEl.className = 'overlayBoxLink';
                linkEl.target = '_blank';
                const imgEl = box.querySelector('.overlayImage img');
                imgEl?.after(linkEl);
            }
            linkEl.href = url;
        } else {
            box?.querySelector('.overlayBoxLink')?.remove();
            box?.classList.remove('hasLink');
        }
    }

    // ---- mode: gallery ----

    gallery() {
        const rel = this.activeElement.getAttribute('rel');
        const galleryImages = [...document.querySelectorAll(`[rel="${rel}"]`)];
        this.galleryImages = galleryImages;

        const box = this.box;
        box.classList.add('hasGallery');
        box.insertAdjacentHTML('beforeend', this.settings.gallery_prev_html);
        box.insertAdjacentHTML('beforeend', this.settings.gallery_next_html);

        const activeIndex = galleryImages.indexOf(this.activeElement);
        this.galleryButtons(activeIndex, galleryImages.length - 1);

        // Next/previous click handler — delegated through body so we can
        // detach it cleanly on close.
        this._galleryClick = (event) => {
            const btn = event.target instanceof Element
                ? event.target.closest('.overlayBox .js_galleryPrev, .overlayBox .js_galleryNext')
                : null;
            if (!btn) return;
            const nextIndex = parseInt(btn.dataset.index, 10);
            if (Number.isNaN(nextIndex) || nextIndex < 0 || nextIndex > galleryImages.length - 1) return;
            if (this.galleryLoading) return;
            this.galleryLoading = true;

            this.activeElement = galleryImages[nextIndex];

            const content = this.content;
            content.style.width = `${content.offsetWidth}px`;
            content.style.height = `${content.offsetHeight}px`;

            // Fade out the existing inner, swap content, fade back in.
            const oldInner = this.inner;
            fadeOpacityOut(oldInner, this.settings.fade_time, () => {
                this.restoreOverlayContent();
                oldInner.remove();
                const newInner = document.createElement('div');
                newInner.className = 'overlayContentInner';
                content.insertBefore(newInner, content.firstChild);
                content.classList.add('overlayContentLoading');

                this.switchGalleryMode(() => {
                    newInner.style.display = 'none';
                    content.classList.remove('overlayContentLoading');
                    newInner.style.display = '';
                    fadeOpacityIn(newInner, this.settings.fade_time);
                    this.galleryLoading = false;
                    content.removeAttribute('style');
                });

                this.galleryButtons(nextIndex, galleryImages.length - 1);
            });
        };
        document.body.addEventListener('click', this._galleryClick);

        if (this.settings.arrow_controls) {
            this._galleryKeydown = (event) => {
                const key = event.keyCode || event.charCode || 0;
                if (key === 37) {
                    const prev = document.querySelector('.overlayBox .js_galleryPrev');
                    if (prev && prev.offsetParent !== null) prev.click();
                } else if (key === 39) {
                    const next = document.querySelector('.overlayBox .js_galleryNext');
                    if (next && next.offsetParent !== null) next.click();
                }
            };
            document.addEventListener('keydown', this._galleryKeydown);
        }

        if (this.settings.swipe) {
            this._swipeStartX = null;
            this._touchStart = (event) => {
                if (event.target.closest('.overlayContent')) {
                    this._swipeStartX = event.touches[0].clientX;
                }
            };
            this._touchEnd = (event) => {
                if (this._swipeStartX === null) return;
                const endX = (event.changedTouches[0] || {}).clientX;
                const dx = endX - this._swipeStartX;
                this._swipeStartX = null;
                if (Math.abs(dx) < 40) return;
                if (dx < 0) {
                    document.querySelector('.overlayBox .js_galleryNext')?.click();
                } else {
                    document.querySelector('.overlayBox .js_galleryPrev')?.click();
                }
            };
            document.body.addEventListener('touchstart', this._touchStart, { passive: true });
            document.body.addEventListener('touchend', this._touchEnd, { passive: true });
        }

        this.switchGalleryMode(() => this.displayOverlay());
    }

    switchGalleryMode(callback) {
        const el = this.activeElement;
        if (el.dataset.video !== undefined && el.dataset.video !== '') {
            this.videoMode(callback);
        } else if (el.dataset.ajax !== undefined && el.dataset.ajax !== '') {
            this.ajaxResponse(callback);
        } else if (el.dataset.noAjax !== undefined) {
            this.showBlock(callback);
        } else {
            this.imageMode(callback);
        }
    }

    galleryButtons(activeIndex, max) {
        const prev = this.box?.querySelector('.js_galleryPrev');
        const next = this.box?.querySelector('.js_galleryNext');
        if (!prev || !next) return;

        if (activeIndex > 0) prev.classList.remove('arrowInactive');
        else prev.classList.add('arrowInactive');

        if (activeIndex < max) next.classList.remove('arrowInactive');
        else next.classList.add('arrowInactive');

        prev.dataset.index = String(activeIndex - 1);
        next.dataset.index = String(activeIndex + 1);
    }

    // ---- mode: video ----

    initVideo() {
        const video = this.box?.querySelector('.overlayVideo');
        const iframe = video?.querySelector('iframe');
        if (!video || !iframe) return;

        const ratio = 100 * (iframe.height || iframe.offsetHeight) / (iframe.width || iframe.offsetWidth);
        video.style.height = '0';
        video.style.width = '100%';
        video.style.paddingTop = `${ratio}%`;
        iframe.style.width = '100%';
        iframe.style.height = '100%';
    }

    videoMode(callback) {
        const url = this.activeElement.getAttribute('href');
        const autoplay = this.settings.video_settings.autoplay ? '?autoplay=1' : '';
        const { width, height } = this.settings.video_settings;

        // Build the embed iframe via DOM APIs (not innerHTML) so the href
        // cannot inject markup. The capture groups are also restricted to
        // safe ID characters to refuse anything that doesn't look like a
        // real video ID.
        let src = null;
        const lower = url.toLowerCase();

        if (lower.includes('youtu')) {
            const m = url.match(/(?:https?:\/{2})?(?:w{3}\.)?youtu(?:be)?\.(?:com|be)(?:\/watch\?v=|\/)([A-Za-z0-9_-]+)/);
            if (m) {
                src = `//www.youtube.com/embed/${m[1]}${autoplay}?rel=0`;
            }
        } else if (lower.includes('vimeo')) {
            const m = url.match(/https?:\/\/(?:www\.)?vimeo\.com\/(\d+)(?:$|\/)/);
            if (m) {
                src = `//player.vimeo.com/video/${m[1]}${autoplay}`;
            }
        }

        if (src) {
            this.inner.classList.add('overlayVideo');
            const container = this.box.querySelector('.overlayVideo');
            container.replaceChildren();
            const iframe = document.createElement('iframe');
            iframe.setAttribute('src', src);
            iframe.setAttribute('width', String(width));
            iframe.setAttribute('height', String(height));
            iframe.setAttribute('frameborder', '0');
            iframe.setAttribute('allowfullscreen', '');
            iframe.setAttribute('webkitallowfullscreen', '');
            iframe.setAttribute('mozallowfullscreen', '');
            container.appendChild(iframe);
        }

        this.initVideo();
        this.imageTitle();
        callback();
        this.resize();
    }

    // ---- open / close ----

    displayOverlay() {
        const box = this.box;
        const outer = this.outer;
        if (!box || !outer) return;

        // Close button.
        box.insertAdjacentHTML('beforeend', this.settings.button_html);

        if (this.settings.content_class) {
            box.classList.add(this.settings.content_class);
        }

        outer.classList.remove('overlayLoading');

        this.settings.beforeOpen();
        this.resize();

        // Fade box in.
        fadeOpacityIn(box, this.settings.fade_time, () => {
            this.settings.onOpen();
        });

        // Close button click.
        box.querySelector('.overlayCloseButton')?.addEventListener('click', (event) => {
            event.preventDefault();
            this.overlayClose();
        });

        this._onResize = () => {
            this.checkResponsive();
            this.resize();
        };
        window.addEventListener('resize', this._onResize);
        window.addEventListener('orientationchange', this._onResize);
    }

    overlayOpen() {
        const url = this.trigger.getAttribute('href') || '';
        const rel = this.trigger.getAttribute('rel');
        const noAjax = this.trigger.dataset.noAjax;
        this.activeElement = this.trigger;

        const extension = url.split('.').pop().toLowerCase();
        const scrollOnOpen = window.scrollY;
        document.body.dataset.storedScroll = String(scrollOnOpen);

        const siteWrapper = document.querySelector(this.settings.site_wrapper);
        if (siteWrapper) {
            siteWrapper.style.position = 'relative';
            siteWrapper.style.top = `${-scrollOnOpen}px`;
        }

        document.body.classList.add('noScroll', 'blur');

        // Build the overlay markup.
        const outer = document.createElement('div');
        outer.className = 'overlayBoxOuter overlayLoading';
        outer.innerHTML = '<div class="overlayBox"><div class="overlayContent"><div class="overlayContentInner"></div></div></div>';
        document.body.appendChild(outer);

        // Click-outside-to-close.
        outer.addEventListener('click', (event) => {
            if (event.target === outer) {
                this.overlayClose();
            }
        });

        // Esc-to-close.
        if (this.settings.close_on_esc) {
            this._onKeydown = (event) => {
                const key = event.keyCode || event.charCode || 0;
                if (key === 27) {
                    this.overlayClose();
                }
            };
            document.addEventListener('keydown', this._onKeydown);
        }

        // Pick a mode.
        const isGallery = rel && document.querySelectorAll(`[rel="${rel}"]`).length > 1 && this.settings.gallery === true;
        if (isGallery) {
            this.gallery();
        } else if (IMAGE_EXTENSIONS.includes(extension)) {
            this.imageMode(() => this.displayOverlay());
        } else if (this.settings.video === true) {
            this.videoMode(() => this.displayOverlay());
        } else if (noAjax !== undefined) {
            this.showBlock(() => this.displayOverlay());
        } else {
            this.ajaxResponse(() => this.displayOverlay());
        }
    }

    overlayClose() {
        this.settings.onClose();

        if (this._galleryClick) {
            document.body.removeEventListener('click', this._galleryClick);
            this._galleryClick = null;
        }
        if (this._galleryKeydown) {
            document.removeEventListener('keydown', this._galleryKeydown);
            this._galleryKeydown = null;
        }
        if (this._touchStart) {
            document.body.removeEventListener('touchstart', this._touchStart);
            this._touchStart = null;
        }
        if (this._touchEnd) {
            document.body.removeEventListener('touchend', this._touchEnd);
            this._touchEnd = null;
        }
        if (this._onKeydown) {
            document.removeEventListener('keydown', this._onKeydown);
            this._onKeydown = null;
        }

        document.body.classList.remove('noScroll', 'blur');
        this.restoreOverlayContent();

        const outer = this.outer;
        if (!outer) {
            this.destroy();
            return;
        }
        fadeOpacityOut(outer, 300, () => this.destroy());
    }

    destroy() {
        document.querySelector('.overlayCloseButton')?.remove();
        this.outer?.remove();

        const siteWrapper = document.querySelector(this.settings.site_wrapper);
        if (siteWrapper) {
            siteWrapper.style.top = '0';
            siteWrapper.style.position = '';
        }

        const storedScroll = parseInt(document.body.dataset.storedScroll || '0', 10);
        window.scrollTo({ top: storedScroll });

        if (this._onResize) {
            window.removeEventListener('resize', this._onResize);
            window.removeEventListener('orientationchange', this._onResize);
            this._onResize = null;
        }
    }

    // ---- entry point ----

    open() {
        this.readResponsive();
        this.checkResponsive();
        this.overlayOpen();
    }

    // ---- static factory: attach to triggers ----

    static attach(target, options) {
        const nodes = typeof target === 'string'
            ? document.querySelectorAll(target)
            : (target && 'length' in target ? target : [target]);
        [...nodes].forEach((el) => {
            if (!el) return;
            // Avoid double-binding the same element.
            if (overlayBoundElements.has(el)) return;
            overlayBoundElements.set(el, true);
            el.addEventListener('click', (event) => {
                event.preventDefault();
                const instance = new Overlay(el, options);
                instance.open();
            });
        });
    }
}

// Legacy global — used by templates and other modules until they migrate.
window.mtcOverlay = function mtcOverlay(target, options) {
    Overlay.attach(target, options);
};
