/**
 * Exact-Match Datasheet Formatter
 *
 * Generates TXT datasheets matching the original QuickBASIC DATASHEETWRITE output.
 * Requires a DB record (with raw_data) and model specs from spec-reader.
 */

const { getFamily } = require('../parsers/spec-reader');

// -------------------------------------------------------------------------
// DATA LINES: parameter names and units per family
// -------------------------------------------------------------------------

const DATA_LINES = {
    SCM5B: [
        ['Supply Current, Nom', 'mA'],      // 1
        ['Supply Current, Max', 'mA'],       // 2
        ['Exc. Current #1', 'uA'],           // 3
        ['Exc. Current #2', 'uA'],           // 4
        ['Exc. Current Match', 'uA'],        // 5
        ['Output Resistance', 'ohms'],       // 6
        ['CJC Gain', 'uV/C'],               // 7
        ['Exc. Voltage', 'V'],               // 8
        ['Exc. Load Reg.', 'ppm/mA'],        // 9
        ['Vout Reg. w/ Load', '%'],           // 10
        ['Exc. Current Limit', 'mA'],        // 11
        ['Linearity', '%'],                  // 12
        ['Accuracy', '%'],                   // 13
        ['Lead R Effect', 'C/ohm'],          // 14
        ['Supply Sensitivity', 'uV/%'],      // 15
        ['Input Resistance', 'Mohms'],       // 16
        ['Open Input Response', 'V'],        // 17
        ['Frequency Response', 'dB'],        // 18
        ['Step Response', '%'],              // 19
        ['Output Noise', 'uVrms'],           // 20
        ['Over-range Response', 'V'],        // 21
    ],
    '8B': [
        ['Supply Current, Nom', 'mA'],
        ['Supply Current, Max', 'mA'],
        ['Exc. Current #1', 'uA'],
        ['Exc. Current #2', 'uA'],
        ['Exc. Current Match', 'uA'],
        ['Output Resistance', 'ohms'],
        ['CJC Gain', 'uV/C'],
        ['Exc. Voltage', 'V'],
        ['Exc. Load Reg.', 'ppm/mA'],
        ['Vout Reg. w/ Load', '%'],
        ['Exc. Current Limit', 'mA'],
        ['Linearity', '%'],
        ['Accuracy', '%'],
        ['Lead R Effect', 'C/ohm'],
        ['Supply Sensitivity', 'ppm/%'],
        ['Input Resistance', 'Mohms'],
        ['Open Input Response', 'V'],
        ['Frequency Response', 'dB'],
        ['Step Response', '%'],
        ['Output Noise', 'uVrms'],
        ['Over-range Response', 'V'],
    ],
    DSCA: [
        ['Supply Current, Nom', 'mA'],
        ['Supply Current @ Max Load', 'mA'],
        ['Linearity, 0mA Load', '%'],
        ['Accuracy, 0mA Load', '%'],
        ['Linearity, 5mA Load', '%'],
        ['Accuracy, 5mA Load', '%'],
        ['Linearity, 50mA Load', '%'],
        ['Accuracy, 50mA Load', '%'],
        ['Positive Current Limit', 'mA'],
        ['Negative Current Limit', 'mA'],
        ['Overrange', '%'],
        ['Power Supply Sensitivity', '%/%'],
        ['Input Resistance', 'Mohms'],
        ['Frequency Response', 'dB'],
        ['Step Response', '%'],
        ['Output Noise', ''],
        ['Compliance', '%'],
        ['Accuracy @ 5 ohm load', '%'],
    ],
    SCM7B: [
        ['Supply Current', 'mA'],                    // 1
        ['Supply Current w/ Load', 'mA'],             // 2
        ['Bias Current', 'nA'],                       // 3
        ['Input Resistance', 'kohms'],                // 4
        ['Offset Calibration', 'mV'],                 // 5
        ['Gain Calibration', 'mV'],                   // 6
        ['Linearity/Conformity', '%'],                // 7
        ['Accuracy', '%'],                            // 8
        ['VLoop @ 0 mA (Vs = 18V)', 'V'],             // 9
        ['             (Vs = 35V)', 'V'],              // 10
        ['VLoop @ 4 mA (Vs = 18V)', 'V'],             // 11
        ['             (Vs = 35V)', 'V'],              // 12
        ['VLoop @ 20mA (Vs = 18V)', 'V'],              // 13
        ['             (Vs = 35V)', 'V'],              // 14
        ['VLoop Peak Ripple', 'mV'],                  // 15
        ['High Excitation Current', 'uA'],            // 16
        ['Low Excitation Current', 'uA'],             // 17
        ['Output Effective Power', 'mW'],             // 18
        ['Supply Sensitivity', '%/%Vs'],              // 19
        ['Open Sensor Response', 'V'],                // 20
        ['Lead Resistance Effect', 'C/ohm'],          // 21
        ['CJC Gain', 'uV/C'],                        // 22
        ['100kHz Output Noise', 'uVrms'],             // 23
        ['Attenuation', 'dB'],                        // 24
        ['150ms Step Response', 'V'],                  // 25
        ['Output Noise', 'mVpk'],                     // 26
        ['Over-Range', 'V'],                          // 27
        ['Under-Range', 'V'],                         // 28
        ['Open Loop Detect', 'mA'],                   // 29
        ['Error @ Max Rload', '%'],                   // 30
        ['Pass-Through Error', '%'],                  // 31
    ],
    DSCT: [
        ['Under-range Limit', 'mA'],
        ['Over-range Limit', 'mA'],
        ['Error @ Vloop = 10.8V', '%'],
        ['Error @ Vloop = 60V', '%'],
        ['Minus f.s. Exc. Current', 'uA'],
        ['Plus f.s. Exc. Current', 'uA'],
        ['Current Source Matching', '%'],
        ['Linearity / Conformity', '%'],
        ['Accuracy', '%'],
        ['Lead Resistance Effects', 'C/ohm'],
        ['Loop Voltage Sensitivity', '%/V'],
        ['Input Resistance', 'Mohm'],
        ['Open Thermocouple Response', 'mA'],
        ['Frequency Response', 'dB'],
        ['Step Response', '%'],
        ['Output Noise', 'uArms'],
    ],
};

