{% load i18n %}
{% with prefix=prefix|default:"rfid" %}
  <div id="{{ prefix }}-scanner">
    {% if toggle_url or admin_view_url or public_view_url or request.user.is_staff %}
      <div class="rfid-mode-toggle">
        {% if request.user.is_staff %}
          <button id="{{ prefix }}-deep-read" class="button" type="button" aria-pressed="false">{% trans "Deep Read" %}</button>
          <a id="{{ prefix }}-configure" class="button" href="#" style="display:none;"></a>
        {% endif %}
        <button id="{{ prefix }}-local-request" class="button secondary" type="button">{% trans "Connect Local Scanner" %}</button>
        {% if toggle_url %}
          <a id="{{ prefix }}-mode-toggle" class="button" href="{{ toggle_url }}" data-toggle-url="{{ toggle_url }}">{{ toggle_label }}</a>
        {% endif %}
        {% if request.user.is_staff and admin_view_url %}
          <a class="button secondary" href="{{ admin_view_url }}">{% trans "Admin View" %}</a>
        {% elif request.user.is_staff and public_view_url %}
          <a class="button secondary" href="{{ public_view_url }}">{% trans "Public View" %}</a>
        {% endif %}
        <label class="rfid-endian-toggle">
          <input type="checkbox" id="{{ prefix }}-endian-toggle" value="LITTLE">
          <span>{% trans "Little Endian" %}</span>
        </label>
      </div>
    {% endif %}
  <p id="{{ prefix }}-status">{% trans "Setting up scanner..." %}</p>
  <div id="{{ prefix }}-command-output" class="rfid-command-output" hidden>
    <div class="rfid-command-header">
      <h3>{% trans "External Command Output" %}</h3>
      <span class="rfid-command-status" id="{{ prefix }}-command-status"></span>
    </div>
    <div class="rfid-command-section" id="{{ prefix }}-command-stdout-section" hidden>
      <span class="rfid-command-label">{% trans "stdout" %}</span>
      <pre id="{{ prefix }}-command-stdout"></pre>
    </div>
    <div class="rfid-command-section" id="{{ prefix }}-command-stderr-section" hidden>
      <span class="rfid-command-label">{% trans "stderr" %}</span>
      <pre id="{{ prefix }}-command-stderr"></pre>
    </div>
    <p class="rfid-command-error" id="{{ prefix }}-command-error" hidden></p>
  </div>
  {% if table_mode %}
    <div class="rfid-table-mode">
      <table class="rfid-history" id="{{ prefix }}-history">
        <thead>
          <tr>
            <th>{% trans "Time" %}</th>
            <th>{% trans "Label" %}</th>
            <th>{% trans "Type" %}</th>
            <th>{% trans "Color" %}</th>
            <th>{% trans "Valid" %}</th>
            {% if request.user.is_staff %}
              <th>{% trans "RFID" %}</th>
            {% endif %}
            {% if show_release_info %}
              <th>{% trans "Released" %}</th>
              <th>{% trans "Reference" %}</th>
            {% endif %}
          </tr>
        </thead>
        <tbody id="{{ prefix }}-history-body"></tbody>
      </table>
      <p class="rfid-history-total"><span class="rfid-history-total-label">{% trans "Total scanned:" %}</span> <span id="{{ prefix }}-total-count">0</span></p>
    </div>
  {% else %}
    <div class="field"><span class="label">{% trans "Label" %}:</span><span class="value" id="{{ prefix }}-label"></span></div>
    <div class="field"><span class="label">{% trans "Type" %}:</span><span class="value" id="{{ prefix }}-kind"></span></div>
    <div class="field"><span class="label">{% trans "Color" %}:</span><span class="value" id="{{ prefix }}-color"></span></div>
    <div class="field"><span class="label">{% trans "Valid" %}:</span><span class="value" id="{{ prefix }}-allowed"></span></div>
    {% if request.user.is_staff %}
      <div class="field"><span class="label">{% trans "RFID" %}:</span><span class="value" id="{{ prefix }}-rfid"></span></div>
    {% endif %}
    {% if show_release_info %}
      <div class="field"><span class="label">{% trans "Released" %}:</span><span class="value" id="{{ prefix }}-released"></span></div>
      <div class="field"><span class="label">{% trans "Reference" %}:</span><span class="value" id="{{ prefix }}-reference"></span></div>
    {% endif %}
    {% if request.user.is_staff %}
      <div id="{{ prefix }}-deep-details" class="deep-read-details" style="display:none;">
        <h3>{% trans "Deep Read Details" %}</h3>
        <div class="deep-read-keys">
          <div class="deep-read-key">
            <span class="label">{% trans "Key A" %}:</span>
            <span class="value" id="{{ prefix }}-key-a"></span>
          </div>
          <div class="deep-read-key">
            <span class="label">{% trans "Key B" %}:</span>
            <span class="value" id="{{ prefix }}-key-b"></span>
          </div>
        </div>
        <p class="deep-read-summary" id="{{ prefix }}-deep-status"></p>
        <ul id="{{ prefix }}-deep-blocks" class="deep-read-blocks"></ul>
      </div>
    {% endif %}
  {% endif %}
</div>
<style>
#{{ prefix }}-scanner {
  --rfid-table-border: #d0d5dd;
  --rfid-header-bg: #f1f5f9;
  --rfid-header-text: #1f2937;
  --rfid-row-bg: #ffffff;
  --rfid-row-alt-bg: #f8fafc;
  --rfid-row-text: #111827;
  --rfid-row-hover-bg: #e0f2fe;
}

