import {Controller} from '@hotwired/stimulus';

// NOTE: @mapbox/search-js-web is loaded lazily via dynamic import() — only
// AFTER the user has accepted the consent bar. The import side-effect-registers
// `<mapbox-address-autofill>` as a custom element. If we imported eagerly here,
// the element would upgrade as soon as this controller's bundle loaded, and
// would start firing /autofill/v1/suggest/ requests on every keystroke —
// without our accessToken (because we set it inside activate(), which is
// gated on consent). Mapbox then rejected the requests with 401 and, as a
// side effect, mutated the inner <input>'s `name` attribute to append
// " address-search", which broke the surrounding LiveComponent
// ("Invalid model name"). Lazy-loading keeps the field a plain <input>
// pre-consent — no API calls, no name mutation, no LiveComponent breakage.

export default class extends Controller {
    static values = {
        token: String,
        country: {type: String, default: 'de'},
        language: {type: String, default: 'de'},
    };

    static targets = ['missingNumber'];

    declare readonly tokenValue: string;
    declare readonly countryValue: string;
    declare readonly languageValue: string;
    declare readonly missingNumberTarget: HTMLElement;
    declare readonly hasMissingNumberTarget: boolean;

    private nameObserver: MutationObserver | null = null;
    private addressPicked = false;
    private consentListener: (() => void) | null = null;
    private activated = false;
    private submitCleaner: ((e: Event) => void) | null = null;
    private observedForm: HTMLFormElement | null = null;

    connect() {
        if (!this.tokenValue) {
            return;
        }

        // Install the input-name cleaner immediately — even before the SDK
        // is loaded. If for any reason `<mapbox-address-autofill>` upgrades
        // anyway (e.g. another component on the page loads the SDK eagerly),
        // the LiveComponent's `name` attribute stays clean.
        this.keepInputNameClean();

        // Belt-and-suspenders: also strip the suffix synchronously the moment
        // the surrounding form starts to submit, in the capture phase — so we
        // run *before* LiveComponent's submit handler serialises the form.
        // The MutationObserver above should catch every mutation, but Mapbox's
        // SDK has been observed re-corrupting the `name` on input/focus/blur
        // events in some versions, and there's no guarantee a microtask fires
        // between that re-write and the user clicking save. If the name is
        // still dirty when LiveComponent runs `new FormData(form)`, the
        // submitted key reads `…[street] address-search` and Symfony's form
        // binder silently drops the field → `street: ""` on the server.
        this.installSubmitCleaner();

        if ('true' === localStorage.getItem('mapbox-consent')) {
            void this.activate();

            return;
        }

        this.consentListener = () => void this.activate();
        window.addEventListener('mapbox-consent-given', this.consentListener, {once: true});
    }

    disconnect() {
        if (this.consentListener) {
            window.removeEventListener('mapbox-consent-given', this.consentListener);
            this.consentListener = null;
        }
        this.nameObserver?.disconnect();
        this.nameObserver = null;
        if (this.observedForm && this.submitCleaner) {
            this.observedForm.removeEventListener('submit', this.submitCleaner, true);
        }
        this.observedForm = null;
        this.submitCleaner = null;
    }

    private async activate() {
        if (this.activated) {
            return;
        }
        this.activated = true;

        // Dynamic import — fetched (and `<mapbox-address-autofill>` registered)
        // only at this point. Before consent the field is just a plain
        // <input>; nothing has gone over the wire to Mapbox.
        await import('@mapbox/search-js-web');

        await customElements.whenDefined('mapbox-address-autofill');

        const el = this.element as any;
        el.accessToken = this.tokenValue;
        el.options = {
            country: this.countryValue,
            language: this.languageValue,
        };

        this.watchHouseNumber();
    }

    private get streetInput(): HTMLInputElement | null {
        const input = this.element.querySelector('input');

        return input instanceof HTMLInputElement ? input : null;
    }

    private keepInputNameClean() {
        const input = this.streetInput;
        if (!input) {
            return;
        }

        // Mapbox's <mapbox-address-autofill> appends ' address-search' to the
        // input's `name` when it upgrades. LiveComponent then reads that name
        // as the model id and throws "Invalid model name". Strip on every
        // attribute mutation.
        this.stripInputNameSuffix();
        this.nameObserver = new MutationObserver(() => this.stripInputNameSuffix());
        this.nameObserver.observe(input, {attributes: true, attributeFilter: ['name']});
    }

    private stripInputNameSuffix() {
        const input = this.streetInput;
        if (!input) {
            return;
        }
        const suffix = ' address-search';
        if (input.name.endsWith(suffix)) {
            input.name = input.name.slice(0, -suffix.length);
        }
    }

    private installSubmitCleaner() {
        const form = this.element.closest('form');
        if (!form) {
            return;
        }

        // Capture phase: this listener fires before LiveComponent's bubble-phase
        // `live#action:prevent` runs FormData() on the form. Strip the suffix
        // here so the serialised key is `…[street]`, not `…[street] address-search`.
        this.submitCleaner = () => this.stripInputNameSuffix();
        this.observedForm = form;
        form.addEventListener('submit', this.submitCleaner, true);
    }

    private watchHouseNumber() {
        const input = this.streetInput;
        if (!input || !this.hasMissingNumberTarget) {
            return;
        }

        // A suggestion was retrieved: let Mapbox settle the value, then evaluate.
        this.element.addEventListener('retrieve', () => {
            this.addressPicked = true;
            setTimeout(() => this.evaluateHouseNumber(), 0);
        });

        // Re-evaluate as the user edits (e.g. adds the number → hint disappears).
        input.addEventListener('input', () => this.evaluateHouseNumber());
    }

    private evaluateHouseNumber() {
        const input = this.streetInput;
        if (!input || !this.hasMissingNumberTarget) {
            return;
        }

        const value = input.value.trim();
        const missing = this.addressPicked && '' !== value && !/\d/.test(value);
        this.missingNumberTarget.hidden = !missing;
    }
}