// -------------------------------------------------------------------------
// Sensor type number mapping (for input column headers)
// -------------------------------------------------------------------------

function getSensorNum(sentype) {
    if (!sentype) return 1;
    const s = sentype.toUpperCase().trim();
    if (s === 'V' || s === 'MV') return 1;
    if (s === 'MA') return 2;
    if (s.includes('JTC') || s === 'J') return 3;
    if (s.includes('KTC') || s === 'K') return 4;
    if (s.includes('TTC') || s === 'T') return 5;
    if (s.includes('ETC') || s === 'E' || s.includes('RTC') || s.includes('STC') || s.includes('NTC') || s.includes('BTC')) return 6;
    if (s.includes('RTD')) return 7;
    if (s === 'FBRIDGE' || s === 'HBRIDGE') return 8;
    if (s === '2WTX') return 9;
    return 1; // default voltage
}

// -------------------------------------------------------------------------
// Parse raw_data from DB record
// -------------------------------------------------------------------------

function parseRawData(rawData, family) {
    if (!rawData) return null;

    const lines = rawData.split('\n').map(l => l.trim()).filter(l => l.length > 0);
    if (lines.length < 8) return null;

    const result = {
        modelLine: '',
        accuracy: [],    // 5 points: { stim, calc, meas, error, status }
        stepResponse: 0,
        statusEntries: [],
    };

    let lineIdx = 0;

    // Line 0: model name (quoted)
    result.modelLine = lines[lineIdx++].replace(/"/g, '').trim();

    // Lines 1-5: accuracy points
    for (let i = 0; i < 5 && lineIdx < lines.length; i++) {
        const parts = parseCSVLine(lines[lineIdx++]);
        if (parts.length >= 5) {
            result.accuracy.push({
                stim: parseFloat(parts[0]),
                calc: parseFloat(parts[1]),
                meas: parseFloat(parts[2]),
                error: parseFloat(parts[3]),
                status: parts[4].replace(/"/g, '').trim(),
            });
        }
    }

    // Next line: step response / placeholders
    if (lineIdx < lines.length) {
        const parts = parseCSVLine(lines[lineIdx++]);
        // SCM5B/8B: "0","0",value  DSCT: just value
        const lastVal = parts[parts.length - 1];
        result.stepResponse = parseFloat(lastVal) || 0;
    }

    // Remaining lines: STATUS groups
    // SCM5B/8B: groups of 5, DSCT: groups of 4
    const groupSize = (family === 'DSCT') ? 4 : 5;
    while (lineIdx < lines.length) {
        const line = lines[lineIdx];
        // Stop if we hit the serial/date line
        if (line.match(/^"\d+-\d+[A-Za-z]?","/)) break;
        const parts = parseCSVLine(line);
        for (const p of parts) {
            result.statusEntries.push(p.replace(/"/g, ''));
        }
        lineIdx++;
    }

    return result;
}

// Simple CSV parser that handles quoted strings
function parseCSVLine(line) {
    const parts = [];
    let current = '';
    let inQuotes = false;

    for (let i = 0; i < line.length; i++) {
        const ch = line[i];
        if (ch === '"') {
            inQuotes = !inQuotes;
        } else if (ch === ',' && !inQuotes) {
            parts.push(current.trim());
            current = '';
        } else {
            current += ch;
        }
    }
    parts.push(current.trim());
    return parts;
}

// -------------------------------------------------------------------------
// Format measured value from STATUS entry
// -------------------------------------------------------------------------

/**
 * Format a number matching QuickBASIC STR$() behavior:
 * - Positive numbers get a leading space
 * - Leading zeros before decimal are dropped (0.03 -> .03)
 * - Rounds to 6 significant digits to clean IEEE 754 artifacts
 */
function r(val, fixedDecimals) {
    if (val == null || isNaN(val)) return '0';
    const rounded = parseFloat(val.toPrecision(6));
    let str;
    if (fixedDecimals != null) {
        str = rounded.toFixed(fixedDecimals);
    } else {
        str = String(rounded);
    }
    // QB STR$() drops leading zero: "0.03" -> ".03"
    str = str.replace(/^0\./, '.').replace(/^-0\./, '-.');
    // QB STR$() prepends space for positive numbers
    if (rounded >= 0 && !str.startsWith(' ')) {
        str = ' ' + str;
    }
    return str;
}

/**
 * Parse STATUS$ entry and format measured value matching QB PRINT USING.
 * QB format strings all produce exactly 6 characters for the number:
 *   "0" -> "###### &"  (integer, 6 digits)
 *   "1" -> "####.# &"  (1 decimal, 6 chars)
 *   "2" -> "####.# &"  (same as 1)
 *   "3" -> "##.### &"  (3 decimals, 6 chars)
 *   "4" -> "#.#### &"  (4 decimals, 6 chars)
 */
function formatMeasured(statusStr) {
    if (!statusStr || statusStr.length <= 4) return null;

    const passFail = statusStr.substring(0, 4); // "PASS" or "FAIL"
    const decimalDigit = statusStr[statusStr.length - 1];
    const valueStr = statusStr.substring(5, statusStr.length - 1).trim();
    const value = parseFloat(valueStr);

    if (isNaN(value)) return { passFail, formatted: valueStr, width: 6 };

    // QB PRINT USING: right-justified in 6 character positions
    // Negative sign takes one digit position
    let formatted;
    switch (decimalDigit) {
        case '0': formatted = Math.round(value).toString().padStart(6); break;
        case '1': formatted = value.toFixed(1).padStart(6); break;
        case '2': formatted = value.toFixed(1).padStart(6); break;
        case '3': formatted = value.toFixed(3).padStart(6); break;
        case '4': formatted = value.toFixed(4).padStart(6); break;
        default:  formatted = value.toFixed(1).padStart(6); break;
    }

    return { passFail, formatted, value };
}

// -------------------------------------------------------------------------
// Format TSPEC display string from spec values
// -------------------------------------------------------------------------

function buildTSpecs(specs, family, stepResponse) {
    if (!specs) return [];
    const tspecs = [];

    if (family === 'SCM5B' || family === '8B') {
        tspecs[1]  = ' < ' + r(specs.ISMAXNEXCL);
        tspecs[2]  = ' < ' + r(specs.ISMAXFEXCL);
        tspecs[3]  = '   ' + r(specs.IEXC);
        tspecs[4]  = '   ' + r(specs.IEXC);
        const imatchtol = (specs.IMATCHTOL || 0) / 100;
        tspecs[5]  = '+/-' + r(specs.IEXC * imatchtol, 0);
        tspecs[6]  = family === '8B' ? ' < 50' : ' < ' + r(specs.OUTRES || 55);
        tspecs[7]  = ''; // CJC gain - computed from polynomial, skip for now
        if (specs.VEXC) {
            const vexcAcc = Math.round(specs.VEXCACC / 100 * specs.VEXC * 1000) / 1000;
            tspecs[8] = r(specs.VEXC, 1) + '+/-' + r(vexcAcc, 3);
        } else {
            tspecs[8] = '';
        }
        tspecs[9]  = '+/-' + r(specs.EXCLOADREG);
        const acc125 = Math.round((specs.ACCURACY * 1.25) * 100) / 100;
        tspecs[10] = '+/-' + r(acc125);
        tspecs[11] = ' < ' + r(specs.EXCIMAX);
        tspecs[12] = '+/-' + r(specs.LINEAR);
        tspecs[13] = '+/-' + r(specs.ACCURACY);
        tspecs[14] = '+/-' + r(stepResponse || 0, 1);
        tspecs[15] = '+/-' + r(specs.PSS || 0);
        tspecs[16] = ' >=' + r(specs.INPUTRES);
        if (specs.VOPENINMIN != null && specs.VOPENINMAX != null) {
            tspecs[17] = r(specs.VOPENINMIN, 2) + ' to ' + r(specs.VOPENINMAX, 2);
        } else {
            tspecs[17] = '';
        }
        tspecs[18] = r(specs.ATTEN) + '+/-' + r(specs.ATTENTOL);
        tspecs[19] = r(specs.STEPRMIN || 0) + ' to ' + r(specs.STEPRMAX || 0);
        tspecs[20] = ' < ' + r(specs.OUTNOISE);
        tspecs[21] = tspecs[17]; // duplicate
    } else if (family === 'DSCA') {
        tspecs[1]  = ' < ' + r(specs.ISMAXNL || 0);
        tspecs[2]  = ' < ' + r(specs.ISMAXFL || 0);
        tspecs[3]  = '+/-' + r(specs.LINEAR1 || 0);
        tspecs[4]  = '+/-' + r(specs.ACCURACY1 || 0);
        tspecs[5]  = '+/-' + r(specs.LINEAR2 || 0);
        tspecs[6]  = '+/-' + r(specs.ACCURACY2 || 0);
        tspecs[7]  = '+/-' + r(specs.LINEAR3 || 0);
        tspecs[8]  = '+/-' + r(specs.ACCURACY3 || 0);
        tspecs[9]  = ' < ' + r(specs.ILIMIT || 0);
        tspecs[10] = ' > ' + r(-(specs.ILIMIT || 0));
        tspecs[11] = ' > ' + r(specs.PERCOVER || 0);
        tspecs[12] = '+/-' + r(specs.PSS || 0);
        tspecs[13] = ' >=' + r(specs.INPUTRES || 0);
        tspecs[14] = r(specs.ATTEN || 0) + '+/-' + r(specs.ATTENTOL || 0);
        tspecs[15] = r(specs.STEPRMIN || 0) + ' to ' + r(specs.STEPRMAX || 0);
        tspecs[16] = ' <=' + r(specs.OUTNOISE || 0);
        tspecs[17] = '+/-' + r(specs.COMPLIANCE || 0);
        tspecs[18] = '+/-' + r((specs.ACCURACY1 || 0) * 2);
    } else if (family === 'DSCT') {
        tspecs[1]  = ''; // computed at runtime
        tspecs[2]  = ''; // computed at runtime
        tspecs[3]  = ' < 1';
        tspecs[4]  = ' < 1';
        const iexcmTol = specs.MODNAME && specs.MODNAME.startsWith('DSCT') ? 0.05 : 0.02;
        tspecs[5]  = Math.round(specs.IEXCMFS || 0) + '+/-' + Math.round((specs.IEXCMFS || 0) * iexcmTol);
        tspecs[6]  = Math.round(specs.IEXCPFS || 0) + '+/-' + Math.round((specs.IEXCPFS || 0) * iexcmTol);
        tspecs[7]  = '+/-' + r(specs.IMATCHTOL || 0);
        tspecs[8]  = '+/- ' + r(specs.LINEAR || 0);
        tspecs[9]  = '+/- ' + r(specs.ACCURACY || 0);
        tspecs[10] = '+/-' + r(stepResponse || 0, 1);
        tspecs[11] = '+/-' + r(specs.VSEN || 0);
        tspecs[12] = ' >=' + r(specs.INPUTRES || 0);
        const iopentc = specs.IOPENTC || 0;
        const maxout = specs.MAXOUT || 20;
        tspecs[13] = (iopentc > maxout ? ' > ' : ' < ') + r(iopentc);
        tspecs[14] = r(specs.ATTEN || 0) + '+/-' + r(specs.ATTENTOL || 0);
        tspecs[15] = r(specs.STEPRMIN || 0) + ' to ' + r(specs.STEPRMAX || 0);
        tspecs[16] = ' < ' + r(specs.OUTNOISE || 0);
    } else if (family === 'SCM7B') {
        const orange = (specs.MAXOUT || 5) - (specs.MINOUT || 0);
        tspecs[1]  = '< ' + r(specs.ISMAXNEXCL + 6);
        tspecs[2]  = '< ' + r(specs.ISMAXFEXCL + 6);
        tspecs[3]  = '+/-' + r(specs.IBIAS || 0);
        tspecs[4]  = ' > ' + r(specs.INPUTRES || 0);
        const calTol = 20 * orange * (specs.CALTOL || 0);
        tspecs[5]  = '+/-' + r(calTol);
        tspecs[6]  = '+/-' + r(calTol);
        tspecs[7]  = '+/-' + r(specs.LINEAR || 0);
        tspecs[8]  = '+/-' + r(specs.ACCURACY || 0);
        if (specs.VEXC) {
            const vexc5 = specs.VEXC * 0.05;
            tspecs[9]  = r(specs.VEXC) + ' +/-' + r(vexc5);
            tspecs[10] = tspecs[9];
        }
        if (specs.VEXCLO) {
            const vlo5 = specs.VEXCLO * 0.05;
            tspecs[11] = r(specs.VEXCLO) + ' +/-' + r(vlo5);
            tspecs[12] = tspecs[11];
        }
        if (specs.VEXCHI) {
            const vhi5 = specs.VEXCHI * 0.05;
            tspecs[13] = r(specs.VEXCHI) + ' +/-' + r(vhi5);
            tspecs[14] = tspecs[13];
        }
        tspecs[15] = ' < 50';
        tspecs[16] = ' < ' + r(specs.EXCIMAX || 0);
        tspecs[17] = ' > ' + r(specs.EXCIMIN || 0);
        tspecs[18] = ' > ' + r(specs.PE || 0);
        tspecs[19] = '+/-' + r(specs.PSS || 0);
        tspecs[20] = '';  // Open TC - needs runtime calc
        tspecs[21] = '+/-' + r(specs.LEADRERR || 0);
        tspecs[22] = '';  // CJC - needs seebeck polynomial
        tspecs[23] = ' < ' + r(specs.OUTNOISERMS || 0);
        tspecs[24] = r(specs.ATTEN || 0) + '+/-' + r(specs.ATTENTOL || 0);
        // Step response
        if (specs.STEPRESP && specs.STEPTOL) {
            const lowV = specs.STEPRESP - specs.STEPTOL;
            const highV = specs.STEPRESP + specs.STEPTOL;
            tspecs[25] = r(lowV) + ' to ' + r(highV);
        } else {
            tspecs[25] = '';
        }
        tspecs[26] = ' < ' + r(specs.OUTNOISEVPK || 0);
        tspecs[27] = '+5 to +5.8';
        tspecs[28] = '-.9 to +1';
        tspecs[29] = '0';
        tspecs[30] = '';  // Compliance - needs runtime calc
        tspecs[31] = '+/-' + r(specs.ACCURACY || 0);
    }

    return tspecs;
}

// -------------------------------------------------------------------------
// Format accuracy value based on sensor type
// -------------------------------------------------------------------------

function formatAccuracyLine(point, sensorNum, maxIn) {
    let stimStr;
    if (sensorNum >= 3 && sensorNum <= 6) {
        // Temperature: +####.##
        stimStr = formatSigned(point.stim, 2, 8);
    } else if (sensorNum === 7) {
        // Resistance: #####.##
        stimStr = point.stim.toFixed(2).padStart(8);
    } else {
        // Voltage/Current: +###.###
        const scale = (maxIn != null && maxIn < 1) ? 1000 : 1;
        stimStr = formatSigned(point.stim * scale, 3, 8);
    }

    const calcStr = formatSigned(point.calc, 3, 7);
    const measStr = formatSigned(point.meas, 3, 7);
    const errorStr = formatSigned(point.error, 3, 8);

    return '    ' + stimStr + '        ' + calcStr + '         ' + measStr + '        ' + errorStr + '         ' + point.status;
}

/**
 * Set text at a specific column position (0-indexed) in a string.
 * Pads with spaces if the string is shorter than the target column.
 */
function setCol(str, col, text) {
    while (str.length < col) str += ' ';
    return str + text;
}

/**
 * Pad string to reach a column position (for inline TAB simulation).
 * Returns spaces needed to reach the column from current position.
 */
function padToCol(str, col) {
    const needed = col - str.length;
    return needed > 0 ? ' '.repeat(needed) : ' ';
}

function formatSigned(val, decimals, width) {
    const sign = val >= 0 ? '+' : '';
    const str = sign + val.toFixed(decimals);
    return str.padStart(width);
}

// -------------------------------------------------------------------------
// Main: generate exact-match TXT datasheet
// -------------------------------------------------------------------------

/**
 * Generate an exact-match TXT datasheet from a DB record and model specs.
 * @param {object} record - DB record with raw_data, model_number, serial_number, test_date
 * @param {object} specs - Model spec record from spec-reader
 * @returns {string|null} Formatted TXT datasheet, or null if data is insufficient
 */
function generateExactDatasheet(record, specs) {
    const family = getFamily(record.model_number);
    if (!family) return null;

    const parsed = (family === 'SCM7B')
        ? parse7BRawData(record.raw_data)
        : parseRawData(record.raw_data, family);
    if (!parsed) return null;
    if (family !== 'SCM7B' && parsed.accuracy.length < 5) return null;

    const dataLines = DATA_LINES[family];
    if (!dataLines) return null;

    const sentype = specs ? specs.SENTYPE : '';
    const sensorNum = getSensorNum(sentype);
    const maxIn = specs ? specs.MAXIN : 10;
    const tspecs = specs ? buildTSpecs(specs, family, parsed.stepResponse) : [];

    // Format test date from YYYY-MM-DD to MM-DD-YYYY
    const dateParts = (record.test_date || '').split('-');
    const dateStr = dateParts.length === 3
        ? `${dateParts[1]}-${dateParts[2]}-${dateParts[0]}`
        : record.test_date || '';

    let modelName = specs ? specs.MODNAME : record.model_number;
    // 7B header prepends "SCM" to the model name
    if (family === 'SCM7B' && !modelName.toUpperCase().startsWith('SCM')) {
        modelName = 'SCM' + modelName;
    }

    const lines = [];
    const TAB5 = '    '; // 4 spaces = TAB(5) in QB (0-indexed)

    // ---- Header ----
    lines.push(TAB5 + 'DATAFORTH CORPORATION                         Phone: (520) 741-1404');
    lines.push(TAB5 + '3331 E. Hemisphere Loop                       Fax:   (520) 741-0762');
    lines.push(TAB5 + 'Tucson, AZ  85706  USA                        email: info@dataforth.com');
    lines.push('');
    lines.push('                                TEST DATA SHEET');
    lines.push(TAB5 + '~'.repeat(71));
    // QB: PRINT #9, TAB(5); "Date: "; DATE$
    //     PRINT #9, TAB(5); "Model: "; SPECS.MODNAME
    //     PRINT #9, TAB(5); "SN: "; TAB(12); SN$
    lines.push(TAB5 + 'Date: ' + dateStr);
    lines.push(TAB5 + 'Model: ' + modelName);
    let snLine = TAB5 + 'SN: ';
    snLine = setCol(snLine, 11, record.serial_number); // TAB(12) = index 11
    lines.push(snLine);
    lines.push('');

    // ---- Accuracy Test ----
    // 7B CSV format doesn't include individual accuracy test points (only error pcts in LOGIT)
    // The accuracy data is only in the SHT files, not the DAT files
    if (family === 'SCM7B') {
        // Skip accuracy section entirely for 7B — data not available from DAT format
    } else {
    lines.push('                                 ACCURACY TEST');
    lines.push('');
    lines.push('                    Calculated       Measured');

    // Input column header based on sensor type
    let inputHeader;
    if (sensorNum >= 3 && sensorNum <= 6) {
        inputHeader = '  Temp. (C)';
    } else if (sensorNum === 2 || sensorNum === 9) {
        inputHeader = '   Iin (mA)';
    } else if (sensorNum === 7) {
        inputHeader = ' Rin (ohms)';
    } else {
        inputHeader = (maxIn != null && maxIn < 1) ? '  Vin (mV)' : '   Vin (V)';
    }
    lines.push('    ' + inputHeader + '        Vout (V)        Vout (V)*      Error (%)       Status');
    lines.push(TAB5 + '==========      ==========      ==========      =========      ========');

    for (const point of parsed.accuracy) {
        lines.push(formatAccuracyLine(point, sensorNum, maxIn));
    }
    lines.push('');
    } // end accuracy section conditional

    // ---- Final Test Results ----
    // QB column positions (1-indexed): TAB(31), TAB(47), TAB(60-speclen), TAB(61), TAB(71)
    lines.push('                              FINAL TEST RESULTS');
    lines.push('');
    // QB: TAB(12); "Parameter"; TAB(30); "Measured Value"; TAB(51); "Specification "; TAB(70); "Status"
    let hdr1 = setCol('', 11, 'Parameter');
    hdr1 = setCol(hdr1, 29, 'Measured Value');
    hdr1 = setCol(hdr1, 50, 'Specification ');
    hdr1 = setCol(hdr1, 69, 'Status');
    lines.push(hdr1);
    // QB: TAB(5); "======================="; TAB(30); "==============="; TAB(47); "====================="; TAB(70); "======"
    let hdr2 = setCol('', 4, '=======================');
    hdr2 = setCol(hdr2, 29, '===============');
    hdr2 = setCol(hdr2, 46, '=====================');
    hdr2 = setCol(hdr2, 69, '======');
    lines.push(hdr2);

    for (let i = 0; i < dataLines.length && i < parsed.statusEntries.length; i++) {
        const status = parsed.statusEntries[i];
        if (!status || status.length <= 4) continue; // Skip if no measured data

        const [paramName, paramUnit] = dataLines[i];
        let unit = paramUnit;

        // Unit overrides per QB logic
        if (family === 'SCM5B' || family === '8B') {
            if (i === 13 && sensorNum === 7) unit = 'ohm/ohm';
            if (i === 14 && (sensorNum === 5 || sensorNum === 6)) unit = 'C/V';
        }

        const measured = formatMeasured(status);
        if (!measured) continue;

        // Build line matching QB TAB positions (converting to 0-indexed for string ops)
        // TAB(5): parameter name
        // TAB(31): measured value (6 chars right-justified) + space + unit
        // TAB(60-speclen): spec string right-aligned to end at col 60
        // TAB(61): unit
        // TAB(71): PASS/FAIL
        let line = '';
        line = setCol(line, 4, paramName);                              // TAB(5) = index 4
        line = setCol(line, 30, measured.formatted + ' ' + unit);       // TAB(31) = index 30

        const tspec = tspecs[i + 1]; // 1-indexed in TSPECS
        if (tspec) {
            const specLen = tspec.length;
            line = setCol(line, 59 - specLen, tspec);                   // TAB(60-speclen)
            line = setCol(line, 60, unit);                              // TAB(61) = index 60
        }
        line = setCol(line, 70, measured.passFail);                     // TAB(71) = index 70

        lines.push(line);
    }

    // ---- Footer ----
    // 240 VAC / Hi-Pot (conditional by family/model)
    if (family === 'SCM5B') {
        const mn = (modelName || '').trim();
        if (!mn.startsWith('SCM5BPT') && !mn.startsWith('SCM5B-1369')) {
            lines.push(TAB5 + '240 VAC Withstand' + ''.padEnd(49) + 'PASS');
            lines.push(TAB5 + 'Hi-Pot' + ''.padEnd(60) + 'PASS');
        }
    } else if (family === '8B') {
        const mn = (modelName || '').trim();
        if (!mn.startsWith('8BPT')) {
            lines.push(TAB5 + 'VAC Withstand' + ''.padEnd(53) + 'PASS');
            lines.push(TAB5 + 'Hi-Pot' + ''.padEnd(60) + 'PASS');
        }
    } else if (family === 'SCM7B') {
        const mn = (modelName || '').toUpperCase();
        if (!mn.includes('7BPT')) {
            let vac = setCol(TAB5 + '120VAC Withstand', 70, 'PASS');
            lines.push(vac);
            let hp = setCol(TAB5 + 'Hi-Pot', 70, 'PASS');
            lines.push(hp);
        }
    } else if (family === 'DSCA') {
        lines.push(TAB5 + '240VAC Withstand' + ''.padEnd(50) + 'PASS');
        lines.push(TAB5 + 'Hi-Pot' + ''.padEnd(60) + 'PASS');
    } else if (family === 'DSCT') {
        const mn = (modelName || '').toUpperCase();
        if (!mn.startsWith('SCMHVAS')) {
            lines.push(TAB5 + '240 VAC Withstand' + ''.padEnd(49) + 'PASS');
            lines.push(TAB5 + 'Hi-Pot' + ''.padEnd(60) + 'PASS');
        }
    }

    // Underline + Check List
    lines.push(TAB5 + '_'.repeat(71));
    if (family === 'SCM7B') {
        lines.push('                                Packing Check List');
        lines.push('');
        lines.push(setCol(TAB5 + 'Module Appearance:      _____', 44, 'Mounting Screw:           _____'));
        lines.push('');
        lines.push(setCol(TAB5 + 'Pins Straight:          _____', 44, 'Module Header:            _____'));
        lines.push('');
        lines.push(setCol(TAB5 + 'Tested by:      _____________', 44, 'QC:             _______________'));
    } else if (family !== 'DSCA') {
        lines.push('                                  Check List');
        lines.push('');
        lines.push(setCol(TAB5 + 'Module Appearance:      __X__', 44, 'Mounting Screw:           __X__'));
        lines.push('');
        lines.push(setCol(TAB5 + 'Pins Straight:          __X__', 44, 'Module Header:            __X__'));
    }

    // DSCA current output load note
    if (family === 'DSCA' && specs && specs.OUTSIGTYPE && specs.OUTSIGTYPE.trim().toUpperCase() === 'CURRENT') {
        lines.push(TAB5 + 'Standard output load for test is 250 ohms.');
    }

    lines.push('');
    lines.push(TAB5 + 'It is hereby certified that the above product is in conformance with');
    lines.push(TAB5 + 'all requirements to the extent specified.  This product is not');
    lines.push(TAB5 + 'authorized or warranted for use in life support devices and/or systems.');
    lines.push('');
    lines.push(TAB5 + '* NIST traceable calibration certificates support Measured Value data.');
    lines.push(TAB5 + '  Calibration services are available through ANSI/NCSL Z540-1 and');
    lines.push(TAB5 + '  ISO Guide 25 Certified Metrology Labs.');
    lines.push('');

    return lines.join('\r\n');
}

/**
 * Parse 7B raw_data (single CSV line format)
 * Format: STAGE: MODEL,SN,DATE,VERSION,DMMSERIAL,val1,...val31,err1,...errN
 * val=9999 means not tested, [val] means FAIL
 */
function parse7BRawData(rawData) {
    if (!rawData) return null;

    const match = rawData.match(/^([A-Z-]+):\s*(.*)$/);
    if (!match) return null;

    const parts = match[2].split(',');
    if (parts.length < 36) return null; // model + sn + date + version + dmmserial + 31 values minimum

    const result = {
        modelLine: parts[0].trim(),
        accuracy: [],
        stepResponse: 0,
        statusEntries: [],
    };

    // Values start at index 5 (after model, sn, date, version, dmmserial)
    for (let i = 0; i < 31; i++) {
        const rawVal = (parts[5 + i] || '').trim();

        if (rawVal === '9999' || rawVal === '') {
            // Not tested - push short "PASS" (will be skipped by formatter)
            result.statusEntries.push('PASS');
        } else if (rawVal.startsWith('[')) {
            // FAIL - bracketed value
            const val = rawVal.replace(/[\[\]]/g, '').trim();
            const numVal = parseFloat(val);
            if (isNaN(numVal) || numVal === 0) {
                result.statusEntries.push('FAIL');
            } else {
                const decimals = guessDecimals(numVal);
                result.statusEntries.push('FAIL ' + val + decimals);
            }
        } else {
            // PASS with value
            const numVal = parseFloat(rawVal);
            if (isNaN(numVal)) {
                result.statusEntries.push('PASS');
            } else {
                const decimals = guessDecimals(numVal);
                result.statusEntries.push('PASS ' + rawVal.trim() + decimals);
            }
        }
    }

    // Error percentages follow the 31 values - these are the accuracy test point errors
    const errorStart = 5 + 31;
    for (let i = errorStart; i < parts.length; i++) {
        const val = parseFloat((parts[i] || '').trim());
        if (!isNaN(val)) {
            result.accuracy.push({
                stim: 0,  // Stimulus not stored in 7B CSV format
                calc: 0,
                meas: 0,
                error: val * 100, // Convert fraction to percentage
                status: 'PASS',
            });
        }
    }

    return result;
}

/**
 * Guess the decimal format digit based on value magnitude
 */
function guessDecimals(val) {
    const abs = Math.abs(val);
    if (abs === 0) return '0';
    if (abs >= 100) return '0';
    if (abs >= 10) return '1';
    if (abs >= 1) return '1';
    if (abs >= 0.1) return '3';
    return '4';
}

module.exports = { generateExactDatasheet, parseRawData, parse7BRawData, DATA_LINES };