@media (prefers-color-scheme: dark) {
  #{{ prefix }}-scanner {
    --rfid-table-border: rgba(255, 255, 255, 0.24);
    --rfid-header-bg: rgba(255, 255, 255, 0.08);
    --rfid-header-text: #f9fafb;
    --rfid-row-bg: rgba(17, 24, 39, 0.7);
    --rfid-row-alt-bg: rgba(17, 24, 39, 0.55);
    --rfid-row-text: #e5e7eb;
    --rfid-row-hover-bg: rgba(59, 130, 246, 0.3);
  }
}

#{{ prefix }}-scanner .rfid-mode-toggle {
  display: flex;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 0.75em;
  margin-bottom: 1em;
  align-items: center;
}

#{{ prefix }}-scanner .rfid-mode-toggle .button {
  text-decoration: none;
  padding: 0.35em 0.9em;
  border-radius: 9999px;
  font-weight: 600;
  background: var(--rfid-header-bg);
  color: var(--rfid-header-text);
  border: 1px solid var(--rfid-table-border);
  transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, border-color 0.2s ease-in-out;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

#{{ prefix }}-scanner .rfid-mode-toggle .button:hover {
  background: var(--rfid-row-hover-bg);
}

#{{ prefix }}-scanner .rfid-mode-toggle .button.is-active {
  background: var(--rfid-row-hover-bg);
  border-color: var(--rfid-row-hover-bg);
  color: var(--rfid-row-text);
}

#{{ prefix }}-scanner .rfid-endian-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  font-weight: 600;
  color: var(--rfid-header-text);
  cursor: pointer;
}

#{{ prefix }}-scanner .rfid-endian-toggle input {
  cursor: pointer;
}

#{{ prefix }}-scanner .rfid-mode-toggle .button.secondary {
  background: transparent;
  color: inherit;
}

#{{ prefix }}-scanner .rfid-mode-toggle .button.secondary:hover {
  background: var(--rfid-header-bg);
}

#{{ prefix }}-scanner .rfid-command-output {
  margin: 0 0 1em;
  padding: 0.85em 1em;
  border: 1px solid var(--rfid-table-border);
  border-radius: 0.5rem;
  background-color: var(--rfid-row-alt-bg);
  color: var(--rfid-row-text);
}

#{{ prefix }}-scanner .rfid-command-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75em;
  margin-bottom: 0.75em;
}

#{{ prefix }}-scanner .rfid-command-output h3 {
  margin: 0;
  font-size: 1.05em;
}

#{{ prefix }}-scanner .rfid-command-status {
  font-weight: 600;
  font-size: 0.95em;
}

#{{ prefix }}-scanner .rfid-command-section {
  margin-bottom: 0.75em;
}

#{{ prefix }}-scanner .rfid-command-section:last-of-type {
  margin-bottom: 0.5em;
}

#{{ prefix }}-scanner .rfid-command-label {
  display: block;
  font-weight: 600;
  margin-bottom: 0.25em;
}

#{{ prefix }}-scanner .rfid-command-section pre {
  margin: 0;
  padding: 0.6em 0.75em;
  border: 1px solid var(--rfid-table-border);
  border-radius: 0.4rem;
  background-color: var(--rfid-row-bg);
  font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-size: 0.95em;
  white-space: pre-wrap;
  word-break: break-word;
}

#{{ prefix }}-scanner .rfid-command-error {
  margin: 0;
  font-size: 0.95em;
  color: #b91c1c;
}

