GGn Upload Templator

Auto-fill upload forms using torrent file data with configurable templates

2025-10-01 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

// ==UserScript==
// @name        GGn Upload Templator
// @namespace   https://greatest.deepsurf.us/
// @version     0.9
// @description Auto-fill upload forms using torrent file data with configurable templates
// @author      leveldesigner
// @license     Unlicense
// @source      https://github.com/lvldesigner/userscripts/tree/main/ggn-upload-templator
// @supportURL  https://github.com/lvldesigner/userscripts/tree/main/ggn-upload-templator
// @icon        https://gazellegames.net/favicon.ico
// @match       https://*.gazellegames.net/upload.php*
// @grant       GM_addStyle
// ==/UserScript==

(function() {
  "use strict";
  const DEFAULT_CONFIG = {
    TARGET_FORM_SELECTOR: "#upload_table",
    SUBMIT_KEYBINDING: true,
    CUSTOM_SUBMIT_KEYBINDING: "Ctrl+Enter",
    APPLY_KEYBINDING: true,
    CUSTOM_APPLY_KEYBINDING: "Ctrl+Shift+A",
    CUSTOM_FIELD_SELECTORS: [],
    IGNORED_FIELDS_BY_DEFAULT: [
      "linkgroup",
      "groupid",
      "apikey",
      "type",
      "amazonuri",
      "googleplaybooksuri",
      "goodreadsuri",
      "isbn",
      "scan_dpi",
      "other_dpi",
      "release_desc",
      "anonymous",
      "dont_check_rules",
      "title",
      "tags",
      "image",
      "gameswebsiteuri",
      "wikipediauri",
      "album_desc",
      "submit_upload"
    ]
  };
  const logDebug = (...messages) => {
    const css = "color: #4dd0e1; font-weight: 900;";
    console.debug("%c[GGn Upload Templator]", css, ...messages);
  };
  function getCurrentFormData(config) {
    const formData = {};
    const formSelector = config.TARGET_FORM_SELECTOR || "form";
    const targetForm = document.querySelector(formSelector);
    const defaultSelector = "input[name], select[name], textarea[name]";
    const customSelectors = config.CUSTOM_FIELD_SELECTORS || [];
    const fieldSelector = customSelectors.length > 0 ? `${defaultSelector}, ${customSelectors.join(", ")}` : defaultSelector;
    const inputs = targetForm ? targetForm.querySelectorAll(fieldSelector) : document.querySelectorAll(fieldSelector);
    inputs.forEach((input) => {
      const isCustomField = isElementMatchedByCustomSelector(input, config);
      const hasValidIdentifier = isCustomField ? input.name || input.id || input.getAttribute("data-field") || input.getAttribute("data-name") : input.name;
      if (!hasValidIdentifier) return;
      if (!isCustomField && (input.type === "file" || input.type === "button" || input.type === "submit")) {
        return;
      }
      const fieldName = input.name || input.id || input.getAttribute("data-field") || input.getAttribute("data-name");
      if (fieldName) {
        if (input.type === "radio" && formData[fieldName]) {
          return;
        }
        const fieldInfo = {
          value: isCustomField ? input.value || input.textContent || input.getAttribute("data-value") || "" : input.type === "checkbox" || input.type === "radio" ? input.checked : input.value || "",
          label: getFieldLabel(input, config),
          type: input.tagName.toLowerCase(),
          inputType: input.type || "custom"
        };
        if (input.type === "radio") {
          const radioGroup = document.querySelectorAll(
            `input[name="${fieldName}"][type="radio"]`
          );
          fieldInfo.radioOptions = Array.from(radioGroup).map((radio) => ({
            value: radio.value,
            checked: radio.checked,
            label: getFieldLabel(radio, config) || radio.value
          }));
          const selectedRadio = Array.from(radioGroup).find(
            (radio) => radio.checked
          );
          fieldInfo.selectedValue = selectedRadio ? selectedRadio.value : "";
          fieldInfo.value = fieldInfo.selectedValue;
        }
        if (input.tagName.toLowerCase() === "select") {
          fieldInfo.options = Array.from(input.options).map((option) => ({
            value: option.value,
            text: option.textContent.trim(),
            selected: option.selected
          }));
          fieldInfo.selectedValue = input.value;
        }
        formData[fieldName] = fieldInfo;
      }
    });
    return formData;
  }
  function isElementMatchedByCustomSelector(element, config) {
    const customSelectors = config.CUSTOM_FIELD_SELECTORS || [];
    if (customSelectors.length === 0) return false;
    return customSelectors.some((selector) => {
      try {
        return element.matches(selector);
      } catch (e) {
        console.warn(`Invalid custom selector: ${selector}`, e);
        return false;
      }
    });
  }
  function cleanLabelText(text) {
    if (!text) return text;
    const tempElement = document.createElement("div");
    tempElement.innerHTML = text;
    const linkElements = tempElement.querySelectorAll("a");
    linkElements.forEach((link) => {
      link.remove();
    });
    let cleanedText = tempElement.textContent || tempElement.innerText || "";
    cleanedText = cleanedText.trim();
    if (cleanedText.endsWith(":")) {
      cleanedText = cleanedText.slice(0, -1).trim();
    }
    return cleanedText;
  }
  function getFieldLabel(input, config) {
    const isCustomField = isElementMatchedByCustomSelector(input, config);
    if (isCustomField) {
      const parent = input.parentElement;
      if (parent) {
        const labelElement = parent.querySelector("label");
        if (labelElement) {
          const rawText = labelElement.innerHTML || labelElement.textContent || "";
          const cleanedText = cleanLabelText(rawText);
          return cleanedText || input.id || input.name || "Custom Field";
        }
        const labelClassElement = parent.querySelector('*[class*="label"]');
        if (labelClassElement) {
          const rawText = labelClassElement.innerHTML || labelClassElement.textContent || "";
          const cleanedText = cleanLabelText(rawText);
          return cleanedText || input.id || input.name || "Custom Field";
        }
      }
      return input.id || input.name || "Custom Field";
    }
    if (input.type === "radio" && input.id) {
      const parentTd = input.closest("td");
      if (parentTd) {
        const associatedLabel = parentTd.querySelector(
          `label[for="${input.id}"]`
        );
        if (associatedLabel) {
          const rawText = associatedLabel.innerHTML || associatedLabel.textContent || "";
          const cleanedText = cleanLabelText(rawText);
          return cleanedText || input.value;
        }
      }
    }
    const parentRow = input.closest("tr");
    if (parentRow) {
      const labelCell = parentRow.querySelector("td.label");
      if (labelCell) {
        const rawText = labelCell.innerHTML || labelCell.textContent || "";
        const cleanedText = cleanLabelText(rawText);
        return cleanedText ? `${cleanedText} (${input.name})` : input.name;
      }
    }
    return input.name;
  }
  function findElementByFieldName(fieldName, config) {
    config.TARGET_FORM_SELECTOR ? `${config.TARGET_FORM_SELECTOR} ` : "";
    const defaultSelector = "input[name], select[name], textarea[name]";
    const customSelectors = config.CUSTOM_FIELD_SELECTORS || [];
    const fieldSelector = customSelectors.length > 0 ? `${defaultSelector}, ${customSelectors.join(", ")}` : defaultSelector;
    const targetForm = config.TARGET_FORM_SELECTOR ? document.querySelector(config.TARGET_FORM_SELECTOR) : null;
    const inputs = targetForm ? targetForm.querySelectorAll(fieldSelector) : document.querySelectorAll(fieldSelector);
    for (const input of inputs) {
      const isCustomField = isElementMatchedByCustomSelector(input, config);
      const hasValidIdentifier = isCustomField ? input.name || input.id || input.getAttribute("data-field") || input.getAttribute("data-name") : input.name;
      if (!hasValidIdentifier) continue;
      if (!isCustomField && (input.type === "file" || input.type === "button" || input.type === "submit")) {
        continue;
      }
      const elementFieldName = input.name || input.id || input.getAttribute("data-field") || input.getAttribute("data-name");
      if (elementFieldName === fieldName) {
        return input;
      }
    }
    return null;
  }
  class TorrentUtils {
    // Parse torrent file for metadata
    static async parseTorrentFile(file) {
      const arrayBuffer = await file.arrayBuffer();
      const data = new Uint8Array(arrayBuffer);
      try {
        const [torrent] = TorrentUtils.decodeBencode(data);
        return {
          name: torrent.info?.name || file.name,
          comment: torrent.comment || "",
          files: torrent.info?.files?.map((f) => ({
            path: f.path.join("/"),
            length: f.length
          })) || [
            {
              path: torrent.info?.name || file.name,
              length: torrent.info?.length
            }
          ]
        };
      } catch (e) {
        console.warn("Could not parse torrent file:", e);
        return { name: file.name, comment: "", files: [] };
      }
    }
    static parseCommentVariables(comment) {
      if (!comment || typeof comment !== "string") return {};
      const variables = {};
      const pairs = comment.split(";");
      for (const pair of pairs) {
        const trimmedPair = pair.trim();
        if (!trimmedPair) continue;
        const eqIndex = trimmedPair.indexOf("=");
        if (eqIndex === -1) continue;
        const key = trimmedPair.substring(0, eqIndex).trim();
        const value = trimmedPair.substring(eqIndex + 1).trim();
        if (key) {
          variables[`_${key}`] = value;
        }
      }
      return variables;
    }
    // Simple bencode decoder
    static decodeBencode(data, offset = 0) {
      const char = String.fromCharCode(data[offset]);
      if (char === "d") {
        const dict = {};
        offset++;
        while (data[offset] !== 101) {
          const [key, newOffset1] = TorrentUtils.decodeBencode(data, offset);
          const [value, newOffset2] = TorrentUtils.decodeBencode(
            data,
            newOffset1
          );
          dict[key] = value;
          offset = newOffset2;
        }
        return [dict, offset + 1];
      }
      if (char === "l") {
        const list = [];
        offset++;
        while (data[offset] !== 101) {
          const [value, newOffset] = TorrentUtils.decodeBencode(data, offset);
          list.push(value);
          offset = newOffset;
        }
        return [list, offset + 1];
      }
      if (char === "i") {
        offset++;
        let num = "";
        while (data[offset] !== 101) {
          num += String.fromCharCode(data[offset]);
          offset++;
        }
        return [parseInt(num), offset + 1];
      }
      if (char >= "0" && char <= "9") {
        let lengthStr = "";
        while (data[offset] !== 58) {
          lengthStr += String.fromCharCode(data[offset]);
          offset++;
        }
        const length = parseInt(lengthStr);
        offset++;
        const str = new TextDecoder("utf-8", { fatal: false }).decode(
          data.slice(offset, offset + length)
        );
        return [str, offset + length];
      }
      throw new Error("Invalid bencode data");
    }
  }
  function parseTemplate(mask, torrentName, greedyMatching = true) {
    if (!mask || !torrentName) return {};
    let regexPattern = mask.replace(/\\\$/g, "___ESCAPED_DOLLAR___").replace(/\\\{/g, "___ESCAPED_LBRACE___").replace(/\\\}/g, "___ESCAPED_RBRACE___").replace(/\\\\/g, "___ESCAPED_BACKSLASH___").replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\$\\\{([^}]+)\\\}/g, (match, varName, offset, string) => {
      if (greedyMatching) {
        return `(?<${varName}>.+)`;
      } else {
        const remainingString = string.slice(offset + match.length);
        const hasMoreVariables = /\\\$\\\{[^}]+\\\}/.test(remainingString);
        if (hasMoreVariables) {
          return `(?<${varName}>.*?)`;
        } else {
          return `(?<${varName}>.+)`;
        }
      }
    }).replace(/___ESCAPED_DOLLAR___/g, "\\$").replace(/___ESCAPED_LBRACE___/g, "\\{").replace(/___ESCAPED_RBRACE___/g, "\\}").replace(/___ESCAPED_BACKSLASH___/g, "\\\\");
    try {
      const regex = new RegExp(regexPattern, "i");
      const match = torrentName.match(regex);
      return match?.groups || {};
    } catch (e) {
      console.warn("Invalid template regex:", e);
      return {};
    }
  }
  function validateMaskWithDetails(mask) {
    if (!mask) {
      return {
        valid: true,
        errors: [],
        warnings: [],
        info: [],
        variables: { valid: [], invalid: [], reserved: [] }
      };
    }
    const errors = [];
    const warnings = [];
    const info = [];
    const validVars = [];
    const invalidVars = [];
    const reservedVars = [];
    const seenVars = /* @__PURE__ */ new Set();
    const duplicates = /* @__PURE__ */ new Set();
    try {
      const parsed = parseMaskStructure(mask);
      if (parsed.optionalCount > 0) {
        info.push({ type: "info", message: `${parsed.optionalCount} optional block${parsed.optionalCount === 1 ? "" : "s"} defined` });
      }
    } catch (e) {
      const posMatch = e.message.match(/position (\d+)/);
      const position = posMatch ? parseInt(posMatch[1], 10) : 0;
      const rangeEnd = e.rangeEnd !== void 0 ? e.rangeEnd : position + 2;
      errors.push({ type: "error", message: e.message, position, rangeEnd });
    }
    const unclosedPattern = /\$\{[^}]*$/;
    if (unclosedPattern.test(mask)) {
      const position = mask.lastIndexOf("${");
      const rangeEnd = mask.length;
      errors.push({ type: "error", message: 'Unclosed variable: missing closing brace "}"', position, rangeEnd });
    }
    const emptyVarPattern = /\$\{\s*\}/g;
    let emptyMatch;
    while ((emptyMatch = emptyVarPattern.exec(mask)) !== null) {
      const position = emptyMatch.index;
      const rangeEnd = position + emptyMatch[0].length;
      errors.push({ type: "error", message: "Empty variable: ${}", position, rangeEnd });
    }
    const nestedPattern = /\$\{[^}]*\$\{/g;
    let nestedMatch;
    while ((nestedMatch = nestedPattern.exec(mask)) !== null) {
      const position = nestedMatch.index;
      const rangeEnd = nestedMatch.index + nestedMatch[0].length;
      errors.push({ type: "error", message: "Nested braces are not allowed", position, rangeEnd });
    }
    const varPattern = /\$\{([^}]+)\}/g;
    let match;
    const varPositions = /* @__PURE__ */ new Map();
    while ((match = varPattern.exec(mask)) !== null) {
      const varName = match[1].trim();
      const position = match.index;
      if (varName !== match[1]) {
        warnings.push({ type: "warning", message: `Variable "\${${match[1]}}" has leading or trailing whitespace`, position });
      }
      if (!/^[a-zA-Z0-9_]+$/.test(varName)) {
        invalidVars.push(varName);
        const rangeEnd = position + match[0].length;
        errors.push({ type: "error", message: `Invalid variable name "\${${varName}}": only letters, numbers, and underscores allowed`, position, rangeEnd });
        continue;
      }
      if (varName.startsWith("_")) {
        reservedVars.push(varName);
        warnings.push({ type: "warning", message: `Variable "\${${varName}}" uses reserved prefix "_" (reserved for comment variables)`, position });
        continue;
      }
      if (/^\d/.test(varName)) {
        warnings.push({ type: "warning", message: `Variable "\${${varName}}" starts with a number (potentially confusing)`, position });
      }
      if (varName.length > 50) {
        warnings.push({ type: "warning", message: `Variable "\${${varName}}" is very long (${varName.length} characters)`, position });
      }
      if (seenVars.has(varName)) {
        duplicates.add(varName);
        if (!varPositions.has(varName)) {
          varPositions.set(varName, position);
        }
      } else {
        seenVars.add(varName);
        varPositions.set(varName, position);
      }
      validVars.push(varName);
    }
    if (duplicates.size > 0) {
      const firstDuplicatePos = Math.min(...Array.from(duplicates).map((v) => varPositions.get(v)));
      warnings.push({ type: "warning", message: `Duplicate variables: ${Array.from(duplicates).map((v) => `\${${v}}`).join(", ")}`, position: firstDuplicatePos });
    }
    const totalVars = validVars.length + reservedVars.length;
    if (totalVars > 0) {
      info.push({ type: "info", message: `${totalVars} variable${totalVars === 1 ? "" : "s"} defined` });
    }
    if (totalVars === 0 && mask.length > 0) {
      info.push({ type: "info", message: "No variables defined. Add variables like ${name} to extract data." });
    }
    return {
      valid: errors.length === 0,
      errors,
      warnings,
      info,
      variables: { valid: validVars, invalid: invalidVars, reserved: reservedVars }
    };
  }
  function interpolate(template, data, commentVariables = {}) {
    if (!template) return template;
    const allData = { ...data, ...commentVariables };
    return template.replace(/\$\{([^}]+)\}/g, (match, key) => {
      const value = allData[key];
      return value !== void 0 && value !== null && value !== "" ? value : "";
    });
  }
  function findMatchingOption(options, variableValue, matchType) {
    if (!options || !variableValue) return null;
    const normalizedValue = variableValue.toLowerCase();
    for (const option of options) {
      const optionText = option.textContent ? option.textContent.toLowerCase() : option.text.toLowerCase();
      const optionValue = option.value.toLowerCase();
      let matches = false;
      switch (matchType) {
        case "exact":
          matches = optionText === normalizedValue || optionValue === normalizedValue;
          break;
        case "contains":
          matches = optionText.includes(normalizedValue) || optionValue.includes(normalizedValue);
          break;
        case "starts":
          matches = optionText.startsWith(normalizedValue) || optionValue.startsWith(normalizedValue);
          break;
        case "ends":
          matches = optionText.endsWith(normalizedValue) || optionValue.endsWith(normalizedValue);
          break;
      }
      if (matches) {
        return {
          value: option.value,
          text: option.textContent || option.text
        };
      }
    }
    return null;
  }
  function generateCombinationsDescending(count) {
    const total = Math.pow(2, count);
    const combinations = [];
    for (let i = total - 1; i >= 0; i--) {
      const combo = [];
      for (let j = 0; j < count; j++) {
        combo.push((i & 1 << j) !== 0);
      }
      combinations.push(combo);
    }
    return combinations;
  }
  function buildMaskFromCombination(parts, combo) {
    let result = "";
    let optionalIndex = 0;
    for (const part of parts) {
      if (part.type === "required") {
        result += part.content;
      } else if (part.type === "optional") {
        if (combo[optionalIndex]) {
          result += part.content;
        }
        optionalIndex++;
      }
    }
    return result;
  }
  function parseMaskStructure(mask) {
    if (!mask) {
      return { parts: [], optionalCount: 0 };
    }
    const parts = [];
    let current = "";
    let i = 0;
    let optionalCount = 0;
    let inOptional = false;
    let optionalStart = -1;
    while (i < mask.length) {
      if (mask[i] === "\\" && i + 1 < mask.length) {
        current += mask.slice(i, i + 2);
        i += 2;
        continue;
      }
      if (mask[i] === "{" && mask[i + 1] === "?") {
        if (inOptional) {
          let nestedEnd = i + 2;
          while (nestedEnd < mask.length) {
            if (mask[nestedEnd] === "\\" && nestedEnd + 1 < mask.length) {
              nestedEnd += 2;
              continue;
            }
            if (mask[nestedEnd] === "?" && mask[nestedEnd + 1] === "}") {
              nestedEnd += 2;
              break;
            }
            nestedEnd++;
          }
          const error = new Error(`Nested optional blocks not allowed at position ${i}`);
          error.rangeEnd = nestedEnd;
          throw error;
        }
        if (current) {
          parts.push({ type: "required", content: current });
          current = "";
        }
        inOptional = true;
        optionalStart = i;
        i += 2;
        continue;
      }
      if (mask[i] === "?" && mask[i + 1] === "}" && inOptional) {
        if (current.trim() === "") {
          throw new Error(`Empty optional block at position ${optionalStart}`);
        }
        parts.push({ type: "optional", content: current });
        current = "";
        inOptional = false;
        optionalCount++;
        i += 2;
        continue;
      }
      current += mask[i];
      i++;
    }
    if (inOptional) {
      throw new Error(`Unclosed optional block starting at position ${optionalStart}`);
    }
    if (current) {
      parts.push({ type: "required", content: current });
    }
    if (optionalCount > 8) {
      throw new Error(`Too many optional blocks (${optionalCount}). Maximum is 8.`);
    }
    return { parts, optionalCount };
  }
  function parseTemplateWithOptionals(mask, torrentName) {
    try {
      const parsed = parseMaskStructure(mask);
      if (parsed.optionalCount === 0) {
        return parseTemplate(mask, torrentName);
      }
      const combinations = generateCombinationsDescending(parsed.optionalCount);
      for (const combo of combinations) {
        const maskVariant = buildMaskFromCombination(parsed.parts, combo);
        const extracted = parseTemplate(maskVariant, torrentName);
        if (Object.keys(extracted).length > 0) {
          return {
            ...extracted,
            _matchedOptionals: combo,
            _optionalCount: parsed.optionalCount
          };
        }
      }
      return {};
    } catch (e) {
      throw e;
    }
  }
  function testMaskAgainstSamples(mask, sampleNames) {
    const validation = validateMaskWithDetails(mask);
    const sampleArray = Array.isArray(sampleNames) ? sampleNames : sampleNames.split("\n").map((s) => s.trim()).filter((s) => s);
    return {
      validation,
      results: sampleArray.map((name) => {
        try {
          const parsed = parseTemplateWithOptionals(mask, name);
          const { _matchedOptionals, _optionalCount, ...variables } = parsed;
          const matched = Object.keys(variables).length > 0;
          const positions = {};
          if (matched) {
            for (const [varName, value] of Object.entries(variables)) {
              const index = name.indexOf(value);
              if (index !== -1) {
                positions[varName] = { start: index, end: index + value.length };
              }
            }
          }
          return {
            name,
            matched,
            variables,
            positions,
            optionalInfo: _matchedOptionals ? {
              matched: _matchedOptionals.filter((x) => x).length,
              total: _optionalCount
            } : null
          };
        } catch (e) {
          return {
            name,
            matched: false,
            variables: {},
            positions: {},
            error: e.message
          };
        }
      })
    };
  }
  function updateMaskHighlighting(maskInput, overlayDiv) {
    if (!maskInput || !overlayDiv) return;
    const text = maskInput.value;
    const varPattern = /\$\{([^}]*)\}?/g;
    const optionalBlocks = findOptionalBlocks(text);
    const nestedOptionalErrors = findNestedOptionalErrors(text);
    const varMatches = [];
    let match;
    while ((match = varPattern.exec(text)) !== null) {
      varMatches.push({ match, index: match.index });
    }
    let highlightedHTML = buildLayeredHighlighting(text, optionalBlocks, varMatches, nestedOptionalErrors);
    overlayDiv.innerHTML = highlightedHTML;
    overlayDiv.scrollTop = maskInput.scrollTop;
    overlayDiv.scrollLeft = maskInput.scrollLeft;
  }
  function buildLayeredHighlighting(text, optionalBlocks, varMatches, nestedOptionalErrors) {
    let result = "";
    const segments = [];
    for (let i = 0; i < text.length; i++) {
      const inOptional = optionalBlocks.find((block) => i >= block.start && i < block.end);
      const varMatch = varMatches.find((v) => i >= v.index && i < v.index + v.match[0].length);
      const inNestedError = nestedOptionalErrors.find((err) => i >= err.start && i < err.end);
      const currentSegment = segments[segments.length - 1];
      if (currentSegment && currentSegment.inOptional === !!inOptional && currentSegment.varMatch === varMatch && currentSegment.inNestedError === !!inNestedError) {
        currentSegment.end = i + 1;
      } else {
        segments.push({
          start: i,
          end: i + 1,
          inOptional: !!inOptional,
          varMatch,
          inNestedError: !!inNestedError
        });
      }
    }
    for (const segment of segments) {
      const content = text.slice(segment.start, segment.end);
      let html = escapeHtml$1(content);
      if (segment.inNestedError) {
        if (segment.inOptional) {
          html = `<span class="gut-highlight-optional"><span class="gut-highlight-error">${html}</span></span>`;
        } else {
          html = `<span class="gut-highlight-error">${html}</span>`;
        }
      } else if (segment.varMatch) {
        const varName = segment.varMatch.match[1];
        const fullMatch = segment.varMatch.match[0];
        const isUnclosed = !fullMatch.endsWith("}");
        const isEmpty = varName.trim() === "";
        const isInvalid = varName && !/^[a-zA-Z0-9_]+$/.test(varName.trim());
        const isReserved = varName.trim().startsWith("_");
        let varClass = "gut-highlight-variable";
        if (isUnclosed || isEmpty) {
          varClass = "gut-highlight-error";
        } else if (isInvalid) {
          varClass = "gut-highlight-error";
        } else if (isReserved) {
          varClass = "gut-highlight-warning";
        }
        if (segment.inOptional) {
          html = `<span class="gut-highlight-optional"><span class="${varClass}">${html}</span></span>`;
        } else {
          html = `<span class="${varClass}">${html}</span>`;
        }
      } else if (segment.inOptional) {
        html = `<span class="gut-highlight-optional">${html}</span>`;
      }
      result += html;
    }
    return result;
  }
  function findOptionalBlocks(text) {
    const blocks = [];
    let i = 0;
    while (i < text.length) {
      if (text[i] === "\\" && i + 1 < text.length) {
        i += 2;
        continue;
      }
      if (text[i] === "{" && text[i + 1] === "?") {
        const start = i;
        i += 2;
        let depth = 1;
        while (i < text.length && depth > 0) {
          if (text[i] === "\\" && i + 1 < text.length) {
            i += 2;
            continue;
          }
          if (text[i] === "{" && text[i + 1] === "?") {
            depth++;
            i += 2;
          } else if (text[i] === "?" && text[i + 1] === "}") {
            depth--;
            if (depth === 0) {
              i += 2;
              blocks.push({ start, end: i });
              break;
            }
            i += 2;
          } else {
            i++;
          }
        }
        if (depth > 0) {
          blocks.push({ start, end: text.length });
        }
      } else {
        i++;
      }
    }
    return blocks;
  }
  function findNestedOptionalErrors(text) {
    const errors = [];
    let i = 0;
    let inOptional = false;
    while (i < text.length) {
      if (text[i] === "\\" && i + 1 < text.length) {
        i += 2;
        continue;
      }
      if (text[i] === "{" && text[i + 1] === "?") {
        if (inOptional) {
          const nestedStart = i;
          i += 2;
          let nestedEnd = i;
          while (nestedEnd < text.length) {
            if (text[nestedEnd] === "\\" && nestedEnd + 1 < text.length) {
              nestedEnd += 2;
              continue;
            }
            if (text[nestedEnd] === "?" && text[nestedEnd + 1] === "}") {
              nestedEnd += 2;
              break;
            }
            nestedEnd++;
          }
          errors.push({ start: nestedStart, end: nestedEnd });
          continue;
        }
        inOptional = true;
        i += 2;
        continue;
      }
      if (text[i] === "?" && text[i + 1] === "}") {
        inOptional = false;
        i += 2;
        continue;
      }
      i++;
    }
    return errors;
  }
  const ICON_ERROR = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.5"/><path d="M4.5 4.5L9.5 9.5M9.5 4.5L4.5 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>';
  const ICON_WARNING = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7 1L13 12H1L7 1Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M7 5.5V8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="10" r="0.5" fill="currentColor"/></svg>';
  const ICON_INFO = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.5"/><path d="M7 6.5V10.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="4.5" r="0.5" fill="currentColor"/></svg>';
  function renderStatusMessages(container, validation) {
    if (!container || !validation) return;
    const { errors, warnings, info, valid } = validation;
    const messages = [...errors, ...warnings, ...info];
    if (messages.length === 0 && valid) {
      container.innerHTML = `<div class="gut-status-message gut-status-info">${ICON_INFO} Add variables like \${name} to extract data.</div>`;
      container.classList.add("visible");
      return;
    }
    if (messages.length === 0) {
      container.innerHTML = "";
      container.classList.remove("visible");
      return;
    }
    const sortedMessages = messages.sort((a, b) => {
      if (a.position !== void 0 && b.position !== void 0) {
        return a.position - b.position;
      }
      if (a.position !== void 0) return -1;
      if (b.position !== void 0) return 1;
      const priority = { error: 0, warning: 1, info: 2 };
      return priority[a.type] - priority[b.type];
    });
    const priorityMessage = sortedMessages.slice(0, 3);
    const html = priorityMessage.map((msg) => {
      let className = "gut-status-message";
      let icon = "";
      switch (msg.type) {
        case "error":
          className += " gut-status-error";
          icon = ICON_ERROR;
          break;
        case "warning":
          className += " gut-status-warning";
          icon = ICON_WARNING;
          break;
        case "info":
          className += " gut-status-info";
          icon = ICON_INFO;
          break;
      }
      return `<div class="${className}">${icon} ${escapeHtml$1(msg.message)}</div>`;
    }).join("");
    if (sortedMessages.length > 3) {
      const remaining = sortedMessages.length - 3;
      const remainingHtml = `<div class="gut-status-message gut-status-info">+ ${remaining} more message${remaining === 1 ? "" : "s"}</div>`;
      container.innerHTML = html + remainingHtml;
    } else {
      container.innerHTML = html;
    }
    container.classList.add("visible");
  }
  function escapeHtml$1(text) {
    const div = document.createElement("div");
    div.textContent = text;
    return div.innerHTML;
  }
  const MODAL_HTML = (instance) => `
  <div class="gut-modal-content">
    <div class="gut-modal-tabs">
      <button class="gut-tab-btn active" data-tab="templates">Templates</button>
      <button class="gut-tab-btn" data-tab="settings">Settings</button>
      <button class="gut-tab-btn" data-tab="sandbox">Mask Sandbox</button>
    </div>

    <div class="gut-tab-content active" id="templates-tab">
      ${Object.keys(instance.templates).length === 0 ? '<div style="padding: 20px; text-align: center; color: #888;">No templates found. Create a template first.</div>' : `<div class="gut-template-list">
            ${Object.keys(instance.templates).map(
    (name) => `
                <div class="gut-template-item">
                  <span class="gut-template-name">${instance.escapeHtml(name)}</span>
                  <div class="gut-template-actions">
                    <button class="gut-btn gut-btn-secondary gut-btn-small" data-action="edit" data-template="${instance.escapeHtml(name)}">Edit</button>
                    <button class="gut-btn gut-btn-secondary gut-btn-small" data-action="clone" data-template="${instance.escapeHtml(name)}">Clone</button>
                    <button class="gut-btn gut-btn-danger gut-btn-small" data-action="delete" data-template="${instance.escapeHtml(name)}">Delete</button>
                  </div>
                </div>
              `
  ).join("")}
          </div>`}
    </div>

    <div class="gut-tab-content" id="settings-tab">
      <div class="gut-form-group">
        <label for="setting-form-selector">Target Form Selector:</label>
        <input type="text" id="setting-form-selector" value="${instance.escapeHtml(instance.config.TARGET_FORM_SELECTOR)}" placeholder="#upload_table">
      </div>

       <div class="gut-form-group">
         <div class="gut-keybinding-controls">
           <label class="gut-checkbox-label">
             <input type="checkbox" id="setting-submit-keybinding" ${instance.config.SUBMIT_KEYBINDING ? "checked" : ""}>
             <span class="gut-checkbox-text">\u26A1 Enable form submission keybinding: <span class="gut-keybinding-text">${instance.config.CUSTOM_SUBMIT_KEYBINDING || "Ctrl+Enter"}</span></span>
           </label>
           <button type="button" id="record-submit-keybinding-btn" class="gut-btn gut-btn-secondary gut-btn-small">Record</button>
         </div>
         <input type="hidden" id="custom-submit-keybinding-input" value="${instance.config.CUSTOM_SUBMIT_KEYBINDING || "Ctrl+Enter"}">
       </div>

       <div class="gut-form-group">
         <div class="gut-keybinding-controls">
           <label class="gut-checkbox-label">
             <input type="checkbox" id="setting-apply-keybinding" ${instance.config.APPLY_KEYBINDING ? "checked" : ""}>
             <span class="gut-checkbox-text">\u26A1 Enable apply template keybinding: <span class="gut-keybinding-text">${instance.config.CUSTOM_APPLY_KEYBINDING || "Ctrl+Shift+A"}</span></span>
           </label>
           <button type="button" id="record-apply-keybinding-btn" class="gut-btn gut-btn-secondary gut-btn-small">Record</button>
         </div>
         <input type="hidden" id="custom-apply-keybinding-input" value="${instance.config.CUSTOM_APPLY_KEYBINDING || "Ctrl+Shift+A"}">
       </div>

      <div class="gut-form-group">
        <label for="setting-custom-selectors">Custom Field Selectors (one per line):</label>
        <textarea id="setting-custom-selectors" rows="4" placeholder="div[data-field]
.custom-input[name]
button[data-value]">${(instance.config.CUSTOM_FIELD_SELECTORS || []).join("\n")}</textarea>
        <div style="font-size: 12px; color: #888; margin-top: 5px;">
          Additional CSS selectors to find form fields. e.g: <a href="#" id="ggn-infobox-link" class="gut-link">GGn Infobox</a>
        </div>
      </div>

      <div class="gut-form-group" id="custom-selectors-preview-group" style="display: none;">
        <label id="matched-elements-label">Matched Elements:</label>
        <div id="custom-selectors-matched" class="gut-extracted-vars">
          <div class="gut-no-variables">No elements matched by custom selectors.</div>
        </div>
      </div>

      <div class="gut-form-group">
        <label for="setting-ignored-fields">Ignored Fields (one per line):</label>
        <textarea id="setting-ignored-fields" rows="6" placeholder="linkgroup
groupid
apikey">${instance.config.IGNORED_FIELDS_BY_DEFAULT.join("\n")}</textarea>
      </div>

      <div class="gut-form-group">
        <div style="display: flex; justify-content: space-between; align-items: center;">
          <div style="display: flex; gap: 10px;">
            <button class="gut-btn gut-btn-primary" id="save-settings">Save Settings</button>
            <button class="gut-btn gut-btn-secondary" id="reset-settings">Reset to Defaults</button>
          </div>
          <button class="gut-btn gut-btn-danger" id="delete-all-config">Delete All Local Config</button>
        </div>
      </div>
    </div>

    ${SANDBOX_TAB_HTML(instance)}

    <div class="gut-modal-actions">
      <button class="gut-btn" id="close-manager">Close</button>
    </div>
  </div>
`;
  const VARIABLES_MODAL_HTML = (variables) => `
  <div class="gut-modal-content">
    <h2>Available Variables</h2>

    <div class="gut-form-group">
      <div class="gut-extracted-vars">
        ${Object.keys(variables).length === 0 ? '<div class="gut-no-variables">No variables available. Select a template with a torrent name mask to see extracted variables.</div>' : Object.entries(variables).map(
    ([name, value]) => `
                  <div class="gut-variable-item">
                    <span class="gut-variable-name">\${${name}}</span>
                    <span class="gut-variable-value">${value || '<em style="color: #666;">empty</em>'}</span>
                  </div>
                `
  ).join("")}
      </div>
    </div>

    <div class="gut-modal-actions">
      <button class="gut-btn" id="close-variables-modal">Close</button>
    </div>
  </div>
`;
  const TEMPLATE_SELECTOR_HTML = (instance) => `
  <option value="">Select Template</option>
  ${Object.keys(instance.templates).map(
    (name) => `<option value="${name}" ${name === instance.selectedTemplate ? "selected" : ""}>${name}</option>`
  ).join("")}
`;
  const TEMPLATE_LIST_HTML = (instance) => Object.keys(instance.templates).length === 0 ? '<div style="padding: 20px; text-align: center; color: #888;">No templates found. Close this dialog and create a template first.</div>' : `<div class="gut-template-list">
         ${Object.keys(instance.templates).map(
    (name) => `
             <div class="gut-template-item">
               <span class="gut-template-name">${instance.escapeHtml(name)}</span>
               <div class="gut-template-actions">
                 <button class="gut-btn gut-btn-secondary gut-btn-small" data-action="edit" data-template="${instance.escapeHtml(name)}">Edit</button>
                 <button class="gut-btn gut-btn-secondary gut-btn-small" data-action="clone" data-template="${instance.escapeHtml(name)}">Clone</button>
                 <button class="gut-btn gut-btn-danger gut-btn-small" data-action="delete" data-template="${instance.escapeHtml(name)}">Delete</button>
               </div>
             </div>
           `
  ).join("")}
       </div>`;
  const TEMPLATE_CREATOR_HTML = (formData, instance, editTemplateName, editTemplate, selectedTorrentName) => `
  <div class="gut-modal-content">
    <h2>
      ${editTemplateName ? '<button class="gut-modal-back-btn" id="back-to-manager" title="Back to Template Manager">&lt;</button>' : ""}
      ${editTemplateName ? "Edit Template" : "Create Template"}
    </h2>

    <div class="gut-form-group">
      <label for="template-name">Template Name:</label>
      <input type="text" id="template-name" placeholder="e.g., Magazine Template" value="${editTemplateName ? instance.escapeHtml(editTemplateName) : ""}">
    </div>

    <div class="gut-form-group">
      <label for="sample-torrent">Sample Torrent Name (for preview):</label>
      <input type="text" id="sample-torrent" value="${instance.escapeHtml(selectedTorrentName)}" placeholder="e.g., PCWorld - Issue 05 - 01-2024.zip">
    </div>

    <div class="gut-form-group" style="margin-bottom: 8px;">
      <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
        <label for="torrent-mask" style="margin-bottom: 0;">Torrent Name Mask:</label>
        <a href="#" id="test-mask-sandbox-link" class="gut-link" style="font-size: 11px;">Test mask in sandbox \u2192</a>
      </div>
      <div class="gut-mask-input-container">
        <div class="gut-mask-highlight-overlay" id="mask-highlight-overlay"></div>
        <input type="text" id="torrent-mask" autocomplete="off" class="gut-mask-input" placeholder="e.g., \${magazine} - Issue \${issue} - \${month}-\${year}.\${ext}" value="${editTemplate ? instance.escapeHtml(editTemplate.mask) : ""}">
      </div>
      <div class="gut-mask-cursor-info" id="mask-cursor-info"></div>
      <div class="gut-mask-status-container" id="mask-status-container"></div>
    </div>

    <div class="gut-form-group">
      <label>Extracted Variables:</label>
      <div id="extracted-variables" class="gut-extracted-vars">
        <div class="gut-no-variables">No variables defined yet. Add variables like \${name} to your mask.</div>
      </div>
    </div>

    <div class="gut-form-group">
      <div style="display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 10px;">
        <label style="margin: 0;">Form Fields:</label>
        <div style="display: flex; align-items: center; gap: 10px;">
          <input type="text" id="field-filter" placeholder="Filter fields..." autocomplete="off" style="padding: 6px 8px; border: 1px solid #404040; border-radius: 3px; background: #2a2a2a; color: #e0e0e0; font-size: 12px; min-width: 150px;">
          <button type="button" class="gut-btn gut-btn-secondary" id="toggle-unselected" style="padding: 6px 12px; font-size: 12px; white-space: nowrap;">Show Unselected</button>
        </div>
      </div>
      <div class="gut-field-list">
        ${Object.entries(formData).map(([name, fieldData]) => {
    const isIgnoredByDefault = instance.config.IGNORED_FIELDS_BY_DEFAULT.includes(
      name.toLowerCase()
    );
    const isInTemplate = editTemplate && editTemplate.fieldMappings.hasOwnProperty(name);
    const templateValue = isInTemplate ? editTemplate.fieldMappings[name] : null;
    let shouldBeChecked = isInTemplate || !isIgnoredByDefault;
    if (editTemplate && editTemplate.customUnselectedFields) {
      const customField = editTemplate.customUnselectedFields.find(
        (f) => f.field === name
      );
      if (customField) {
        shouldBeChecked = customField.selected;
      }
    }
    return `
               <div class="gut-field-row ${isIgnoredByDefault && !isInTemplate && !shouldBeChecked ? "gut-hidden" : ""}">
                 ${fieldData.type === "select" ? (() => {
      const hasVariableMatching = editTemplate && editTemplate.variableMatching && editTemplate.variableMatching[name];
      hasVariableMatching ? editTemplate.variableMatching[name] : null;
      const isVariableMode = hasVariableMatching;
      return `<div style="display: flex; align-items: flex-start; width: 100%;">
                           <a href="#" class="gut-link gut-variable-toggle" data-field="${name}" data-state="${isVariableMode ? "on" : "off"}">Match from variable: ${isVariableMode ? "ON" : "OFF"}</a>
                         </div>`;
    })() : ""}
                 <input type="checkbox" ${shouldBeChecked ? "checked" : ""} data-field="${name}">
                 <label title="${name}">${fieldData.label}:</label>
                 ${fieldData.type === "select" ? (() => {
      const hasVariableMatching = editTemplate && editTemplate.variableMatching && editTemplate.variableMatching[name];
      const variableConfig = hasVariableMatching ? editTemplate.variableMatching[name] : null;
      const isVariableMode = hasVariableMatching;
      return `<div class="gut-select-container" style="display: flex; flex-direction: column; gap: 4px; flex: 1;">
                             <div style="display: flex; flex-direction: column; align-items: flex-end;">
                               <select data-template="${name}" class="template-input gut-select select-static-mode" style="width: 100%; ${isVariableMode ? "display: none;" : ""}">
                                 ${fieldData.options.map((option) => {
        let selected = option.selected;
        if (templateValue && templateValue === option.value) {
          selected = true;
        }
        return `<option value="${instance.escapeHtml(option.value)}" ${selected ? "selected" : ""}>${instance.escapeHtml(option.text)}</option>`;
      }).join("")}
                               </select>
                             </div>
                            <div class="gut-variable-controls" data-field="${name}" style="display: ${isVariableMode ? "flex" : "none"}; gap: 8px;">
                              <select class="gut-match-type" data-field="${name}" style="padding: 6px 8px; border: 1px solid #404040; border-radius: 3px; background: #1a1a1a; color: #e0e0e0; font-size: 12px;">
                              <option value="exact" ${variableConfig && variableConfig.matchType === "exact" ? "selected" : ""}>Is exactly</option>
                              <option value="contains" ${variableConfig && variableConfig.matchType === "contains" ? "selected" : ""}>Contains</option>
                              <option value="starts" ${variableConfig && variableConfig.matchType === "starts" ? "selected" : ""}>Starts with</option>
                              <option value="ends" ${variableConfig && variableConfig.matchType === "ends" ? "selected" : ""}>Ends with</option>
                            </select>
                            <input type="text" class="gut-variable-input" data-field="${name}" placeholder="\${variable_name}" value="${variableConfig ? instance.escapeHtml(variableConfig.variableName) : ""}" style="flex: 1; padding: 6px 8px; border: 1px solid #404040; border-radius: 3px; background: #1a1a1a; color: #e0e0e0; font-size: 12px;">
                            </div>
                          </div>`;
    })() : fieldData.inputType === "checkbox" ? `<input type="checkbox" ${templateValue !== null ? templateValue ? "checked" : "" : fieldData.value ? "checked" : ""} data-template="${name}" class="template-input">` : fieldData.inputType === "radio" ? `<select data-template="${name}" class="template-input gut-select">
                              ${fieldData.radioOptions.map((option) => {
      let selected = option.checked;
      if (templateValue && templateValue === option.value) {
        selected = true;
      }
      return `<option value="${instance.escapeHtml(option.value)}" ${selected ? "selected" : ""}>${instance.escapeHtml(option.label)}</option>`;
    }).join("")}
                            </select>` : fieldData.type === "textarea" ? `<textarea data-template="${name}" class="template-input" rows="4" style="resize: vertical; width: 100%;">${templateValue !== null ? instance.escapeHtml(String(templateValue)) : instance.escapeHtml(String(fieldData.value))}</textarea>` : `<input type="text" value="${templateValue !== null ? instance.escapeHtml(String(templateValue)) : instance.escapeHtml(String(fieldData.value))}" data-template="${name}" class="template-input">`}
                 <span class="gut-preview" data-preview="${name}"></span>
               </div>
             `;
  }).join("")}
      </div>
    </div>

    <div class="gut-modal-actions">
      <button class="gut-btn" id="cancel-template">Cancel</button>
      <button class="gut-btn gut-btn-primary" id="save-template">${editTemplateName ? "Update Template" : "Save Template"}</button>
    </div>
  </div>
`;
  const SANDBOX_TAB_HTML = (instance) => {
    const savedSets = instance.sandboxSets || {};
    const currentSet = instance.currentSandboxSet || "";
    return `
    <div class="gut-tab-content" id="sandbox-tab">
      <div style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 15px;">
        <div style="display: flex; align-items: center; gap: 8px;">
          <select id="sandbox-set-select" class="gut-select" style="flex: 1;">
            <option value="">New test set</option>
            ${Object.keys(savedSets).map(
      (name) => `<option value="${instance.escapeHtml(name)}" ${name === currentSet ? "selected" : ""}>${instance.escapeHtml(name)}</option>`
    ).join("")}
          </select>
          <button class="gut-btn gut-btn-secondary gut-btn-small" id="save-sandbox-set" title="Save or update test set">Save</button>
          <button class="gut-btn gut-btn-secondary gut-btn-small" id="rename-sandbox-set" style="display: none;" title="Rename current test set">Rename</button>
          <button class="gut-btn gut-btn-danger gut-btn-small" id="delete-sandbox-set" style="display: none;" title="Delete current test set">Delete</button>
        </div>
        <div style="display: flex; justify-content: flex-start;">
          <a href="#" id="reset-sandbox-fields" class="gut-link" style="font-size: 11px;">Reset fields</a>
        </div>
      </div>

      <div class="gut-form-group">
        <label for="sandbox-mask-input">Mask:</label>
        <div class="gut-mask-input-container">
          <div class="gut-mask-highlight-overlay" id="sandbox-mask-display"></div>
          <input type="text" id="sandbox-mask-input" autocomplete="off" class="gut-mask-input" placeholder="\${artist} - \${album} {?[\${year}]?}">
        </div>
        <div class="gut-mask-cursor-info" id="sandbox-mask-cursor-info"></div>
        <div class="gut-mask-status-container" id="sandbox-mask-status"></div>
      </div>

      <div class="gut-form-group">
        <label for="sandbox-sample-input">Sample Torrent Names (one per line):</label>
        <textarea id="sandbox-sample-input" rows="8" style="font-family: 'Fira Code', monospace; font-size: 13px; resize: vertical; width: 100%;" placeholder="Artist Name - Album Title [2024]
Another Artist - Some Album
Third Example - Test [2023]"></textarea>
      </div>

      <div class="gut-form-group">
        <label id="sandbox-results-label">Match Results:</label>
        <div id="sandbox-results" class="gut-sandbox-results">
          <div class="gut-no-variables">Enter a mask and sample names to see match results.</div>
        </div>
      </div>
    </div>
  `;
  };
  const MAIN_UI_HTML = (instance) => `
  <div id="ggn-upload-templator-controls" class="ggn-upload-templator-controls" style="align-items: flex-end;">
    <div style="display: flex; flex-direction: column; gap: 5px;">
      <div style="display: flex; justify-content: space-between; align-items: center;">
        <label for="template-selector" style="font-size: 12px; color: #b0b0b0; margin: 0;">Select template</label>
        <a href="#" id="edit-selected-template-btn" class="gut-link" style="${instance.selectedTemplate && instance.selectedTemplate !== "none" && instance.templates[instance.selectedTemplate] ? "" : "display: none;"}">Edit</a>
      </div>
       <div style="display: flex; gap: 10px; align-items: center;">
         <select id="template-selector" class="gut-select">
           <option value="">Select Template</option>
           ${Object.keys(instance.templates).map(
    (name) => `<option value="${name}" ${name === instance.selectedTemplate ? "selected" : ""}>${name}</option>`
  ).join("")}
         </select>
       </div>
    </div>
    <button type="button" id="apply-template-btn" class="gut-btn gut-btn-primary">Apply Template</button>
    <button type="button" id="create-template-btn" class="gut-btn gut-btn-primary">+ Create Template</button>
    <button id="manage-templates-btn" type="button" class="gut-btn gut-btn-secondary" title="Manage Templates & Settings">Settings</button>
  </div>
  <div id="variables-row" style="display: none; padding: 10px 0; font-size: 12px; cursor: pointer; user-select: none;"></div>
`;
  function setupMaskValidation(maskInput, cursorInfoElement, statusContainer, overlayElement, onValidationChange = null) {
    const updateCursorInfo = (validation) => {
      if (!validation || validation.errors.length === 0) {
        cursorInfoElement.textContent = "";
        cursorInfoElement.style.display = "none";
        return;
      }
      const firstError = validation.errors[0];
      const errorPos = firstError.position !== void 0 ? firstError.position : null;
      if (errorPos === null) {
        cursorInfoElement.textContent = "";
        cursorInfoElement.style.display = "none";
        return;
      }
      const pos = maskInput.selectionStart;
      const maskValue = maskInput.value;
      cursorInfoElement.style.display = "block";
      const errorRangeEnd = firstError.rangeEnd !== void 0 ? firstError.rangeEnd : errorPos + 1;
      if (pos >= errorPos && pos < errorRangeEnd) {
        const charAtError = errorPos < maskValue.length ? maskValue[errorPos] : "";
        cursorInfoElement.innerHTML = `<span style="color: #f44336;">\u26A0 Error at position ${errorPos}${charAtError ? ` ('${escapeHtml(charAtError)}')` : " (end)"}</span>`;
      } else {
        const charAtPos = pos !== null && pos < maskValue.length ? maskValue[pos] : "";
        const charAtError = errorPos < maskValue.length ? maskValue[errorPos] : "";
        cursorInfoElement.innerHTML = `Cursor: ${pos}${charAtPos ? ` ('${escapeHtml(charAtPos)}')` : " (end)"} | <span style="color: #f44336;">Error: ${errorPos}${charAtError ? ` ('${escapeHtml(charAtError)}')` : " (end)"}</span>`;
      }
    };
    const performValidation = () => {
      const validation = validateMaskWithDetails(maskInput.value);
      updateMaskHighlighting(maskInput, overlayElement);
      renderStatusMessages(statusContainer, validation);
      updateCursorInfo(validation);
      if (onValidationChange) {
        onValidationChange(validation);
      }
      return validation;
    };
    maskInput.addEventListener("input", performValidation);
    maskInput.addEventListener("click", () => {
      const validation = validateMaskWithDetails(maskInput.value);
      updateCursorInfo(validation);
    });
    maskInput.addEventListener("keyup", () => {
      const validation = validateMaskWithDetails(maskInput.value);
      updateCursorInfo(validation);
    });
    maskInput.addEventListener("focus", () => {
      const validation = validateMaskWithDetails(maskInput.value);
      updateCursorInfo(validation);
    });
    return performValidation;
  }
  function injectUI(instance) {
    const fileInput = document.querySelector('input[type="file"]');
    if (!fileInput) {
      console.warn("No file input found on page, UI injection aborted");
      return;
    }
    const existingUI = document.getElementById("ggn-upload-templator-ui");
    if (existingUI) {
      existingUI.remove();
    }
    const uiContainer = document.createElement("div");
    uiContainer.id = "ggn-upload-templator-ui";
    uiContainer.innerHTML = MAIN_UI_HTML(instance);
    try {
      fileInput.parentNode.insertBefore(uiContainer, fileInput);
    } catch (error) {
      console.error("Failed to insert UI container:", error);
      return;
    }
    try {
      const createBtn = document.getElementById("create-template-btn");
      const templateSelector = document.getElementById("template-selector");
      const manageBtn = document.getElementById("manage-templates-btn");
      const editBtn = document.getElementById("edit-selected-template-btn");
      const applyBtn = document.getElementById("apply-template-btn");
      if (createBtn) {
        createBtn.addEventListener(
          "click",
          async () => await instance.showTemplateCreator()
        );
      }
      if (templateSelector) {
        templateSelector.addEventListener(
          "change",
          (e) => instance.selectTemplate(e.target.value)
        );
      }
      if (manageBtn) {
        manageBtn.addEventListener(
          "click",
          () => instance.showTemplateAndSettingsManager()
        );
      }
      if (editBtn) {
        editBtn.addEventListener("click", (e) => {
          e.preventDefault();
          instance.editTemplate(instance.selectedTemplate);
        });
      }
      if (applyBtn) {
        applyBtn.addEventListener(
          "click",
          () => instance.applyTemplateToCurrentTorrent()
        );
      }
      const variablesRow = document.getElementById("variables-row");
      if (variablesRow) {
        variablesRow.addEventListener("click", () => {
          instance.showVariablesModal();
        });
      }
    } catch (error) {
      console.error("Failed to bind UI events:", error);
    }
  }
  async function showTemplateCreator(instance, editTemplateName = null, editTemplate = null) {
    const formData = getCurrentFormData(instance.config);
    if (Object.keys(formData).length === 0) {
      alert("No form fields found on this page.");
      return;
    }
    let selectedTorrentName = "";
    let commentVariables = {};
    const fileInputs = instance.config.TARGET_FORM_SELECTOR ? document.querySelectorAll(
      `${instance.config.TARGET_FORM_SELECTOR} input[type="file"]`
    ) : document.querySelectorAll('input[type="file"]');
    for (const input of fileInputs) {
      if (input.files && input.files[0] && input.files[0].name.toLowerCase().endsWith(".torrent")) {
        try {
          const torrentData = await TorrentUtils.parseTorrentFile(input.files[0]);
          selectedTorrentName = torrentData.name || "";
          commentVariables = TorrentUtils.parseCommentVariables(
            torrentData.comment
          );
          break;
        } catch (error) {
          console.warn("Could not parse selected torrent file:", error);
        }
      }
    }
    const modal = document.createElement("div");
    modal.className = "gut-modal";
    modal.innerHTML = TEMPLATE_CREATOR_HTML(
      formData,
      instance,
      editTemplateName,
      editTemplate,
      selectedTorrentName
    );
    document.body.appendChild(modal);
    const maskInput = modal.querySelector("#torrent-mask");
    const sampleInput = modal.querySelector("#sample-torrent");
    const templateInputs = modal.querySelectorAll(".template-input");
    const cursorInfo = modal.querySelector("#mask-cursor-info");
    const toggleBtn = modal.querySelector("#toggle-unselected");
    const filterInput = modal.querySelector("#field-filter");
    const filterFields = () => {
      const filterValue = filterInput.value.toLowerCase();
      const fieldRows = modal.querySelectorAll(".gut-field-row");
      const fieldList = modal.querySelector(".gut-field-list");
      let visibleCount = 0;
      const existingMessage = fieldList.querySelector(".gut-no-results");
      if (existingMessage) {
        existingMessage.remove();
      }
      fieldRows.forEach((row) => {
        const checkbox = row.querySelector('input[type="checkbox"]');
        const label = row.querySelector("label");
        const fieldName = checkbox.dataset.field.toLowerCase();
        const labelText = label.textContent.toLowerCase();
        const matchesFilter = !filterValue || fieldName.includes(filterValue) || labelText.includes(filterValue);
        const shouldShowBasedOnSelection = checkbox.checked || !instance.hideUnselectedFields;
        const shouldShow = matchesFilter && shouldShowBasedOnSelection;
        if (shouldShow) {
          row.classList.remove("gut-hidden");
          visibleCount++;
        } else {
          row.classList.add("gut-hidden");
        }
      });
      if (filterValue && visibleCount === 0) {
        const noResultsMessage = document.createElement("div");
        noResultsMessage.className = "gut-no-results";
        noResultsMessage.style.cssText = "padding: 20px; text-align: center; color: #888; font-style: italic;";
        noResultsMessage.textContent = `No fields found matching "${filterValue}"`;
        fieldList.appendChild(noResultsMessage);
      }
    };
    const toggleUnselectedFields = () => {
      instance.hideUnselectedFields = !instance.hideUnselectedFields;
      localStorage.setItem(
        "ggn-upload-templator-hide-unselected",
        JSON.stringify(instance.hideUnselectedFields)
      );
      toggleBtn.textContent = instance.hideUnselectedFields ? "Show Unselected" : "Hide Unselected";
      filterFields();
    };
    toggleBtn.textContent = instance.hideUnselectedFields ? "Show Unselected" : "Hide Unselected";
    filterFields();
    toggleBtn.addEventListener("click", toggleUnselectedFields);
    filterInput.addEventListener("input", filterFields);
    const overlayDiv = modal.querySelector("#mask-highlight-overlay");
    const statusContainer = modal.querySelector("#mask-status-container");
    const saveButton = modal.querySelector("#save-template");
    const performValidation = setupMaskValidation(
      maskInput,
      cursorInfo,
      statusContainer,
      overlayDiv,
      (validation) => {
        saveButton.disabled = !validation.valid;
        updatePreviews();
      }
    );
    const updatePreviews = () => {
      const mask = maskInput.value;
      const sample = sampleInput.value;
      const validation = validateMaskWithDetails(mask);
      const parseResult = parseTemplateWithOptionals(mask, sample);
      const maskExtracted = { ...parseResult };
      delete maskExtracted._matchedOptionals;
      delete maskExtracted._optionalCount;
      const allVariables = { ...commentVariables, ...maskExtracted };
      const extractedVarsContainer = modal.querySelector("#extracted-variables");
      if (Object.keys(allVariables).length === 0) {
        const hasMaskVariables = validation.variables.valid.length > 0 || validation.variables.reserved.length > 0;
        if (hasMaskVariables) {
          extractedVarsContainer.innerHTML = '<div class="gut-no-variables">Select a torrent file or provide a sample torrent name to extract variables.</div>';
        } else {
          extractedVarsContainer.innerHTML = '<div class="gut-no-variables">No variables defined yet. Add variables like ${name} to your mask.</div>';
        }
      } else {
        extractedVarsContainer.innerHTML = Object.entries(allVariables).map(
          ([varName, varValue]) => `
            <div class="gut-variable-item">
              <span class="gut-variable-name">\${${escapeHtml(varName)}}</span>
              <span class="gut-variable-value ${varValue ? "" : "empty"}">${varValue ? escapeHtml(varValue) : "(empty)"}</span>
            </div>
          `
        ).join("");
      }
      if (parseResult._matchedOptionals && parseResult._optionalCount) {
        const matchCount = parseResult._matchedOptionals.filter((x) => x).length;
        const optionalInfo = document.createElement("div");
        optionalInfo.className = "gut-variable-item";
        optionalInfo.style.cssText = "background: #2a4a3a; border-left: 3px solid #4caf50;";
        optionalInfo.innerHTML = `
        <span class="gut-variable-name" style="color: #4caf50;">Optional blocks</span>
        <span class="gut-variable-value">Matched ${matchCount}/${parseResult._optionalCount}</span>
      `;
        extractedVarsContainer.appendChild(optionalInfo);
      }
      templateInputs.forEach((input) => {
        const fieldName = input.dataset.template;
        const preview = modal.querySelector(`[data-preview="${fieldName}"]`);
        if (input.type === "checkbox") {
          preview.textContent = input.checked ? "\u2713 checked" : "\u2717 unchecked";
          preview.className = "gut-preview";
        } else if (input.tagName.toLowerCase() === "select") {
          const variableToggle = modal.querySelector(
            `.gut-variable-toggle[data-field="${fieldName}"]`
          );
          const isVariableMode = variableToggle && variableToggle.dataset.state === "on";
          if (isVariableMode) {
            const variableInput = modal.querySelector(
              `.gut-variable-input[data-field="${fieldName}"]`
            );
            const matchTypeSelect = modal.querySelector(
              `.gut-match-type[data-field="${fieldName}"]`
            );
            const variableName = variableInput ? variableInput.value.trim() : "";
            const matchType = matchTypeSelect ? matchTypeSelect.value : "exact";
            if (variableName && allVariables[variableName.replace(/^\$\{|\}$/g, "")]) {
              const variableValue = allVariables[variableName.replace(/^\$\{|\}$/g, "")];
              const matchedOption = findMatchingOption(
                input.options,
                variableValue,
                matchType
              );
              if (matchedOption) {
                preview.textContent = `\u2192 "${matchedOption.text}" (matched "${variableValue}" using ${matchType})`;
                preview.className = "gut-preview active visible";
              } else {
                preview.textContent = `\u2192 No match found for "${variableValue}" using ${matchType}`;
                preview.className = "gut-preview visible";
              }
            } else if (variableName) {
              preview.textContent = `\u2192 Variable ${variableName} not found in extracted data`;
              preview.className = "gut-preview visible";
            } else {
              preview.textContent = "";
              preview.className = "gut-preview";
            }
          } else {
            preview.textContent = "";
            preview.className = "gut-preview";
          }
        } else {
          const inputValue = input.value || "";
          const interpolated = interpolate(inputValue, allVariables);
          if (inputValue.includes("${") && Object.keys(allVariables).length > 0) {
            preview.textContent = `\u2192 ${interpolated}`;
            preview.className = "gut-preview active visible";
          } else {
            preview.textContent = "";
            preview.className = "gut-preview";
          }
        }
      });
    };
    [maskInput, sampleInput, ...templateInputs].forEach((input) => {
      input.addEventListener("input", updatePreviews);
      input.addEventListener("change", updatePreviews);
    });
    maskInput.addEventListener("scroll", () => {
      const overlayDiv2 = modal.querySelector("#mask-highlight-overlay");
      if (overlayDiv2) {
        overlayDiv2.scrollTop = maskInput.scrollTop;
        overlayDiv2.scrollLeft = maskInput.scrollLeft;
      }
    });
    performValidation();
    updatePreviews();
    modal.addEventListener("change", (e) => {
      if (e.target.type === "checkbox") {
        filterFields();
      }
    });
    modal.querySelector("#cancel-template").addEventListener("click", () => {
      document.body.removeChild(modal);
    });
    modal.querySelector("#save-template").addEventListener("click", () => {
      instance.saveTemplate(modal, editTemplateName);
    });
    modal.addEventListener("click", (e) => {
      if (e.target === modal) {
        document.body.removeChild(modal);
      }
    });
    const handleEscKey = (e) => {
      if (e.key === "Escape" && document.body.contains(modal)) {
        document.body.removeChild(modal);
        document.removeEventListener("keydown", handleEscKey);
      }
    };
    document.addEventListener("keydown", handleEscKey);
    modal.addEventListener("click", (e) => {
      if (e.target.classList.contains("gut-variable-toggle")) {
        e.preventDefault();
        const fieldName = e.target.dataset.field;
        const currentState = e.target.dataset.state;
        const newState = currentState === "off" ? "on" : "off";
        e.target.dataset.state = newState;
        e.target.textContent = `Match from variable: ${newState.toUpperCase()}`;
        const staticSelect = modal.querySelector(
          `select.select-static-mode[data-template="${fieldName}"]`
        );
        const variableControls = modal.querySelector(
          `.gut-variable-controls[data-field="${fieldName}"]`
        );
        if (newState === "on") {
          staticSelect.classList.add("hidden");
          variableControls.classList.add("visible");
        } else {
          staticSelect.classList.remove("hidden");
          variableControls.classList.remove("visible");
        }
        updatePreviews();
      }
    });
    const variableInputs = modal.querySelectorAll(
      ".gut-variable-input, .gut-match-type"
    );
    variableInputs.forEach((input) => {
      input.addEventListener("input", updatePreviews);
      input.addEventListener("change", updatePreviews);
    });
    const backBtn = modal.querySelector("#back-to-manager");
    if (backBtn) {
      backBtn.addEventListener("click", () => {
        document.body.removeChild(modal);
        instance.showTemplateAndSettingsManager();
      });
    }
    const sandboxLink = modal.querySelector("#test-mask-sandbox-link");
    if (sandboxLink) {
      sandboxLink.addEventListener("click", (e) => {
        e.preventDefault();
        const mask = maskInput.value;
        const sample = sampleInput.value;
        document.body.removeChild(modal);
        instance.showSandboxWithMask(mask, sample);
      });
    }
  }
  function showVariablesModal(instance, variables) {
    const modal = document.createElement("div");
    modal.className = "gut-modal";
    modal.innerHTML = VARIABLES_MODAL_HTML(variables);
    document.body.appendChild(modal);
    modal.querySelector("#close-variables-modal").addEventListener("click", () => {
      document.body.removeChild(modal);
    });
    modal.addEventListener("click", (e) => {
      if (e.target === modal) {
        document.body.removeChild(modal);
      }
    });
    const handleEscKey = (e) => {
      if (e.key === "Escape" && document.body.contains(modal)) {
        document.body.removeChild(modal);
        document.removeEventListener("keydown", handleEscKey);
      }
    };
    document.addEventListener("keydown", handleEscKey);
  }
  function escapeHtml(text) {
    const div = document.createElement("div");
    div.textContent = text;
    return div.innerHTML;
  }
  function renderSandboxResults(modal, testResults) {
    const resultsContainer = modal.querySelector("#sandbox-results");
    const resultsLabel = modal.querySelector("#sandbox-results-label");
    if (!resultsContainer || !testResults || testResults.results.length === 0) {
      resultsContainer.innerHTML = '<div class="gut-no-variables">Enter a mask and sample names to see match results.</div>';
      resultsLabel.textContent = "Match Results:";
      return;
    }
    const matchCount = testResults.results.filter((r) => r.matched).length;
    const totalCount = testResults.results.length;
    resultsLabel.textContent = `Match Results (${matchCount}/${totalCount} matched):`;
    const html = testResults.results.map((result, resultIndex) => {
      const isMatch = result.matched;
      const icon = isMatch ? "\u2713" : "\u2717";
      const className = isMatch ? "gut-sandbox-match" : "gut-sandbox-no-match";
      let variablesHtml = "";
      if (isMatch && Object.keys(result.variables).length > 0) {
        variablesHtml = '<div class="gut-sandbox-variables" style="display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px;">' + Object.entries(result.variables).map(
          ([key, value]) => `<div class="gut-variable-item" style="margin: 0; flex: 0 0 auto; cursor: pointer;" data-result-index="${resultIndex}" data-var-name="${escapeHtml(key)}">
            <span class="gut-variable-name">\${${escapeHtml(key)}}</span><span style="display: inline-block; color: #898989; margin: 0 8px;"> = </span><span class="gut-variable-value">${value ? escapeHtml(value) : "(empty)"}</span>
          </div>`
        ).join("") + "</div>";
        if (result.optionalInfo) {
          variablesHtml += `<div style="margin-top: 8px; font-size: 11px; color: #4caf50;">
          Optional blocks: ${result.optionalInfo.matched}/${result.optionalInfo.total} matched
        </div>`;
        }
      }
      return `
      <div class="${className}" style="margin-bottom: 12px; padding: 8px; background: #1e1e1e; border-left: 3px solid ${isMatch ? "#4caf50" : "#f44336"}; border-radius: 4px;" data-result-index="${resultIndex}">
        <div style="display: flex; align-items: center; gap: 8px;">
          <span style="font-size: 16px; color: ${isMatch ? "#4caf50" : "#f44336"};">${icon}</span>
          <span class="gut-sandbox-sample-name" style="flex: 1; font-family: 'Fira Code', monospace; font-size: 13px;" data-result-index="${resultIndex}">${escapeHtml(result.name)}</span>
        </div>
        ${variablesHtml}
      </div>
    `;
    }).join("");
    resultsContainer.innerHTML = html;
    resultsContainer._testResults = testResults;
    if (!resultsContainer._hasEventListeners) {
      resultsContainer.addEventListener(
        "mouseenter",
        (e) => {
          if (e.target.classList.contains("gut-variable-item")) {
            const resultIndex = parseInt(e.target.dataset.resultIndex);
            const varName = e.target.dataset.varName;
            const currentResults = resultsContainer._testResults;
            if (!currentResults || !currentResults.results[resultIndex]) {
              return;
            }
            const result = currentResults.results[resultIndex];
            if (result.positions && result.positions[varName]) {
              const sampleNameEl = resultsContainer.querySelector(
                `.gut-sandbox-sample-name[data-result-index="${resultIndex}"]`
              );
              const pos = result.positions[varName];
              const name = result.name;
              const before = escapeHtml(name.substring(0, pos.start));
              const highlight = escapeHtml(name.substring(pos.start, pos.end));
              const after = escapeHtml(name.substring(pos.end));
              sampleNameEl.innerHTML = `${before}<span style="background: #bb86fc; color: #000; padding: 2px 4px; border-radius: 2px;">${highlight}</span>${after}`;
            }
          }
        },
        true
      );
      resultsContainer.addEventListener(
        "mouseleave",
        (e) => {
          if (e.target.classList.contains("gut-variable-item")) {
            const resultIndex = parseInt(e.target.dataset.resultIndex);
            const currentResults = resultsContainer._testResults;
            if (!currentResults || !currentResults.results[resultIndex]) {
              return;
            }
            const result = currentResults.results[resultIndex];
            const sampleNameEl = resultsContainer.querySelector(
              `.gut-sandbox-sample-name[data-result-index="${resultIndex}"]`
            );
            sampleNameEl.textContent = result.name;
          }
        },
        true
      );
      resultsContainer._hasEventListeners = true;
    }
  }
  function parseKeybinding(keybinding) {
    const parts = keybinding.split("+").map((k) => k.trim().toLowerCase());
    return {
      ctrl: parts.includes("ctrl"),
      meta: parts.includes("cmd") || parts.includes("meta"),
      shift: parts.includes("shift"),
      alt: parts.includes("alt"),
      key: parts.find((k) => !["ctrl", "cmd", "meta", "shift", "alt"].includes(k)) || "enter"
    };
  }
  function matchesKeybinding(event, keys) {
    return event.key.toLowerCase() === keys.key && !!event.ctrlKey === keys.ctrl && !!event.metaKey === keys.meta && !!event.shiftKey === keys.shift && !!event.altKey === keys.alt;
  }
  function buildKeybindingFromEvent(event) {
    const keys = [];
    if (event.ctrlKey) keys.push("Ctrl");
    if (event.metaKey) keys.push("Cmd");
    if (event.shiftKey) keys.push("Shift");
    if (event.altKey) keys.push("Alt");
    keys.push(event.key.charAt(0).toUpperCase() + event.key.slice(1));
    return keys.join("+");
  }
  const style = '#ggn-upload-templator-ui {\n    background: #1a1a1a;\n    border: 1px solid #404040;\n    border-radius: 6px;\n    padding: 15px;\n    margin: 15px 0;\n    font-family:\n        -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\n    color: #e0e0e0;\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n}\n\n.ggn-upload-templator-controls {\n    display: flex;\n    gap: 10px;\n    align-items: center;\n    flex-wrap: wrap;\n}\n\n.gut-btn {\n    padding: 8px 16px;\n    border: none;\n    border-radius: 4px;\n    cursor: pointer;\n    font-size: 14px;\n    font-weight: 500;\n    transition: all 0.2s ease;\n    text-decoration: none;\n    outline: none;\n    box-sizing: border-box;\n    height: auto;\n}\n\n.gut-btn-primary {\n    background: #0d7377;\n    color: #ffffff;\n    border: 1px solid #0d7377;\n}\n\n.gut-btn-primary:hover {\n    background: #0a5d61;\n    border-color: #0a5d61;\n    transform: translateY(-1px);\n}\n\n.gut-btn-danger {\n    background: #d32f2f;\n    color: #ffffff;\n    border: 1px solid #d32f2f;\n}\n\n.gut-btn-danger:hover:not(:disabled) {\n    background: #b71c1c;\n    border-color: #b71c1c;\n    transform: translateY(-1px);\n}\n\n.gut-btn:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n    transform: none;\n}\n\n.gut-btn:not(:disabled):active {\n    transform: translateY(0);\n}\n\n.gut-select {\n    padding: 8px 12px;\n    border: 1px solid #404040;\n    border-radius: 4px;\n    font-size: 14px;\n    min-width: 200px;\n    background: #2a2a2a;\n    color: #e0e0e0;\n    box-sizing: border-box;\n    outline: none;\n    height: auto;\n    margin: 0 !important;\n}\n\n.gut-select:focus {\n    border-color: #0d7377;\n    box-shadow: 0 0 0 2px rgba(13, 115, 119, 0.2);\n}\n\n.gut-modal {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0, 0, 0, 0.8);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    z-index: 10000;\n    padding: 20px;\n    box-sizing: border-box;\n}\n\n.gut-modal-content {\n    background: #1a1a1a;\n    border: 1px solid #404040;\n    border-radius: 8px;\n    padding: 24px;\n    max-width: 800px;\n    max-height: 80vh;\n    overflow-y: auto;\n    width: 90%;\n    color: #e0e0e0;\n    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n    box-sizing: border-box;\n}\n\n.gut-modal h2 {\n    margin: 0 0 20px 0;\n    color: #ffffff;\n    font-size: 24px;\n    font-weight: 600;\n    text-align: left;\n    position: relative;\n    display: flex;\n    align-items: center;\n    gap: 10px;\n}\n\n.gut-modal-back-btn {\n    background: none;\n    border: none;\n    color: #e0e0e0;\n    font-size: 16px;\n    cursor: pointer;\n    padding: 8px;\n    border-radius: 4px;\n    transition:\n        color 0.2s ease,\n        background-color 0.2s ease;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 40px;\n    height: 40px;\n    flex-shrink: 0;\n    font-family: monospace;\n    font-weight: bold;\n}\n\n.gut-modal-back-btn:hover {\n    color: #ffffff;\n    background-color: #333333;\n}\n\n.gut-form-group {\n    margin-bottom: 15px;\n}\n\n.gut-form-group label {\n    display: block;\n    margin-bottom: 5px;\n    font-weight: 500;\n    color: #b0b0b0;\n    font-size: 14px;\n}\n\n.gut-form-group input,\n.gut-form-group textarea {\n    width: 100%;\n    padding: 8px 12px;\n    border: 1px solid #404040;\n    border-radius: 4px;\n    font-size: 14px;\n    box-sizing: border-box;\n    background: #2a2a2a;\n    color: #e0e0e0;\n    outline: none;\n    transition: border-color 0.2s ease;\n    height: auto;\n}\n\n.gut-form-group input:focus,\n.gut-form-group textarea:focus {\n    border-color: #0d7377;\n    box-shadow: 0 0 0 2px rgba(13, 115, 119, 0.2);\n}\n\n.gut-form-group input::placeholder,\n.gut-form-group textarea::placeholder {\n    color: #666666;\n}\n\n.gut-field-list {\n    max-height: 300px;\n    overflow-y: auto;\n    border: 1px solid #404040;\n    border-radius: 4px;\n    padding: 10px;\n    background: #0f0f0f;\n}\n\n.gut-field-row {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    margin-bottom: 8px;\n    padding: 8px;\n    background: #2a2a2a;\n    border-radius: 4px;\n    border: 1px solid #404040;\n    flex-wrap: wrap;\n}\n\n.gut-field-row:hover {\n    background: #333333;\n}\n\n.gut-field-row:not(:has(input[type="checkbox"]:checked)) {\n    opacity: 0.6;\n}\n\n.gut-field-row.gut-hidden {\n    display: none;\n}\n\n.gut-field-row input[type="checkbox"] {\n    width: auto;\n    margin: 0;\n    accent-color: #0d7377;\n    cursor: pointer;\n}\n\n.gut-field-row label {\n    min-width: 150px;\n    margin: 0;\n    font-size: 13px;\n    color: #b0b0b0;\n    cursor: help;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.gut-field-row input[type="text"],\n.gut-field-row select {\n    flex: 1;\n    margin: 0;\n    padding: 6px 8px;\n    border: 1px solid #404040;\n    border-radius: 3px;\n    background: #1a1a1a;\n    color: #e0e0e0;\n    font-size: 12px;\n    outline: none;\n    height: auto;\n}\n\n.gut-field-row input[type="text"]:focus {\n    border-color: #0d7377;\n    box-shadow: 0 0 0 1px rgba(13, 115, 119, 0.3);\n}\n\n.gut-preview {\n    color: #888888;\n    font-style: italic;\n    font-size: 11px;\n    word-break: break-all;\n    flex-basis: 100%;\n    margin-top: 4px;\n    padding-left: 20px;\n    white-space: pre-wrap;\n    display: none;\n}\n\n.gut-preview.active {\n    color: #4dd0e1;\n    font-weight: bold;\n    font-style: normal;\n}\n\n.gut-preview.visible {\n    display: block;\n}\n\n.gut-modal-actions {\n    display: flex;\n    gap: 10px;\n    justify-content: flex-end;\n    margin-top: 20px;\n    padding-top: 20px;\n    border-top: 1px solid #404040;\n}\n\n.gut-status {\n    position: fixed;\n    top: 20px;\n    right: 20px;\n    background: #2e7d32;\n    color: #ffffff;\n    padding: 12px 20px;\n    border-radius: 6px;\n    z-index: 10001;\n    font-size: 14px;\n    font-weight: 500;\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n    border: 1px solid #4caf50;\n    animation: slideInRight 0.3s ease-out;\n}\n\n.gut-status.error {\n    background: #d32f2f;\n    border-color: #f44336;\n}\n\n@keyframes slideInRight {\n    from {\n        transform: translateX(100%);\n        opacity: 0;\n    }\n    to {\n        transform: translateX(0);\n        opacity: 1;\n    }\n}\n\n.gut-template-list {\n    max-height: 400px;\n    overflow-y: auto;\n    border: 1px solid #404040;\n    border-radius: 4px;\n    background: #0f0f0f;\n}\n\n.gut-template-item {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 12px 16px;\n    border-bottom: 1px solid #404040;\n    background: #2a2a2a;\n    transition: background-color 0.2s ease;\n}\n\n.gut-template-item:hover {\n    background: #333333;\n}\n\n.gut-template-item:last-child {\n    border-bottom: none;\n}\n\n.gut-template-name {\n    font-weight: 500;\n    color: #e0e0e0;\n    flex: 1;\n    margin-right: 10px;\n}\n\n.gut-template-actions {\n    display: flex;\n    gap: 8px;\n}\n\n.gut-btn-small {\n    padding: 6px 12px;\n    font-size: 12px;\n    min-width: auto;\n}\n\n.gut-btn-secondary {\n    background: #555555;\n    color: #ffffff;\n    border: 1px solid #555555;\n}\n\n.gut-btn-secondary:hover:not(:disabled) {\n    background: #666666;\n    border-color: #666666;\n    transform: translateY(-1px);\n}\n\n/* Tab styles for modal */\n.gut-modal-tabs {\n    display: flex;\n    border-bottom: 1px solid #404040;\n    margin-bottom: 20px;\n}\n\n.gut-tab-btn {\n    padding: 12px 20px;\n    background: transparent;\n    border: none;\n    color: #b0b0b0;\n    cursor: pointer;\n    font-size: 14px;\n    font-weight: 500;\n    border-bottom: 2px solid transparent;\n    transition: all 0.2s ease;\n    height: auto;\n}\n\n.gut-tab-btn:hover {\n    color: #e0e0e0;\n    background: #2a2a2a;\n}\n\n.gut-tab-btn.active {\n    color: #ffffff;\n    border-bottom-color: #0d7377;\n}\n\n.gut-tab-content {\n    display: none;\n}\n\n.gut-tab-content.active {\n    display: block;\n}\n\n/* Keybinding controls styling */\n.gut-keybinding-controls {\n    display: flex !important;\n    align-items: center !important;\n    gap: 10px !important;\n    padding: 8px 12px !important;\n    background: #2a2a2a !important;\n    border: 1px solid #404040 !important;\n    border-radius: 4px !important;\n    transition: border-color 0.2s ease !important;\n    margin: 0 !important;\n}\n\n.gut-keybinding-controls:hover {\n    border-color: #0d7377 !important;\n}\n\n/* Checkbox label styling */\n.gut-checkbox-label {\n    display: flex !important;\n    align-items: center !important;\n    gap: 10px !important;\n    cursor: pointer !important;\n    margin: 0 !important;\n}\n\n.gut-checkbox-label input[type="checkbox"] {\n    width: auto !important;\n    margin: 0 !important;\n    accent-color: #0d7377 !important;\n    cursor: pointer !important;\n}\n\n.gut-checkbox-text {\n    font-size: 14px !important;\n    font-weight: 500 !important;\n    color: #b0b0b0 !important;\n    user-select: none !important;\n}\n\n.gut-keybinding-text {\n    color: #4dd0e1 !important;\n    font-family: monospace !important;\n}\n\n.gut-variable-toggle {\n    font-size: 11px !important;\n    padding: 2px 6px !important;\n    white-space: nowrap !important;\n}\n\n/* Scrollbar styling for webkit browsers */\n.gut-field-list::-webkit-scrollbar,\n.gut-modal-content::-webkit-scrollbar {\n    width: 8px;\n}\n\n.gut-field-list::-webkit-scrollbar-track,\n.gut-modal-content::-webkit-scrollbar-track {\n    background: #0f0f0f;\n    border-radius: 4px;\n}\n\n.gut-field-list::-webkit-scrollbar-thumb,\n.gut-modal-content::-webkit-scrollbar-thumb {\n    background: #404040;\n    border-radius: 4px;\n}\n\n.gut-field-list::-webkit-scrollbar-thumb:hover,\n.gut-modal-content::-webkit-scrollbar-thumb:hover {\n    background: #555555;\n}\n\n/* Extracted variables section */\n.gut-extracted-vars {\n    border: 1px solid #404040;\n    border-radius: 4px;\n    background: #0f0f0f;\n    padding: 12px;\n    min-height: 80px;\n    max-height: 300px;\n    overflow-y: auto;\n}\n\n.gut-extracted-vars:has(.gut-no-variables) {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.gut-no-variables {\n    color: #666666;\n    font-style: italic;\n    text-align: center;\n    padding: 20px 10px;\n}\n\n.gut-variable-item {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 8px 12px;\n    margin-bottom: 6px;\n    background: #2a2a2a;\n    border: 1px solid #404040;\n    border-radius: 4px;\n    transition: background-color 0.2s ease;\n}\n\n.gut-variable-item:last-child {\n    margin-bottom: 0;\n}\n\n.gut-variable-item:hover {\n    background: #333333;\n}\n\n.gut-variable-name {\n    font-weight: 500;\n    color: #4dd0e1;\n    font-family: monospace;\n    font-size: 13px;\n}\n\n.gut-variable-value {\n    color: #e0e0e0;\n    font-size: 12px;\n    max-width: 60%;\n    word-break: break-all;\n    text-align: right;\n}\n\n.gut-variable-value.empty {\n    color: #888888;\n    font-style: italic;\n}\n\n/* Generic hyperlink style for secondary links */\n.gut-link {\n    font-size: 12px !important;\n    color: #b0b0b0 !important;\n    text-decoration: underline !important;\n    text-underline-offset: 2px !important;\n    cursor: pointer !important;\n    transition: color 0.2s ease !important;\n}\n\n.gut-link:hover {\n    color: #4dd0e1 !important;\n}\n\n.gut-variable-toggle {\n    font-size: 11px !important;\n    padding: 2px 6px !important;\n    margin-left: auto !important;\n    align-self: flex-start !important;\n    white-space: nowrap !important;\n}\n\n#variables-row {\n    cursor: pointer;\n    color: #b0b0b0;\n    transition: color 0.2s ease;\n    display: inline-block;\n}\n\n#variables-row:hover {\n    color: #4dd0e1;\n}\n\n#mask-validation-warning {\n    display: none;\n    background: #b63535;\n    color: #ffffff;\n    padding: 10px 12px;\n    border-radius: 4px;\n    margin-top: 8px;\n    font-size: 13px;\n    border: 1px solid #b71c1c;\n}\n\n#mask-validation-warning.visible {\n    display: block;\n}\n\n.gut-variable-controls {\n    display: none;\n    gap: 8px;\n    align-items: center;\n    flex: 1;\n}\n\n.gut-variable-controls.visible {\n    display: flex;\n}\n\n.gut-variable-input {\n    flex: 1;\n    min-width: 120px;\n}\n\n.gut-match-type {\n    min-width: 100px;\n}\n\n.select-static-mode {\n    display: block;\n}\n\n.select-static-mode.hidden {\n    display: none;\n}\n\n.gut-mask-input-container {\n    position: relative;\n    width: 100%;\n}\n\n.gut-mask-highlight-overlay {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    padding: 8px 12px;\n    border: 1px solid transparent;\n    border-radius: 4px;\n    font-size: 14px;\n    font-family: "Fira Code", monospace;\n    color: transparent;\n    background: #2a2a2a;\n    pointer-events: none;\n    overflow: hidden;\n    white-space: pre;\n    word-wrap: normal;\n    box-sizing: border-box;\n    line-height: normal;\n    letter-spacing: normal;\n    word-spacing: normal;\n    font-variant-ligatures: none;\n}\n\n.gut-mask-input {\n    position: relative;\n    z-index: 1;\n    background: transparent !important;\n    caret-color: #e0e0e0;\n    font-family: "Fira Code", monospace;\n    font-variant-ligatures: none;\n    letter-spacing: normal;\n    word-spacing: normal;\n}\n\n.gut-highlight-variable {\n    color: transparent;\n    background: #2d5a5e;\n    padding: 2px 0;\n    border-radius: 2px;\n}\n\n.gut-highlight-optional {\n    color: transparent;\n    background: #4f2d6a;\n    padding: 2px 0;\n    border-radius: 2px;\n}\n\n.gut-highlight-warning {\n    color: transparent;\n    background: #4d3419;\n    padding: 2px 0;\n    border-radius: 2px;\n}\n\n.gut-highlight-error {\n    color: transparent;\n    background: #963a33;\n    padding: 2px 0;\n    border-radius: 2px;\n}\n\n.gut-mask-cursor-info {\n    font-size: 11px;\n    color: #888888;\n    margin-top: 4px;\n    min-height: 16px;\n    font-family: monospace;\n    display: none;\n}\n\n.gut-mask-cursor-info:empty {\n    display: none;\n}\n\n.gut-mask-status-container {\n    display: none;\n    margin-top: 8px;\n    padding: 8px 12px;\n    border-radius: 4px;\n    background: #0f0f0f;\n    border: 1px solid #404040;\n    animation: slideDown 0.2s ease-out;\n}\n\n.gut-mask-status-container.visible {\n    display: block;\n}\n\n.gut-status-message {\n    font-size: 13px;\n    padding: 4px 0;\n    line-height: 1.4;\n    display: flex;\n    align-items: center;\n    gap: 6px;\n}\n\n.gut-status-message svg {\n    flex-shrink: 0;\n    vertical-align: middle;\n}\n\n.gut-status-message:not(:last-child) {\n    margin-bottom: 6px;\n    padding-bottom: 6px;\n    border-bottom: 1px solid #2a2a2a;\n}\n\n.gut-status-error {\n    color: #f44336;\n}\n\n.gut-status-warning {\n    color: #ff9800;\n}\n\n.gut-status-info {\n    color: #888888;\n}\n\n.gut-status-success {\n    color: #4caf50;\n}\n\n@keyframes slideDown {\n    from {\n        opacity: 0;\n        transform: translateY(-4px);\n    }\n    to {\n        opacity: 1;\n        transform: translateY(0);\n    }\n}\n\n.gut-sandbox-results {\n    margin-top: 12px;\n    padding: 12px;\n    background: #0f0f0f;\n    border: 1px solid #404040;\n    border-radius: 4px;\n    max-height: 400px;\n    overflow-y: auto;\n}\n\n.gut-sandbox-match,\n.gut-sandbox-no-match {\n    padding: 10px;\n    margin-bottom: 10px;\n    border-radius: 4px;\n    border-left: 3px solid;\n}\n\n.gut-sandbox-match {\n    background: rgba(76, 175, 80, 0.1);\n    border-left-color: #4caf50;\n}\n\n.gut-sandbox-no-match {\n    background: rgba(244, 67, 54, 0.1);\n    border-left-color: #f44336;\n}\n\n.gut-sandbox-sample-name {\n    font-family: "Fira Code", monospace;\n    font-size: 13px;\n    margin-bottom: 6px;\n    display: flex;\n    align-items: center;\n    gap: 8px;\n}\n\n.gut-sandbox-sample-name svg {\n    flex-shrink: 0;\n}\n\n.gut-sandbox-variables {\n    margin-top: 8px;\n    padding-top: 8px;\n    border-top: 1px solid #2a2a2a;\n}\n\n.gut-sandbox-variables-title {\n    font-size: 11px;\n    font-weight: 500;\n    color: #888;\n    margin-bottom: 6px;\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n}\n\n.gut-sandbox-variable-item {\n    font-size: 12px;\n    padding: 3px 0;\n    display: flex;\n    gap: 8px;\n    align-items: baseline;\n}\n\n.gut-sandbox-variable-name {\n    color: #64b5f6;\n    font-family: "Fira Code", monospace;\n}\n\n.gut-sandbox-variable-value {\n    color: #a5d6a7;\n    font-family: "Fira Code", monospace;\n}\n\n.gut-sandbox-optionals {\n    margin-top: 6px;\n    font-size: 11px;\n    color: #b39ddb;\n}\n';
  const firaCodeFont = `
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:[email protected]&display=swap');
`;
  GM_addStyle(firaCodeFont);
  GM_addStyle(style);
  class GGnUploadTemplator {
    constructor() {
      try {
        this.templates = JSON.parse(
          localStorage.getItem("ggn-upload-templator-templates") || "{}"
        );
      } catch (error) {
        console.error("Failed to load templates:", error);
        this.templates = {};
      }
      try {
        this.selectedTemplate = localStorage.getItem("ggn-upload-templator-selected") || null;
      } catch (error) {
        console.error("Failed to load selected template:", error);
        this.selectedTemplate = null;
      }
      try {
        this.hideUnselectedFields = JSON.parse(
          localStorage.getItem("ggn-upload-templator-hide-unselected") || "true"
        );
      } catch (error) {
        console.error("Failed to load hide unselected setting:", error);
        this.hideUnselectedFields = true;
      }
      try {
        this.config = {
          ...DEFAULT_CONFIG,
          ...JSON.parse(
            localStorage.getItem("ggn-upload-templator-settings") || "{}"
          )
        };
      } catch (error) {
        console.error("Failed to load config:", error);
        this.config = { ...DEFAULT_CONFIG };
      }
      try {
        this.sandboxSets = JSON.parse(
          localStorage.getItem("ggn-upload-templator-sandbox-sets") || "{}"
        );
      } catch (error) {
        console.error("Failed to load sandbox sets:", error);
        this.sandboxSets = {};
      }
      try {
        this.currentSandboxSet = localStorage.getItem("ggn-upload-templator-sandbox-current") || "";
      } catch (error) {
        console.error("Failed to load current sandbox set:", error);
        this.currentSandboxSet = "";
      }
      logDebug("Initialized core state", {
        templates: Object.keys(this.templates),
        selectedTemplate: this.selectedTemplate,
        hideUnselectedFields: this.hideUnselectedFields,
        config: this.config
      });
      this.init();
    }
    init() {
      logDebug("Initializing...");
      try {
        injectUI(this);
      } catch (error) {
        console.error("UI injection failed:", error);
      }
      try {
        this.watchFileInputs();
      } catch (error) {
        console.error("File input watching setup failed:", error);
      }
      if (this.config.SUBMIT_KEYBINDING) {
        try {
          this.setupSubmitKeybinding();
        } catch (error) {
          console.error("Submit keybinding setup failed:", error);
        }
      }
      if (this.config.APPLY_KEYBINDING) {
        try {
          this.setupApplyKeybinding();
        } catch (error) {
          console.error("Apply keybinding setup failed:", error);
        }
      }
      logDebug("Initialized");
    }
    // Show template creation modal
    async showTemplateCreator(editTemplateName = null, editTemplate = null) {
      await showTemplateCreator(this, editTemplateName, editTemplate);
    }
    // Get current variables (mask + comment)
    async getCurrentVariables() {
      const commentVariables = {};
      const maskVariables = {};
      if (this.selectedTemplate && this.selectedTemplate !== "none") {
        const template = this.templates[this.selectedTemplate];
        if (template) {
          const fileInputs = this.config.TARGET_FORM_SELECTOR ? document.querySelectorAll(
            `${this.config.TARGET_FORM_SELECTOR} input[type="file"]`
          ) : document.querySelectorAll('input[type="file"]');
          for (const input of fileInputs) {
            if (input.files && input.files[0] && input.files[0].name.toLowerCase().endsWith(".torrent")) {
              try {
                const torrentData = await TorrentUtils.parseTorrentFile(
                  input.files[0]
                );
                Object.assign(
                  commentVariables,
                  TorrentUtils.parseCommentVariables(torrentData.comment)
                );
                const parseResult = parseTemplateWithOptionals(
                  template.mask,
                  torrentData.name
                );
                const { _matchedOptionals, _optionalCount, ...extracted } = parseResult;
                Object.assign(maskVariables, extracted);
                break;
              } catch (error) {
                console.warn("Could not parse torrent file:", error);
              }
            }
          }
        }
      }
      return {
        all: { ...commentVariables, ...maskVariables },
        comment: commentVariables,
        mask: maskVariables
      };
    }
    // Show variables modal
    async showVariablesModal() {
      const variables = await this.getCurrentVariables();
      showVariablesModal(this, variables.all);
    }
    // Update variable count display
    async updateVariableCount() {
      const variables = await this.getCurrentVariables();
      const commentCount = Object.keys(variables.comment).length;
      const maskCount = Object.keys(variables.mask).length;
      const totalCount = commentCount + maskCount;
      const variablesRow = document.getElementById("variables-row");
      if (variablesRow) {
        if (totalCount === 0) {
          variablesRow.style.display = "none";
        } else {
          variablesRow.style.display = "";
          const parts = [];
          if (commentCount > 0) {
            parts.push(`Comment [${commentCount}]`);
          }
          if (maskCount > 0) {
            parts.push(`Mask [${maskCount}]`);
          }
          variablesRow.innerHTML = `Available variables: ${parts.join(", ")}`;
        }
      }
    }
    // Save template from modal
    saveTemplate(modal, editingTemplateName = null) {
      const name = modal.querySelector("#template-name").value.trim();
      const mask = modal.querySelector("#torrent-mask").value.trim();
      if (!name || !mask) {
        alert("Please provide both template name and torrent mask.");
        return;
      }
      if (editingTemplateName && name !== editingTemplateName && this.templates[name] || !editingTemplateName && this.templates[name]) {
        if (!confirm(`Template "${name}" already exists. Overwrite?`)) {
          return;
        }
      }
      const fieldMappings = {};
      const variableMatchingConfig = {};
      const checkedFields = modal.querySelectorAll(
        '.gut-field-row input[type="checkbox"]:checked'
      );
      checkedFields.forEach((checkbox) => {
        const fieldName = checkbox.dataset.field;
        const templateInput = modal.querySelector(
          `[data-template="${fieldName}"]`
        );
        if (templateInput) {
          if (templateInput.type === "checkbox") {
            fieldMappings[fieldName] = templateInput.checked;
          } else if (templateInput.tagName.toLowerCase() === "select") {
            const variableToggle = modal.querySelector(
              `.gut-variable-toggle[data-field="${fieldName}"]`
            );
            const isVariableMode = variableToggle && variableToggle.dataset.state === "on";
            if (isVariableMode) {
              const variableInput = modal.querySelector(
                `.gut-variable-input[data-field="${fieldName}"]`
              );
              const matchTypeSelect = modal.querySelector(
                `.gut-match-type[data-field="${fieldName}"]`
              );
              variableMatchingConfig[fieldName] = {
                variableName: variableInput ? variableInput.value.trim() : "",
                matchType: matchTypeSelect ? matchTypeSelect.value : "exact"
              };
              fieldMappings[fieldName] = variableInput ? variableInput.value.trim() : "";
            } else {
              fieldMappings[fieldName] = templateInput.value;
            }
          } else {
            fieldMappings[fieldName] = templateInput.value;
          }
        }
      });
      const allFieldRows = modal.querySelectorAll(".gut-field-row");
      const customUnselectedFields = [];
      allFieldRows.forEach((row) => {
        const checkbox = row.querySelector('input[type="checkbox"]');
        if (checkbox) {
          const fieldName = checkbox.dataset.field;
          const isDefaultIgnored = this.config.IGNORED_FIELDS_BY_DEFAULT.includes(
            fieldName.toLowerCase()
          );
          const isCurrentlyChecked = checkbox.checked;
          if (isDefaultIgnored && isCurrentlyChecked || !isDefaultIgnored && !isCurrentlyChecked) {
            customUnselectedFields.push({
              field: fieldName,
              selected: isCurrentlyChecked
            });
          }
        }
      });
      if (editingTemplateName && name !== editingTemplateName) {
        delete this.templates[editingTemplateName];
        if (this.selectedTemplate === editingTemplateName) {
          this.selectedTemplate = name;
          localStorage.setItem("ggn-upload-templator-selected", name);
        }
      }
      this.templates[name] = {
        mask,
        fieldMappings,
        customUnselectedFields: customUnselectedFields.length > 0 ? customUnselectedFields : void 0,
        variableMatching: Object.keys(variableMatchingConfig).length > 0 ? variableMatchingConfig : void 0
      };
      localStorage.setItem(
        "ggn-upload-templator-templates",
        JSON.stringify(this.templates)
      );
      this.updateTemplateSelector();
      this.updateVariableCount();
      const action = editingTemplateName ? "updated" : "saved";
      this.showStatus(`Template "${name}" ${action} successfully!`);
      document.body.removeChild(modal);
    }
    // Update template selector dropdown
    updateTemplateSelector() {
      const selector = document.getElementById("template-selector");
      if (!selector) return;
      selector.innerHTML = TEMPLATE_SELECTOR_HTML(this);
      this.updateEditButtonVisibility();
    }
    // Update edit button visibility based on selected template
    updateEditButtonVisibility() {
      const editBtn = document.getElementById("edit-selected-template-btn");
      if (!editBtn) return;
      const shouldShow = this.selectedTemplate && this.selectedTemplate !== "none" && this.templates[this.selectedTemplate];
      editBtn.style.display = shouldShow ? "" : "none";
    }
    // Select template
    selectTemplate(templateName) {
      this.selectedTemplate = templateName || null;
      if (templateName) {
        localStorage.setItem("ggn-upload-templator-selected", templateName);
      } else {
        localStorage.removeItem("ggn-upload-templator-selected");
      }
      this.updateEditButtonVisibility();
      this.updateVariableCount();
      if (templateName === "none") {
        this.showStatus("No template selected - auto-fill disabled");
      } else if (templateName) {
        this.showStatus(`Template "${templateName}" selected`);
        this.checkAndApplyToExistingTorrent(templateName);
      }
    }
    // Apply template to form
    applyTemplate(templateName, torrentName, commentVariables = {}) {
      const template = this.templates[templateName];
      if (!template) return;
      const extracted = parseTemplateWithOptionals(template.mask, torrentName);
      let appliedCount = 0;
      Object.entries(template.fieldMappings).forEach(
        ([fieldName, valueTemplate]) => {
          const firstElement = findElementByFieldName(fieldName, this.config);
          if (firstElement && firstElement.type === "radio") {
            const formPrefix = this.config.TARGET_FORM_SELECTOR ? `${this.config.TARGET_FORM_SELECTOR} ` : "";
            const radioButtons = document.querySelectorAll(
              `${formPrefix}input[name="${fieldName}"][type="radio"]`
            );
            const newValue = interpolate(
              String(valueTemplate),
              extracted,
              commentVariables
            );
            radioButtons.forEach((radio) => {
              if (radio.hasAttribute("disabled")) {
                radio.removeAttribute("disabled");
              }
              const shouldBeChecked = radio.value === newValue;
              if (shouldBeChecked !== radio.checked) {
                radio.checked = shouldBeChecked;
                if (shouldBeChecked) {
                  radio.dispatchEvent(new Event("input", { bubbles: true }));
                  radio.dispatchEvent(new Event("change", { bubbles: true }));
                  appliedCount++;
                }
              }
            });
          } else if (firstElement) {
            if (firstElement.hasAttribute("disabled")) {
              firstElement.removeAttribute("disabled");
            }
            if (firstElement.type === "checkbox") {
              let newChecked;
              if (typeof valueTemplate === "boolean") {
                newChecked = valueTemplate;
              } else {
                const interpolated = interpolate(
                  String(valueTemplate),
                  extracted,
                  commentVariables
                );
                newChecked = /^(true|1|yes|on)$/i.test(interpolated);
              }
              if (newChecked !== firstElement.checked) {
                firstElement.checked = newChecked;
                firstElement.dispatchEvent(new Event("input", { bubbles: true }));
                firstElement.dispatchEvent(
                  new Event("change", { bubbles: true })
                );
                appliedCount++;
              }
            } else {
              const interpolated = interpolate(
                String(valueTemplate),
                extracted,
                commentVariables
              );
              if (firstElement.value !== interpolated) {
                firstElement.value = interpolated;
                firstElement.dispatchEvent(new Event("input", { bubbles: true }));
                firstElement.dispatchEvent(
                  new Event("change", { bubbles: true })
                );
                appliedCount++;
              }
            }
          }
        }
      );
      if (appliedCount > 0) {
        this.showStatus(
          `Template "${templateName}" applied to ${appliedCount} field(s)`
        );
      }
    }
    // Check for existing torrent file and apply template
    async checkAndApplyToExistingTorrent(templateName) {
      if (!templateName || templateName === "none") return;
      const fileInputs = this.config.TARGET_FORM_SELECTOR ? document.querySelectorAll(
        `${this.config.TARGET_FORM_SELECTOR} input[type="file"]`
      ) : document.querySelectorAll('input[type="file"]');
      for (const input of fileInputs) {
        if (input.files && input.files[0] && input.files[0].name.toLowerCase().endsWith(".torrent")) {
          try {
            const torrentData = await TorrentUtils.parseTorrentFile(
              input.files[0]
            );
            const commentVariables = TorrentUtils.parseCommentVariables(
              torrentData.comment
            );
            this.applyTemplate(templateName, torrentData.name, commentVariables);
            return;
          } catch (error) {
            console.warn("Could not parse existing torrent file:", error);
          }
        }
      }
    }
    // Watch file inputs for changes (no auto-application)
    watchFileInputs() {
      const fileInputs = this.config.TARGET_FORM_SELECTOR ? document.querySelectorAll(
        `${this.config.TARGET_FORM_SELECTOR} input[type="file"]`
      ) : document.querySelectorAll('input[type="file"]');
      fileInputs.forEach((input) => {
        input.addEventListener("change", (e) => {
          if (e.target.files[0] && e.target.files[0].name.toLowerCase().endsWith(".torrent")) {
            this.showStatus(
              "Torrent file selected. Click 'Apply Template' to fill form."
            );
            this.updateVariableCount();
          }
        });
      });
    }
    // Apply template to the currently selected torrent file
    async applyTemplateToCurrentTorrent() {
      if (!this.selectedTemplate || this.selectedTemplate === "none") {
        this.showStatus("No template selected", "error");
        return;
      }
      const fileInputs = this.config.TARGET_FORM_SELECTOR ? document.querySelectorAll(
        `${this.config.TARGET_FORM_SELECTOR} input[type="file"]`
      ) : document.querySelectorAll('input[type="file"]');
      for (const input of fileInputs) {
        if (input.files && input.files[0] && input.files[0].name.toLowerCase().endsWith(".torrent")) {
          try {
            const torrentData = await TorrentUtils.parseTorrentFile(
              input.files[0]
            );
            const commentVariables = TorrentUtils.parseCommentVariables(
              torrentData.comment
            );
            this.applyTemplate(
              this.selectedTemplate,
              torrentData.name,
              commentVariables
            );
            return;
          } catch (error) {
            console.error("Error processing torrent file:", error);
            this.showStatus("Error processing torrent file", "error");
          }
        }
      }
      this.showStatus("No torrent file selected", "error");
    }
    // Setup global keybinding for form submission
    setupSubmitKeybinding() {
      const keybinding = this.config.CUSTOM_SUBMIT_KEYBINDING || "Ctrl+Enter";
      const keys = parseKeybinding(keybinding);
      document.addEventListener("keydown", (e) => {
        if (matchesKeybinding(e, keys)) {
          e.preventDefault();
          const targetForm = document.querySelector(
            this.config.TARGET_FORM_SELECTOR
          );
          if (targetForm) {
            const submitButton = targetForm.querySelector(
              'input[type="submit"], button[type="submit"]'
            ) || targetForm.querySelector(
              'input[name*="submit"], button[name*="submit"]'
            ) || targetForm.querySelector(".submit-btn, #submit-btn");
            if (submitButton) {
              this.showStatus(`Form submitted via ${keybinding}`);
              submitButton.click();
            } else {
              this.showStatus(`Form submitted via ${keybinding}`);
              targetForm.submit();
            }
          }
        }
      });
    }
    // Setup global keybinding for applying template
    setupApplyKeybinding() {
      const keybinding = this.config.CUSTOM_APPLY_KEYBINDING || "Ctrl+Shift+A";
      const keys = parseKeybinding(keybinding);
      document.addEventListener("keydown", (e) => {
        if (matchesKeybinding(e, keys)) {
          e.preventDefault();
          this.applyTemplateToCurrentTorrent();
        }
      });
    }
    // Show combined template and settings manager modal
    showTemplateAndSettingsManager() {
      const modal = document.createElement("div");
      modal.className = "gut-modal";
      modal.innerHTML = MODAL_HTML(this);
      document.body.appendChild(modal);
      modal.querySelectorAll(".gut-tab-btn").forEach((btn) => {
        btn.addEventListener("click", (e) => {
          const tabName = e.target.dataset.tab;
          modal.querySelectorAll(".gut-tab-btn").forEach((b) => b.classList.remove("active"));
          e.target.classList.add("active");
          modal.querySelectorAll(".gut-tab-content").forEach((c) => c.classList.remove("active"));
          modal.querySelector(`#${tabName}-tab`).classList.add("active");
        });
      });
      const customSelectorsTextarea = modal.querySelector(
        "#setting-custom-selectors"
      );
      const previewGroup = modal.querySelector("#custom-selectors-preview-group");
      const matchedContainer = modal.querySelector("#custom-selectors-matched");
      const updateCustomSelectorsPreview = () => {
        const selectorsText = customSelectorsTextarea.value.trim();
        const selectors = selectorsText.split("\n").map((selector) => selector.trim()).filter((selector) => selector);
        const originalSelectors = this.config.CUSTOM_FIELD_SELECTORS;
        this.config.CUSTOM_FIELD_SELECTORS = selectors;
        if (selectors.length === 0) {
          previewGroup.style.display = "none";
          this.config.CUSTOM_FIELD_SELECTORS = originalSelectors;
          return;
        }
        previewGroup.style.display = "block";
        let matchedElements = [];
        const formSelector = modal.querySelector("#setting-form-selector").value.trim() || this.config.TARGET_FORM_SELECTOR;
        const targetForm = document.querySelector(formSelector);
        selectors.forEach((selector) => {
          try {
            const elements = targetForm ? targetForm.querySelectorAll(selector) : document.querySelectorAll(selector);
            Array.from(elements).forEach((element) => {
              const tagName = element.tagName.toLowerCase();
              const id = element.id;
              const name = element.name || element.getAttribute("name");
              const classes = element.className || "";
              const label = getFieldLabel(element, this.config);
              const elementId = element.id || element.name || `${tagName}-${Array.from(element.parentNode.children).indexOf(element)}`;
              if (!matchedElements.find((e) => e.elementId === elementId)) {
                matchedElements.push({
                  elementId,
                  element,
                  tagName,
                  id,
                  name,
                  classes,
                  label,
                  selector
                });
              }
            });
          } catch (e) {
            console.warn(`Invalid custom selector: ${selector}`, e);
          }
        });
        const matchedElementsLabel = modal.querySelector(
          "#matched-elements-label"
        );
        if (matchedElements.length === 0) {
          matchedElementsLabel.textContent = "Matched Elements:";
          matchedContainer.innerHTML = '<div class="gut-no-variables">No elements matched by custom selectors.</div>';
        } else {
          matchedElementsLabel.textContent = `Matched Elements (${matchedElements.length}):`;
          matchedContainer.innerHTML = matchedElements.map((item) => {
            const displayName = item.label || item.name || item.id || `${item.tagName}`;
            const displayInfo = [
              item.tagName.toUpperCase(),
              item.id ? `#${item.id}` : "",
              item.name ? `name="${item.name}"` : "",
              item.classes ? `.${item.classes.split(" ").filter((c) => c).join(".")}` : ""
            ].filter((info) => info).join(" ");
            return `
              <div class="gut-variable-item">
                <span class="gut-variable-name">${this.escapeHtml(displayName)}</span>
                <span class="gut-variable-value">${this.escapeHtml(displayInfo)}</span>
              </div>
            `;
          }).join("");
        }
        this.config.CUSTOM_FIELD_SELECTORS = originalSelectors;
      };
      updateCustomSelectorsPreview();
      customSelectorsTextarea.addEventListener(
        "input",
        updateCustomSelectorsPreview
      );
      modal.querySelector("#setting-form-selector").addEventListener("input", updateCustomSelectorsPreview);
      modal.querySelector("#ggn-infobox-link")?.addEventListener("click", (e) => {
        e.preventDefault();
        const currentValue = customSelectorsTextarea.value.trim();
        const ggnInfoboxSelector = ".infobox-input-holder input";
        if (!currentValue.includes(ggnInfoboxSelector)) {
          const newValue = currentValue ? `${currentValue}
${ggnInfoboxSelector}` : ggnInfoboxSelector;
          customSelectorsTextarea.value = newValue;
          updateCustomSelectorsPreview();
        }
      });
      modal.querySelector("#save-settings")?.addEventListener("click", () => {
        this.saveSettings(modal);
      });
      modal.querySelector("#reset-settings")?.addEventListener("click", () => {
        if (confirm(
          "Reset all settings to defaults? This will require a page reload."
        )) {
          this.resetSettings(modal);
        }
      });
      modal.querySelector("#delete-all-config")?.addEventListener("click", () => {
        if (confirm(
          "\u26A0\uFE0F WARNING: This will permanently delete ALL GGn Upload Templator data including templates, settings, and selected template.\n\nThis action CANNOT be undone!\n\nAre you sure you want to continue?"
        )) {
          this.deleteAllConfig();
        }
      });
      const sandboxMaskInput = modal.querySelector("#sandbox-mask-input");
      const sandboxMaskDisplay = modal.querySelector("#sandbox-mask-display");
      const sandboxSampleInput = modal.querySelector("#sandbox-sample-input");
      const sandboxResultsContainer = modal.querySelector("#sandbox-results");
      const sandboxSetSelect = modal.querySelector("#sandbox-set-select");
      const saveBtn = modal.querySelector("#save-sandbox-set");
      const renameBtn = modal.querySelector("#rename-sandbox-set");
      const deleteBtn = modal.querySelector("#delete-sandbox-set");
      const sandboxCursorInfo = modal.querySelector("#sandbox-mask-cursor-info");
      const sandboxStatusContainer = modal.querySelector("#sandbox-mask-status");
      let sandboxDebounceTimeout = null;
      let currentLoadedSet = this.currentSandboxSet || "";
      const updateButtonStates = () => {
        if (currentLoadedSet && currentLoadedSet !== "") {
          saveBtn.textContent = "Update";
          renameBtn.style.display = "";
          deleteBtn.style.display = "";
        } else {
          saveBtn.textContent = "Save";
          renameBtn.style.display = "none";
          deleteBtn.style.display = "none";
        }
      };
      updateButtonStates();
      const updateSandboxTest = () => {
        const mask = sandboxMaskInput.value;
        const sampleText = sandboxSampleInput.value.trim();
        const samples = sampleText.split("\n").map((s) => s.trim()).filter((s) => s);
        if (!mask || samples.length === 0) {
          sandboxResultsContainer.innerHTML = '<div class="gut-no-variables">Enter a mask and sample torrent names to test.</div>';
          return;
        }
        const result = testMaskAgainstSamples(mask, samples);
        renderSandboxResults(modal, result);
      };
      const debouncedUpdateSandboxTest = () => {
        if (sandboxDebounceTimeout) {
          clearTimeout(sandboxDebounceTimeout);
        }
        sandboxDebounceTimeout = setTimeout(updateSandboxTest, 300);
      };
      setupMaskValidation(
        sandboxMaskInput,
        sandboxCursorInfo,
        sandboxStatusContainer,
        sandboxMaskDisplay,
        () => {
          debouncedUpdateSandboxTest();
        }
      );
      sandboxMaskInput?.addEventListener("scroll", () => {
        sandboxMaskDisplay.scrollTop = sandboxMaskInput.scrollTop;
        sandboxMaskDisplay.scrollLeft = sandboxMaskInput.scrollLeft;
      });
      sandboxSampleInput?.addEventListener("input", debouncedUpdateSandboxTest);
      sandboxSetSelect?.addEventListener("change", () => {
        const value = sandboxSetSelect.value;
        if (!value || value === "") {
          currentLoadedSet = "";
          this.currentSandboxSet = "";
          updateButtonStates();
          return;
        }
        const sets = JSON.parse(
          localStorage.getItem("ggn-upload-templator-sandbox-sets") || "{}"
        );
        const data = sets[value];
        if (data) {
          sandboxMaskInput.value = data.mask || "";
          sandboxSampleInput.value = data.samples || "";
          updateMaskHighlighting(sandboxMaskInput, sandboxMaskDisplay);
          updateSandboxTest();
          currentLoadedSet = value;
          this.currentSandboxSet = value;
          localStorage.setItem("ggn-upload-templator-sandbox-current", value);
          updateButtonStates();
        }
      });
      saveBtn?.addEventListener("click", () => {
        if (currentLoadedSet && currentLoadedSet !== "") {
          const data = {
            mask: sandboxMaskInput.value,
            samples: sandboxSampleInput.value
          };
          this.saveSandboxSet(currentLoadedSet, data);
          this.showStatus(`Test set "${currentLoadedSet}" updated successfully!`);
        } else {
          const name = prompt("Enter a name for this test set:");
          if (name && name.trim()) {
            const trimmedName = name.trim();
            const data = {
              mask: sandboxMaskInput.value,
              samples: sandboxSampleInput.value
            };
            this.saveSandboxSet(trimmedName, data);
            this.currentSandboxSet = trimmedName;
            currentLoadedSet = trimmedName;
            localStorage.setItem("ggn-upload-templator-sandbox-current", trimmedName);
            const existingOption = sandboxSetSelect.querySelector(`option[value="${trimmedName}"]`);
            if (existingOption) {
              existingOption.selected = true;
            } else {
              const newOption = document.createElement("option");
              newOption.value = trimmedName;
              newOption.textContent = trimmedName;
              sandboxSetSelect.appendChild(newOption);
              newOption.selected = true;
            }
            updateButtonStates();
            this.showStatus(`Test set "${trimmedName}" saved successfully!`);
          }
        }
      });
      deleteBtn?.addEventListener("click", () => {
        if (!currentLoadedSet || currentLoadedSet === "") {
          return;
        }
        if (confirm(`Delete test set "${currentLoadedSet}"?`)) {
          this.deleteSandboxSet(currentLoadedSet);
          const option = sandboxSetSelect.querySelector(
            `option[value="${currentLoadedSet}"]`
          );
          if (option) {
            option.remove();
          }
          sandboxSetSelect.value = "";
          currentLoadedSet = "";
          this.currentSandboxSet = "";
          localStorage.setItem("ggn-upload-templator-sandbox-current", "");
          sandboxMaskInput.value = "";
          sandboxSampleInput.value = "";
          sandboxResultsContainer.innerHTML = '<div class="gut-no-variables">Enter a mask and sample torrent names to test.</div>';
          updateButtonStates();
          this.showStatus(`Test set deleted successfully!`);
        }
      });
      renameBtn?.addEventListener("click", () => {
        if (!currentLoadedSet || currentLoadedSet === "") {
          return;
        }
        const newName = prompt(`Rename test set "${currentLoadedSet}" to:`, currentLoadedSet);
        if (!newName || !newName.trim() || newName.trim() === currentLoadedSet) {
          return;
        }
        const trimmedName = newName.trim();
        if (this.sandboxSets[trimmedName]) {
          alert(`A test set named "${trimmedName}" already exists.`);
          return;
        }
        const data = this.sandboxSets[currentLoadedSet];
        this.sandboxSets[trimmedName] = data;
        delete this.sandboxSets[currentLoadedSet];
        localStorage.setItem(
          "ggn-upload-templator-sandbox-sets",
          JSON.stringify(this.sandboxSets)
        );
        const option = sandboxSetSelect.querySelector(
          `option[value="${currentLoadedSet}"]`
        );
        if (option) {
          option.value = trimmedName;
          option.textContent = trimmedName;
          option.selected = true;
        }
        currentLoadedSet = trimmedName;
        this.currentSandboxSet = trimmedName;
        localStorage.setItem("ggn-upload-templator-sandbox-current", trimmedName);
        this.showStatus(`Test set renamed to "${trimmedName}" successfully!`);
      });
      const resetFieldsLink = modal.querySelector("#reset-sandbox-fields");
      resetFieldsLink?.addEventListener("click", (e) => {
        e.preventDefault();
        sandboxMaskInput.value = "";
        sandboxSampleInput.value = "";
        sandboxResultsContainer.innerHTML = '<div class="gut-no-variables">Enter a mask and sample names to see match results.</div>';
        const resultsLabel = modal.querySelector("#sandbox-results-label");
        if (resultsLabel) {
          resultsLabel.textContent = "Match Results:";
        }
        updateMaskHighlighting(sandboxMaskInput, sandboxMaskDisplay);
      });
      if (sandboxMaskInput && currentLoadedSet && currentLoadedSet !== "") {
        const sets = JSON.parse(
          localStorage.getItem("ggn-upload-templator-sandbox-sets") || "{}"
        );
        const data = sets[currentLoadedSet];
        if (data) {
          sandboxMaskInput.value = data.mask || "";
          sandboxSampleInput.value = data.samples || "";
          updateMaskHighlighting(sandboxMaskInput, sandboxMaskDisplay);
          updateSandboxTest();
        }
      } else if (sandboxMaskInput) {
        updateMaskHighlighting(sandboxMaskInput, sandboxMaskDisplay);
        if (sandboxMaskInput.value && sandboxSampleInput.value) {
          updateSandboxTest();
        }
      }
      let isRecording = false;
      const setupRecordKeybindingHandler = (inputSelector, keybindingSpanIndex, recordBtnSelector) => {
        modal.querySelector(recordBtnSelector)?.addEventListener("click", () => {
          const input = modal.querySelector(inputSelector);
          const keybindingSpans = modal.querySelectorAll(".gut-keybinding-text");
          const keybindingSpan = keybindingSpans[keybindingSpanIndex];
          const recordBtn = modal.querySelector(recordBtnSelector);
          recordBtn.textContent = "Press keys...";
          recordBtn.disabled = true;
          isRecording = true;
          const handleKeydown = (e) => {
            e.preventDefault();
            const isModifierKey = ["Control", "Alt", "Shift", "Meta"].includes(
              e.key
            );
            if (e.key === "Escape") {
              recordBtn.textContent = "Record";
              recordBtn.disabled = false;
              isRecording = false;
              document.removeEventListener("keydown", handleKeydown);
              return;
            }
            if (!isModifierKey) {
              const keybinding = buildKeybindingFromEvent(e);
              input.value = keybinding;
              if (keybindingSpan) {
                keybindingSpan.textContent = keybinding;
              }
              recordBtn.textContent = "Record";
              recordBtn.disabled = false;
              isRecording = false;
              document.removeEventListener("keydown", handleKeydown);
            }
          };
          document.addEventListener("keydown", handleKeydown);
        });
      };
      setupRecordKeybindingHandler(
        "#custom-submit-keybinding-input",
        0,
        "#record-submit-keybinding-btn"
      );
      setupRecordKeybindingHandler(
        "#custom-apply-keybinding-input",
        1,
        "#record-apply-keybinding-btn"
      );
      modal.addEventListener("click", (e) => {
        if (e.target === modal) {
          document.body.removeChild(modal);
          return;
        }
        const action = e.target.dataset.action;
        const templateName = e.target.dataset.template;
        if (action && templateName) {
          switch (action) {
            case "edit":
              document.body.removeChild(modal);
              this.editTemplate(templateName);
              break;
            case "clone":
              this.cloneTemplate(templateName);
              this.refreshTemplateManager(modal);
              break;
            case "delete":
              if (confirm(`Delete template "${templateName}"?`)) {
                this.deleteTemplate(templateName);
                this.refreshTemplateManager(modal);
              }
              break;
          }
        }
      });
      modal.querySelector("#close-manager").addEventListener("click", () => {
        document.body.removeChild(modal);
      });
      const handleEscKey = (e) => {
        if (e.key === "Escape" && document.body.contains(modal) && !isRecording) {
          document.body.removeChild(modal);
          document.removeEventListener("keydown", handleEscKey);
        }
      };
      document.addEventListener("keydown", handleEscKey);
    }
    // Save settings from modal
    saveSettings(modal) {
      const formSelector = modal.querySelector("#setting-form-selector").value.trim();
      const submitKeybinding = modal.querySelector(
        "#setting-submit-keybinding"
      ).checked;
      const customSubmitKeybinding = modal.querySelector("#custom-submit-keybinding-input").value.trim();
      const applyKeybinding = modal.querySelector(
        "#setting-apply-keybinding"
      ).checked;
      const customApplyKeybinding = modal.querySelector("#custom-apply-keybinding-input").value.trim();
      const customSelectorsText = modal.querySelector("#setting-custom-selectors").value.trim();
      const customSelectors = customSelectorsText.split("\n").map((selector) => selector.trim()).filter((selector) => selector);
      const ignoredFieldsText = modal.querySelector("#setting-ignored-fields").value.trim();
      const ignoredFields = ignoredFieldsText.split("\n").map((field) => field.trim()).filter((field) => field);
      this.config = {
        TARGET_FORM_SELECTOR: formSelector || DEFAULT_CONFIG.TARGET_FORM_SELECTOR,
        SUBMIT_KEYBINDING: submitKeybinding,
        CUSTOM_SUBMIT_KEYBINDING: customSubmitKeybinding || DEFAULT_CONFIG.CUSTOM_SUBMIT_KEYBINDING,
        APPLY_KEYBINDING: applyKeybinding,
        CUSTOM_APPLY_KEYBINDING: customApplyKeybinding || DEFAULT_CONFIG.CUSTOM_APPLY_KEYBINDING,
        CUSTOM_FIELD_SELECTORS: customSelectors.length > 0 ? customSelectors : DEFAULT_CONFIG.CUSTOM_FIELD_SELECTORS,
        IGNORED_FIELDS_BY_DEFAULT: ignoredFields.length > 0 ? ignoredFields : DEFAULT_CONFIG.IGNORED_FIELDS_BY_DEFAULT
      };
      localStorage.setItem(
        "ggn-upload-templator-settings",
        JSON.stringify(this.config)
      );
      this.showStatus(
        "Settings saved successfully! Reload the page for some changes to take effect."
      );
    }
    // Reset settings to defaults
    resetSettings(modal) {
      localStorage.removeItem("ggn-upload-templator-settings");
      this.config = { ...DEFAULT_CONFIG };
      modal.querySelector("#setting-form-selector").value = this.config.TARGET_FORM_SELECTOR;
      modal.querySelector("#setting-submit-keybinding").checked = this.config.SUBMIT_KEYBINDING;
      modal.querySelector("#custom-submit-keybinding-input").value = this.config.CUSTOM_SUBMIT_KEYBINDING;
      modal.querySelector("#setting-apply-keybinding").checked = this.config.APPLY_KEYBINDING;
      modal.querySelector("#custom-apply-keybinding-input").value = this.config.CUSTOM_APPLY_KEYBINDING;
      modal.querySelector("#setting-custom-selectors").value = this.config.CUSTOM_FIELD_SELECTORS.join("\n");
      modal.querySelector("#setting-ignored-fields").value = this.config.IGNORED_FIELDS_BY_DEFAULT.join("\n");
      const submitKeybindingSpan = modal.querySelector(".gut-keybinding-text");
      submitKeybindingSpan.textContent = this.config.CUSTOM_SUBMIT_KEYBINDING;
      const applyKeybindingSpans = modal.querySelectorAll(".gut-keybinding-text");
      if (applyKeybindingSpans.length > 1) {
        applyKeybindingSpans[1].textContent = this.config.CUSTOM_APPLY_KEYBINDING;
      }
      this.showStatus(
        "Settings reset to defaults! Reload the page for changes to take effect."
      );
    }
    // Delete all local configuration
    deleteAllConfig() {
      localStorage.removeItem("ggn-upload-templator-templates");
      localStorage.removeItem("ggn-upload-templator-selected");
      localStorage.removeItem("ggn-upload-templator-hide-unselected");
      localStorage.removeItem("ggn-upload-templator-settings");
      this.templates = {};
      this.selectedTemplate = null;
      this.hideUnselectedFields = true;
      this.config = { ...DEFAULT_CONFIG };
      this.updateTemplateSelector();
      this.showStatus(
        "All local configuration deleted! Reload the page for changes to take full effect.",
        "success"
      );
    }
    // Delete template by name
    deleteTemplate(templateName) {
      delete this.templates[templateName];
      localStorage.setItem(
        "ggn-upload-templator-templates",
        JSON.stringify(this.templates)
      );
      if (this.selectedTemplate === templateName) {
        this.selectedTemplate = null;
        localStorage.removeItem("ggn-upload-templator-selected");
      }
      this.updateTemplateSelector();
      this.showStatus(`Template "${templateName}" deleted`);
    }
    // Clone template
    cloneTemplate(templateName) {
      const originalTemplate = this.templates[templateName];
      if (!originalTemplate) return;
      const cloneName = `${templateName} (Clone)`;
      this.templates[cloneName] = {
        mask: originalTemplate.mask,
        fieldMappings: { ...originalTemplate.fieldMappings },
        customUnselectedFields: originalTemplate.customUnselectedFields ? [...originalTemplate.customUnselectedFields] : void 0,
        variableMatching: originalTemplate.variableMatching ? { ...originalTemplate.variableMatching } : void 0
      };
      localStorage.setItem(
        "ggn-upload-templator-templates",
        JSON.stringify(this.templates)
      );
      this.updateTemplateSelector();
      this.showStatus(`Template "${cloneName}" created`);
    }
    // Edit template
    editTemplate(templateName) {
      const template = this.templates[templateName];
      if (!template) return;
      this.showTemplateCreator(templateName, template);
    }
    // Refresh template manager modal content
    refreshTemplateManager(modal) {
      const templateList = modal.querySelector(".gut-template-list");
      if (!templateList) return;
      templateList.innerHTML = TEMPLATE_LIST_HTML(this);
    }
    // Show status message
    showStatus(message, type = "success") {
      const existing = document.querySelector(".gut-status");
      if (existing) existing.remove();
      const status = document.createElement("div");
      status.className = "gut-status";
      status.textContent = message;
      if (type === "error") {
        status.classList.add("error");
      }
      document.body.appendChild(status);
      setTimeout(() => {
        if (status.parentNode) {
          status.parentNode.removeChild(status);
        }
      }, 3e3);
    }
    // Escape HTML to prevent XSS
    escapeHtml(text) {
      const div = document.createElement("div");
      div.textContent = text;
      return div.innerHTML;
    }
    saveSandboxSet(name, data) {
      this.sandboxSets[name] = data;
      localStorage.setItem(
        "ggn-upload-templator-sandbox-sets",
        JSON.stringify(this.sandboxSets)
      );
    }
    deleteSandboxSet(name) {
      delete this.sandboxSets[name];
      localStorage.setItem(
        "ggn-upload-templator-sandbox-sets",
        JSON.stringify(this.sandboxSets)
      );
      if (this.currentSandboxSet === name) {
        this.currentSandboxSet = "";
        localStorage.setItem("ggn-upload-templator-sandbox-current", "");
      }
    }
    showSandboxWithMask(mask, sample) {
      this.showTemplateAndSettingsManager();
      setTimeout(() => {
        const modal = document.querySelector(".gut-modal");
        if (!modal) return;
        const sandboxTabBtn = modal.querySelector('[data-tab="sandbox"]');
        if (sandboxTabBtn) {
          sandboxTabBtn.click();
        }
        setTimeout(() => {
          const sandboxMaskInput = modal.querySelector("#sandbox-mask-input");
          const sandboxMaskDisplay = modal.querySelector("#sandbox-mask-display");
          const sandboxSampleInput = modal.querySelector("#sandbox-sample-input");
          if (sandboxMaskInput && sandboxSampleInput) {
            sandboxMaskInput.value = mask;
            sandboxSampleInput.value = sample;
            updateMaskHighlighting(sandboxMaskInput, sandboxMaskDisplay);
            sandboxMaskInput.dispatchEvent(new Event("input", { bubbles: true }));
          }
        }, 50);
      }, 50);
    }
  }
  logDebug("Script loaded (readyState:", document.readyState, ")");
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => {
      logDebug("Initializing after DOMContentLoaded");
      try {
        new GGnUploadTemplator();
      } catch (error) {
        console.error("Failed to initialize:", error);
      }
    });
  } else {
    logDebug("Initializing immediately (DOM already ready)");
    try {
      new GGnUploadTemplator();
    } catch (error) {
      console.error("Failed to initialize:", error);
    }
  }
})();