/*!
 * PapaParse - Fast, in-browser CSV (de)serializer
 * https://www.papaparse.com/
 * (Included as bundled dependency - deobfuscated for readability)
 */
!(function (globalScope, factory) {
    if (typeof define === 'function' && define.amd) {
        define([], factory);
    } else if (typeof module === 'object' && typeof exports !== 'undefined') {
        module.exports = factory();
    } else {
        globalScope.Papa = factory();
    }
}(this, function papaParseFactory() {
    'use strict';

    var self_ =
        typeof self !== 'undefined' ? self :
            typeof window !== 'undefined' ? window :
                void 0 !== self_ ? self_ : {};

    var isWorkerThread = !self_.document && !!self_.postMessage;
    var IS_PAPA_WORKER = self_.IS_PAPA_WORKER || false;
    var workerRegistry = {};
    var workerIdCounter = 0;

    var Papa = {
        parse: function (input, config) {
            config = config || {};
            var dynamicTyping = config.dynamicTyping || false;
            if (isFunction(dynamicTyping)) {
                config.dynamicTypingFunction = dynamicTyping;
                dynamicTyping = {};
            }
            config.dynamicTyping = dynamicTyping;
            config.transform = !!isFunction(config.transform) && config.transform;

            if (config.worker && Papa.WORKERS_SUPPORTED) {
                var worker = (function () {
                    if (!Papa.WORKERS_SUPPORTED) return false;
                    var blobUrl = (
                        Papa.BLOB_URL || (Papa.BLOB_URL = (self_.URL || self_.webkitURL || null).createObjectURL(
                            new Blob([
                                'var global = (function() { if (typeof self !== \'undefined\') { return self; } if (typeof window !== \'undefined\') { return window; } if (typeof global !== \'undefined\') { return global; } return {}; })(); global.IS_PAPA_WORKER=true; ',
                                '(', papaParseFactory.toString(), ')();'
                            ], { type: 'text/javascript' })
                        ))
                    );
                    var w = new self_.Worker(blobUrl);
                    w.onmessage = onWorkerMessage;
                    w.id = workerIdCounter++;
                    workerRegistry[w.id] = w;
                    return w;
                }());
                worker.userStep = config.step;
                worker.userChunk = config.chunk;
                worker.userComplete = config.complete;
                worker.userError = config.error;
                config.step = isFunction(config.step);
                config.chunk = isFunction(config.chunk);
                config.complete = isFunction(config.complete);
                config.error = isFunction(config.error);
                delete config.worker;
                worker.postMessage({ input: input, config: config, workerId: worker.id });
                return;
            }

            var streamer = null;
            Papa.NODE_STREAM_INPUT;
            if (typeof input === 'string') {
                input = (function stripBOM(str) {
                    return str.charCodeAt(0) === 0xFEFF ? str.slice(1) : str;
                }(input));
                streamer = config.download ? new NetworkStreamer(config) : new StringStreamer(config);
            } else if (input.readable === true && isFunction(input.read) && isFunction(input.on)) {
                streamer = new ReadableStreamStreamer(config);
            } else if ((self_.File && input instanceof File) || input instanceof Object) {
                streamer = new FileStreamer(config);
            }
            return streamer.stream(input);
        },

        unparse: function (data, config) {
            var quotes = false;
            var header = true;
            var delimiter = ',';
            var newline = '\r\n';
            var quoteChar = '"';
            var escapeChar = quoteChar + quoteChar;
            var skipEmptyLines = false;
            var columns = null;
            var escapeFormulae = false;

            (function applyConfig() {
                if (typeof config === 'object') {
                    if (typeof config.delimiter === 'string' &&
                        !Papa.BAD_DELIMITERS.filter(function (c) { return config.delimiter.indexOf(c) !== -1; }).length) {
                        delimiter = config.delimiter;
                    }
                    if (typeof config.quotes === 'boolean' || typeof config.quotes === 'function' || Array.isArray(config.quotes)) {
                        quotes = config.quotes;
                    }
                    if (typeof config.skipEmptyLines === 'boolean' || typeof config.skipEmptyLines === 'string') {
                        skipEmptyLines = config.skipEmptyLines;
                    }
                    if (typeof config.newline === 'string') newline = config.newline;
                    if (typeof config.quoteChar === 'string') quoteChar = config.quoteChar;
                    if (typeof config.header === 'boolean') header = config.header;
                    if (Array.isArray(config.columns)) {
                        if (config.columns.length === 0) throw new Error('Option columns is empty');
                        columns = config.columns;
                    }
                    if (config.escapeChar !== undefined) escapeChar = config.escapeChar + quoteChar;
                    if (typeof config.escapeFormulae === 'boolean' || config.escapeFormulae instanceof RegExp) {
                        escapeFormulae = config.escapeFormulae instanceof RegExp
                            ? config.escapeFormulae
                            : /^[=+\-@\t\r].*$/;
                    }
                }
            }());

            var quoteCharRegex = new RegExp(escapeRegExp(quoteChar), 'g');
            if (typeof data === 'string') data = JSON.parse(data);

            if (Array.isArray(data)) {
                if (!data.length || Array.isArray(data[0])) return serializeRows(null, data, skipEmptyLines);
                if (typeof data[0] === 'object') return serializeRows(columns || Object.keys(data[0]), data, skipEmptyLines);
            } else if (typeof data === 'object') {
                if (typeof data.data === 'string') data.data = JSON.parse(data.data);
                if (Array.isArray(data.data)) {
                    data.fields = data.fields || (data.meta && data.meta.fields) || columns;
                    data.fields = data.fields || (
                        Array.isArray(data.data[0]) ? data.fields :
                            typeof data.data[0] === 'object' ? Object.keys(data.data[0]) : []
                    );
                    if (!Array.isArray(data.data[0]) && typeof data.data[0] !== 'object') {
                        data.data = [data.data];
                    }
                }
                return serializeRows(data.fields || [], data.data || [], skipEmptyLines);
            }
            throw new Error('Unable to serialize unrecognized input');

            function serializeRows(fields, rows, skipEmpty) {
                var output = '';
                if (typeof fields === 'string') fields = JSON.parse(fields);
                if (typeof rows === 'string') rows = JSON.parse(rows);
                var hasFields = Array.isArray(fields) && fields.length > 0;
                var rowIsObject = !Array.isArray(rows[0]);

                if (hasFields && header) {
                    for (var i = 0; i < fields.length; i++) {
                        if (i > 0) output += delimiter;
                        output += formatCell(fields[i], i);
                    }
                    if (rows.length > 0) output += newline;
                }

                for (var r = 0; r < rows.length; r++) {
                    var numCols = hasFields ? fields.length : rows[r].length;
                    var isEmpty = hasFields ? Object.keys(rows[r]).length === 0 : rows[r].length === 0;
                    var skip = false;

                    if (skipEmpty && !hasFields) {
                        skip = skipEmpty === 'greedy'
                            ? rows[r].join('').trim() === ''
                            : rows[r].length === 1 && rows[r][0].length === 0;
                    }
                    if (skipEmpty === 'greedy' && hasFields) {
                        var vals = [];
                        for (var c = 0; c < numCols; c++) {
                            var key = rowIsObject ? fields[c] : c;
                            vals.push(rows[r][key]);
                        }
                        skip = vals.join('').trim() === '';
                    }

                    if (!skip) {
                        for (var col = 0; col < numCols; col++) {
                            if (col > 0 && !isEmpty) output += delimiter;
                            var colKey = hasFields && rowIsObject ? fields[col] : col;
                            output += formatCell(rows[r][colKey], col);
                        }
                        if (r < rows.length - 1 && (!skipEmpty || (numCols > 0 && !isEmpty))) {
                            output += newline;
                        }
                    }
                }
                return output;
            }

            function formatCell(value, colIndex) {
                if (value == null) return '';
                if (value.constructor === Date) return JSON.stringify(value).slice(1, 19);
                var needsEscape = false;
                if (escapeFormulae && typeof value === 'string' && escapeFormulae.test(value)) {
                    value = "'" + value;
                    needsEscape = true;
                }
                var str = value.toString().replace(quoteCharRegex, escapeChar);
                needsEscape = needsEscape ||
                    quotes === true ||
                    (typeof quotes === 'function' && quotes(value, colIndex)) ||
                    (Array.isArray(quotes) && quotes[colIndex]) ||
                    (function containsBadChar(s, badChars) {
                        for (var i = 0; i < badChars.length; i++) {
                            if (s.indexOf(badChars[i]) > -1) return true;
                        }
                        return false;
                    }(str, Papa.BAD_DELIMITERS)) ||
                    str.indexOf(delimiter) > -1 ||
                    str.charAt(0) === ' ' ||
                    str.charAt(str.length - 1) === ' ';
                return needsEscape ? quoteChar + str + quoteChar : str;
            }
        }
    };

    Papa.RECORD_SEP = String.fromCharCode(30);
    Papa.UNIT_SEP = String.fromCharCode(31);
    Papa.BYTE_ORDER_MARK = '\uFEFF';
    Papa.BAD_DELIMITERS = ['\r', '\n', '"', Papa.BYTE_ORDER_MARK];
    Papa.WORKERS_SUPPORTED = !isWorkerThread && !!self_.Worker;
    Papa.NODE_STREAM_INPUT = 1;
    Papa.LocalChunkSize = 10485760;
    Papa.RemoteChunkSize = 5242880;
    Papa.DefaultDelimiter = ',';
    Papa.Parser = CsvParser;
    Papa.ParserHandle = ParserHandle;
    Papa.NetworkStreamer = NetworkStreamer;
    Papa.FileStreamer = FileStreamer;
    Papa.StringStreamer = StringStreamer;
    Papa.ReadableStreamStreamer = ReadableStreamStreamer;

    if (self_.jQuery) {
        var $ = self_.jQuery;
        $.fn.parse = function (options) {
            var config = options.config || {};
            var queue = [];
            this.each(function () {
                if (
                    $(this).prop('tagName').toUpperCase() !== 'INPUT' ||
                    $(this).attr('type').toLowerCase() !== 'file' ||
                    !self_.FileReader ||
                    !this.files ||
                    this.files.length === 0
                ) return true;
                for (var i = 0; i < this.files.length; i++) {
                    queue.push({ file: this.files[i], inputElem: this, instanceConfig: $.extend({}, config) });
                }
            });
            processQueue();
            return this;

            function processQueue() {
                if (queue.length === 0) {
                    if (isFunction(options.complete)) options.complete();
                    return;
                }
                var item = queue[0];
                if (isFunction(options.before)) {
                    var result = options.before(item.file, item.inputElem);
                    if (typeof result === 'object') {
                        if (result.action === 'abort') {
                            if (isFunction(options.error)) options.error({ name: 'AbortError' }, item.file, item.inputElem, result.reason);
                            return;
                        }
                        if (result.action === 'skip') { dequeue(); return; }
                        if (typeof result.config === 'object') item.instanceConfig = $.extend(item.instanceConfig, result.config);
                    } else if (result === 'skip') { dequeue(); return; }
                }
                var origComplete = item.instanceConfig.complete;
                item.instanceConfig.complete = function (results) {
                    if (isFunction(origComplete)) origComplete(results, item.file, item.inputElem);
                    dequeue();
                };
                Papa.parse(item.file, item.instanceConfig);
            }

            function dequeue() {
                queue.splice(0, 1);
                processQueue();
            }
        };
    }

    // ─── Streamer Base ───────────────────────────────────────────────────────────

    function BaseStreamer(config) {
        this._handle = null;
        this._finished = false;
        this._completed = false;
        this._halted = false;
        this._input = null;
        this._baseIndex = 0;
        this._partialLine = '';
        this._rowCount = 0;
        this._start = 0;
        this._nextChunk = null;
        this.isFirstChunk = true;
        this._completeResults = { data: [], errors: [], meta: {} };

        (function init(cfg) {
            var c = cloneConfig(cfg);
            c.chunkSize = parseInt(c.chunkSize);
            if (!cfg.step && !cfg.chunk) c.chunkSize = null;
            this._handle = new ParserHandle(c);
            this._handle.streamer = this;
            this._config = c;
        }).call(this, config);

        this.parseChunk = function (chunk, isLastChunk) {
            if (this.isFirstChunk && isFunction(this._config.beforeFirstChunk)) {
                var modified = this._config.beforeFirstChunk(chunk);
                if (modified !== undefined) chunk = modified;
            }
            this.isFirstChunk = false;
            this._halted = false;

            var combined = this._partialLine + chunk;
            this._partialLine = '';
            var results = this._handle.parse(combined, this._baseIndex, !this._finished);

            if (!this._handle.paused() && !this._handle.aborted()) {
                var cursor = results.meta.cursor;
                if (!this._finished) {
                    this._partialLine = combined.substring(cursor - this._baseIndex);
                    this._baseIndex = cursor;
                }
                if (results && results.data) this._rowCount += results.data.length;

                var done = this._finished || (this._config.preview && this._rowCount >= this._config.preview);

                if (IS_PAPA_WORKER) {
                    self_.postMessage({ results: results, workerId: Papa.WORKER_ID, finished: done });
                } else {
                    if (isFunction(this._config.chunk) && !isLastChunk) {
                        this._config.chunk(results, this._handle);
                        if (this._handle.paused() || this._handle.aborted()) {
                            this._halted = true;
                            return;
                        }
                        results = undefined;
                        this._completeResults = undefined;
                    }
                }

                if (!this._config.step && !this._config.chunk) {
                    this._completeResults.data = this._completeResults.data.concat(results.data);
                    this._completeResults.errors = this._completeResults.errors.concat(results.errors);
                    this._completeResults.meta = results.meta;
                }
                if (!this._completed && done && isFunction(this._config.complete) && !(results && results.meta.aborted)) {
                    this._config.complete(this._completeResults, this._input);
                    this._completed = true;
                }
                if (!done && !(results && results.meta.paused)) this._nextChunk();
                return results;
            }
            this._halted = true;
        };

        this._sendError = function (error) {
            if (isFunction(this._config.error)) {
                this._config.error(error);
            } else if (IS_PAPA_WORKER && this._config.error) {
                self_.postMessage({ workerId: Papa.WORKER_ID, error: error, finished: false });
            }
        };
    }

    function NetworkStreamer(config) {
        config = config || {};
        if (!config.chunkSize) config.chunkSize = Papa.RemoteChunkSize;
        BaseStreamer.call(this, config);
        var xhr;

        this._nextChunk = isWorkerThread
            ? function () { this._readChunk(); this._chunkLoaded(); }
            : function () { this._readChunk(); };

        this.stream = function (url) { this._input = url; this._nextChunk(); };

        this._readChunk = function () {
            if (this._finished) { this._chunkLoaded(); return; }
            xhr = new XMLHttpRequest();
            if (this._config.withCredentials) xhr.withCredentials = this._config.withCredentials;
            if (!isWorkerThread) {
                xhr.onload = bindThis(this._chunkLoaded, this);
                xhr.onerror = bindThis(this._chunkError, this);
            }
            xhr.open(this._config.downloadRequestBody ? 'POST' : 'GET', this._input, !isWorkerThread);
            if (this._config.downloadRequestHeaders) {
                for (var h in this._config.downloadRequestHeaders) {
                    xhr.setRequestHeader(h, this._config.downloadRequestHeaders[h]);
                }
            }
            if (this._config.chunkSize) {
                var end = this._start + this._config.chunkSize - 1;
                xhr.setRequestHeader('Range', 'bytes=' + this._start + '-' + end);
            }
            try { xhr.send(this._config.downloadRequestBody); }
            catch (e) { this._chunkError(e.message); }
            if (isWorkerThread && xhr.status === 0) this._chunkError();
        };

        this._chunkLoaded = function () {
            if (xhr.readyState !== 4) return;
            if (xhr.status < 200 || xhr.status >= 400) {
                this._chunkError();
            } else {
                this._start += this._config.chunkSize ? this._config.chunkSize : xhr.responseText.length;
                this._finished = !this._config.chunkSize || this._start >= (function getContentLength(x) {
                    var cr = x.getResponseHeader('Content-Range');
                    return cr === null ? -1 : parseInt(cr.substring(cr.lastIndexOf('/') + 1));
                }(xhr));
                this.parseChunk(xhr.responseText);
            }
        };

        this._chunkError = function (msg) {
            this._sendError(new Error(xhr.statusText || msg));
        };
    }

    function FileStreamer(config) {
        config = config || {};
        if (!config.chunkSize) config.chunkSize = Papa.LocalChunkSize;
        BaseStreamer.call(this, config);
        var reader, slicer;
        var hasFileReader = typeof FileReader !== 'undefined';

        this.stream = function (file) {
            this._input = file;
            slicer = file.slice || file.webkitSlice || file.mozSlice;
            if (hasFileReader) {
                reader = new FileReader();
                reader.onload = bindThis(this._chunkLoaded, this);
                reader.onerror = bindThis(this._chunkError, this);
            } else {
                reader = new FileReaderSync();
            }
            this._nextChunk();
        };

        this._nextChunk = function () {
            if (!this._finished && !(this._config.preview && !(this._rowCount < this._config.preview))) {
                this._readChunk();
            }
        };

        this._readChunk = function () {
            var input = this._input;
            if (this._config.chunkSize) {
                var end = Math.min(this._start + this._config.chunkSize, this._input.size);
                input = slicer.call(input, this._start, end);
            }
            var result = reader.readAsText(input, this._config.encoding);
            if (!hasFileReader) this._chunkLoaded({ target: { result: result } });
        };

        this._chunkLoaded = function (e) {
            this._start += this._config.chunkSize;
            this._finished = !this._config.chunkSize || this._start >= this._input.size;
            this.parseChunk(e.target.result);
        };

        this._chunkError = function () { this._sendError(reader.error); };
    }

    function StringStreamer(config) {
        config = config || {};
        BaseStreamer.call(this, config);
        var remaining;

        this.stream = function (str) { remaining = str; return this._nextChunk(); };

        this._nextChunk = function () {
            if (!this._finished) {
                var chunk, size = this._config.chunkSize;
                if (size) {
                    chunk = remaining.substring(0, size);
                    remaining = remaining.substring(size);
                } else {
                    chunk = remaining;
                    remaining = '';
                }
                this._finished = !remaining;
                this.parseChunk(chunk);
            }
        };
    }

    function ReadableStreamStreamer(config) {
        config = config || {};
        BaseStreamer.call(this, config);
        var queue = [], paused = true, streamEnded = false;

        this.pause = function () { BaseStreamer.prototype.pause.apply(this, arguments); this._input.pause(); };
        this.resume = function () { BaseStreamer.prototype.resume.apply(this, arguments); this._input.resume(); };

        this.stream = function (stream) {
            this._input = stream;
            stream.on('data', this._streamData);
            stream.on('end', this._streamEnd);
            stream.on('error', this._streamError);
        };

        this._checkIsFinished = function () {
            if (streamEnded && queue.length === 1) this._finished = true;
        };

        this._nextChunk = function () {
            this._checkIsFinished();
            if (queue.length) this.parseChunk(queue.shift());
            else paused = true;
        };

        this._streamData = bindThis(function (data) {
            try {
                queue.push(typeof data === 'string' ? data : data.toString(this._config.encoding));
                if (paused) { paused = false; this._checkIsFinished(); this.parseChunk(queue.shift()); }
            } catch (e) { this._streamError(e); }
        }, this);

        this._streamError = bindThis(function (e) { this._streamCleanUp(); this._sendError(e); }, this);
        this._streamEnd = bindThis(function () { this._streamCleanUp(); streamEnded = true; this._streamData(''); }, this);
        this._streamCleanUp = bindThis(function () {
            this._input.removeListener('data', this._streamData);
            this._input.removeListener('end', this._streamEnd);
            this._input.removeListener('error', this._streamError);
        }, this);
    }

    // ─── ParserHandle ────────────────────────────────────────────────────────────

    function ParserHandle(config) {
        var MAX_SAFE = Math.pow(2, 53);
        var MIN_SAFE = -MAX_SAFE;
        var NUM_REGEX = /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/;
        var ISO_DATE_REGEX = /^((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)))$/;
        var self = this;
        var rowNum = 0;
        var rowsProcessed = 0;
        var isPaused = false;
        var isAborted = false;
        var headerFields = [];
        var results = { data: [], errors: [], meta: {} };

        if (isFunction(config.step)) {
            var origStep = config.step;
            config.step = function (stepResults) {
                results = stepResults;
                if (needsHeader()) {
                    applyMeta();
                } else {
                    applyMeta();
                    if (results.data.length === 0) return;
                    rowNum += stepResults.data.length;
                    if (config.preview && rowNum > config.preview) {
                        parser.abort();
                    } else {
                        results.data = results.data[0];
                        origStep(results, self);
                    }
                }
            };
        }

        function isEmptyRow(row) {
            return config.skipEmptyLines === 'greedy'
                ? row.join('').trim() === ''
                : row.length === 1 && row[0].length === 0;
        }

        function applyMeta() {
            if (results && delimUndetectable) {
                addError('Delimiter', 'UndetectableDelimiter',
                    "Unable to auto-detect delimiting character; defaulted to '" + Papa.DefaultDelimiter + "'");
                delimUndetectable = false;
            }
            if (config.skipEmptyLines) {
                results.data = results.data.filter(function (row) { return !isEmptyRow(row); });
            }
            if (needsHeader()) {
                (function extractHeaders() {
                    if (!results) return;
                    if (Array.isArray(results.data[0])) {
                        for (var i = 0; needsHeader() && i < results.data.length; i++) {
                            results.data[i].forEach(addField);
                        }
                        results.data.splice(0, 1);
                    } else {
                        results.data.forEach(addField);
                    }

                    function addField(field, idx) {
                        if (isFunction(config.transformHeader)) field = config.transformHeader(field, idx);
                        headerFields.push(field);
                    }
                }());
            }
            (function applyTransformations() {
                if (!results || (!config.header && !config.dynamicTyping && !config.transform)) return results;

                function transformRow(row, rowIdx) {
                    var obj = config.header ? {} : [];
                    for (var i = 0; i < row.length; i++) {
                        var field = i;
                        var val = row[i];
                        if (config.header) {
                            field = i >= headerFields.length ? '__parsed_extra' : headerFields[i];
                        }
                        if (config.transform) val = config.transform(val, field);
                        val = castValue(field, val);
                        if (field === '__parsed_extra') {
                            obj[field] = obj[field] || [];
                            obj[field].push(val);
                        } else {
                            obj[field] = val;
                        }
                    }
                    if (config.header) {
                        if (i > headerFields.length) {
                            addError('FieldMismatch', 'TooManyFields',
                                'Too many fields: expected ' + headerFields.length + ' fields but parsed ' + i, rowsProcessed + rowIdx);
                        } else if (i < headerFields.length) {
                            addError('FieldMismatch', 'TooFewFields',
                                'Too few fields: expected ' + headerFields.length + ' fields but parsed ' + i, rowsProcessed + rowIdx);
                        }
                    }
                    return obj;
                }

                var numRows = 1;
                if (!results.data.length || Array.isArray(results.data[0])) {
                    results.data = results.data.map(transformRow);
                    numRows = results.data.length;
                } else {
                    results.data = transformRow(results.data, 0);
                }
                if (config.header && results.meta) results.meta.fields = headerFields;
                rowsProcessed += numRows;
                return results;
            }());
        }

        function needsHeader() {
            return config.header && headerFields.length === 0;
        }

        function castValue(field, value) {
            var f = field;
            if (config.dynamicTypingFunction && config.dynamicTyping[f] === undefined) {
                config.dynamicTyping[f] = config.dynamicTypingFunction(f);
            }
            if (config.dynamicTyping[f] === true || config.dynamicTyping === true) {
                if (value === 'true' || value === 'TRUE') return true;
                if (value === 'false' || value === 'FALSE') return false;
                if (NUM_REGEX.test(value)) {
                    var n = parseFloat(value);
                    if (MIN_SAFE < n && n < MAX_SAFE) return n;
                }
                if (ISO_DATE_REGEX.test(value)) return new Date(value);
                if (value === '') return null;
            }
            return value;
        }

        function addError(type, code, message, row) {
            var err = { type: type, code: code, message: message };
            if (row !== undefined) err.row = row;
            results.errors.push(err);
        }

        var inputStr, parser, delimUndetectable;

        this.parse = function (input, baseIndex, isChunk) {
            var quoteChar = config.quoteChar || '"';
            if (!config.newline) {
                config.newline = (function detectNewline(str, qChar) {
                    str = str.substring(0, 1048576);
                    var qRx = new RegExp(escapeRegExp(qChar) + '([^]*?)' + escapeRegExp(qChar), 'gm');
                    str = str.replace(qRx, '');
                    var crSplit = str.split('\r');
                    var lfSplit = str.split('\n');
                    var lfWins = lfSplit.length > 1 && lfSplit[0].length < crSplit[0].length;
                    if (crSplit.length === 1 || lfWins) return '\n';
                    var crlfCount = 0;
                    for (var i = 0; i < crSplit.length; i++) {
                        if (crSplit[i][0] === '\n') crlfCount++;
                    }
                    return crlfCount >= crSplit.length / 2 ? '\r\n' : '\r';
                }(input, quoteChar));
            }

            delimUndetectable = false;

            if (config.delimiter) {
                if (isFunction(config.delimiter)) {
                    config.delimiter = config.delimiter(input);
                    results.meta.delimiter = config.delimiter;
                }
            } else {
                var detected = (function guessDelimiter(str, newline, skipEmpty, comments, candidates) {
                    candidates = candidates || [',', '\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP];
                    var bestDelim, bestScore, bestCount;
                    for (var d = 0; d < candidates.length; d++) {
                        var delim = candidates[d], variance = 0, avgFields = 0, emptyRows = 0, prevCount;
                        var testResult = new CsvParser({ comments: comments, delimiter: delim, newline: newline, preview: 10 }).parse(str);
                        for (var r = 0; r < testResult.data.length; r++) {
                            if (skipEmpty && isEmptyRow(testResult.data[r])) { emptyRows++; continue; }
                            var fieldCount = testResult.data[r].length;
                            avgFields += fieldCount;
                            if (prevCount !== undefined && fieldCount > 0) {
                                variance += Math.abs(fieldCount - prevCount);
                                prevCount = fieldCount;
                            } else { prevCount = fieldCount; }
                        }
                        if (testResult.data.length > 0) avgFields /= (testResult.data.length - emptyRows);
                        if ((bestScore === undefined || variance <= bestScore) &&
                            (bestCount === undefined || bestCount < avgFields) &&
                            avgFields > 1.99) {
                            bestScore = variance;
                            bestDelim = delim;
                            bestCount = avgFields;
                        }
                    }
                    return { successful: !!(config.delimiter = bestDelim), bestDelimiter: bestDelim };
                }(input, config.newline, config.skipEmptyLines, config.comments, config.delimitersToGuess));

                if (detected.successful) {
                    config.delimiter = detected.bestDelimiter;
                } else {
                    delimUndetectable = true;
                    config.delimiter = Papa.DefaultDelimiter;
                }
                results.meta.delimiter = config.delimiter;
            }

            var parserConfig = cloneConfig(config);
            if (config.preview && config.header) parserConfig.preview++;
            inputStr = input;
            parser = new CsvParser(parserConfig);
            results = parser.parse(inputStr, baseIndex, isChunk);
            applyMeta();
            return isPaused ? { meta: { paused: true } } : results || { meta: { paused: false } };
        };

        this.paused = function () { return isPaused; };
        this.pause = function () {
            isPaused = true;
            parser.abort();
            inputStr = isFunction(config.chunk) ? '' : inputStr.substring(parser.getCharIndex());
        };
        this.resume = function () {
            if (self.streamer._halted) {
                isPaused = false;
                self.streamer.parseChunk(inputStr, true);
            } else {
                setTimeout(self.resume, 3);
            }
        };
        this.aborted = function () { return isAborted; };
        this.abort = function () {
            isAborted = true;
            parser.abort();
            results.meta.aborted = true;
            if (isFunction(config.complete)) config.complete(results);
            inputStr = '';
        };
    }

    function escapeRegExp(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    // ─── CsvParser ───────────────────────────────────────────────────────────────

    function CsvParser(config) {
        config = config || {};
        var delimiter = config.delimiter;
        var newline = config.newline;
        var comments = config.comments;
        var step = config.step;
        var preview = config.preview;
        var fastMode = config.fastMode;
        var quoteChar = config.quoteChar === undefined || config.quoteChar === null ? '"' : config.quoteChar;
        var escapeChar = quoteChar;
        if (config.escapeChar !== undefined) escapeChar = config.escapeChar;

        if (typeof delimiter !== 'string' || Papa.BAD_DELIMITERS.indexOf(delimiter) > -1) delimiter = ',';
        if (comments === delimiter) throw new Error('Comment character same as delimiter');
        if (comments === true) comments = '#';
        else if (typeof comments !== 'string' || Papa.BAD_DELIMITERS.indexOf(comments) > -1) comments = false;
        if (newline !== '\n' && newline !== '\r' && newline !== '\r\n') newline = '\n';

        var charIndex = 0, aborted = false;

        this.parse = function (input, baseIndex, isChunk) {
            if (typeof input !== 'string') throw new Error('Input must be a string');
            var inputLen = input.length;
            var delimLen = delimiter.length;
            var newlineLen = newline.length;
            var commentLen = comments.length;
            var hasStep = isFunction(step);
            var rows = [], errors = [], row = [];
            var cursor = charIndex = 0;

            if (!input) return finish();

            if (config.header && !baseIndex) {
                var firstLine = input.split(newline)[0].split(delimiter);
                var seen = {}, deduped = [], hadDupes = false;
                for (var h in firstLine) {
                    var rawField = firstLine[h];
                    if (isFunction(config.transformHeader)) rawField = config.transformHeader(rawField, h);
                    var unique = rawField;
                    var count = seen[rawField] || 0;
                    if (count > 0) { hadDupes = true; unique = rawField + '_' + count; }
                    seen[rawField] = count + 1;
                    while (deduped.includes(unique)) unique = unique + '_' + count;
                    deduped.push(unique);
                }
                if (hadDupes) {
                    var lines = input.split(newline);
                    lines[0] = deduped.join(delimiter);
                    input = lines.join(newline);
                }
            }

            if (fastMode || (fastMode !== false && input.indexOf(quoteChar) === -1)) {
                var rawLines = input.split(newline);
                for (var i = 0; i < rawLines.length; i++) {
                    row = rawLines[i];
                    charIndex += row.length;
                    if (i !== rawLines.length - 1) charIndex += newline.length;
                    else if (isChunk) return finish();
                    if (!comments || row.substring(0, commentLen) !== comments) {
                        if (hasStep) {
                            rows = [];
                            pushRow(row.split(delimiter));
                            doStep();
                            if (aborted) return finish();
                        } else {
                            pushRow(row.split(delimiter));
                        }
                        if (preview && preview <= i) return finish(true);
                    }
                }
                return finish();
            }

            var delimIdx = input.indexOf(delimiter, charIndex);
            var newlineIdx = input.indexOf(newline, charIndex);
            var escRx = new RegExp(escapeRegExp(escapeChar) + escapeRegExp(quoteChar), 'g');
            var quoteIdx = input.indexOf(quoteChar, charIndex);

            for (; ;) {
                if (input[charIndex] !== quoteChar) {
                    if (comments && row.length === 0 && input.substring(charIndex, charIndex + commentLen) === comments) {
                        if (newlineIdx === -1) return finish();
                        charIndex = newlineIdx + newlineLen;
                        newlineIdx = input.indexOf(newline, charIndex);
                        delimIdx = input.indexOf(delimiter, charIndex);
                        continue;
                    }
                    if (delimIdx !== -1 && (delimIdx < newlineIdx || newlineIdx === -1)) {
                        row.push(input.substring(charIndex, delimIdx));
                        charIndex = delimIdx + delimLen;
                        delimIdx = input.indexOf(delimiter, charIndex);
                    } else {
                        if (newlineIdx === -1) break;
                        row.push(input.substring(charIndex, newlineIdx));
                        nextLine(newlineIdx + newlineLen);
                        if (hasStep) { doStep(); if (aborted) return finish(); }
                        if (preview && rows.length >= preview) return finish(true);
                    }
                } else {
                    quoteIdx = charIndex;
                    charIndex++;
                    for (; ;) {
                        quoteIdx = input.indexOf(quoteChar, quoteIdx + 1);
                        if (quoteIdx === -1) {
                            if (!isChunk) errors.push({ type: 'Quotes', code: 'MissingQuotes', message: 'Quoted field unterminated', row: rows.length, index: charIndex });
                            return finishField();
                        }
                        if (quoteIdx === inputLen - 1) return finishField(input.substring(charIndex, quoteIdx).replace(escRx, quoteChar));
                        if (quoteChar !== escapeChar || input[quoteIdx + 1] !== quoteChar) {
                            if (quoteChar === escapeChar || quoteIdx === 0 || input[quoteIdx - 1] !== escapeChar) {
                                if (delimIdx !== -1 && delimIdx < quoteIdx + 1) delimIdx = input.indexOf(delimiter, quoteIdx + 1);
                                if (newlineIdx !== -1 && newlineIdx < quoteIdx + 1) newlineIdx = input.indexOf(newline, quoteIdx + 1);
                                var trailingWS = getTrailingWhitespace(delimIdx === -1 ? newlineIdx : Math.min(delimIdx, newlineIdx));
                                if (input.substr(quoteIdx + 1 + trailingWS, delimLen) === delimiter) {
                                    row.push(input.substring(charIndex, quoteIdx).replace(escRx, quoteChar));
                                    charIndex = quoteIdx + 1 + trailingWS + delimLen;
                                    if (input[charIndex] !== quoteChar) quoteIdx = input.indexOf(quoteChar, charIndex);
                                    delimIdx = input.indexOf(delimiter, charIndex);
                                    newlineIdx = input.indexOf(newline, charIndex);
                                    break;
                                }
                                var nlWS = getTrailingWhitespace(newlineIdx);
                                if (input.substring(quoteIdx + 1 + nlWS, quoteIdx + 1 + nlWS + newlineLen) === newline) {
                                    row.push(input.substring(charIndex, quoteIdx).replace(escRx, quoteChar));
                                    nextLine(quoteIdx + 1 + nlWS + newlineLen);
                                    delimIdx = input.indexOf(delimiter, charIndex);
                                    quoteIdx = input.indexOf(quoteChar, charIndex);
                                    if (hasStep) { doStep(); if (aborted) return finish(); }
                                    if (preview && rows.length >= preview) return finish(true);
                                    break;
                                }
                                errors.push({ type: 'Quotes', code: 'InvalidQuotes', message: 'Trailing quote on quoted field is malformed', row: rows.length, index: charIndex });
                                quoteIdx++;
                            }
                        } else { quoteIdx++; }
                    }
                }
            }
            return finishField();

            function pushRow(r) { rows.push(r); cursor = charIndex; }

            function getTrailingWhitespace(nextPos) {
                var len = 0;
                if (nextPos !== -1) {
                    var between = input.substring(quoteIdx + 1, nextPos);
                    if (between && between.trim() === '') len = between.length;
                }
                return len;
            }

            function finishField(val) {
                if (!isChunk) {
                    if (val === undefined) val = input.substring(charIndex);
                    row.push(val);
                    charIndex = inputLen;
                    pushRow(row);
                    if (hasStep) doStep();
                }
                return finish();
            }

            function nextLine(idx) {
                charIndex = idx;
                pushRow(row);
                row = [];
                newlineIdx = input.indexOf(newline, charIndex);
            }

            function finish(truncated) {
                return {
                    data: rows,
                    errors: errors,
                    meta: {
                        delimiter: delimiter,
                        linebreak: newline,
                        aborted: aborted,
                        truncated: !!truncated,
                        cursor: cursor + (baseIndex || 0)
                    }
                };
            }

            function doStep() {
                step(finish());
                rows = [];
                errors = [];
            }
        };

        this.abort = function () { aborted = true; };
        this.getCharIndex = function () { return charIndex; };
    }

    // ─── Worker messaging ────────────────────────────────────────────────────────

    function onWorkerMessage(e) {
        var msg = e.data;
        var worker = workerRegistry[msg.workerId];
        var abort = false;
        if (msg.error) {
            worker.userError(msg.error, msg.file);
        } else if (msg.results && msg.results.data) {
            var handle = {
                abort: function () {
                    abort = true;
                    finishWorker(msg.workerId, { data: [], errors: [], meta: { aborted: true } });
                },
                pause: notImplemented,
                resume: notImplemented
            };
            if (isFunction(worker.userStep)) {
                for (var i = 0; i < msg.results.data.length && !abort; i++) {
                    worker.userStep({ data: msg.results.data[i], errors: msg.results.errors, meta: msg.results.meta }, handle);
                }
                delete msg.results;
            } else if (isFunction(worker.userChunk)) {
                worker.userChunk(msg.results, handle, msg.file);
                delete msg.results;
            }
        }
        if (msg.finished && !abort) finishWorker(msg.workerId, msg.results);
    }

    function finishWorker(id, results) {
        var worker = workerRegistry[id];
        if (isFunction(worker.userComplete)) worker.userComplete(results);
        worker.terminate();
        delete workerRegistry[id];
    }

    function notImplemented() { throw new Error('Not implemented.'); }

    function cloneConfig(obj) {
        if (typeof obj !== 'object' || obj === null) return obj;
        var clone = Array.isArray(obj) ? [] : {};
        for (var key in obj) clone[key] = cloneConfig(obj[key]);
        return clone;
    }

    function bindThis(fn, ctx) {
        return function () { fn.apply(ctx, arguments); };
    }

    function isFunction(fn) { return typeof fn === 'function'; }

    if (IS_PAPA_WORKER) {
        self_.onmessage = function (e) {
            var msg = e.data;
            if (Papa.WORKER_ID === undefined && msg) Papa.WORKER_ID = msg.workerId;
            if (typeof msg.input === 'string') {
                self_.postMessage({ workerId: Papa.WORKER_ID, results: Papa.parse(msg.input, msg.config), finished: true });
            } else if ((self_.File && msg.input instanceof File) || msg.input instanceof Object) {
                var results = Papa.parse(msg.input, msg.config);
                if (results) self_.postMessage({ workerId: Papa.WORKER_ID, results: results, finished: true });
            }
        };
    }

    (NetworkStreamer.prototype = Object.create(BaseStreamer.prototype)).constructor = NetworkStreamer;
    (FileStreamer.prototype = Object.create(BaseStreamer.prototype)).constructor = FileStreamer;
    (StringStreamer.prototype = Object.create(StringStreamer.prototype)).constructor = StringStreamer;
    (ReadableStreamStreamer.prototype = Object.create(BaseStreamer.prototype)).constructor = ReadableStreamStreamer;

    return Papa;
}));


// =============================================================================
//  GSPR / Chinese Seller Chrome Extension – Main Content Script
//  (Deobfuscated & membership/credit gating removed)
// =============================================================================

// ─── Address-parsing helpers ─────────────────────────────────────────────────

/** Maps ISO-3166-1 alpha-2 codes → country names */
const COUNTRY_CODE_MAP = {
    'AD': 'Andorra', 'AE': 'United Arab Emirates', 'AF': 'Afghanistan',
    'AG': 'Antigua and Barbuda', 'AI': 'Anguilla', 'AL': 'Albania',
    'AM': 'Armenia', 'AO': 'Angola', 'AR': 'Argentina', 'AT': 'Austria',
    'AU': 'Australia', 'AZ': 'Azerbaijan', 'BA': 'Bosnia and Herzegovina',
    'BB': 'Barbados', 'BD': 'Bangladesh', 'BE': 'Belgium', 'BF': 'Burkina Faso',
    'BG': 'Bulgaria', 'BH': 'Bahrain', 'BI': 'Burundi', 'BJ': 'Benin',
    'BN': 'Brunei', 'BO': 'Bolivia', 'BR': 'Brazil', 'BS': 'Bahamas',
    'BT': 'Bhutan', 'BW': 'Botswana', 'BY': 'Belarus', 'BZ': 'Belize',
    'CA': 'Canada', 'CD': 'Democratic Republic of the Congo',
    'CF': 'Central African Republic', 'CG': 'Republic of the Congo',
    'CH': 'Switzerland', 'CI': "Côte d'Ivoire", 'CL': 'Chile',
    'CM': 'Cameroon', 'CN': 'China', 'CO': 'Colombia', 'CR': 'Costa Rica',
    'CU': 'Cuba', 'CV': 'Cabo Verde', 'CY': 'Cyprus', 'CZ': 'Czech Republic',
    'DE': 'Germany', 'DJ': 'Djibouti', 'DK': 'Denmark', 'DM': 'Dominica',
    'DO': 'Dominican Republic', 'DZ': 'Algeria', 'EC': 'Ecuador',
    'EE': 'Estonia', 'EG': 'Egypt', 'EH': 'Western Sahara', 'ER': 'Eritrea',
    'ES': 'Spain', 'ET': 'Ethiopia', 'FI': 'Finland', 'FJ': 'Fiji',
    'FM': 'Micronesia', 'FR': 'France', 'GA': 'Gabon', 'GB': 'United Kingdom',
    'GD': 'Grenada', 'GE': 'Georgia', 'GH': 'Ghana', 'GI': 'Gibraltar',
    'GL': 'Greenland', 'GM': 'Gambia', 'GN': 'Guinea', 'GQ': 'Equatorial Guinea',
    'GR': 'Greece', 'GT': 'Guatemala', 'GW': 'Guinea-Bissau', 'GY': 'Guyana',
    'HK': 'Hong Kong', 'HN': 'Honduras', 'HR': 'Croatia', 'HT': 'Haiti',
    'HU': 'Hungary', 'ID': 'Indonesia', 'IE': 'Ireland', 'IL': 'Israel',
    'IN': 'India', 'IQ': 'Iraq', 'IR': 'Iran', 'IS': 'Iceland', 'IT': 'Italy',
    'JM': 'Jamaica', 'JO': 'Jordan', 'JP': 'Japan', 'KE': 'Kenya',
    'KG': 'Kyrgyzstan', 'KH': 'Cambodia', 'KI': 'Kiribati', 'KM': 'Comoros',
    'KN': 'Saint Kitts and Nevis', 'KP': 'North Korea', 'KR': 'South Korea',
    'KW': 'Kuwait', 'KZ': 'Kazakhstan', 'LA': 'Laos', 'LB': 'Lebanon',
    'LC': 'Saint Lucia', 'LI': 'Liechtenstein', 'LK': 'Sri Lanka',
    'LR': 'Liberia', 'LS': 'Lesotho', 'LT': 'Lithuania', 'LU': 'Luxembourg',
    'LV': 'Latvia', 'LY': 'Libya', 'MA': 'Morocco', 'MC': 'Monaco',
    'MD': 'Moldova', 'ME': 'Montenegro', 'MG': 'Madagascar',
    'MH': 'Marshall Islands', 'MK': 'North Macedonia', 'ML': 'Mali',
    'MM': 'Myanmar', 'MN': 'Mongolia', 'MR': 'Mauritania', 'MT': 'Malta',
    'MU': 'Mauritius', 'MV': 'Maldives', 'MW': 'Malawi', 'MX': 'Mexico',
    'MY': 'Malaysia', 'MZ': 'Mozambique', 'NA': 'Namibia', 'NE': 'Niger',
    'NG': 'Nigeria', 'NI': 'Nicaragua', 'NL': 'Netherlands', 'NO': 'Norway',
    'NP': 'Nepal', 'NR': 'Nauru', 'NZ': 'New Zealand', 'OM': 'Oman',
    'PA': 'Panama', 'PE': 'Peru', 'PG': 'Papua New Guinea', 'PH': 'Philippines',
    'PK': 'Pakistan', 'PL': 'Poland', 'PT': 'Portugal', 'PW': 'Palau',
    'PY': 'Paraguay', 'QA': 'Qatar', 'RO': 'Romania', 'RS': 'Serbia',
    'RU': 'Russia', 'RW': 'Rwanda', 'SA': 'Saudi Arabia', 'SB': 'Solomon Islands',
    'SC': 'Seychelles', 'SD': 'Sudan', 'SE': 'Sweden', 'SG': 'Singapore',
    'SI': 'Slovenia', 'SK': 'Slovakia', 'SL': 'Sierra Leone', 'SM': 'San Marino',
    'SN': 'Senegal', 'SO': 'Somalia', 'SR': 'Suriname',
    'ST': 'São Tomé and Príncipe', 'SV': 'El Salvador', 'SY': 'Syria',
    'SZ': 'Eswatini', 'TD': 'Chad', 'TG': 'Togo', 'TH': 'Thailand',
    'TJ': 'Tajikistan', 'TL': 'Timor-Leste', 'TM': 'Turkmenistan',
    'TN': 'Tunisia', 'TO': 'Tonga', 'TR': 'Turkey', 'TT': 'Trinidad and Tobago',
    'TV': 'Tuvalu', 'TW': 'Taiwan', 'TZ': 'Tanzania', 'UA': 'Ukraine',
    'UG': 'Uganda', 'US': 'United States', 'UY': 'Uruguay', 'UZ': 'Uzbekistan',
    'VA': 'Vatican City', 'VC': 'Saint Vincent and the Grenadines',
    'VE': 'Venezuela', 'VN': 'Vietnam', 'VU': 'Vanuatu', 'WS': 'Samoa',
    'YE': 'Yemen', 'ZA': 'South Africa', 'ZM': 'Zambia', 'ZW': 'Zimbabwe'
};

const LEGAL_SUFFIXES = [
    'gmbh', 'ltd', 'co.', 'ag', 'inc', 'corp', 'corporation', 'limited',
    'llc', 'llp', 'plc', 'lp', 'lllp', 'ulc', 's.à r.l', 'sarl', 'srl',
    's.r.l.', 's.r.l', 's.l.', 's.l', 's.a.', 's.a', 'spa', 's.p.a', 'sa',
    'sas', 'gmbh & co. kg', 'kgaa', 'kg', 'bv', 'nv', 'oy', 'ab', 'as',
    'a/s', 'pte.', 'pvt.', 'pty.', 'pte', 'pvt', 'pty', 'kabushiki kaisha',
    'zrt', 'gongsi', 'limitada', 'sociedad limitada'
];

const isEmail = s => s.includes('@');
const isUrl = s => s.includes('http');
const isPhone = s => !isEmail(s) && !isUrl(s) && /^(\+?\d[\d\s\-()]+)$/.test(s);

/** Returns true if the line looks like a company name (has a legal suffix). */
function looksLikeCompany(line) {
    if (!line) return false;
    const words = line.toLowerCase().split(/[^a-z0-9.]+/).filter(Boolean);
    function stripTrailingPunct(w) { return w.replace(/[^a-z0-9.]+$/g, ''); }
    for (let word of words) {
        word = stripTrailingPunct(word);
        for (const suffix of LEGAL_SUFFIXES) {
            if (word === suffix.toLowerCase()) return true;
        }
    }
    return false;
}

/**
 * Finds an ISO country code in a space-separated string.
 * Checks for both the code itself (e.g. "DE") and the full name (e.g. "Germany").
 */
function findCountryCode(str) {
    const tokens = str.split(/\s+/)
        .map(t => t.replace(/[^A-Za-z]/g, ''))
        .filter(Boolean);
    for (const token of tokens) {
        const upper = token.toUpperCase();
        if (COUNTRY_CODE_MAP[upper]) return upper;
        const match = Object.entries(COUNTRY_CODE_MAP)
            .find(([, name]) => name.toUpperCase() === upper);
        if (match) return match[0];
    }
    return null;
}

/**
 * Primary address parser (international, works well for most EU/US formats).
 * Returns a structured address object.
 */
function parseAddressPrimary(rawText) {
    const lines = rawText.split('\n');
    let nonEmptyLines = [];
    for (const line of lines) {
        if (line.trim() === '') break;
        nonEmptyLines.push(line.trim());
    }
    const filtered = nonEmptyLines.filter(l => l !== '');

    const result = {
        name: null, country: null, address1: null, address2: null,
        city: null, postalCode: null, stateOrProvince: null,
        phoneNumber: null, emailAddress: null, website: null
    };

    if (filtered.length === 0) return result;

    // ── Detect country ──────────────────────────────────────────────────────
    let foundCountry = false, countryCode = null;
    for (const line of filtered) {
        if (!line.includes(',')) continue;
        const parts = line.split(',').map(p => p.trim()).filter(Boolean);
        for (const part of parts) {
            const code = findCountryCode(part);
            if (code) { foundCountry = true; countryCode = code; break; }
        }
        if (foundCountry) break;
    }
    if (!foundCountry) return result;
    result.country = countryCode;

    // ── Find location line (the line with city, state, country) ─────────────
    let locationLineIndex = -1;
    const candidates = [];
    for (let i = 0; i < filtered.length; i++) {
        const line = filtered[i];
        const parts = line.split(',').map(p => p.trim()).filter(Boolean);
        let hasCountry = false;
        for (const p of parts) { if (findCountryCode(p)) { hasCountry = true; break; } }
        if (hasCountry && parts.length >= 3) { locationLineIndex = i; break; }
        candidates.push(line);
    }

    // ── Determine company/person name ────────────────────────────────────────
    let name = candidates.find(looksLikeCompany);
    if (!name) {
        name = candidates.find(line => {
            const words = line.split(/\s+/);
            let titleCaseCount = 0;
            for (const w of words) { if (/^[A-Z][a-z]/.test(w)) titleCaseCount++; }
            return titleCaseCount >= 2;
        }) || (candidates.length > 0 ? candidates[0] : null);
    }
    if (!name && filtered.length > 0) name = filtered[0];
    result.name = name || null;

    const nameIdx = candidates.indexOf(result.name);
    const afterName = nameIdx !== -1 ? candidates.slice(nameIdx + 1) : candidates;

    if (afterName.length > 0) {
        result.address1 = afterName[0];
    } else {
        const ni = filtered.indexOf(result.name);
        if (ni !== -1 && ni + 1 < filtered.length) result.address1 = filtered[ni + 1];
    }

    if (afterName.length > 1) {
        const addr2 = afterName.slice(1).find(l => !isEmail(l) && !isUrl(l));
        if (addr2) result.address2 = addr2;
    }

    // ── Parse city / postal / state from location line ───────────────────────
    let city = null, postalCode = null, stateOrProvince = null;
    if (locationLineIndex !== -1) {
        const locParts = filtered[locationLineIndex].split(',').map(p => p.trim()).filter(Boolean);
        let countryPartIdx = -1;
        for (let i = locParts.length - 1; i >= 0; i--) {
            if (findCountryCode(locParts[i])) { countryPartIdx = i; break; }
        }
        if (countryPartIdx !== -1) {
            const before = locParts.slice(0, countryPartIdx);
            const after = locParts.slice(countryPartIdx + 1);
            if (after.length > 0 && /\d/.test(after[0])) postalCode = after[0];
            if (before.length > 2) { city = before.slice(0, -1).join(', '); stateOrProvince = before[before.length - 1]; }
            else if (before.length === 2) { city = before[0]; stateOrProvince = before[1]; }
            else if (before.length === 1) { city = before[0]; }
        }
    }

    result.city = city || result.country;
    result.postalCode = postalCode || result.country;
    result.stateOrProvince = stateOrProvince || result.country;
    if (result.postalCode && result.postalCode.length > 9) {
        result.postalCode = result.postalCode.substring(0, 9);
    }

    // ── Extract contact fields ───────────────────────────────────────────────
    for (const line of filtered) {
        if (!result.emailAddress && isEmail(line)) result.emailAddress = line;
        if (!result.website && isUrl(line)) result.website = line;
        if (!result.phoneNumber && isPhone(line)) result.phoneNumber = line;
        if (result.emailAddress && result.website && result.phoneNumber) break;
    }

    // ── Fix overly long city (fallback) ──────────────────────────────────────
    try {
        if (result.city && result.city.length > 50) {
            const fallback = filtered.find(l => l.includes(result.country + ','));
            if (fallback) result.city = fallback.split(',')[0].trim();
        }
    } catch (e) { console.error('Error processing city:', e); }

    try {
        if (result.city && result.city.length <= 2) {
            const phoneIdx = filtered.indexOf(result.phoneNumber);
            const emailIdx = filtered.indexOf(result.emailAddress);
            const prev = phoneIdx > -1 ? filtered[phoneIdx - 1]
                : emailIdx > -1 ? filtered[emailIdx - 1] : null;
            if (prev) {
                const parts = prev.split(',');
                if (parts.length > 0) {
                    result.city = parts[0].trim();
                    result.country = parts[1].trim();
                    result.postalCode = parts[parts.length - 1].trim();
                }
            }
        }
    } catch (e) { /* silent */ }

    return result;
}

/**
 * Secondary address parser – specialised for DE/ES/AT/CH/GB/US formats.
 * Falls back gracefully when primary heuristics fail.
 */
function parseAddressSecondary(rawText) {
    const normalise = s => s.replace(/\s+/g, ' ').trim();
    const isEmailAddr = s => /@/.test(s);
    const isHttpUrl = s => /https?:\/\//i.test(s);
    const isPhoneNum = s => /^\+?\d[\d\s\-()]{5,}$/.test(s);
    const COMPANY_SUFFIX_RX = /\b(S\.?L\.?U?\.?|S\.?A\.?|GMBH|AG|LLC|INC)\b\.?/gi;

    const KNOWN_CITIES = {
        'frankfurt': { c: 'DE', s: 'HE' }, 'hamburg': { c: 'DE', s: 'HH' },
        'berlin': { c: 'DE', s: 'BE' }, 'munich': { c: 'DE', s: 'BY' },
        'stuttgart': { c: 'DE', s: 'BW' }, 'leipzig': { c: 'DE', s: 'SN' },
        'madrid': { c: 'ES', s: 'M' }, 'barcelona': { c: 'ES', s: 'B' },
        'fuenlabrada': { c: 'ES', s: 'M' }, 'valencia': { c: 'ES', s: 'V' },
        'sevilla': { c: 'ES', s: 'SE' }, 'bilbao': { c: 'ES', s: 'BI' },
        'vienna': { c: 'AT', s: 'W' }, 'graz': { c: 'AT', s: 'ST' },
        'zurich': { c: 'CH', s: 'ZH' }
    };

    const POSTAL_PATTERNS = [
        {
            rx: /^\d{5}$/,
            handler: pc => {
                const n = +pc;
                if (n >= 60000 && n <= 69999) return { c: 'DE', s: 'HE' };
                if (n >= 10000 && n <= 19999) return { c: 'DE', s: 'SN' };
                if (n >= 20000 && n <= 29999) return { c: 'DE', s: 'SH' };
                return { c: 'DE' };
            }
        },
        {
            rx: /^\d{5}$/,
            handler: pc => {
                const stateMap = { 28: 'M', '08': 'B', 46: 'V', 41: 'SE', 48: 'BI' };
                const s = stateMap[pc.slice(0, 2)];
                return s ? { c: 'ES', s } : { c: 'ES' };
            }
        },
        {
            rx: /^\d{4}$/,
            handler: pc => {
                const n = +pc;
                if (n >= 100 && n <= 1999) return { c: 'AT', s: 'W' };
                if (n >= 8000 && n <= 8999) return { c: 'CH', s: 'ZH' };
                return null;
            }
        },
        { rx: /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i, handler: () => ({ c: 'GB' }) },
        { rx: /^\d{5}(?:-\d{4})?$/, handler: () => ({ c: 'US' }) }
    ];

    const WORD_HINTS = [
        { word: /straße|str\./i, c: 'DE' },
        { word: /sociedad limitada|s\.l\./i, c: 'ES' }
    ];

    const COUNTRY_NAMES = {
        'ES': 'spain', 'DE': 'germany', 'AT': 'austria',
        'CH': 'switzerland', 'FR': 'france', 'GB': 'united kingdom'
    };

    const lines = rawText.split('\n')
        .map(l => l.replace(/\s+/g, ' ').trim())
        .filter(Boolean);

    const contacts = { email: null, phone: null, url: null };
    lines.forEach(l => {
        if (!contacts.email && isEmailAddr(l)) contacts.email = l;
        else if (!contacts.url && isHttpUrl(l)) contacts.url = l;
        else if (!contacts.phone && isPhoneNum(l)) contacts.phone = l;
    });

    const addressLines = lines.filter(l => !(isEmailAddr(l) || isHttpUrl(l) || isPhoneNum(l)));
    const stripped = addressLines.map(l => l.replace(COMPANY_SUFFIX_RX, '').trim());

    let loc = { c: null, s: null, city: null, postal: null };

    // Try country name match
    for (const l of stripped) {
        for (const [code, name] of Object.entries(COUNTRY_NAMES)) {
            if (l.toLowerCase().endsWith(name)) { loc.c = code; break; }
        }
        if (loc.c) break;
    }

    // Try known city match
    if (!loc.c) {
        outer: for (const l of stripped) {
            const norm = normaliseForComparison(l);
            for (const [city, info] of Object.entries(KNOWN_CITIES)) {
                if (norm.includes(city)) {
                    loc = { ...loc, ...info, city: info.display || city };
                    break outer;
                }
            }
        }
    }

    // Try postal code patterns
    if (!loc.c) {
        for (const l of stripped) {
            for (const { rx, handler } of POSTAL_PATTERNS) {
                const m = l.match(rx);
                if (m) {
                    const found = handler(m[0]);
                    if (found) { loc = { ...loc, ...found, postal: m[0] }; break; }
                }
            }
            if (loc.c) break;
        }
    }

    // Try word hints
    if (!loc.c) {
        for (const { word, c } of WORD_HINTS) {
            if (stripped.some(l => word.test(l))) { loc.c = c; break; }
        }
    }

    const result = {
        name: null, country: loc.c || null, stateOrProvince: loc.s || null,
        city: loc.city ? capitalize(loc.city) : null,
        postalCode: loc.postal || null,
        address1: null, address2: null,
        phoneNumber: contacts.phone,
        emailAddress: contacts.email,
        website: contacts.url
    };

    if (!addressLines.length) return result;
    result.name = addressLines[0];

    const rest = addressLines.slice(1);
    if (!rest.length) return result;

    let locationIdx = rest.findIndex(l =>
        (result.postalCode && l.includes(result.postalCode)) ||
        (loc.city && l.toLowerCase().includes(loc.city.toLowerCase()))
    );
    if (locationIdx === -1) locationIdx = rest.length - 1;

    const locationLine = rest[locationIdx];
    const beforeLocLine = rest.slice(0, locationIdx).join(', ');
    const locParts = locationLine.split(',').map(p => p.trim()).filter(Boolean);

    let postalIdx = locParts.findIndex(p => /\d{4,6}/.test(p));
    if (postalIdx > -1) {
        const beforePostal = locParts.slice(0, postalIdx);
        const afterPostal = locParts.slice(postalIdx + 1);
        const postalMatch = locParts[postalIdx].match(/(\d{4,6})/)[1];
        result.postalCode = result.postalCode || postalMatch;

        if (!result.country && afterPostal.length) {
            const last = afterPostal[afterPostal.length - 1].toLowerCase();
            if (/spain|españa/.test(last)) result.country = 'ES';
            else if (/germany|deutschland/.test(last)) result.country = 'DE';
            else if (/austria|österreich/.test(last)) result.country = 'AT';
        }
        if (!result.city && beforePostal.length) result.city = beforePostal.pop();
        if (!result.stateOrProvince && beforePostal.length) result.stateOrProvince = beforePostal.pop();
        result.address1 = beforeLocLine || locParts.slice(0, postalIdx - (result.stateOrProvince ? 1 : 0) - (result.city ? 1 : 0)).join(', ');
    } else {
        result.city = result.city || locationLine;
        result.address1 = beforeLocLine || '';
    }

    if (!result.address1 || !result.address1.trim()) {
        result.address1 = beforeLocLine || locParts.slice(0, postalIdx).join(', ');
    }
    return result;
}

/** Strips accents & whitespace for loose city matching. */
function normaliseForComparison(str) {
    return str.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[\s\-]/g, '');
}