#{{ prefix }}-scanner .field {
  display: flex;
  margin: 0.5em 0;
  font-size: 1.2em;
}
#{{ prefix }}-scanner .label {
  width: 7em;
  font-weight: bold;
}
#{{ prefix }}-scanner .value {
  flex: 1;
}
#{{ prefix }}-scanner .rfid-table-mode {
  margin-top: 1em;
}
#{{ prefix }}-scanner .rfid-history {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  border: 1px solid var(--rfid-table-border);
  border-radius: 0.5rem;
  overflow: hidden;
  background-color: var(--rfid-row-bg);
  color: var(--rfid-row-text);
}
#{{ prefix }}-scanner .rfid-history th,
#{{ prefix }}-scanner .rfid-history td {
  padding: 0.5em 0.75em;
  text-align: left;
  border-right: 1px solid var(--rfid-table-border);
}
#{{ prefix }}-scanner .rfid-history th:last-child,
#{{ prefix }}-scanner .rfid-history td:last-child {
  border-right: none;
}
#{{ prefix }}-scanner .rfid-history thead th {
  background-color: var(--rfid-header-bg);
  color: var(--rfid-header-text);
  font-weight: 600;
  border-bottom: 1px solid var(--rfid-table-border);
}
#{{ prefix }}-scanner .rfid-history tbody tr {
  background-color: var(--rfid-row-bg);
  transition: background-color 0.2s ease-in-out;
}
#{{ prefix }}-scanner .rfid-history tbody tr:nth-child(odd) {
  background-color: var(--rfid-row-alt-bg);
}
#{{ prefix }}-scanner .rfid-history tbody tr:hover {
  background-color: var(--rfid-row-hover-bg);
}
#{{ prefix }}-scanner .rfid-history-total {
  font-weight: bold;
  margin-top: 0.75em;
  color: var(--rfid-row-text);
}
#{{ prefix }}-scanner .deep-read-details {
  margin-top: 1.5em;
  padding: 1em 1.25em;
  border: 1px solid var(--rfid-table-border);
  border-radius: 0.5rem;
  background-color: var(--rfid-row-alt-bg);
  color: var(--rfid-row-text);
}
#{{ prefix }}-scanner .deep-read-details h3 {
  margin: 0 0 0.75em;
  font-size: 1.1em;
}
#{{ prefix }}-scanner .deep-read-keys {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75em;
  margin: 0 0 0.75em;
}
#{{ prefix }}-scanner .deep-read-key {
  display: flex;
  gap: 0.5em;
  align-items: baseline;
}
#{{ prefix }}-scanner .deep-read-key .label {
  width: auto;
  font-weight: 600;
}
#{{ prefix }}-scanner .deep-read-key .value {
  font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-size: 0.95em;
}
#{{ prefix }}-scanner .deep-read-summary {
  margin: 0 0 0.75em;
  font-size: 0.95em;
}
#{{ prefix }}-scanner .deep-read-blocks {
  margin: 0;
  padding-left: 1.25em;
  list-style: disc;
  font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-size: 0.9em;
}
#{{ prefix }}-scanner .deep-read-blocks li {
  margin-bottom: 0.35em;
}
#{{ prefix }}-scanner .deep-read-blocks li:last-child {
  margin-bottom: 0;
}
#{{ prefix }}-status {
  font-size: 1.2em;
  margin-bottom: 1em;
}
</style>
<script>
(function(){
  const statusEl = document.getElementById('{{ prefix }}-status');
  const labelEl = document.getElementById('{{ prefix }}-label');
  const kindEl = document.getElementById('{{ prefix }}-kind');
  const colorEl = document.getElementById('{{ prefix }}-color');
  const allowedEl = document.getElementById('{{ prefix }}-allowed');
  const rfidEl = document.getElementById('{{ prefix }}-rfid');
  const releasedEl = document.getElementById('{{ prefix }}-released');
  const referenceEl = document.getElementById('{{ prefix }}-reference');
  const deepBtn = document.getElementById('{{ prefix }}-deep-read');
  const configureEl = document.getElementById('{{ prefix }}-configure');
  const modeToggleBtn = document.getElementById('{{ prefix }}-mode-toggle');
  const localRequestBtn = document.getElementById('{{ prefix }}-local-request');
  const deepDetailsEl = document.getElementById('{{ prefix }}-deep-details');
  const keyAEl = document.getElementById('{{ prefix }}-key-a');
  const keyBEl = document.getElementById('{{ prefix }}-key-b');
  const deepBlocksEl = document.getElementById('{{ prefix }}-deep-blocks');
  const deepStatusEl = document.getElementById('{{ prefix }}-deep-status');
  const commandOutputEl = document.getElementById('{{ prefix }}-command-output');
  const commandStatusEl = document.getElementById('{{ prefix }}-command-status');
  const commandStdoutSection = document.getElementById('{{ prefix }}-command-stdout-section');
  const commandStdoutEl = document.getElementById('{{ prefix }}-command-stdout');
  const commandStderrSection = document.getElementById('{{ prefix }}-command-stderr-section');
  const commandStderrEl = document.getElementById('{{ prefix }}-command-stderr');
  const commandErrorEl = document.getElementById('{{ prefix }}-command-error');
  const endiannessToggle = document.getElementById('{{ prefix }}-endian-toggle');
  const adminTemplate = '{{ admin_change_url_template|default:""|escapejs }}';
  const tableMode = {% if table_mode %}true{% else %}false{% endif %};
  const baseScanUrl = '{{ scan_url|escapejs }}';
  const defaultEndianness = ('{{ default_endianness|default:"BIG"|upper|escapejs }}' || 'BIG').toUpperCase();
  let requestedEndianness = defaultEndianness;

  const text = {
    yes: '{{ _("Yes")|escapejs }}',
    no: '{{ _("No")|escapejs }}',
    verified: '{{ _("verified")|escapejs }}',
    unverified: '{{ _("unverified")|escapejs }}',
    keyValueState: '{{ _("%(value)s (%(state)s)")|escapejs }}',
    blockLabel: '{{ _("Block %(number)s")|escapejs }}',
    keyLabel: '{{ _("Key %(key)s")|escapejs }}',
    commandSucceeded: '{{ _("Command succeeded (exit code %(code)s)")|escapejs }}',
    commandFailed: '{{ _("Command failed (exit code %(code)s)")|escapejs }}',
    commandError: '{{ _("Command error")|escapejs }}',
    commandOutput: '{{ _("Command output")|escapejs }}',
    deepReadEnabled: '{{ _("Deep Read enabled. Hold the card on the reader a bit longer to complete the read.")|escapejs }}',
    deepReadDisabled: '{{ _("Deep Read disabled.")|escapejs }}',
    deepRead: '{{ _("Deep Read")|escapejs }}',
    noReadableBlocks: '{{ _("No readable data blocks.")|escapejs }}',
    discoveredBlockSingular: '{{ _("Discovered %(count)s data block.")|escapejs }}',
    discoveredBlockPlural: '{{ _("Discovered %(count)s data blocks.")|escapejs }}',
    noReadableWithKeys: '{{ _("No data blocks were readable with the current keys.")|escapejs }}',
    scannerNotDetected: '{{ _("RFID reader not detected. Please connect the reader.")|escapejs }}',
    unableValidate: '{{ _("Unable to validate RFID")|escapejs }}',
    localValidationFailed: '{{ _("Local scanner validation failed")|escapejs }}',
    localScannerNotReadable: '{{ _("Local scanner is not readable")|escapejs }}',
    browserNoSupport: '{{ _("This browser does not support connecting to local scanners.")|escapejs }}',
    selectScanner: '{{ _("Select a local scanner to connect.")|escapejs }}',
    scannerConnected: '{{ _("Scanner Connected")|escapejs }}',
    localConnectedAwaiting: '{{ _("Local scanner connected. Awaiting RFID...")|escapejs }}',
    scannerReady: '{{ _("Scanner ready")|escapejs }}',
    scannerReadyNetwork: '{{ _("Scanner ready (network)")|escapejs }}',
    scannerReadyLocal: '{{ _("Scanner ready (local)")|escapejs }}',
    additionalLocalReady: '{{ _("Local scanner also ready")|escapejs }}',
    networkReadyAfterLocal: '{{ _("Network scanner ready after local setup")|escapejs }}',
    localDisconnected: '{{ _("Local scanner disconnected")|escapejs }}',
    localConnectFailed: '{{ _("Failed to connect to local scanner")|escapejs }}',
    webSerialUnsupported: '{{ _("Web Serial API is not supported in this browser.")|escapejs }}',
    requestFailed: '{{ _("Request failed")|escapejs }}',
    deepReadFailed: '{{ _("Deep read failed")|escapejs }}',
    attemptingDeepRead: '{{ _("Attempting deep read...")|escapejs }}',
    settingUpScanner: '{{ _("Setting up scanner...")|escapejs }}',
    connectingSavedScanner: '{{ _("Connecting to saved local scanner...")|escapejs }}',
    requestLocalScanner: '{{ _("Requesting local scanner...")|escapejs }}',
    localScanner: '{{ _("Local Scanner")|escapejs }}',
    configureRfid: '{{ _("Configure RFID %(label)s")|escapejs }}',
    rfidStatusState: '{{ _("RFID %(label)s %(state)s")|escapejs }}',
    rfidStatus: '{{ _("RFID %(label)s")|escapejs }}',
    rfidStateOnly: '{{ _("RFID %(state)s")|escapejs }}',
    rfidOnly: '{{ _("RFID")|escapejs }}',
    createdStatus: '{{ _("Created %(status)s")|escapejs }}',
    okText: '{{ _("OK")|escapejs }}',
    badText: '{{ _("BAD")|escapejs }}',
  };

  function formatText(template, replacements){
    if(typeof template !== 'string' || !template){
      return '';
    }
    const values = replacements || {};
    const placeholderPattern = /%\(([^)]+)\)(?:[-+0 #]*\d*(?:\.\d+)?)?[a-zA-Z]/g;
    const replaced = template.replace(placeholderPattern, (match, key) => {
      if(!Object.prototype.hasOwnProperty.call(values, key)){
        return match;
      }
      const replacement = values[key];
      return replacement === undefined || replacement === null ? '' : String(replacement);
    });
    return replaced.replace(/%%/g, '%');
  }

  const historyBody = tableMode ? document.getElementById('{{ prefix }}-history-body') : null;
  const totalCountEl = tableMode ? document.getElementById('{{ prefix }}-total-count') : null;
  const isStaff = {{ request.user.is_staff|yesno:"true,false" }};
  const showReleaseInfo = {{ show_release_info|yesno:"true,false" }};
  const percentLabelWarnings = new Set();

  let historyCount = 0;
  let localPort = null;
  let localReader = null;
  let localConnected = false;
  let lastLocalValue = null;
  let lastLocalAt = 0;
  let lastScanRfid = null;
  let lastScanData = null;
  let firstReadySource = null;
  let pollActive = false;
  let pollTimer = null;
  const historyEntries = tableMode ? new Map() : null;
  let deepReadActive = false;
  let lastDeepDetailsRfid = null;

  if(modeToggleBtn){
    const toggleTarget = modeToggleBtn.getAttribute('data-toggle-url') || modeToggleBtn.getAttribute('href');
    if(toggleTarget){
      modeToggleBtn.addEventListener('click', function(event){
        event.preventDefault();
        window.location.href = toggleTarget;
      });
    }
  }

  function booleanText(value){
    if(value === undefined || value === null){
      return '';
    }
    return value ? text.yes : text.no;
  }

  function normalizeKeyValue(value){
    if(typeof value === 'string'){
      return value.trim().toUpperCase();
    }
    if(value === undefined || value === null){
      return '';
    }
    return String(value);
  }

  function sanitizeLabelValue(labelValue){
    if(labelValue === undefined || labelValue === null){
      return '';
    }
    const labelString = String(labelValue);
    if(!labelString){
      return '';
    }
    if(labelString.includes('%')){
      if(!percentLabelWarnings.has(labelString)){
        console.warn('[RFID] Unexpected % character detected in label; sanitizing for display.', {rawLabel: labelString});
        percentLabelWarnings.add(labelString);
      }
      const cleaned = labelString.replace(/%+/g, '').trim();
      return cleaned || labelString;
    }
    return labelString;
  }

  function normalizeEndianness(value){
    if(typeof value === 'string'){
      const normalized = value.trim().toUpperCase();
      if(normalized === 'LITTLE' || normalized === 'BIG'){
        return normalized;
      }
    }
    return 'BIG';
  }

  function getRequestedEndianness(){
    return requestedEndianness === 'LITTLE' ? 'LITTLE' : 'BIG';
  }

  function convertEndianness(value, fromEndian, toEndian){
    const source = normalizeEndianness(fromEndian);
    const target = normalizeEndianness(toEndian);
    if(typeof value !== 'string'){
      return '';
    }
    const sanitized = value.replace(/\s+/g, '').toUpperCase();
    if(!sanitized){
      return sanitized;
    }
    if(source === target){
      return sanitized;
    }
    if(sanitized.length % 2 !== 0){
      return sanitized;
    }
    const parts = sanitized.match(/.{1,2}/g) || [];
    return parts.reverse().join('');
  }

  function formatKeyText(value, verified){
    const normalized = normalizeKeyValue(value);
    if(!normalized){
      return '';
    }
    if(verified === undefined || verified === null){
      return normalized;
    }
    const stateText = verified ? text.verified : text.unverified;
    return formatText(text.keyValueState, {value: normalized, state: stateText});
  }

  function formatBlockEntry(entry){
    if(!entry || typeof entry !== 'object'){
      return null;
    }
    const parts = [];
    if(typeof entry.block === 'number' && Number.isFinite(entry.block)){
      parts.push(formatText(text.blockLabel, {number: entry.block}));
    }
    if(entry.key){
      parts.push(formatText(text.keyLabel, {key: entry.key}));
    }
    let dataText = '';
    if(Array.isArray(entry.data)){
      dataText = entry.data
        .map((value) => {
          const number = Number(value);
          if(Number.isFinite(number)){
            return number.toString(16).padStart(2, '0').toUpperCase();
          }
          if(typeof value === 'string' && value){
            return value.toUpperCase();
          }
          return '';
        })
        .filter(Boolean)
        .join(' ');
    } else if(entry.data !== undefined && entry.data !== null){
      dataText = String(entry.data);
    }
    if(dataText){
      parts.push(dataText);
    }
    if(!parts.length){
      return null;
    }
    return parts.join(' – ');
  }

  async function readJsonResponse(response){
    const contentType = response && response.headers && typeof response.headers.get === 'function'
      ? (response.headers.get('content-type') || '')
      : '';
    if(contentType.toLowerCase().includes('json')){
      return response.json();
    }
    const error = new Error(text.requestFailed);
    error.status = response ? response.status : undefined;
    error.statusText = response ? response.statusText : undefined;
    try {
      error.body = await response.text();
    } catch(_err){
      error.body = '';
    }
    throw error;
  }

  function clearCommandOutput(){
    if(commandOutputEl){
      commandOutputEl.hidden = true;
    }
    if(commandStatusEl){
      commandStatusEl.textContent = '';
    }
    if(commandStdoutEl){
      commandStdoutEl.textContent = '';
    }
    if(commandStdoutSection){
      commandStdoutSection.hidden = true;
    }
    if(commandStderrEl){
      commandStderrEl.textContent = '';
    }
    if(commandStderrSection){
      commandStderrSection.hidden = true;
    }
    if(commandErrorEl){
      commandErrorEl.textContent = '';
      commandErrorEl.hidden = true;
    }
  }

  function updateCommandOutput(data){
    if(!commandOutputEl){
      return;
    }
    const details = data && typeof data.command_output === 'object' ? data.command_output : null;
    if(!details){
      clearCommandOutput();
      return;
    }
    const stdoutText = details && details.stdout !== undefined && details.stdout !== null
      ? String(details.stdout)
      : '';
    const stderrText = details && details.stderr !== undefined && details.stderr !== null
      ? String(details.stderr)
      : '';
    const errorText = details && details.error !== undefined && details.error !== null
      ? String(details.error)
      : '';
    let returnCode = details && details.returncode !== undefined ? details.returncode : null;
    let numericReturnCode = null;
    let returnCodeText = '';
    if(typeof returnCode === 'number' && Number.isFinite(returnCode)){
      numericReturnCode = returnCode;
      returnCodeText = String(returnCode);
    } else if(typeof returnCode === 'string'){
      const trimmed = returnCode.trim();
      if(trimmed){
        const sanitized = trimmed.replace(/%+$/, '');
        if(sanitized){
          const parsed = Number(sanitized);
          if(Number.isFinite(parsed)){
            numericReturnCode = parsed;
            returnCodeText = String(parsed);
          } else {
            returnCodeText = sanitized;
          }
        }
      }
    }
    const hasReturnCode = Boolean(returnCodeText);
    const hasStdout = Boolean(stdoutText);
    const hasStderr = Boolean(stderrText);
    const hasError = Boolean(errorText);
    if(!hasStdout && !hasStderr && !hasError && !hasReturnCode){
      clearCommandOutput();
      return;
    }

    commandOutputEl.hidden = false;
    if(commandStdoutSection && commandStdoutEl){
      commandStdoutEl.textContent = stdoutText;
      commandStdoutSection.hidden = !hasStdout;
    }
    if(commandStderrSection && commandStderrEl){
      commandStderrEl.textContent = stderrText;
      commandStderrSection.hidden = !hasStderr;
    }
    if(commandErrorEl){
      commandErrorEl.textContent = errorText;
      commandErrorEl.hidden = !hasError;
    }

    let statusMessage = '';
    if(hasReturnCode){
      if(numericReturnCode !== null){
        statusMessage = numericReturnCode === 0
          ? formatText(text.commandSucceeded, {code: returnCodeText})
          : formatText(text.commandFailed, {code: returnCodeText});
      } else {
        statusMessage = formatText(text.commandFailed, {code: returnCodeText});
      }
    } else if(hasError){
      statusMessage = text.commandError;
    } else if(hasStdout || hasStderr){
      statusMessage = text.commandOutput;
    }
    if(commandStatusEl){
      commandStatusEl.textContent = statusMessage;
    }
  }

  function clearDeepDetails(){
    lastDeepDetailsRfid = null;
    if(!deepDetailsEl){
      return;
    }
    deepDetailsEl.style.display = 'none';
    if(keyAEl){ keyAEl.textContent = ''; }
    if(keyBEl){ keyBEl.textContent = ''; }
    if(deepStatusEl){ deepStatusEl.textContent = ''; }
    if(deepBlocksEl){ deepBlocksEl.innerHTML = ''; }
  }

  function deepReadStatusText(enabled){
    if(enabled){
      return text.deepReadEnabled;
    }
    return text.deepReadDisabled;
  }

  function setDeepReadState(enabled, options){
    const opts = Object.assign({updateStatus: true, clearDetails: true}, options || {});
    deepReadActive = Boolean(enabled);
    if(deepBtn){
      deepBtn.textContent = text.deepRead;
      deepBtn.classList.toggle('is-active', deepReadActive);
      deepBtn.setAttribute('aria-pressed', deepReadActive ? 'true' : 'false');
    }
    if(opts.updateStatus){
      statusEl.textContent = deepReadStatusText(deepReadActive);
    }
    if(!deepReadActive && opts.clearDetails){
      clearDeepDetails();
    }
  }

  function normalizeRfidValue(value){
    if(value === undefined || value === null){
      return null;
    }
    const normalizedValue = String(value);
    return normalizedValue ? normalizedValue : null;
  }

  function updateDeepDetails(data, rfidValue){
    if(!deepDetailsEl || tableMode){
      return;
    }
    const normalizedRfid = normalizeRfidValue(rfidValue);
    const dumpEntries = Array.isArray(data && data.dump) ? data.dump : [];
    const hasDump = dumpEntries.length > 0;
    const isDeep = Boolean(data && data.deep_read);
    if(!isDeep && !hasDump){
      if(normalizedRfid && lastDeepDetailsRfid && normalizedRfid !== lastDeepDetailsRfid){
        clearDeepDetails();
      }
      return;
    }
    if(isDeep){
      setDeepReadState(true, {updateStatus: false, clearDetails: false});
    }
    deepDetailsEl.style.display = 'block';
    if(normalizedRfid){
      lastDeepDetailsRfid = normalizedRfid;
    }
    const keys = (data && data.keys) || {};
    if(keyAEl){
      const keyAValue = keys.a !== undefined ? keys.a : keys.key_a;
      const keyAVerified =
        keys.a_verified !== undefined ? keys.a_verified : keys.key_a_verified;
      keyAEl.textContent = formatKeyText(keyAValue, keyAVerified);
    }
    if(keyBEl){
      const keyBValue = keys.b !== undefined ? keys.b : keys.key_b;
      const keyBVerified =
        keys.b_verified !== undefined ? keys.b_verified : keys.key_b_verified;
      keyBEl.textContent = formatKeyText(keyBValue, keyBVerified);
    }
    if(deepBlocksEl){
      deepBlocksEl.innerHTML = '';
      dumpEntries.forEach((entry) => {
        const entryText = formatBlockEntry(entry);
        if(entryText){
          const item = document.createElement('li');
          item.textContent = entryText;
          deepBlocksEl.appendChild(item);
        }
      });
      if(!deepBlocksEl.childElementCount){
        const item = document.createElement('li');
        item.textContent = text.noReadableBlocks;
        deepBlocksEl.appendChild(item);
      }
    }
    if(deepStatusEl){
      if(hasDump){
        const count = dumpEntries.length;
        const template = count === 1 ? text.discoveredBlockSingular : text.discoveredBlockPlural;
        deepStatusEl.textContent = formatText(template, {count});
      } else {
        deepStatusEl.textContent = text.noReadableWithKeys;
      }
    }
  }

  function validText(data){
    if(data.allowed === undefined || data.allowed === null){
      return '';
    }
    return data.allowed ? text.yes : text.no;
  }

  function buildHistoryCells(data, labelValue, displayLabel, displayRfid){
    const labelText = displayLabel !== undefined ? displayLabel : labelValue;
    const values = [
      new Date().toLocaleTimeString(),
      labelText,
      data.kind || '',
      data.color || '',
      validText(data),
    ];
    if(isStaff){
      values.push(displayRfid || '');
    }
    if(showReleaseInfo){
      values.push(booleanText(data.released));
      values.push(data.reference || '');
    }
    return values;
  }

  function appendHistoryRow(data, labelValue, displayLabel, displayRfid){
    if(!historyBody){
      return;
    }
    const key = data.rfid || (labelValue ? `label:${labelValue}` : null);
    if(!key){
      return;
    }
    if(historyEntries && historyEntries.has(key)){
      const existingRow = historyEntries.get(key);
      if(existingRow && existingRow.parentElement === historyBody){
        historyBody.removeChild(existingRow);
      }
    }
    const row = document.createElement('tr');
    buildHistoryCells(data, labelValue, displayLabel, displayRfid).forEach((value) => {
      const cell = document.createElement('td');
      cell.textContent = value;
      row.appendChild(cell);
    });
    historyBody.insertBefore(row, historyBody.firstChild);
    if(historyEntries){
      historyEntries.set(key, row);
      historyCount = historyEntries.size;
    } else {
      historyCount += 1;
    }
    if(totalCountEl){
      totalCountEl.textContent = String(historyCount);
    }
  }

  function showError(message){
    console.error(message);
    const defaultMsg = text.scannerNotDetected;
    clearDeepDetails();
    clearCommandOutput();
    const messageText = typeof message === 'string' && message
      ? message
      : (message && typeof message.message === 'string' ? message.message : '');
    statusEl.textContent = messageText || defaultMsg;
  }

  function updateFromData(data, options){
    const opts = Object.assign({updateHistory: true, triggerActions: true}, options || {});
    if(!data || data.error){
      if(data && data.error){
        if(opts.triggerActions){
          showError(data.error);
        } else {
          statusEl.textContent = data.error;
        }
      }
      return;
    }
    if(!data.rfid){
      if(typeof data.status === 'string' && data.status){
        statusEl.textContent = data.status;
      }
      return;
    }
    const rfidValue = normalizeRfidValue(data.rfid);
    const storedEndianness = normalizeEndianness(data.endianness);
    const desiredEndianness = getRequestedEndianness();
    const displayRfid = rfidValue
      ? convertEndianness(rfidValue, storedEndianness, desiredEndianness)
      : '';
    const rfidKey = rfidValue !== null
      ? rfidValue
      : (data.rfid !== undefined && data.rfid !== null ? String(data.rfid) : null);
    if(opts.triggerActions && lastScanRfid !== null && rfidKey !== null && rfidKey !== lastScanRfid){
      clearCommandOutput();
    }
    if(rfidKey !== null){
      lastScanRfid = rfidKey;
    }
    const labelValue = data.label_id === undefined || data.label_id === null ? '' : data.label_id;
    const displayLabel = sanitizeLabelValue(labelValue);
    if(labelEl){ labelEl.textContent = displayLabel; }
    if(kindEl){ kindEl.textContent = data.kind || ''; }
    if(rfidEl){ rfidEl.textContent = displayRfid || ''; }
    if(colorEl){ colorEl.textContent = data.color || ''; }
    if(allowedEl){ allowedEl.textContent = validText(data); }
    if(releasedEl){ releasedEl.textContent = booleanText(data.released); }
    if(referenceEl){ referenceEl.textContent = data.reference || ''; }
    const hasReferenceLink = data.reference && /^https?:\/\//i.test(data.reference);
    if(opts.triggerActions){
      if(referenceEl && hasReferenceLink){
        const w = window.open(data.reference, '_blank');
        if(w){ w.focus(); }
      } else if(!referenceEl && tableMode && showReleaseInfo && hasReferenceLink){
        const w = window.open(data.reference, '_blank');
        if(w){ w.focus(); }
      }
    }
    if(configureEl){
      const labelForText = displayLabel || labelValue;
      configureEl.textContent = formatText(text.configureRfid, {label: labelForText});
      configureEl.href = adminTemplate ? adminTemplate.replace('0', labelValue) : '#';
      configureEl.style.display = 'inline';
    }
    updateDeepDetails(data, rfidValue);
    updateCommandOutput(data);
    if(deepBtn && data.deep_read){
      setDeepReadState(true, {updateStatus: false, clearDetails: false});
    }
    const hasLabel = Boolean(displayLabel || labelValue);
    const okText = data.allowed === undefined ? '' : (data.allowed ? text.okText : text.badText);
    let statusMsg = '';
    if(okText){
      const template = hasLabel ? text.rfidStatusState : text.rfidStateOnly;
      statusMsg = formatText(template, {label: displayLabel || labelValue, state: okText}).trim();
    } else if(hasLabel){
      statusMsg = formatText(text.rfidStatus, {label: displayLabel || labelValue}).trim();
    } else {
      statusMsg = text.rfidOnly;
    }
    statusEl.textContent = data.created ? formatText(text.createdStatus, {status: statusMsg}) : statusMsg;
    lastScanData = Object.assign({}, data);
    if(tableMode && opts.updateHistory){
      appendHistoryRow(data, labelValue, displayLabel, displayRfid);
    }
  }

  async function requestScanData(){
    try {
      const resp = await fetch(buildScanUrl(), {
        credentials: 'same-origin',
        headers: {
          'Accept': 'application/json',
          'X-Requested-With': 'XMLHttpRequest',
        },
      });
      const data = await readJsonResponse(resp);
      if(!resp.ok || (data && data.error)){
        showError(data && data.error ? data.error : text.requestFailed);
        return null;
      }
      updateFromData(data);
      return data;
    } catch(err){
      showError(err);
    }
    return null;
  }

  function schedulePoll(){
    if(pollTimer){
      clearTimeout(pollTimer);
    }
    pollTimer = window.setTimeout(poll, 200);
  }

  async function poll(){
    if(!pollActive){
      return;
    }
    try {
      await requestScanData();
    } finally {
      schedulePoll();
    }
  }

  function startPolling(){
    if(pollActive){
      return;
    }
    pollActive = true;
    poll();
  }

  async function prepareNetworkScanner(){
    const data = await requestScanData();
    if(data === null){
      startPolling();
      return false;
    }
    if(!firstReadySource){
      firstReadySource = 'network';
      statusEl.textContent = text.scannerReadyNetwork;
    }
    startPolling();
    return true;
  }

  async function startScannerSetup(){
    statusEl.textContent = text.settingUpScanner;
    setupLocalScannerEvents();
    const localPromise = autoConnectLocalScanner();
    const networkReady = await prepareNetworkScanner();
    const localReady = await localPromise;
    if(networkReady && firstReadySource === 'local'){
      statusEl.textContent = text.networkReadyAfterLocal;
    } else if(localReady && firstReadySource === 'network'){
      statusEl.textContent = text.additionalLocalReady;
    } else if(!firstReadySource){
      statusEl.textContent = text.scannerNotDetected;
    }
  }

  function getCookie(name){
    const cookies = document.cookie ? document.cookie.split('; ') : [];
    for(let i = 0; i < cookies.length; i += 1){
      const parts = cookies[i].split('=');
      const key = parts.shift();
      if(key === name){
        return decodeURIComponent(parts.join('='));
      }
    }
    return null;
  }

  function buildScanUrl(){
    try {
      const url = new URL(baseScanUrl || '', window.location.origin);
      url.searchParams.set('endianness', getRequestedEndianness());
      return url.toString();
    } catch(err){
      return baseScanUrl;
    }
  }

  async function sendLocalScan(rfidValue){
    const headers = {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'X-Requested-With': 'XMLHttpRequest',
    };
    const csrf = getCookie('csrftoken');
    if(csrf){
      headers['X-CSRFToken'] = csrf;
    }
    try {
      const resp = await fetch(baseScanUrl, {
        method: 'POST',
        credentials: 'same-origin',
        headers,
        body: JSON.stringify({rfid: rfidValue, endianness: getRequestedEndianness()})
      });
      const data = await readJsonResponse(resp);
      if(!resp.ok || (data && data.error)){
        showError((data && data.error) || text.unableValidate);
        return;
      }
      updateFromData(data);
    } catch(err){
      console.error(err);
      showError(text.localValidationFailed);
    }
  }

  function handleLocalScan(rawValue){
    if(!rawValue){
      return;
    }
    const cleaned = rawValue.replace(/[^0-9A-Fa-f]/g, '').toUpperCase();
    if(!cleaned){
      return;
    }
    const now = Date.now();
    const desired = getRequestedEndianness();
    const dedupeKey = `${desired}:${cleaned}`;
    if(lastLocalValue === dedupeKey && now - lastLocalAt < 300){
      return;
    }
    lastLocalValue = dedupeKey;
    lastLocalAt = now;
    sendLocalScan(cleaned);
  }

  async function readFromLocalPort(){
    if(!localPort || !localPort.readable){
      throw new Error(text.localScannerNotReadable);
    }
    localReader = localPort.readable.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    try {
      while(true){
        const {value, done} = await localReader.read();
        if(done){
          break;
        }
        if(value){
          buffer += decoder.decode(value, {stream: true});
          const parts = buffer.split(/[\r\n]+/);
          buffer = parts.pop() || '';
          for(let i = 0; i < parts.length; i += 1){
            handleLocalScan(parts[i].trim());
          }
        }
      }
      buffer += decoder.decode();
      if(buffer.trim()){
        handleLocalScan(buffer.trim());
      }
    } finally {
      if(localReader){
        try { localReader.releaseLock(); } catch(err) { /* ignore */ }
        localReader = null;
      }
    }
  }

  async function connectToLocalPort(port){
    if(!port){
      return false;
    }
    try {
      statusEl.textContent = text.connectingSavedScanner;
      await port.open({baudRate: 9600});
      localPort = port;
      localConnected = true;
      statusEl.textContent = text.localConnectedAwaiting;
      if(!firstReadySource){
        firstReadySource = 'local';
        statusEl.textContent = text.scannerReadyLocal;
      }
      await readFromLocalPort();
      return true;
    } catch(err){
      showError(text.localConnectFailed);
    } finally {
      if(localReader){
        try { await localReader.cancel(); } catch(err) { /* ignore */ }
      }
      if(localPort){
        try { await localPort.close(); } catch(err) { /* ignore */ }
        localPort = null;
      }
      if(localConnected){
        statusEl.textContent = text.localDisconnected;
      }
      localConnected = false;
      localReader = null;
    }
    return false;
  }

  async function requestLocalScanner(){
    if(!('serial' in navigator) || !navigator.serial.requestPort){
      showError(text.browserNoSupport);
      return false;
    }
    statusEl.textContent = text.requestLocalScanner;
    try {
      const port = await navigator.serial.requestPort();
      return await connectToLocalPort(port);
    } catch(err){
      showError(text.localConnectFailed);
    }
    return false;
  }

  async function autoConnectLocalScanner(){
    if(!('serial' in navigator) || !navigator.serial.getPorts){
      if(localRequestBtn){
        localRequestBtn.disabled = true;
      }
      return false;
    }
    const ports = await navigator.serial.getPorts();
    if(!ports.length){
      if(localRequestBtn){
        localRequestBtn.disabled = false;
        localRequestBtn.hidden = false;
      }
      statusEl.textContent = text.selectScanner;
      return false;
    }
    const port = ports[0];
    return await connectToLocalPort(port);
  }

  function setupLocalScannerEvents(){
    if(!('serial' in navigator) || !navigator.serial.addEventListener){
      return;
    }
    navigator.serial.addEventListener('disconnect', (event) => {
      if(localPort && event.target === localPort){
        localPort = null;
        localConnected = false;
        showError(text.localDisconnected);
      }
    });
  }

  if(deepBtn){
    setDeepReadState(false, {updateStatus: false, clearDetails: false});
    deepBtn.addEventListener('click', async () => {
      const enabling = !deepReadActive;
      const headers = {
        'Accept': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
      };
      const csrf = getCookie('csrftoken');
      if(csrf){
        headers['X-CSRFToken'] = csrf;
      }
      deepBtn.disabled = true;
      if(enabling){
        statusEl.textContent = text.attemptingDeepRead;
      }
      try {
        const response = await fetch('{{ deep_read_url|default:"" }}', {
          method: 'POST',
          credentials: 'same-origin',
          headers,
        });
        const data = await readJsonResponse(response);
        if(!response.ok || (data && data.error)){
          const message = data && data.error ? data.error : text.deepReadFailed;
          throw new Error(message);
        }
        const enabled = Boolean(data.enabled);
        setDeepReadState(enabled, {updateStatus: false});
        if(enabled && data.scan){
          updateFromData(data.scan);
          const hasStatus = typeof data.scan.status === 'string' && data.scan.status;
          const hasRfid = Boolean(data.scan.rfid) || Boolean(data.scan.error);
          if(!hasStatus && !hasRfid){
            statusEl.textContent = data.status || deepReadStatusText(enabled);
          }
        } else {
          statusEl.textContent = data.status || deepReadStatusText(enabled);
        }
      } catch(err){
        const message = err && err.message ? err.message : text.deepReadFailed;
        statusEl.textContent = message;
      } finally {
        deepBtn.disabled = false;
      }
    });
  }
  if(endiannessToggle){
    endiannessToggle.checked = requestedEndianness === 'LITTLE';
    endiannessToggle.addEventListener('change', () => {
      requestedEndianness = endiannessToggle.checked ? 'LITTLE' : 'BIG';
      if(lastScanData){
        updateFromData(lastScanData, {updateHistory: false, triggerActions: false});
      }
    });
  }
  if(localRequestBtn){
    localRequestBtn.hidden = true;
    if(!('serial' in navigator) || !(navigator.serial && navigator.serial.requestPort)){
      localRequestBtn.disabled = true;
      localRequestBtn.title = text.webSerialUnsupported;
    } else {
      localRequestBtn.addEventListener('click', async () => {
        localRequestBtn.disabled = true;
        const connected = await requestLocalScanner();
        if(!connected){
          localRequestBtn.disabled = false;
        }
      });
    }
  }
  startScannerSetup();
})();
</script>
{% endwith %}