function capitalize(str) {
    return str.replace(/\b\w/g, c => c.toUpperCase());
}

/**
 * Top-level address parser: tries primary parser first; if it doesn't return
 * both country and city, falls back to the secondary parser.
 */
function parseAddress(rawText) {
    const primary = parseAddressPrimary(rawText);
    return (primary && primary.country && primary.city) ? primary : parseAddressSecondary(rawText);
}

/**
 * Chinese-address / mixed-format parser.
 * Handles Chinese administrative hierarchy and Western fallback.
 */
function parseAddressChinese(rawText) {
    const normaliseWhitespace = s => s.replace(/[，。]/g, ',').replace(/\s+/g, ' ').trim();
    const isEmailAddr = s => /^[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}$/i.test(s);
    const isPhoneNum = s => /^\+?\d[\d\s\-()]{5,}$/.test(s);

    const SUPPORTED_COUNTRIES = { 'CN': 'China', 'DE': 'Germany', 'ES': 'Spain', 'US': 'United States' };
    const NAME_TO_CODE = Object.fromEntries(
        Object.entries(SUPPORTED_COUNTRIES).map(([code, name]) => [name.toLowerCase(), code])
    );
    const resolveCountry = raw => {
        raw = raw.replace(/[^A-Za-z]/g, '').toLowerCase();
        if (SUPPORTED_COUNTRIES[raw.toUpperCase()]) return raw.toUpperCase();
        return NAME_TO_CODE[raw] || null;
    };

    const CN_PROVINCES = [
        'anhui', 'beijing', 'chongqing', 'fujian', 'gansu', 'guangdong', 'guangxi',
        'guizhou', 'hainan', 'hebei', 'heilongjiang', 'henan', 'hubei', 'hunan',
        'jiangsu', 'jiangxi', 'jilin', 'liaoning', 'inner mongolia', 'neimenggu',
        'ningxia', 'qinghai', 'shaanxi', 'shandong', 'shanghai', 'shanxi', 'sichuan',
        'tianjin', 'xinjiang', 'tibet', 'xizang', 'yunnan', 'zhejiang',
        'hong kong', 'macau', 'taiwan'
    ];

    const rawLines = rawText.replace(/<[^>]*>/g, '')
        .split(/[\r\n]+/)
        .flatMap(l => l.split(/·|•|\|/))
        .map(normaliseWhitespace)
        .filter(Boolean);

    const result = {
        name: null, address1: null, address2: null, city: null,
        stateOrProvince: null, postalCode: null, country: null,
        emailAddress: null, phoneNumber: null
    };

    // Extract and remove contact lines
    for (const line of [...rawLines]) {
        if (!result.emailAddress && isEmailAddr(line)) {
            result.emailAddress = line;
            rawLines.splice(rawLines.indexOf(line), 1);
        } else if (!result.phoneNumber && isPhoneNum(line)) {
            result.phoneNumber = line;
            rawLines.splice(rawLines.indexOf(line), 1);
        }
    }

    if (rawLines.length) result.name = rawLines.shift();

    const joined = rawLines.join(', ');

    // Try Chinese administrative hierarchy
    let cnMatch = joined.match(/(.+?),\s*([\w\s]+?)\s+District,\s*([\w\s]+?)\s+City,\s*([\w\s]+?)\s+Province/i);
    if (!cnMatch) {
        cnMatch = joined.match(/(.+?),\s*district,\s*([\w\s]+?)(?:\s+City)?,\s*([\w\s]+?)(?:\s+Province)?,\s*China/i);
        if (cnMatch) cnMatch = [cnMatch[0], cnMatch[1], null, cnMatch[2], cnMatch[3]];
    }
    if (cnMatch) {
        result.address1 = normaliseWhitespace(cnMatch[1]).replace(/,\s*$/, '').replace(/\bDistrict$/i, 'District');
        result.city = normaliseWhitespace(cnMatch[3] || cnMatch[2] || '');
        result.stateOrProvince = normaliseWhitespace(cnMatch[4] || '');
        result.country = 'CN';
        return result;
    }

    // Country detection
    const tokens = joined.split(/[\s,]+/).filter(Boolean);
    for (let i = tokens.length - 1; i >= 0; i--) {
        const code = resolveCountry(tokens[i]);
        if (code) { result.country = code; break; }
    }

    // Postal code
    const postalMatch = joined.match(/\b\d{4,6}\b/);
    if (postalMatch) result.postalCode = postalMatch[0];

    // Split on last comma
    const lastComma = joined.lastIndexOf(',');
    if (lastComma !== -1) {
        result.address1 = joined.slice(0, lastComma).trim();
        const tail = joined.slice(lastComma + 1).trim().split(',');
        result.city = tail[0].trim();
        if (tail[1]) result.stateOrProvince = tail[1].trim();
    } else {
        result.address1 = joined;
    }

    // Chinese province fallback
    if (!result.country && result.stateOrProvince &&
        CN_PROVINCES.includes(result.stateOrProvince.toLowerCase().trim())) {
        result.country = 'CN';
    }

    return result;
}

// =============================================================================
//  MEMBERSHIP GATE
//  Plans with access: Trial (active), Novice, Professional, Prestige.
//  Same three-layer pattern used across all gated pages:
//    _trackerUid absence → logged out, reject immediately.
//    background checkMembership → { allowed } boolean.
//    _cachedPlan → fallback when background auth is unavailable.
// =============================================================================

const CSV_TRACKER_ALLOWED_PLANS = ['trial', 'novice', 'professional', 'prestige'];

async function verifyCsvTrackerAccess() {
    const { _trackerUid } = await chrome.storage.local.get('_trackerUid');
    if (!_trackerUid) {
        showCsvTrackerGateModal('none', null);
        return false;
    }

    const result = await new Promise(resolve => {
        chrome.runtime.sendMessage({ type: 'checkMembership' }, res => {
            void chrome.runtime.lastError;
            resolve(res || null);
        });
    });

    if (result?.allowed === true) return true;

    const { _cachedPlan } = await chrome.storage.local.get('_cachedPlan');
    if (_cachedPlan && CSV_TRACKER_ALLOWED_PLANS.includes(_cachedPlan.toLowerCase().trim())) {
        return true;
    }

    showCsvTrackerGateModal(result?.membership ?? null, _cachedPlan ?? null);
    return false;
}

function showCsvTrackerGateModal(membership, cachedPlan) {
    const existing = document.getElementById('ss-csv-tracker-gate-modal');
    if (existing) existing.remove();

    let bodyText;
    if (membership === 'expired') {
        bodyText = 'Your membership has <strong style="color:#f87171">expired</strong>. Please renew to continue using the CSV Tracker.';
    } else if (cachedPlan && !CSV_TRACKER_ALLOWED_PLANS.includes(cachedPlan.toLowerCase().trim())) {
        bodyText = `Your current plan <strong style="color:#f87171">${cachedPlan}</strong> does not have CSV Tracker access.`;
    } else {
        bodyText = 'No active membership was found. Please log in through the website.';
    }

    const overlay = document.createElement('div');
    overlay.id = 'ss-csv-tracker-gate-modal';
    overlay.style.cssText = [
        'position:fixed', 'inset:0', 'z-index:99999',
        'background:rgba(0,0,0,0.65)', 'backdrop-filter:blur(4px)',
        'display:flex', 'align-items:center', 'justify-content:center',
    ].join(';');

    overlay.innerHTML = `
        <div style="background:#0f1624;border:1px solid #2a3a5c;border-radius:16px;
                    padding:44px 48px;max-width:480px;width:90%;text-align:center;
                    box-shadow:0 12px 60px rgba(0,0,0,0.7);">
            <div style="font-size:52px;margin-bottom:18px;">\u{1F512}</div>
            <h2 style="color:#e2e8f0;font-size:22px;margin:0 0 12px 0;font-weight:700;">Access Restricted</h2>
            <p style="color:#94a3b8;font-size:15px;margin:0 0 20px 0;line-height:1.7;">
                ${bodyText}
            </p>
            <p style="color:#64748b;font-size:13px;margin:0 0 28px 0;line-height:1.6;">
                Eligible plans:<br>
                Trial &nbsp;\u00b7&nbsp; Novice &nbsp;\u00b7&nbsp; Professional &nbsp;\u00b7&nbsp; Prestige
            </p>
            <button id="ss-csv-gate-close"
                style="background:#1e3a5f;color:#e2e8f0;border:1px solid #2a4a7f;
                       border-radius:8px;padding:12px 36px;cursor:pointer;font-size:15px;font-weight:600;">
                Close
            </button>
        </div>`;

    document.body.appendChild(overlay);
    document.getElementById('ss-csv-gate-close').addEventListener('click', () => overlay.remove());
    overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
}

// =============================================================================
//  Extension UI logic
// =============================================================================

let showUnprocessedOnly = false;
const showUnprocessedCheckbox = document.getElementById('showUnprocessedOnly');
showUnprocessedCheckbox.addEventListener('change', e => {
    showUnprocessedOnly = e.target.checked;
    renderTable();
});

/** Defines available bulk actions and which CSV columns they populate. */
const ACTION_CONFIGS = {
    gspr: {
        label: 'Get GSPR',
        dataColumns: [
            'Manufacturer Name', 'Manufacturer AddressLine1', 'Manufacturer AddressLine2',
            'Manufacturer City', 'Manufacturer Country', 'Manufacturer PostalCode',
            'Manufacturer StateOrProvince', 'Manufacturer Phone', 'Manufacturer Email',
            'Responsible Person 1', 'Responsible Person 1 Type',
            'Responsible Person 1 AddressLine1', 'Responsible Person 1 AddressLine2',
            'Responsible Person 1 City', 'Responsible Person 1 Country',
            'Responsible Person 1 PostalCode', 'Responsible Person 1 StateOrProvince',
            'Responsible Person 1 Phone', 'Responsible Person 1 Email',
            'Manufacturer Info Text', 'Responsible Person EU Text'
        ],
        scannedColumn: 'scannedGSPR'
    },
    chineseSeller: {
        label: 'Check Chinese Seller',
        dataColumns: ['Chinese Seller'],
        scannedColumn: 'scannedChinese'
    }
};

let debugMode = false;
let rows = [];        // parsed CSV rows
let selectedHeaders = [];        // columns chosen by the user
let appliedActions = {};        // { actionKey: true/false }
let processedCount = 0;
let concurrency = 1;
let isProcessing = false;
let tableColumns = [];
let currentLineIndex = 0;

const SHOW_ROW_NUMBERS = true;

// ─── DOM references ──────────────────────────────────────────────────────────
const csvUploadEl = document.getElementById('csvUpload');
const headerSelectionEl = document.getElementById('headerSelectionContainer');
const headerCheckboxesEl = document.getElementById('headerCheckboxes');
const confirmHeadersBtn = document.getElementById('confirmHeadersBtn');
const mainSectionEl = document.getElementById('mainSection');
const dataTableEl = document.getElementById('dataTable');
const downloadCsvBtn = document.getElementById('downloadCsvBtn');
const concurrencyInput = document.getElementById('concurrencyInput');
const processedCountEl = document.getElementById('processedCount');
const totalCountEl = document.getElementById('totalCount');
const statusMessageEl = document.getElementById('statusMessage');
const restoreBtn = document.getElementById('restoreFromStorageBtn');
const clearDataBtn = document.getElementById('clearDataBtn');
const runActionsBtn = document.getElementById('runActionsBtn');
const actionCheckboxesEl = document.getElementById('actionCheckboxes');
const startLineInput = document.getElementById('startLineInput');

// ─── Event listeners ─────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', onDOMReady);
csvUploadEl.addEventListener('change', onCsvUpload);
confirmHeadersBtn.addEventListener('click', onConfirmHeaders);
downloadCsvBtn.addEventListener('click', onDownloadCsv);
restoreBtn.addEventListener('click', onRestoreFromStorage);
clearDataBtn.addEventListener('click', onClearData);
runActionsBtn.addEventListener('click', onRunActions);
concurrencyInput.addEventListener('change', e => {
    concurrency = parseInt(e.target.value, 10) || 1;
});

// ─── Initialisation ──────────────────────────────────────────────────────────
async function onDOMReady() {
    await loadFromStorage();
    if (rows.length > 0 && selectedHeaders.length > 0) {
        buildActionCheckboxes();
        renderTable();
        mainSectionEl.style.display = 'block';
        if (startLineInput) startLineInput.value = currentLineIndex;
    }
    document.getElementById('debugModeSwitch').addEventListener('change', e => {
        debugMode = e.target.checked;
        console.log('Debug mode is now', debugMode ? 'ON' : 'OFF');
    });
    buildFilterDropdown('scannedGSPR');

    // ── Membership gate on open ────────────────────────
    verifyCsvTrackerAccess();
    // ────────────────────────────────────────────────
}

// ─── CSV upload ──────────────────────────────────────────────────────────────
async function onCsvUpload(e) {
    const file = e.target.files[0];
    if (!file) return;
    Papa.parse(file, {
        header: true,
        skipEmptyLines: true,
        dynamicTyping: false,
        complete: async result => {
            const data = result.data;
            if (data.length === 0) { alert('CSV appears to be empty or invalid.'); return; }

            rows = data;
            // Coerce scanned* columns to booleans
            rows.forEach(row => {
                for (const key in row) {
                    if (key.startsWith('scanned')) {
                        const v = (row[key] || '').trim().toLowerCase();
                        row[key] = v === 'true';
                    }
                }
            });

            const allColumns = Object.keys(rows[0]);
            appliedActions = {};
            for (const [actionKey, cfg] of Object.entries(ACTION_CONFIGS)) {
                appliedActions[actionKey] = allColumns.includes(cfg.scannedColumn);
            }

            const actionColumns = Object.values(ACTION_CONFIGS)
                .flatMap(cfg => [cfg.scannedColumn, ...cfg.dataColumns]);
            selectedHeaders = allColumns.filter(c => !actionColumns.includes(c));

            if (selectedHeaders.length === 0 || mainSectionEl.style.display === 'none') {
                showHeaderSelectionUI();
            } else {
                mainSectionEl.style.display = 'block';
                buildActionCheckboxes();
                renderTable();
                await saveToStorage();
            }
        },
        error: err => {
            console.error('Error parsing CSV:', err);
            alert('Error parsing CSV. Check the browser console for details.');
        }
    });
}

function showHeaderSelectionUI() {
    headerSelectionEl.style.display = 'block';
    headerCheckboxesEl.innerHTML = '';
    Object.keys(rows[0]).forEach(col => {
        const div = document.createElement('div');
        div.innerHTML = `<label><input type="checkbox" value="${col}" checked> ${col}</label>`;
        headerCheckboxesEl.appendChild(div);
    });
}

async function onConfirmHeaders() {
    const checkboxes = headerCheckboxesEl.querySelectorAll('input[type="checkbox"]');
    selectedHeaders = Array.from(checkboxes).filter(c => c.checked).map(c => c.value);
    if (selectedHeaders.length === 0) { alert('Please select at least one header.'); return; }

    rows = rows.map(row => {
        const filtered = {};
        for (const col of selectedHeaders) {
            if (row.hasOwnProperty(col)) filtered[col] = row[col];
        }
        return filtered;
    });

    appliedActions = {};
    headerSelectionEl.style.display = 'none';
    mainSectionEl.style.display = 'block';
    buildActionCheckboxes();
    renderTable();
    await saveToStorage();
}

// ─── Action checkboxes ───────────────────────────────────────────────────────
function buildActionCheckboxes() {
    actionCheckboxesEl.innerHTML = '';
    for (const [key, cfg] of Object.entries(ACTION_CONFIGS)) {
        const checked = appliedActions[key] ? 'checked' : '';
        const div = document.createElement('div');
        div.innerHTML = `<label><input type="checkbox" class="actionCheck" value="${key}" ${checked}> ${cfg.label}</label>`;
        actionCheckboxesEl.appendChild(div);
    }
}

/** Returns true if every selected action has been completed for this row. */
function isRowFullyProcessed(row) {
    const active = Object.keys(appliedActions).filter(k => appliedActions[k]);
    if (active.length === 0) return false;
    return active.every(k => row[ACTION_CONFIGS[k].scannedColumn] === true);
}

// ─── Table rendering ─────────────────────────────────────────────────────────
function renderTable() {
    dataTableEl.innerHTML = '';

    let columns = [];
    if (SHOW_ROW_NUMBERS) columns.push('Row');
    columns.push(...selectedHeaders);
    for (const [key, active] of Object.entries(appliedActions)) {
        if (active) {
            const cfg = ACTION_CONFIGS[key];
            cfg.dataColumns.forEach(c => { if (!columns.includes(c)) columns.push(c); });
            if (!columns.includes(cfg.scannedColumn)) columns.push(cfg.scannedColumn);
        }
    }
    tableColumns = columns;

    // Header row
    const thead = document.createElement('thead');
    const tr = document.createElement('tr');
    columns.forEach(col => {
        const th = document.createElement('th');
        th.textContent = col;
        if (col.startsWith('scanned')) th.classList.add('scanned-header');
        tr.appendChild(th);
    });
    thead.appendChild(tr);
    dataTableEl.appendChild(thead);

    // Body
    const tbody = document.createElement('tbody');
    let displayed = rows;
    if (showUnprocessedOnly) displayed = displayed.filter(r => !isRowFullyProcessed(r));

    displayed.forEach((row, idx) => {
        const tr = document.createElement('tr');
        tableColumns.forEach(col => {
            const td = document.createElement('td');
            let val = '';
            if (col === 'Row') val = (idx + 1).toString();
            else if (col.startsWith('scanned')) val = row[col] === true ? '✓' : '';
            else val = row[col] ?? '';
            td.textContent = val;
            tr.appendChild(td);
        });
        if (isRowFullyProcessed(row)) tr.classList.add('row-done');
        tbody.appendChild(tr);
    });
    dataTableEl.appendChild(tbody);
    updateCounters();
}

function rowNeedsProcessing(row) {
    const active = Object.keys(appliedActions).filter(k => appliedActions[k]);
    if (active.length === 0) return false;
    return !active.every(k => row[ACTION_CONFIGS[k].scannedColumn] === true);
}

// ─── Storage ─────────────────────────────────────────────────────────────────
async function onRestoreFromStorage() {
    await loadFromStorage();
    if (rows.length === 0) { alert('No saved data found in storage.'); return; }
    if (selectedHeaders.length === 0) { showHeaderSelectionUI(); return; }
    buildActionCheckboxes();
    renderTable();
    mainSectionEl.style.display = 'block';
}

async function onClearData() {
    rows = []; selectedHeaders = []; appliedActions = {};
    processedCount = 0; isProcessing = false;
    dataTableEl.innerHTML = '';
    mainSectionEl.style.display = 'none';
    headerSelectionEl.style.display = 'none';
    await chrome.storage.local.remove(['originalData', 'selectedHeaders', 'appliedActions', 'currentLineIndex']);
    csvUploadEl.value = '';
    statusMessageEl.textContent = '';
    processedCountEl.textContent = '0';
    totalCountEl.textContent = '0';
    alert('CSV data cleared.');
}

// ─── CSV export ──────────────────────────────────────────────────────────────
function onDownloadCsv() {
    const filterEl = document.getElementById('scannedGSPRFilter');
    let filteredRows = [...rows];
    if (filterEl) {
        const val = filterEl.value;
        if (val !== 'all') {
            const target = val === 'true' ? true : val === 'false' ? false : undefined;
            filteredRows = filteredRows.filter(r =>
                (r.scannedGSPR === undefined ? undefined : r.scannedGSPR) === target
            );
        }
    }
    const csv = serializeRowsToCsv(filteredRows);
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = 'exported_data.csv'; a.click();
    URL.revokeObjectURL(url);
}

function serializeRowsToCsv(data) {
    let allColumns = [...selectedHeaders];
    for (const [key, active] of Object.entries(appliedActions)) {
        if (active) {
            const cfg = ACTION_CONFIGS[key];
            cfg.dataColumns.forEach(c => { if (!allColumns.includes(c)) allColumns.push(c); });
            if (!allColumns.includes(cfg.scannedColumn)) allColumns.push(cfg.scannedColumn);
        }
    }
    const serialised = data.map(row => {
        const copy = { ...row };
        allColumns.forEach(col => {
            if (col.startsWith('scanned')) copy[col] = copy[col] === true ? 'true' : 'false';
        });
        return copy;
    });
    return Papa.unparse({ fields: allColumns, data: serialised }, { quotes: true, delimiter: ',', newline: '\n' });
}

// ─── Chrome storage helpers ──────────────────────────────────────────────────
async function saveToStorage() {
    await chrome.storage.local.set({
        originalData: rows,
        selectedHeaders,
        appliedActions,
        currentLineIndex
    });
}

async function loadFromStorage() {
    const stored = await chrome.storage.local.get(['originalData', 'selectedHeaders', 'appliedActions', 'currentLineIndex']);
    if (stored.originalData && stored.selectedHeaders) {
        rows = stored.originalData;
        selectedHeaders = stored.selectedHeaders;
        appliedActions = stored.appliedActions || {};
    }
    if (typeof stored.currentLineIndex === 'number') currentLineIndex = stored.currentLineIndex;
}

// ─── Counters / title ────────────────────────────────────────────────────────
function updateCounters() {
    processedCount = countProcessedRows();
    const total = rows.length;
    const unproc = total - processedCount;
    processedCountEl.textContent = processedCount;
    totalCountEl.textContent = total;
    const unprocessedEl = document.getElementById('unprocessedCount');
    if (unprocessedEl) unprocessedEl.textContent = unproc;
    updateTitle();
}

function updateTitle() {
    const done = countProcessedRows();
    const total = rows.length;
    document.title = `Processed: ${done} / ${total} (Line: ${currentLineIndex})`;
}

function updateRunTitle(batchDone, batchTotal) {
    const done = countProcessedRows();
    const total = rows.length;
    document.title = `Run: ${batchDone}/${batchTotal}, Overall: ${done}/${total}, Current line: ${currentLineIndex}`;
}

function countProcessedRows() {
    const active = Object.entries(ACTION_CONFIGS)
        .filter(([k]) => appliedActions[k])
        .map(([k]) => k);
    if (active.length === 0) return 0;
    return rows.filter(row =>
        active.every(k => row[ACTION_CONFIGS[k].scannedColumn] === true)
    ).length;
}

// ─── Main processing loop ────────────────────────────────────────────────────
async function onRunActions() {
    // ── Membership gate ────────────────────────────────────────
    const hasAccess = await verifyCsvTrackerAccess();
    if (!hasAccess) return;
    // ────────────────────────────────────────────────

    if (isProcessing) return;

    concurrency = parseInt(concurrencyInput.value, 10) || 1;
    const selectedActions = Array.from(actionCheckboxesEl.querySelectorAll('.actionCheck:checked'))
        .map(cb => cb.value);
    if (selectedActions.length === 0) { alert('No actions selected.'); return; }

    // Respect start-line setting
    let startLine = 0;
    if (startLineInput) {
        startLine = parseInt(startLineInput.value, 10) || 0;
        startLine = Math.max(0, Math.min(startLine, rows.length - 1));
        currentLineIndex = startLine;
        await saveToStorage();
    }

    isProcessing = true;
    statusMessageEl.textContent = 'Processing...';

    // Ensure all selected actions are marked active and rows have scanned columns
    for (const actionKey of selectedActions) {
        if (!appliedActions[actionKey]) {
            appliedActions[actionKey] = true;
            const cfg = ACTION_CONFIGS[actionKey];
            for (const row of rows) row[cfg.scannedColumn] = row[cfg.scannedColumn] || false;
            renderTable();
        }
    }

    // Build work queue from unprocessed rows starting at currentLineIndex
    const workQueue = [];
    for (let i = currentLineIndex; i < rows.length; i++) {
        if (!isRowFullyProcessed(rows[i])) workQueue.push({ row: rows[i], idx: i });
    }
    const batchTotal = workQueue.length;
    let batchDone = 0;
    updateRunTitle(batchDone, batchTotal);

    // Pre-pass: use locally-cached text for GSPR if available
    if (selectedActions.includes('gspr')) {
        const cfg = ACTION_CONFIGS.gspr;
        for (let i = 0; i < rows.length; i++) {
            const row = rows[i];
            if (row[cfg.scannedColumn] === true) continue;
            const manufacturerText = row['Manufacturer Info Text'] ?? null;
            const responsiblePersonText = row['Responsible Person EU Text'] ?? null;
            if (manufacturerText && responsiblePersonText) {
                const mfr = buildManufacturerFields(manufacturerText);
                const rp = buildResponsiblePersonFields(responsiblePersonText);
                let success = true;
                if (!manufacturerText || (mfr['Manufacturer Name'] === null && mfr['Manufacturer Country'] === null)) success = false;
                if (!responsiblePersonText || (rp['Responsible Person 1'] === null && rp['Responsible Person 1 Country'] === null)) success = false;
                cfg.dataColumns.forEach(col => {
                    row[col] = rp[col] ?? mfr[col] ?? row[col] ?? null;
                });
                row[cfg.scannedColumn] = success;
            }
        }
        await saveToStorage();
        renderTable();
    }

    // Concurrent processing
    await runConcurrently(workQueue, async ({ row, idx }) => {
        await processRowActions(row, selectedActions);
        batchDone++;
        currentLineIndex = idx;
        await saveToStorage();
        if (startLineInput) startLineInput.value = currentLineIndex;
        updateRunTitle(batchDone, batchTotal);
    });

    statusMessageEl.textContent = 'Processing complete.';
    isProcessing = false;
    await saveToStorage();
    updateTitle();
}

async function processRowActions(row, actions) {
    for (const actionKey of actions) {
        if (!isActionComplete(row, actionKey)) {
            await runSingleAction(row, actionKey);
        }
    }
}

function isActionComplete(row, actionKey) {
    return row[ACTION_CONFIGS[actionKey].scannedColumn] === true;
}

/**
 * Simple concurrency pool – runs up to `concurrency` tasks at once.
 */
async function runConcurrently(queue, handler) {
    let started = 0, running = 0;
    return new Promise(resolve => {
        const next = async () => {
            while (running < concurrency && started < queue.length) {
                const item = queue[started++];
                running++;
                handler(item).then(async () => {
                    running--;
                    updateCounters();
                    await saveToStorage();
                    if (started === queue.length && running === 0) resolve();
                    else next();
                });
            }
        };
        next();
    });
}

// ─── Address → structured field builders ─────────────────────────────────────
function buildManufacturerFields(rawText) {
    if (!rawText || rawText.trim() === '') return {};
    const parsed = parseAddress(rawText);
    return {
        'Manufacturer Name': parsed.name ?? null,
        'Manufacturer AddressLine1': parsed.address1 ?? null,
        'Manufacturer AddressLine2': parsed.address2 ?? null,
        'Manufacturer City': parsed.city ?? null,
        'Manufacturer Country': parsed.country ?? null,
        'Manufacturer PostalCode': parsed.postalCode ?? null,
        'Manufacturer StateOrProvince': parsed.stateOrProvince ?? null,
        'Manufacturer Phone': parsed.phoneNumber ?? null,
        'Manufacturer Email': parsed.emailAddress ?? null
    };
}

function buildResponsiblePersonFields(rawText) {
    if (!rawText || rawText.trim() === '') return {};
    const parsed = parseAddress(rawText);
    return {
        'Responsible Person 1': parsed.name ?? null,
        'Responsible Person 1 Type': 'EUResponsiblePerson',
        'Responsible Person 1 AddressLine1': parsed.address1 ?? null,
        'Responsible Person 1 AddressLine2': parsed.address2 ?? null,
        'Responsible Person 1 City': parsed.city ?? null,
        'Responsible Person 1 Country': parsed.country ?? null,
        'Responsible Person 1 PostalCode': parsed.postalCode ?? null,
        'Responsible Person 1 StateOrProvince': parsed.stateOrProvince ?? null,
        'Responsible Person 1 Phone': parsed.phoneNumber ?? null,
        'Responsible Person 1 Email': parsed.emailAddress ?? null
    };
}

// ─── Individual action runner ────────────────────────────────────────────────
async function runSingleAction(row, actionKey) {
    if (isActionComplete(row, actionKey)) return;

    const cfg = ACTION_CONFIGS[actionKey];
    const customLabel = row['CustomLabel'] || '';

    try {
        if (actionKey === 'gspr') {
            const manufacturerText = row['Manufacturer Info Text'] ?? null;
            const responsiblePersonText = row['Responsible Person EU Text'] ?? null;

            if (manufacturerText || responsiblePersonText) {
                // Parse from locally-available text
                const mfr = buildManufacturerFields(manufacturerText);
                const rp = buildResponsiblePersonFields(responsiblePersonText);
                const merged = { ...mfr, ...rp };
                let success = true;
                if (!manufacturerText || (mfr['Manufacturer Name'] === null && mfr['Manufacturer Country'] === null)) success = false;
                if (!responsiblePersonText || (rp['Responsible Person 1'] === null && rp['Responsible Person 1 Country'] === null)) success = false;
                cfg.dataColumns.forEach(col => {
                    row[col] = merged[col] ?? row[col] ?? null;
                });
                row[cfg.scannedColumn] = success;
            } else {
                // Fetch from Amazon product page
                const fetchedData = await fetchGSPRData(customLabel);
                let success = true;
                if (fetchedData['Manufacturer Name'] === null && fetchedData['Manufacturer Country'] === null) success = false;
                if (fetchedData['Responsible Person 1'] !== null && fetchedData['Responsible Person 1 Country'] === null) success = false;
                cfg.dataColumns.forEach(col => { row[col] = fetchedData[col] ?? null; });
                row[cfg.scannedColumn] = success;
            }

        } else if (actionKey === 'chineseSeller') {
            console.log('Checking if seller is Chinese for customLabel:', customLabel);
            const result = await checkChineseSeller(customLabel);
            cfg.dataColumns.forEach(col => { row[col] = result[col] ?? ''; });
            row[cfg.scannedColumn] = true;

        } else {
            // Generic stub for unknown action types
            cfg.dataColumns.forEach(col => { row[col] = 'Dummy Data'; });
            row[cfg.scannedColumn] = true;
        }
    } catch (err) {
        console.error('Error processing action:', actionKey, err);
        if (debugMode) throw err;
        console.warn('Skipping due to error:', err.message);
    }

    await saveToStorage();
}

// ─── GSPR fetch ──────────────────────────────────────────────────────────────
async function fetchGSPRData(encodedSku) {
    let asin;
    try { asin = atob(encodedSku); }
    catch (e) { throw new Error('Failed to decode customLabel: ' + encodedSku); }

    const { domain } = await chrome.storage.local.get('domain') || {};
    if (!domain) throw new Error('Domain not found in storage');

    const url = `https://www.amazon.${domain}/dp/${asin}/?th=1&psc=1`;
    console.log('Fetching GSPR data for URL: ' + url);

    let response;
    try {
        response = await new Promise(resolve => {
            chrome.runtime.sendMessage({ type: 'get_gspr', url }, resolve);
        });
    } catch (e) {
        throw new Error('Failed to get GSPR data from background: ' + e.message);
    }

    if (!response || !response.gspr) throw new Error('GSPR data not found for SKU: ' + encodedSku);

    const gspr = response.gspr;
    const mfr = gspr.manufacturerInfo || {};
    const rp = gspr.responsiblePersonEU || {};
    const mfrText = gspr.manufacturerInfoText;
    const rpText = gspr.responsiblePersonEUText;

    return {
        'Manufacturer Name': mfr.name ?? null,
        'Manufacturer AddressLine1': mfr.address1 ?? null,
        'Manufacturer AddressLine2': mfr.address2 ?? null,
        'Manufacturer City': mfr.city ?? null,
        'Manufacturer Country': mfr.country ?? null,
        'Manufacturer PostalCode': mfr.postalCode ?? null,
        'Manufacturer StateOrProvince': mfr.stateOrProvince ?? null,
        'Manufacturer Phone': mfr.phoneNumber ?? null,
        'Manufacturer Email': mfr.emailAddress ?? null,
        'Responsible Person 1': rp.name ?? null,
        'Responsible Person 1 Type': 'EUResponsiblePerson',
        'Responsible Person 1 AddressLine1': rp.address1 ?? null,
        'Responsible Person 1 AddressLine2': rp.address2 ?? null,
        'Responsible Person 1 City': rp.city ?? null,
        'Responsible Person 1 Country': rp.country ?? null,
        'Responsible Person 1 PostalCode': rp.postalCode ?? null,
        'Responsible Person 1 StateOrProvince': rp.stateOrProvince ?? null,
        'Responsible Person 1 Phone': rp.phoneNumber ?? null,
        'Responsible Person 1 Email': rp.emailAddress ?? null,
        'Manufacturer Info Text': mfrText ?? null,
        'Responsible Person EU Text': rpText ?? null
    };
}

// ─── Chinese seller check ────────────────────────────────────────────────────
/**
 * Stub implementation – replace with a real API call or heuristic.
 * Returns { 'Chinese Seller': 'true' | 'false' }.
 */
async function checkChineseSeller(customLabel) {
    await new Promise(resolve => setTimeout(resolve, 50));
    return { 'Chinese Seller': Math.random() > 0.5 ? 'true' : 'false' };
}

// ─── Filter dropdown ─────────────────────────────────────────────────────────
function buildFilterDropdown(columnName) {
    const filterContainer = document.getElementById('filtersContainer');
    const uniqueValues = getUniqueColumnValues(columnName);

    const label = document.createElement('label');
    label.textContent = `Filter by ${columnName}: `;

    const select = document.createElement('select');
    select.id = columnName + 'Filter';

    const allOpt = document.createElement('option');
    allOpt.value = 'all'; allOpt.textContent = 'All';
    select.appendChild(allOpt);

    for (const val of uniqueValues) {
        const opt = document.createElement('option');
        const label2 = val === true ? 'true' : val === false ? 'false' : 'undefined';
        opt.value = label2; opt.textContent = label2;
        select.appendChild(opt);
    }

    label.appendChild(select);
    filterContainer.appendChild(label);
}

function getUniqueColumnValues(columnName) {
    const seen = new Set();
    for (const row of rows) {
        const v = row[columnName] === undefined ? undefined : row[columnName];
        seen.add(v);
    }
    return Array.from(seen);
}



