const { useEffect, useMemo, useRef, useState } = React;
const { createRoot } = ReactDOM;

(() => {
  const runtime = window.qtRuntime || {};
  const parseJsonScript =
    typeof runtime.parseJsonScript === 'function'
      ? runtime.parseJsonScript
      : (id, fallback) => {
          const node = document.getElementById(id);
          if (!node) return fallback;
          try {
            return JSON.parse(node.textContent || '{}');
          } catch (error) {
            return fallback;
          }
        };

  const readLanguageCookie =
    typeof runtime.readLanguageCookie === 'function'
      ? runtime.readLanguageCookie
      : () => {
          const source = document.cookie || '';
          const item = source
            .split('; ')
            .find((entry) => entry.indexOf('qt_lang=') === 0);
          return item ? decodeURIComponent(item.split('=')[1]) : null;
        };

  const writeLanguageCookie =
    typeof runtime.writeLanguageCookie === 'function'
      ? runtime.writeLanguageCookie
      : (value) => {
          document.cookie = `qt_lang=${encodeURIComponent(value)};path=/;max-age=31536000;samesite=lax`;
        };

  const copyText = typeof runtime.copyText === 'function' ? runtime.copyText : async () => false;
  const stableStringify =
    typeof runtime.stableStringify === 'function'
      ? runtime.stableStringify
      : (value) => JSON.stringify(value);
  const encodeUrlState =
    typeof runtime.encodeUrlState === 'function'
      ? runtime.encodeUrlState
      : (state) => btoa(unescape(encodeURIComponent(JSON.stringify({ v: 1, s: state }))));
  const decodeUrlState =
    typeof runtime.decodeUrlState === 'function'
      ? runtime.decodeUrlState
      : (encoded) => JSON.parse(decodeURIComponent(escape(atob(encoded)))).s;
  const setUrlStateParam =
    typeof runtime.setUrlStateParam === 'function'
      ? runtime.setUrlStateParam
      : () => false;
  const getUrlStateParam =
    typeof runtime.getUrlStateParam === 'function'
      ? runtime.getUrlStateParam
      : () => null;
  const loadScriptOnce = typeof runtime.loadScriptOnce === 'function' ? runtime.loadScriptOnce : null;
  const createHistoryStore =
    typeof runtime.createHistoryStore === 'function'
      ? runtime.createHistoryStore
      : () => ({ load: () => [], save: () => true, push: () => [], clear: () => [] });
  const postServerState = typeof runtime.postServerState === 'function' ? runtime.postServerState : null;
  const fetchServerState = typeof runtime.fetchServerState === 'function' ? runtime.fetchServerState : null;

  const translations = parseJsonScript('home-translations', {});

  const csrfToken = (() => {
    const meta = document.querySelector('meta[name="csrf-token"]');
    if (!meta) return '';
    return String(meta.getAttribute('content') || '');
  })();

  const recaptchaSiteKey = (() => {
    const mount = document.getElementById('app');
    if (mount && mount.dataset && mount.dataset.recaptchaSitekey) {
      return String(mount.dataset.recaptchaSitekey);
    }
    return '';
  })();

  const UI = {
    en: {
      title: 'QR Generator',
      subtitle:
        'Template-driven QR payloads, bulk CSV generation, print export, scanability checks, and dynamic links with stats.',
      modeStatic: 'Static',
      modeDynamic: 'Dynamic',
      type: 'Type',
      payload: 'Payload preview',
      copyPayload: 'Copy payload',
      share: 'Copy share link',
      copy: 'Copy',
      export: 'Download / Export',
      reset: 'Reset',
      swap: 'Swap colors',
      settings: 'Settings',
      history: 'History',
      preview: 'QR preview',
      navMenu: 'Tools menu',
      statusReady: 'Ready.',
      statusCopied: 'Copied to clipboard.',
      statusReset: 'State reset to defaults.',
      statusShared: 'Share link copied.',
      statusExported: 'Export generated.',
      statusSavedDynamic: 'Dynamic link saved.',
      statusLoadedStats: 'Statistics loaded.',
      statusBatchDone: 'Batch generation completed.',
      statusBatchPartial: 'Batch cancelled. Partial ZIP generated.',
      statusBatchCancelled: 'Batch cancelled.',
      statusError: 'Action failed.',
      errors: {
        urlRequired: 'Field: URL. Reason: URL is required. Fix: enter an http/https URL.',
        baseUrlRequired: 'Field: Base URL. Reason: Base URL is required. Fix: enter an http/https URL.',
        invalidScheme: 'Field: URL. Reason: invalid scheme. Fix: use http:// or https://.',
        wifiSsid: 'Field: SSID. Reason: SSID is required. Fix: enter your network name.',
        wifiPassword: 'Field: Password. Reason: password is required for WPA/WEP. Fix: enter a password or use nopass.',
        vcardFn: 'Field: FN. Reason: full name is required. Fix: enter FN.',
        smsNumber: 'Field: Number. Reason: recipient number required. Fix: enter phone number.',
        emailTo: 'Field: To. Reason: recipient email required. Fix: enter email address.',
        calendarSummary: 'Field: Summary. Reason: summary required. Fix: enter event title.',
        calendarStartEnd: 'Field: Start/End. Reason: both dates required. Fix: set start and end time.',
        geoLatLon: 'Field: Lat/Lon. Reason: invalid coordinates. Fix: lat -90..90 and lon -180..180.',
        deepLink: 'Field: Scheme URL. Reason: deep link required. Fix: enter scheme URL.',
        recaptcha: 'Complete the reCAPTCHA challenge to continue.',
      },
      templates: {
        url: 'URL',
        url_utm: 'URL + UTM',
        wifi: 'Wi-Fi',
        vcard: 'vCard',
        sms: 'SMS',
        email: 'Email',
        calendar: 'Calendar (ICS)',
        geo: 'Geo',
        deep_link: 'App deep link',
      },
      dynamic: {
        title: 'Dynamic link',
        id: 'Link ID',
        shortUrl: 'Short URL',
        ttl: 'TTL days (optional)',
        anonymize: 'anonymizeIp',
        save: 'Save dynamic link',
        disable: 'Disable',
        loadStats: 'Load stats',
        exportCsv: 'Export CSV',
      },
      scanability: {
        title: 'Scanability',
        contrast: 'Contrast ratio',
        threshold: 'Contrast threshold',
        logoCoverage: 'Logo coverage',
        strictMode: 'Strict mode',
        recommendedEcc: 'Recommended ECC',
        autoFix: 'Auto-fix contrast',
      },
      batch: {
        title: 'CSV -> ZIP batch',
        upload: 'Upload CSV',
        paste: 'Paste CSV',
        parse: 'Parse CSV',
        generateValid: 'Generate only valid',
        stopFirst: 'Stop on first error',
        generate: 'Generate ZIP',
        cancel: 'Cancel',
        report: 'Validation report',
      },
      exportSettings: {
        title: 'Export settings',
        format: 'Format',
        sizePx: 'PNG size px',
        dpi: 'PNG DPI metadata',
        bg: 'Background',
        pageSize: 'PDF page size',
        marginMm: 'PDF margin (mm)',
        bleedMm: 'PDF bleed (mm)',
        cropMarks: 'Crop marks',
        quietZone: 'Quiet zone modules',
        logoSize: 'Logo size %',
        logoPadding: 'Logo padding modules',
        icsMode: 'ICS embed mode',
      },
      stats: {
        daily: 'Daily views',
        referrers: 'Top referrers',
        platforms: 'Top platforms',
      },
      historyEmpty: 'No history entries yet.',
      csv: {
        rows: 'Rows',
        valid: 'Valid',
        errors: 'Errors',
      },
    },
    lt: {
      title: 'QR generatorius',
      subtitle:
        'QR šablonų payload, masinis CSV generavimas, spaudos eksportas, skaitomumo tikrinimas ir dinaminės nuorodos su statistika.',
      modeStatic: 'Statinis',
      modeDynamic: 'Dinaminis',
      type: 'Tipas',
      payload: 'Payload peržiūra',
      copyPayload: 'Kopijuoti payload',
      share: 'Kopijuoti share nuorodą',
      copy: 'Kopijuoti',
      export: 'Atsisiųsti / Eksportuoti',
      reset: 'Atstatyti',
      swap: 'Sukeisti spalvas',
      settings: 'Nustatymai',
      history: 'Istorija',
      preview: 'QR peržiūra',
      navMenu: 'Įrankių meniu',
      statusReady: 'Paruošta.',
      statusCopied: 'Nukopijuota į iškarpinę.',
      statusReset: 'Būsena atstatyta.',
      statusShared: 'Dalinimosi nuoroda nukopijuota.',
      statusExported: 'Eksportas sugeneruotas.',
      statusSavedDynamic: 'Dinaminė nuoroda išsaugota.',
      statusLoadedStats: 'Statistika įkelta.',
      statusBatchDone: 'Masinis generavimas baigtas.',
      statusBatchPartial: 'Generavimas atšauktas. Dalinis ZIP sugeneruotas.',
      statusBatchCancelled: 'Generavimas atšauktas.',
      statusError: 'Veiksmas nepavyko.',
      errors: {
        urlRequired: 'Laukas: URL. Priežastis: URL privalomas. Taisymas: įveskite http/https URL.',
        baseUrlRequired: 'Laukas: Base URL. Priežastis: base URL privalomas. Taisymas: įveskite http/https URL.',
        invalidScheme: 'Laukas: URL. Priežastis: netinkama schema. Taisymas: naudokite http:// arba https://.',
        wifiSsid: 'Laukas: SSID. Priežastis: SSID privalomas. Taisymas: įveskite tinklo pavadinimą.',
        wifiPassword: 'Laukas: Password. Priežastis: WPA/WEP reikalauja slaptažodžio. Taisymas: įveskite slaptažodį arba pasirinkite nopass.',
        vcardFn: 'Laukas: FN. Priežastis: vardas privalomas. Taisymas: įveskite FN.',
        smsNumber: 'Laukas: Number. Priežastis: numeris privalomas. Taisymas: įveskite telefono numerį.',
        emailTo: 'Laukas: To. Priežastis: el. paštas privalomas. Taisymas: įveskite el. paštą.',
        calendarSummary: 'Laukas: Summary. Priežastis: pavadinimas privalomas. Taisymas: įveskite įvykio pavadinimą.',
        calendarStartEnd: 'Laukas: Start/End. Priežastis: abi datos privalomos. Taisymas: nustatykite pradžią ir pabaigą.',
        geoLatLon: 'Laukas: Lat/Lon. Priežastis: neteisingos koordinatės. Taisymas: lat -90..90, lon -180..180.',
        deepLink: 'Laukas: Scheme URL. Priežastis: deep link privalomas. Taisymas: įveskite scheme URL.',
        recaptcha: 'Prieš tęsdami užbaikite reCAPTCHA patikrą.',
      },
      templates: {
        url: 'URL',
        url_utm: 'URL + UTM',
        wifi: 'Wi-Fi',
        vcard: 'vCard',
        sms: 'SMS',
        email: 'Email',
        calendar: 'Kalendorius (ICS)',
        geo: 'Geo',
        deep_link: 'Programėlės deep link',
      },
      dynamic: {
        title: 'Dinaminė nuoroda',
        id: 'Nuorodos ID',
        shortUrl: 'Trumpa nuoroda',
        ttl: 'TTL dienos (nebūtina)',
        anonymize: 'anonymizeIp',
        save: 'Išsaugoti dinaminę nuorodą',
        disable: 'Išjungti',
        loadStats: 'Įkelti statistiką',
        exportCsv: 'Eksportuoti CSV',
      },
      scanability: {
        title: 'Skaitomumas',
        contrast: 'Kontrasto santykis',
        threshold: 'Kontrasto slenkstis',
        logoCoverage: 'Logotipo uždengimas',
        strictMode: 'Griežtas režimas',
        recommendedEcc: 'Rekomenduojamas ECC',
        autoFix: 'Automatinis kontrasto pataisymas',
      },
      batch: {
        title: 'CSV -> ZIP masinis generavimas',
        upload: 'Įkelti CSV',
        paste: 'Įklijuoti CSV',
        parse: 'Nuskaityti CSV',
        generateValid: 'Generuoti tik validžias eilutes',
        stopFirst: 'Stabdyti ties pirma klaida',
        generate: 'Generuoti ZIP',
        cancel: 'Atšaukti',
        report: 'Validacijos ataskaita',
      },
      exportSettings: {
        title: 'Eksporto nustatymai',
        format: 'Formatas',
        sizePx: 'PNG dydis px',
        dpi: 'PNG DPI metaduomenys',
        bg: 'Fonas',
        pageSize: 'PDF lapo dydis',
        marginMm: 'PDF paraštė (mm)',
        bleedMm: 'PDF bleed (mm)',
        cropMarks: 'Crop marks',
        quietZone: 'Quiet zone moduliai',
        logoSize: 'Logotipo dydis %',
        logoPadding: 'Logotipo padding moduliai',
        icsMode: 'ICS įdėjimo režimas',
      },
      stats: {
        daily: 'Peržiūros pagal dienas',
        referrers: 'Top referreriai',
        platforms: 'Top platformos',
      },
      historyEmpty: 'Istorija tuščia.',
      csv: {
        rows: 'Eilučių',
        valid: 'Validžios',
        errors: 'Klaidos',
      },
    },
  };

  const TYPE_FIELDS = {
    url: ['url'],
    url_utm: ['baseUrl', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'],
    wifi: ['ssid', 'password', 'encryption', 'hidden'],
    vcard: ['fn', 'tel', 'email', 'org', 'title', 'url', 'adr', 'note'],
    sms: ['number', 'message'],
    email: ['to', 'subject', 'body'],
    calendar: ['summary', 'start', 'end', 'location', 'description', 'timezone'],
    geo: ['lat', 'lon', 'query'],
    deep_link: ['schemeUrl', 'fallbackUrl', 'platform'],
  };

  const DEFAULT_FIELDS = {
    url: 'https://example.com',
    baseUrl: 'https://example.com',
    utm_source: '',
    utm_medium: '',
    utm_campaign: '',
    utm_term: '',
    utm_content: '',
    ssid: '',
    password: '',
    encryption: 'WPA',
    hidden: false,
    fn: '',
    tel: '',
    email: '',
    org: '',
    title: '',
    adr: '',
    note: '',
    number: '',
    message: '',
    to: '',
    subject: '',
    body: '',
    summary: '',
    start: '',
    end: '',
    location: '',
    description: '',
    timezone: 'UTC',
    lat: '',
    lon: '',
    query: '',
    schemeUrl: '',
    fallbackUrl: '',
    platform: 'any',
  };

  const DEFAULT_STATE = {
    mode: 'static',
    type: 'url',
    fields: DEFAULT_FIELDS,
    settings: {
      foreground: '#111827',
      background: '#ffffff',
      shape: 'square',
      ecc: 'M',
      quietZoneModules: 4,
      strictMode: true,
      contrastThreshold: 4.5,
      logoSizePercent: 0,
      logoPaddingModules: 1,
      icsMode: 'raw',
      exportFormat: 'png',
      sizePx: 1024,
      dpi: 300,
      pngBackground: 'solid',
      pdfPageSize: 'A4',
      pdfMarginMm: 10,
      pdfBleedMm: 3,
      pdfCropMarks: false,
    },
    dynamic: {
      id: '',
      shortUrl: '',
      status: 'active',
      ttlDays: '',
      anonymizeIp: true,
    },
    logoDataUrl: '',
  };

  const ECC_ORDER = ['L', 'M', 'Q', 'H'];

  const readHexColor = (value, fallback) => {
    const raw = String(value || '').trim().toLowerCase();
    if (/^#[0-9a-f]{6}$/.test(raw)) return raw;
    if (/^#[0-9a-f]{3}$/.test(raw)) {
      return `#${raw
        .slice(1)
        .split('')
        .map((char) => char + char)
        .join('')}`;
    }
    return fallback;
  };

  const parseIsoDateTime = (value) => {
    if (!value) return null;
    const date = new Date(value);
    return Number.isNaN(date.getTime()) ? null : date;
  };

  const formatIcsDate = (date) => {
    const y = date.getUTCFullYear();
    const m = String(date.getUTCMonth() + 1).padStart(2, '0');
    const d = String(date.getUTCDate()).padStart(2, '0');
    const hh = String(date.getUTCHours()).padStart(2, '0');
    const mm = String(date.getUTCMinutes()).padStart(2, '0');
    const ss = String(date.getUTCSeconds()).padStart(2, '0');
    return `${y}${m}${d}T${hh}${mm}${ss}Z`;
  };

  const urlEncodeMailto = (value) => encodeURIComponent(String(value || ''));

  const escapeWifi = (value) => String(value || '').replace(/([\\;,:\"])/g, '\\$1');

  const vcardEscape = (value) =>
    String(value || '')
      .replace(/\\/g, '\\\\')
      .replace(/\n/g, '\\n')
      .replace(/;/g, '\\;')
      .replace(/,/g, '\\,');

  const safeHash = (input) => {
    const source = String(input || '');
    let h = 2166136261;
    for (let i = 0; i < source.length; i += 1) {
      h ^= source.charCodeAt(i);
      h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
    }
    return (h >>> 0).toString(16).padStart(8, '0').slice(0, 8);
  };

  const parseCsvText = (input) => {
    const text = String(input || '');
    const rows = [];
    let row = [];
    let value = '';
    let inQuotes = false;

    for (let i = 0; i < text.length; i += 1) {
      const char = text[i];
      const next = text[i + 1];

      if (inQuotes) {
        if (char === '"' && next === '"') {
          value += '"';
          i += 1;
        } else if (char === '"') {
          inQuotes = false;
        } else {
          value += char;
        }
        continue;
      }

      if (char === '"') {
        inQuotes = true;
      } else if (char === ',') {
        row.push(value);
        value = '';
      } else if (char === '\n') {
        row.push(value);
        rows.push(row);
        row = [];
        value = '';
      } else if (char === '\r') {
        // ignore CR
      } else {
        value += char;
      }
    }

    if (value !== '' || row.length) {
      row.push(value);
      rows.push(row);
    }

    if (!rows.length) {
      return { header: [], rows: [] };
    }

    const header = rows[0].map((item) => String(item || '').trim());
    const body = rows.slice(1).map((cells, index) => {
      const obj = {};
      header.forEach((key, idx) => {
        obj[key] = cells[idx] == null ? '' : String(cells[idx]);
      });
      obj.__row = index + 2;
      return obj;
    });

    return { header, rows: body };
  };

  const relativeLuminance = (hexColor) => {
    const hex = readHexColor(hexColor, '#000000').slice(1);
    const rgb = [0, 2, 4].map((offset) => parseInt(hex.slice(offset, offset + 2), 16) / 255);
    const normalize = (value) => (value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4));
    const [r, g, b] = rgb.map(normalize);
    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
  };

  const contrastRatio = (fg, bg) => {
    const l1 = relativeLuminance(fg);
    const l2 = relativeLuminance(bg);
    const light = Math.max(l1, l2);
    const dark = Math.min(l1, l2);
    return (light + 0.05) / (dark + 0.05);
  };

  const recommendEcc = ({ logoSizePercent, shape, printSize }) => {
    let score = 0;
    if (logoSizePercent >= 20) score += 2;
    else if (logoSizePercent >= 10) score += 1;
    if (shape !== 'square') score += 1;
    if (printSize <= 512) score += 1;
    if (score >= 3) return 'H';
    if (score >= 2) return 'Q';
    if (score >= 1) return 'M';
    return 'L';
  };

  const validateTemplate = (type, fields, t) => {
    const errors = [];
    const hasHttpScheme = (value) => /^https?:\/\//i.test(String(value || ''));

    if (type === 'url') {
      if (!fields.url) {
        errors.push(t.errors.urlRequired);
      } else if (!hasHttpScheme(fields.url) && !/^mailto:/i.test(fields.url)) {
        errors.push(t.errors.invalidScheme);
      }
    }

    if (type === 'url_utm') {
      if (!fields.baseUrl) {
        errors.push(t.errors.baseUrlRequired);
      } else if (!hasHttpScheme(fields.baseUrl)) {
        errors.push(t.errors.invalidScheme);
      }
    }

    if (type === 'wifi') {
      if (!fields.ssid) {
        errors.push(t.errors.wifiSsid);
      }
      if ((fields.encryption === 'WPA' || fields.encryption === 'WEP') && !fields.password) {
        errors.push(t.errors.wifiPassword);
      }
    }

    if (type === 'vcard' && !fields.fn) {
      errors.push(t.errors.vcardFn);
    }

    if (type === 'sms' && !fields.number) {
      errors.push(t.errors.smsNumber);
    }

    if (type === 'email' && !fields.to) {
      errors.push(t.errors.emailTo);
    }

    if (type === 'calendar') {
      if (!fields.summary) {
        errors.push(t.errors.calendarSummary);
      }
      if (!fields.start || !fields.end) {
        errors.push(t.errors.calendarStartEnd);
      }
    }

    if (type === 'geo') {
      const lat = Number(fields.lat);
      const lon = Number(fields.lon);
      if (!Number.isFinite(lat) || !Number.isFinite(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
        errors.push(t.errors.geoLatLon);
      }
    }

    if (type === 'deep_link' && !fields.schemeUrl) {
      errors.push(t.errors.deepLink);
    }

    return errors;
  };

  const buildPayload = (type, fields, settings) => {
    if (type === 'url') {
      return String(fields.url || '').trim();
    }

    if (type === 'url_utm') {
      const base = String(fields.baseUrl || '').trim();
      if (!base) return '';
      let url;
      try {
        url = new URL(base);
      } catch (error) {
        return base;
      }
      ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'].forEach((key) => {
        const value = String(fields[key] || '').trim();
        if (value) {
          url.searchParams.set(key, value);
        } else {
          url.searchParams.delete(key);
        }
      });
      return url.toString();
    }

    if (type === 'wifi') {
      const encryption = String(fields.encryption || 'WPA');
      const hidden = !!fields.hidden;
      return `WIFI:T:${escapeWifi(encryption)};S:${escapeWifi(fields.ssid || '')};P:${escapeWifi(fields.password || '')};H:${hidden ? 'true' : 'false'};;`;
    }

    if (type === 'vcard') {
      const lines = [
        'BEGIN:VCARD',
        'VERSION:3.0',
        `FN:${vcardEscape(fields.fn || '')}`,
      ];
      if (fields.tel) lines.push(`TEL:${vcardEscape(fields.tel)}`);
      if (fields.email) lines.push(`EMAIL:${vcardEscape(fields.email)}`);
      if (fields.org) lines.push(`ORG:${vcardEscape(fields.org)}`);
      if (fields.title) lines.push(`TITLE:${vcardEscape(fields.title)}`);
      if (fields.url) lines.push(`URL:${vcardEscape(fields.url)}`);
      if (fields.adr) lines.push(`ADR:${vcardEscape(fields.adr)}`);
      if (fields.note) lines.push(`NOTE:${vcardEscape(fields.note)}`);
      lines.push('END:VCARD');
      return lines.join('\n');
    }

    if (type === 'sms') {
      const number = String(fields.number || '').trim();
      const body = String(fields.message || '');
      return `SMSTO:${number}:${body}`;
    }

    if (type === 'email') {
      const to = String(fields.to || '').trim();
      const params = [];
      if (fields.subject) params.push(`subject=${urlEncodeMailto(fields.subject)}`);
      if (fields.body) params.push(`body=${urlEncodeMailto(fields.body)}`);
      return `mailto:${to}${params.length ? `?${params.join('&')}` : ''}`;
    }

    if (type === 'calendar') {
      const start = parseIsoDateTime(fields.start);
      const end = parseIsoDateTime(fields.end);
      if (!start || !end) return '';
      const lines = [
        'BEGIN:VCALENDAR',
        'VERSION:2.0',
        'PRODID:-//QuickTools//QR Calendar//EN',
        'BEGIN:VEVENT',
        `UID:${safeHash(`${fields.summary || ''}${fields.start || ''}${fields.end || ''}`)}@getqt.io`,
        `DTSTAMP:${formatIcsDate(new Date())}`,
        `DTSTART:${formatIcsDate(start)}`,
        `DTEND:${formatIcsDate(end)}`,
        `SUMMARY:${vcardEscape(fields.summary || '')}`,
      ];
      if (fields.location) lines.push(`LOCATION:${vcardEscape(fields.location)}`);
      if (fields.description) lines.push(`DESCRIPTION:${vcardEscape(fields.description)}`);
      if (fields.timezone) lines.push(`X-WR-TIMEZONE:${vcardEscape(fields.timezone)}`);
      lines.push('END:VEVENT', 'END:VCALENDAR');
      const raw = lines.join('\n');
      if (settings.icsMode === 'data_uri') {
        return `data:text/calendar;charset=utf-8,${encodeURIComponent(raw)}`;
      }
      return raw;
    }

    if (type === 'geo') {
      const lat = String(fields.lat || '').trim();
      const lon = String(fields.lon || '').trim();
      const query = String(fields.query || '').trim();
      if (query) {
        return `geo:${lat},${lon}?q=${encodeURIComponent(query)}`;
      }
      return `geo:${lat},${lon}`;
    }

    if (type === 'deep_link') {
      return String(fields.schemeUrl || '').trim();
    }

    return '';
  };

  const REQUIRED_MAPPING_FIELDS = {
    url: ['url'],
    url_utm: ['baseUrl'],
    wifi: ['ssid', 'encryption', 'password', 'hidden'],
    vcard: ['fn', 'tel', 'email', 'org', 'title', 'url', 'adr', 'note'],
    sms: ['number', 'message'],
    email: ['to', 'subject', 'body'],
    calendar: ['summary', 'start', 'end', 'location', 'description', 'timezone'],
    geo: ['lat', 'lon', 'query'],
    deep_link: ['schemeUrl', 'fallbackUrl', 'platform'],
  };

  const LANGUAGE_OPTIONS = [
    { id: 'en', label: 'English', flag: '/assets/img/flag-en.svg' },
    { id: 'lt', label: 'Lietuviu', flag: '/assets/img/flag-lt.svg' },
  ];

  const SHAPES = ['square', 'rounded', 'dots', 'classy', 'classy-rounded'];
  const PREVIEW_QR_SIZE = 320;

  const downloadBlob = (blob, filename) => {
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  };

  const ensureJsPdf = async () => {
    if (window.jspdf && window.jspdf.jsPDF) {
      return window.jspdf.jsPDF;
    }
    if (!loadScriptOnce) {
      throw new Error('jsPDF loader unavailable');
    }
    loadScriptOnce('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js', {
      async: true,
      crossorigin: 'anonymous',
    });
    await new Promise((resolve, reject) => {
      const started = Date.now();
      const poll = () => {
        if (window.jspdf && window.jspdf.jsPDF) {
          resolve();
          return;
        }
        if (Date.now() - started > 12000) {
          reject(new Error('jsPDF load timeout'));
          return;
        }
        setTimeout(poll, 120);
      };
      poll();
    });
    return window.jspdf.jsPDF;
  };

  const ensureJsZip = async () => {
    if (window.JSZip) return window.JSZip;
    if (!loadScriptOnce) {
      throw new Error('JSZip loader unavailable');
    }
    loadScriptOnce('https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js', {
      async: true,
      crossorigin: 'anonymous',
    });
    await new Promise((resolve, reject) => {
      const started = Date.now();
      const poll = () => {
        if (window.JSZip) {
          resolve();
          return;
        }
        if (Date.now() - started > 12000) {
          reject(new Error('JSZip load timeout'));
          return;
        }
        setTimeout(poll, 120);
      };
      poll();
    });
    return window.JSZip;
  };

  const buildQrConfig = (payload, state, options = {}) => {
    const dotsMap = {
      square: 'square',
      rounded: 'rounded',
      dots: 'dots',
      classy: 'classy',
      'classy-rounded': 'classy-rounded',
    };
    const settings = state.settings;
    const overrideSize = Number(options.sizePx);
    const sizePx =
      Number.isFinite(overrideSize) && overrideSize > 0
        ? Math.round(overrideSize)
        : Number(settings.sizePx) || 1024;
    const config = {
      width: sizePx,
      height: sizePx,
      type: 'canvas',
      data: payload || ' ',
      margin: Number(settings.quietZoneModules) || 4,
      qrOptions: {
        errorCorrectionLevel: settings.ecc || 'M',
      },
      dotsOptions: {
        type: dotsMap[settings.shape] || 'square',
        color: readHexColor(settings.foreground, '#111827'),
      },
      backgroundOptions: {
        color: settings.pngBackground === 'transparent' ? 'transparent' : readHexColor(settings.background, '#ffffff'),
      },
    };

    if (state.logoDataUrl) {
      const logoScale = Math.min(Math.max(Number(settings.logoSizePercent || 0) / 100, 0.05), 0.35);
      config.image = state.logoDataUrl;
      config.imageOptions = {
        imageSize: logoScale,
        margin: Math.max(0, Number(settings.logoPaddingModules || 0)) * 2,
        hideBackgroundDots: true,
        crossOrigin: 'anonymous',
      };
    }

    return config;
  };

  const createQrBlob = async (payload, appState, targetFormat) => {
    if (!window.QRCodeStyling) {
      throw new Error('QR renderer is unavailable');
    }
    const config = buildQrConfig(payload, appState);
    config.type = targetFormat === 'svg' ? 'svg' : 'canvas';
    const qr = new window.QRCodeStyling(config);

    if (targetFormat === 'svg') {
      const svgBlob = await qr.getRawData('svg');
      if (!(svgBlob instanceof Blob)) {
        throw new Error('SVG export failed');
      }
      return { blob: svgBlob, ext: 'svg', mime: 'image/svg+xml' };
    }

    const pngBlob = await qr.getRawData('png');
    if (!(pngBlob instanceof Blob)) {
      throw new Error('PNG export failed');
    }

    if (targetFormat === 'png') {
      return { blob: pngBlob, ext: 'png', mime: 'image/png' };
    }

    const JsPdf = await ensureJsPdf();
    const settings = appState.settings;
    const pageSize = settings.pdfPageSize || 'A4';
    const margin = Number(settings.pdfMarginMm) || 10;
    const bleed = Number(settings.pdfBleedMm) || 0;

    const pageConfig = (() => {
      if (pageSize === 'Letter') return [215.9, 279.4];
      if (pageSize === 'Custom') return [210, 210];
      return [210, 297];
    })();

    const pdf = new JsPdf({
      orientation: pageConfig[0] >= pageConfig[1] ? 'landscape' : 'portrait',
      unit: 'mm',
      format: pageConfig,
      compress: true,
    });

    const dataUrl = await new Promise((resolve) => {
      const reader = new FileReader();
      reader.onload = () => resolve(String(reader.result || ''));
      reader.readAsDataURL(pngBlob);
    });

    const drawW = pageConfig[0] - margin * 2;
    const drawH = pageConfig[1] - margin * 2;
    const drawSize = Math.min(drawW, drawH);
    const x = (pageConfig[0] - drawSize) / 2;
    const y = (pageConfig[1] - drawSize) / 2;

    pdf.addImage(dataUrl, 'PNG', x, y, drawSize, drawSize);

    if (settings.pdfCropMarks) {
      const crop = 3 + bleed;
      const lines = [
        [x - crop, y, x - 0.5, y],
        [x, y - crop, x, y - 0.5],
        [x + drawSize + 0.5, y, x + drawSize + crop, y],
        [x + drawSize, y - crop, x + drawSize, y - 0.5],
        [x - crop, y + drawSize, x - 0.5, y + drawSize],
        [x, y + drawSize + 0.5, x, y + drawSize + crop],
        [x + drawSize + 0.5, y + drawSize, x + drawSize + crop, y + drawSize],
        [x + drawSize, y + drawSize + 0.5, x + drawSize, y + drawSize + crop],
      ];
      lines.forEach((line) => {
        pdf.line(line[0], line[1], line[2], line[3]);
      });
    }

    const pdfBlob = pdf.output('blob');
    return { blob: pdfBlob, ext: 'pdf', mime: 'application/pdf' };
  };

  const App = () => {
    const languageInit = String(readLanguageCookie() || document.documentElement.getAttribute('lang') || 'en').toLowerCase();
    const [language, setLanguage] = useState(UI[languageInit] ? languageInit : 'en');
    const [state, setState] = useState(() => {
      try {
        const raw = window.localStorage.getItem('qt_qr_state_v4');
        if (!raw) return DEFAULT_STATE;
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== 'object') return DEFAULT_STATE;
        return {
          ...DEFAULT_STATE,
          ...parsed,
          fields: { ...DEFAULT_FIELDS, ...(parsed.fields || {}) },
          settings: { ...DEFAULT_STATE.settings, ...(parsed.settings || {}) },
          dynamic: { ...DEFAULT_STATE.dynamic, ...(parsed.dynamic || {}) },
        };
      } catch (error) {
        return DEFAULT_STATE;
      }
    });

    const [status, setStatus] = useState({
      message: (UI[language] || UI.en).statusReady,
      tone: 'text-slate-500',
    });
    const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
    const [showSettings, setShowSettings] = useState(true);
    const [showHistory, setShowHistory] = useState(false);
    const [showBatch, setShowBatch] = useState(false);
    const [csvText, setCsvText] = useState('');
    const [csvData, setCsvData] = useState({ header: [], rows: [] });
    const [mapping, setMapping] = useState({});
    const [logoUploadName, setLogoUploadName] = useState('');
    const [batchUploadName, setBatchUploadName] = useState('');
    const [batchMode, setBatchMode] = useState('generate_valid');
    const [batchReport, setBatchReport] = useState({ rows: 0, validRows: [], errors: [] });
    const [batchProgress, setBatchProgress] = useState({ active: false, processed: 0, total: 0 });
    const [dynamicStats, setDynamicStats] = useState(null);
    const [dynamicStatsLoading, setDynamicStatsLoading] = useState(false);
    const [recaptchaToken, setRecaptchaToken] = useState('');
    const [recaptchaError, setRecaptchaError] = useState('');

    const historyStoreRef = useRef(createHistoryStore('qt_qr_history_v4', 20));
    const [historyEntries, setHistoryEntries] = useState(() => historyStoreRef.current.load());
    const qrContainerRef = useRef(null);
    const qrRef = useRef(null);
    const batchCancelRef = useRef(false);
    const recaptchaContainerRef = useRef(null);
    const recaptchaWidgetIdRef = useRef(null);

    const pageT = UI[language] || UI.en;
    const logoUploadLabel = language === 'lt' ? 'Logotipo paveikslėlis' : 'Logo image';
    const logoUploadHint = logoUploadName || (language === 'lt' ? 'Pasirinkite logotipo failą' : 'Choose logo image');
    const logoUploadMeta = 'PNG, JPG, SVG, WEBP';
    const batchUploadHint = batchUploadName || (language === 'lt' ? 'Pasirinkite CSV failą' : 'Choose CSV file');
    const batchUploadMeta = '.csv';

    const toolNavLabels = (() => {
      const t = translations[language] || translations.en || {};
      const nav = t.nav || {};
      const fallback = {
        home: language === 'lt' ? 'QR generatorius' : 'QR generator',
        textEditor: language === 'lt' ? 'Teksto redaktorius' : 'Text editor',
        dateTime: language === 'lt' ? 'Data ir laikas' : 'Date & time',
        converter: language === 'lt' ? 'Konverteris' : 'Converter',
        pdfEdit: language === 'lt' ? 'PDF redagavimas' : 'PDF edit',
        coffeeTea: language === 'lt' ? 'Kavos / arbatos įrankis' : 'Coffee & tea counter',
        uuidGenerator: language === 'lt' ? 'UUID generatorius' : 'UUID generator',
        hashChecksum: language === 'lt' ? 'Hash / kontrolinė suma' : 'Hash / checksum',
        passwordGenerator: language === 'lt' ? 'Slaptažodžių generatorius' : 'Password generator',
        csvJsonConverter: language === 'lt' ? 'CSV ↔ JSON keitiklis' : 'CSV ↔ JSON',
        imageUtils: language === 'lt' ? 'Paveikslėlių įrankiai' : 'Image utilities',
        roomPlanner: language === 'lt' ? 'Patalpų 2D planavimas' : '2D room planner',
      };
      return { ...fallback, ...nav };
    })();

    const toolNavItems = [
      { key: 'home', href: '/qr-generator', label: toolNavLabels.home, icon: '/assets/img/icons/tool-home.svg' },
      { key: 'textEditor', href: '/text-editor', label: toolNavLabels.textEditor, icon: '/assets/img/icons/tool-text-editor.svg' },
      { key: 'dateTime', href: '/date-time', label: toolNavLabels.dateTime, icon: '/assets/img/icons/tool-date-time.svg' },
      { key: 'converter', href: '/converter', label: toolNavLabels.converter, icon: '/assets/img/icons/tool-converter.svg' },
      { key: 'pdfEdit', href: '/pdf-edit', label: toolNavLabels.pdfEdit, icon: '/assets/img/icons/tool-pdf-edit.svg' },
      { key: 'coffeeTea', href: '/coffee-tea', label: toolNavLabels.coffeeTea, icon: '/assets/img/icons/tool-coffee-tea.svg' },
      { key: 'uuidGenerator', href: '/uuid-generator', label: toolNavLabels.uuidGenerator, icon: '/assets/img/icons/tool-uuid.svg' },
      { key: 'hashChecksum', href: '/hash-checksum', label: toolNavLabels.hashChecksum, icon: '/assets/img/icons/tool-hash.svg' },
      {
        key: 'passwordGenerator',
        href: '/password-generator',
        label: toolNavLabels.passwordGenerator,
        icon: '/assets/img/icons/tool-password.svg',
      },
      {
        key: 'csvJsonConverter',
        href: '/csv-json-converter',
        label: toolNavLabels.csvJsonConverter,
        icon: '/assets/img/icons/tool-csv-json.svg',
      },
      { key: 'imageUtils', href: '/image-utils', label: toolNavLabels.imageUtils, icon: '/assets/img/icons/tool-image.svg' },
      {
        key: 'roomPlanner',
        href: '/room-planner',
        label: toolNavLabels.roomPlanner,
        icon: '/assets/img/icons/tool-room-planner.svg',
      },
    ];

    const templateErrors = useMemo(() => validateTemplate(state.type, state.fields, pageT), [state.type, state.fields, pageT]);

    const payload = useMemo(
      () => buildPayload(state.type, state.fields, state.settings),
      [state.type, state.fields, state.settings]
    );

    const contrast = useMemo(
      () => contrastRatio(state.settings.foreground, state.settings.background),
      [state.settings.foreground, state.settings.background]
    );

    const logoCoverage = useMemo(() => {
      const logo = Number(state.settings.logoSizePercent || 0);
      const padding = Number(state.settings.logoPaddingModules || 0) * 2.5;
      return Math.min(100, Math.max(0, Math.pow((logo + padding) / 100, 2) * 100));
    }, [state.settings.logoPaddingModules, state.settings.logoSizePercent]);

    const recommendedEcc = useMemo(
      () =>
        recommendEcc({
          logoSizePercent: Number(state.settings.logoSizePercent || 0),
          shape: state.settings.shape,
          printSize: Number(state.settings.sizePx || 1024),
        }),
      [state.settings.logoSizePercent, state.settings.shape, state.settings.sizePx]
    );

    const currentQrData = useMemo(() => {
      if (state.mode === 'dynamic') {
        return state.dynamic.shortUrl || 'https://getqt.io/q/pending';
      }
      return payload || ' ';
    }, [state.dynamic.shortUrl, state.mode, payload]);

    const setStatusMessage = (message, tone = 'text-slate-500') => {
      setStatus({ message, tone });
    };

    useEffect(() => {
      writeLanguageCookie(language);
      document.documentElement.setAttribute('lang', language);
      try {
        window.dispatchEvent(new CustomEvent('qt:language:changed', { detail: { language } }));
      } catch (error) {
        // ignore
      }
    }, [language]);

    useEffect(() => {
      try {
        window.localStorage.setItem('qt_qr_state_v4', JSON.stringify(state));
      } catch (error) {
        // ignore
      }
    }, [state]);

    useEffect(() => {
      if (typeof window === 'undefined') {
        return undefined;
      }

      const scope = document.getElementById('app') || document;
      const init = () => {
        if (typeof window.__qtInitAnimations === 'function') {
          window.__qtInitAnimations(scope);
          return;
        }

        const targets = scope.querySelectorAll('[data-animate]');
        targets.forEach((node) => node.classList.add('is-animated'));
      };

      let rafId = null;
      if (typeof window.requestAnimationFrame === 'function') {
        rafId = window.requestAnimationFrame(init);
      } else {
        init();
      }

      return () => {
        if (rafId !== null && typeof window.cancelAnimationFrame === 'function') {
          window.cancelAnimationFrame(rafId);
        }
      };
    }, [mobileMenuOpen, showBatch, showHistory, showSettings]);

    useEffect(() => {
      const encoded = getUrlStateParam('s');
      const stateId = getUrlStateParam('stateId');

      if (encoded) {
        try {
          const decoded = decodeUrlState(encoded, 1);
          setState((prev) => ({
            ...prev,
            ...decoded,
            fields: { ...DEFAULT_FIELDS, ...(decoded.fields || {}) },
            settings: { ...DEFAULT_STATE.settings, ...(decoded.settings || {}) },
            dynamic: { ...DEFAULT_STATE.dynamic, ...(decoded.dynamic || {}) },
          }));
          return;
        } catch (error) {
          // ignore invalid URL state
        }
      }

      if (stateId && fetchServerState) {
        fetchServerState(stateId, { type: 'qr', consume: false })
          .then((payloadObj) => {
            if (!payloadObj || !payloadObj.state) return;
            const decoded = payloadObj.state;
            setState((prev) => ({
              ...prev,
              ...decoded,
              fields: { ...DEFAULT_FIELDS, ...(decoded.fields || {}) },
              settings: { ...DEFAULT_STATE.settings, ...(decoded.settings || {}) },
              dynamic: { ...DEFAULT_STATE.dynamic, ...(decoded.dynamic || {}) },
            }));
          })
          .catch(() => {
            // ignore
          });
      }
    }, []);

    useEffect(() => {
      if (!window.QRCodeStyling || !qrContainerRef.current) {
        return;
      }

      const previewConfig = buildQrConfig(currentQrData, state, { sizePx: PREVIEW_QR_SIZE });
      if (!qrRef.current) {
        qrRef.current = new window.QRCodeStyling(previewConfig);
        qrRef.current.append(qrContainerRef.current);
      } else {
        qrRef.current.update(previewConfig);
      }

      const applyPreviewSizing = () => {
        if (!qrContainerRef.current) return;
        const rendered = qrContainerRef.current.querySelector('canvas,svg');
        if (!rendered) return;
        rendered.style.width = '100%';
        rendered.style.height = '100%';
        rendered.style.maxWidth = '100%';
        rendered.style.maxHeight = '100%';
        rendered.style.display = 'block';
      };

      if (typeof window.requestAnimationFrame === 'function') {
        window.requestAnimationFrame(applyPreviewSizing);
      } else {
        applyPreviewSizing();
      }
    }, [currentQrData, state]);

    useEffect(() => {
      if (!recaptchaSiteKey || !recaptchaContainerRef.current || typeof window === 'undefined') {
        return;
      }

      const render = () => {
        if (!window.grecaptcha || typeof window.grecaptcha.render !== 'function') {
          return;
        }
        if (recaptchaWidgetIdRef.current !== null) {
          return;
        }

        recaptchaWidgetIdRef.current = window.grecaptcha.render(recaptchaContainerRef.current, {
          sitekey: recaptchaSiteKey,
          callback: (token) => {
            setRecaptchaToken(String(token || ''));
            setRecaptchaError('');
          },
          'expired-callback': () => {
            setRecaptchaToken('');
            setRecaptchaError(pageT.errors.recaptcha);
          },
          'error-callback': () => {
            setRecaptchaToken('');
            setRecaptchaError(pageT.errors.recaptcha);
          },
        });
      };

      if (window.grecaptcha && typeof window.grecaptcha.render === 'function') {
        render();
      } else {
        const listener = () => render();
        window.addEventListener('qtRecaptchaLoaded', listener, { once: true });
        return () => window.removeEventListener('qtRecaptchaLoaded', listener);
      }
      return undefined;
    }, [pageT.errors.recaptcha]);

    const resetRecaptcha = () => {
      if (
        recaptchaSiteKey &&
        recaptchaWidgetIdRef.current !== null &&
        window.grecaptcha &&
        typeof window.grecaptcha.reset === 'function'
      ) {
        try {
          window.grecaptcha.reset(recaptchaWidgetIdRef.current);
        } catch (error) {
          // ignore
        }
      }
      setRecaptchaToken('');
      setRecaptchaError('');
    };

    const persistHistoryEntry = (label) => {
      const entry = {
        id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
        timestamp: new Date().toISOString(),
        label,
        state,
      };
      const list = historyStoreRef.current.push(entry);
      setHistoryEntries(list);
    };

    const onCopyPayload = async () => {
      if (!payload) return;
      const ok = await copyText(payload);
      setStatusMessage(ok ? pageT.statusCopied : pageT.statusError, ok ? 'text-emerald-600' : 'text-rose-600');
    };

    const onSwapColors = () => {
      setState((prev) => ({
        ...prev,
        settings: {
          ...prev.settings,
          foreground: prev.settings.background,
          background: prev.settings.foreground,
        },
      }));
    };

    const onReset = () => {
      setState(DEFAULT_STATE);
      setCsvText('');
      setCsvData({ header: [], rows: [] });
      setMapping({});
      setLogoUploadName('');
      setBatchUploadName('');
      setBatchReport({ rows: 0, validRows: [], errors: [] });
      setDynamicStats(null);
      resetRecaptcha();
      setStatusMessage(pageT.statusReset, 'text-slate-500');
      persistHistoryEntry('reset');
    };

    const onAutoFixContrast = () => {
      const blackContrast = contrastRatio('#000000', state.settings.background);
      const whiteContrast = contrastRatio('#ffffff', state.settings.background);
      setState((prev) => ({
        ...prev,
        settings: {
          ...prev.settings,
          foreground: blackContrast >= whiteContrast ? '#000000' : '#ffffff',
        },
      }));
    };

    const canExportWithStrict = () => {
      if (!state.settings.strictMode) return true;
      if (contrast < Number(state.settings.contrastThreshold || 4.5)) return false;
      if (logoCoverage > 28) return false;
      const selectedIndex = ECC_ORDER.indexOf(state.settings.ecc || 'M');
      const requiredIndex = ECC_ORDER.indexOf(recommendedEcc);
      if (selectedIndex < requiredIndex) return false;
      return true;
    };

    const securityPreflight = async () => {
      if (!recaptchaSiteKey) {
        return true;
      }
      if (!recaptchaToken) {
        setRecaptchaError(pageT.errors.recaptcha);
        throw new Error(pageT.errors.recaptcha);
      }
      const body = new URLSearchParams();
      body.set('qr_text', currentQrData || payload || ' ');
      body.set('g-recaptcha-response', recaptchaToken);
      if (csrfToken) body.set('csrf_token', csrfToken);

      const headers = {
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
      };
      if (csrfToken) headers['X-CSRF-TOKEN'] = csrfToken;

      const response = await fetch('/qr/create_temp_qr.php', {
        method: 'POST',
        headers,
        body: body.toString(),
      });
      const data = await response.json().catch(() => null);
      if (!response.ok || !data) {
        throw new Error((data && data.error) || 'Security verification failed');
      }
      return true;
    };

    const onExport = async () => {
      try {
        if (templateErrors.length > 0) {
          setStatusMessage(templateErrors[0], 'text-rose-600');
          return;
        }

        if (!canExportWithStrict()) {
          setStatusMessage('Strict mode blocked export due to scanability checks.', 'text-rose-600');
          return;
        }

        await securityPreflight();

        const exportFormat = state.settings.exportFormat;
        const result = await createQrBlob(currentQrData, state, exportFormat);
        const filenameBase = `qr_${safeHash(`${currentQrData}${Date.now()}`)}`;
        downloadBlob(result.blob, `${filenameBase}.${result.ext}`);
        setStatusMessage(pageT.statusExported, 'text-emerald-600');
        persistHistoryEntry(`export:${result.ext}`);
      } catch (error) {
        setStatusMessage((error && error.message) || pageT.statusError, 'text-rose-600');
      } finally {
        if (recaptchaSiteKey) {
          resetRecaptcha();
        }
      }
    };

    const toSerializableState = () => ({
      mode: state.mode,
      type: state.type,
      fields: state.fields,
      settings: state.settings,
      dynamic: state.dynamic,
      logoDataUrl: state.logoDataUrl,
    });

    const onCopyShareLink = async () => {
      try {
        const serializable = toSerializableState();
        const encoded = encodeUrlState(serializable, 1);

        if (encoded.length <= 1200) {
          setUrlStateParam('s', encoded, 'replace');
          setUrlStateParam('stateId', '', 'replace');
          const ok = await copyText(window.location.href);
          setStatusMessage(ok ? pageT.statusShared : pageT.statusError, ok ? 'text-emerald-600' : 'text-rose-600');
          return;
        }

        if (!postServerState) {
          throw new Error('Server fallback is unavailable for large state');
        }

        const server = await postServerState(serializable, {
          type: 'qr',
          ttlSeconds: 86400,
          singleUse: false,
          csrfToken,
        });

        setUrlStateParam('s', '', 'replace');
        setUrlStateParam('stateId', server.stateId, 'replace');
        const ok = await copyText(window.location.href);
        setStatusMessage(ok ? pageT.statusShared : pageT.statusError, ok ? 'text-emerald-600' : 'text-rose-600');
      } catch (error) {
        setStatusMessage((error && error.message) || pageT.statusError, 'text-rose-600');
      }
    };

    const onSaveDynamic = async () => {
      try {
        if (templateErrors.length > 0) {
          setStatusMessage(templateErrors[0], 'text-rose-600');
          return;
        }

        const ttlDaysValue = Number(state.dynamic.ttlDays || '0');
        const ttlSeconds = Number.isFinite(ttlDaysValue) && ttlDaysValue > 0 ? Math.floor(ttlDaysValue * 86400) : null;

        const targetUrl = (() => {
          if (state.type === 'url') return state.fields.url;
          if (state.type === 'url_utm') return buildPayload('url_utm', state.fields, state.settings);
          if (state.type === 'deep_link') return state.fields.fallbackUrl || null;
          if (/^https?:\/\//i.test(payload)) return payload;
          return null;
        })();

        const requestPayload = {
          action: 'upsert',
          id: state.dynamic.id || undefined,
          type: state.type,
          targetUrl,
          targetPayload: payload || null,
          deepLinkUrl: state.type === 'deep_link' ? state.fields.schemeUrl || null : null,
          fallbackUrl: state.type === 'deep_link' ? state.fields.fallbackUrl || null : null,
          platform: state.type === 'deep_link' ? state.fields.platform || 'any' : 'any',
          status: state.dynamic.status || 'active',
          ttlSeconds,
          anonymizeIp: !!state.dynamic.anonymizeIp,
          ...(csrfToken ? { csrf_token: csrfToken } : {}),
        };

        const response = await fetch('/qr/dynamic_link.php', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            ...(csrfToken ? { 'X-CSRF-TOKEN': csrfToken } : {}),
          },
          body: JSON.stringify(requestPayload),
        });

        const result = await response.json().catch(() => null);
        if (!response.ok || !result || !result.link || !result.shortUrl) {
          throw new Error((result && result.error) || 'Failed to save dynamic link');
        }

        setState((prev) => ({
          ...prev,
          dynamic: {
            ...prev.dynamic,
            id: result.link.id || prev.dynamic.id,
            shortUrl: result.shortUrl,
            status: result.link.status || prev.dynamic.status,
          },
        }));

        setStatusMessage(pageT.statusSavedDynamic, 'text-emerald-600');
        persistHistoryEntry(`dynamic:${result.link.id || 'saved'}`);
      } catch (error) {
        setStatusMessage((error && error.message) || pageT.statusError, 'text-rose-600');
      }
    };

    const onDisableDynamic = async () => {
      if (!state.dynamic.id) return;
      try {
        const response = await fetch('/qr/dynamic_link.php', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            ...(csrfToken ? { 'X-CSRF-TOKEN': csrfToken } : {}),
          },
          body: JSON.stringify({
            action: 'disable',
            id: state.dynamic.id,
            ...(csrfToken ? { csrf_token: csrfToken } : {}),
          }),
        });

        const result = await response.json().catch(() => null);
        if (!response.ok || !result || !result.ok) {
          throw new Error((result && result.error) || 'Failed to disable link');
        }

        setState((prev) => ({
          ...prev,
          dynamic: {
            ...prev.dynamic,
            status: 'disabled',
          },
        }));
        setStatusMessage('Dynamic link disabled.', 'text-amber-600');
      } catch (error) {
        setStatusMessage((error && error.message) || pageT.statusError, 'text-rose-600');
      }
    };

    const onLoadStats = async () => {
      if (!state.dynamic.id) return;
      setDynamicStatsLoading(true);
      try {
        const response = await fetch(`/qr/dynamic_stats.php?id=${encodeURIComponent(state.dynamic.id)}`);
        const result = await response.json().catch(() => null);
        if (!response.ok || !result || !result.stats) {
          throw new Error((result && result.error) || 'Failed to load stats');
        }
        setDynamicStats(result.stats);
        setStatusMessage(pageT.statusLoadedStats, 'text-emerald-600');
      } catch (error) {
        setStatusMessage((error && error.message) || pageT.statusError, 'text-rose-600');
      } finally {
        setDynamicStatsLoading(false);
      }
    };

    const onExportStatsCsv = () => {
      if (!state.dynamic.id) return;
      const url = `/qr/dynamic_stats.php?id=${encodeURIComponent(state.dynamic.id)}&format=csv`;
      window.open(url, '_blank', 'noopener');
    };

    const onParseCsv = () => {
      const parsed = parseCsvText(csvText);
      setCsvData(parsed);
      const defaults = {};
      const required = REQUIRED_MAPPING_FIELDS[state.type] || [];
      required.forEach((field) => {
        const exact = parsed.header.find((col) => col.toLowerCase() === field.toLowerCase());
        defaults[field] = exact || '';
      });
      setMapping(defaults);
      setBatchReport({ rows: parsed.rows.length, validRows: [], errors: [] });
    };

    const buildFieldsFromCsvRow = (rowObj) => {
      const result = { ...state.fields };
      const required = REQUIRED_MAPPING_FIELDS[state.type] || [];
      required.forEach((field) => {
        const column = mapping[field];
        if (!column) return;
        const raw = rowObj[column];
        result[field] = raw == null ? '' : String(raw);
      });
      return result;
    };

    const validateBatch = () => {
      const rows = Array.isArray(csvData.rows) ? csvData.rows : [];
      const validRows = [];
      const errors = [];

      rows.forEach((rowObj) => {
        const candidateFields = buildFieldsFromCsvRow(rowObj);
        const rowErrors = validateTemplate(state.type, candidateFields, pageT);
        if (rowErrors.length) {
          errors.push({ row: rowObj.__row, field: 'template', message: rowErrors[0] });
        } else {
          validRows.push({ row: rowObj, fields: candidateFields });
        }
      });

      const report = {
        rows: rows.length,
        validRows,
        errors,
      };

      setBatchReport(report);
      return report;
    };

    const generateBatch = async () => {
      const report = validateBatch();
      if (!report.rows) {
        setStatusMessage('No CSV rows to process.', 'text-amber-600');
        return;
      }
      if (batchMode === 'stop_first' && report.errors.length) {
        setStatusMessage(report.errors[0].message, 'text-rose-600');
        return;
      }

      const targets = report.validRows;
      if (!targets.length) {
        setStatusMessage('No valid rows to generate.', 'text-amber-600');
        return;
      }

      batchCancelRef.current = false;
      setBatchProgress({ active: true, processed: 0, total: targets.length });

      try {
        const JSZipRef = await ensureJsZip();
        const zip = new JSZipRef();

        for (let index = 0; index < targets.length; index += 1) {
          if (batchCancelRef.current) {
            break;
          }

          const item = targets[index];
          const generatedPayload = buildPayload(state.type, item.fields, state.settings);
          const format = state.settings.exportFormat;
          const blobResult = await createQrBlob(generatedPayload, { ...state, fields: item.fields }, format);

          const fileNameRaw = String(item.row.filename || '').trim();
          const safeName = fileNameRaw ? fileNameRaw.replace(/[^a-zA-Z0-9._-]/g, '_') : '';
          const fileName = safeName || `qr_${item.row.__row}_${safeHash(generatedPayload)}.${blobResult.ext}`;
          zip.file(fileName, blobResult.blob);

          setBatchProgress((prev) => ({ ...prev, processed: index + 1 }));
        }

        if (batchCancelRef.current) {
          zip.file('PARTIAL_GENERATION.txt', 'Generation cancelled by user. ZIP contains partial output.');
        }

        const zipBlob = await zip.generateAsync({ type: 'blob' });
        const stamp = new Date().toISOString().replace(/[:.]/g, '-');
        const suffix = batchCancelRef.current ? '_partial' : '';
        downloadBlob(zipBlob, `qr-batch-${stamp}${suffix}.zip`);
        setStatusMessage(
          batchCancelRef.current ? pageT.statusBatchPartial : pageT.statusBatchDone,
          batchCancelRef.current ? 'text-amber-600' : 'text-emerald-600'
        );
      } catch (error) {
        setStatusMessage((error && error.message) || pageT.statusError, 'text-rose-600');
      } finally {
        setBatchProgress((prev) => ({ ...prev, active: false }));
      }
    };

    const onCancelBatch = () => {
      batchCancelRef.current = true;
      setStatusMessage(pageT.statusBatchCancelled, 'text-amber-600');
    };

    const renderTypeFields = () => {
      const fields = state.fields;
      const onField = (key, value) => {
        setState((prev) => ({
          ...prev,
          fields: {
            ...prev.fields,
            [key]: value,
          },
        }));
      };

      if (state.type === 'url') {
        return (
          <label className="flex flex-col gap-2 text-sm text-slate-700">
            URL
            <input
              type="text"
              value={fields.url}
              onChange={(event) => onField('url', event.target.value)}
              className="rounded-xl border border-slate-200 px-3 py-2"
            />
          </label>
        );
      }

      if (state.type === 'url_utm') {
        return (
          <div className="grid gap-3 sm:grid-cols-2">
            <label className="flex flex-col gap-2 text-sm text-slate-700 sm:col-span-2">
              Base URL
              <input
                type="text"
                value={fields.baseUrl}
                onChange={(event) => onField('baseUrl', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            {['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'].map((key) => (
              <label key={key} className="flex flex-col gap-2 text-sm text-slate-700">
                {key}
                <input
                  type="text"
                  value={fields[key]}
                  onChange={(event) => onField(key, event.target.value)}
                  className="rounded-xl border border-slate-200 px-3 py-2"
                />
              </label>
            ))}
          </div>
        );
      }

      if (state.type === 'wifi') {
        return (
          <div className="grid gap-3 sm:grid-cols-2">
            <label className="flex flex-col gap-2 text-sm text-slate-700">
              SSID
              <input
                type="text"
                value={fields.ssid}
                onChange={(event) => onField('ssid', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700">
              Encryption
              <select
                value={fields.encryption}
                onChange={(event) => onField('encryption', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              >
                <option value="WPA">WPA</option>
                <option value="WEP">WEP</option>
                <option value="nopass">nopass</option>
              </select>
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700 sm:col-span-2">
              Password
              <input
                type="text"
                value={fields.password}
                onChange={(event) => onField('password', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="inline-flex items-center gap-2 text-sm text-slate-700 sm:col-span-2">
              <input
                type="checkbox"
                checked={!!fields.hidden}
                onChange={(event) => onField('hidden', event.target.checked)}
                className="h-4 w-4"
              />
              Hidden network
            </label>
          </div>
        );
      }

      if (state.type === 'vcard') {
        const keys = ['fn', 'tel', 'email', 'org', 'title', 'url', 'adr', 'note'];
        return (
          <div className="grid gap-3 sm:grid-cols-2">
            {keys.map((key) => (
              <label key={key} className={`flex flex-col gap-2 text-sm text-slate-700 ${key === 'note' ? 'sm:col-span-2' : ''}`}>
                {key.toUpperCase()}
                {key === 'note' ? (
                  <textarea
                    value={fields[key]}
                    onChange={(event) => onField(key, event.target.value)}
                    className="min-h-[80px] rounded-xl border border-slate-200 px-3 py-2"
                  />
                ) : (
                  <input
                    type="text"
                    value={fields[key]}
                    onChange={(event) => onField(key, event.target.value)}
                    className="rounded-xl border border-slate-200 px-3 py-2"
                  />
                )}
              </label>
            ))}
          </div>
        );
      }

      if (state.type === 'sms') {
        return (
          <div className="grid gap-3 sm:grid-cols-2">
            <label className="flex flex-col gap-2 text-sm text-slate-700">
              Number
              <input
                type="text"
                value={fields.number}
                onChange={(event) => onField('number', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700 sm:col-span-2">
              Message
              <textarea
                value={fields.message}
                onChange={(event) => onField('message', event.target.value)}
                className="min-h-[80px] rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
          </div>
        );
      }

      if (state.type === 'email') {
        return (
          <div className="grid gap-3 sm:grid-cols-2">
            <label className="flex flex-col gap-2 text-sm text-slate-700 sm:col-span-2">
              To
              <input
                type="text"
                value={fields.to}
                onChange={(event) => onField('to', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700 sm:col-span-2">
              Subject
              <input
                type="text"
                value={fields.subject}
                onChange={(event) => onField('subject', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700 sm:col-span-2">
              Body
              <textarea
                value={fields.body}
                onChange={(event) => onField('body', event.target.value)}
                className="min-h-[80px] rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
          </div>
        );
      }

      if (state.type === 'calendar') {
        return (
          <div className="grid gap-3 sm:grid-cols-2">
            <label className="flex flex-col gap-2 text-sm text-slate-700 sm:col-span-2">
              Summary
              <input
                type="text"
                value={fields.summary}
                onChange={(event) => onField('summary', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700">
              Start
              <input
                type="datetime-local"
                value={fields.start}
                onChange={(event) => onField('start', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700">
              End
              <input
                type="datetime-local"
                value={fields.end}
                onChange={(event) => onField('end', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700">
              Location
              <input
                type="text"
                value={fields.location}
                onChange={(event) => onField('location', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700">
              Timezone
              <input
                type="text"
                value={fields.timezone}
                onChange={(event) => onField('timezone', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700 sm:col-span-2">
              Description
              <textarea
                value={fields.description}
                onChange={(event) => onField('description', event.target.value)}
                className="min-h-[80px] rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
          </div>
        );
      }

      if (state.type === 'geo') {
        return (
          <div className="grid gap-3 sm:grid-cols-2">
            <label className="flex flex-col gap-2 text-sm text-slate-700">
              Latitude
              <input
                type="number"
                step="any"
                value={fields.lat}
                onChange={(event) => onField('lat', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700">
              Longitude
              <input
                type="number"
                step="any"
                value={fields.lon}
                onChange={(event) => onField('lon', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700 sm:col-span-2">
              Query
              <input
                type="text"
                value={fields.query}
                onChange={(event) => onField('query', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
          </div>
        );
      }

      if (state.type === 'deep_link') {
        return (
          <div className="grid gap-3 sm:grid-cols-2">
            <label className="flex flex-col gap-2 text-sm text-slate-700 sm:col-span-2">
              Scheme URL
              <input
                type="text"
                value={fields.schemeUrl}
                onChange={(event) => onField('schemeUrl', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700 sm:col-span-2">
              Fallback URL
              <input
                type="text"
                value={fields.fallbackUrl}
                onChange={(event) => onField('fallbackUrl', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>
            <label className="flex flex-col gap-2 text-sm text-slate-700">
              Platform
              <select
                value={fields.platform}
                onChange={(event) => onField('platform', event.target.value)}
                className="rounded-xl border border-slate-200 px-3 py-2"
              >
                <option value="any">Any</option>
                <option value="ios">iOS</option>
                <option value="android">Android</option>
              </select>
            </label>
          </div>
        );
      }

      return null;
    };

    const settingsPanel = (
      <div className="space-y-4 rounded-2xl border border-slate-200 bg-white/90 p-4">
        <h3 className="text-sm font-semibold text-slate-800">{pageT.exportSettings.title}</h3>
        <div className="grid gap-3 sm:grid-cols-2">
          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            Foreground
            <input
              type="color"
              value={state.settings.foreground}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, foreground: event.target.value },
                }))
              }
              className="h-10 w-full rounded-md border border-slate-200"
            />
          </label>
          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            Background
            <input
              type="color"
              value={state.settings.background}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, background: event.target.value },
                }))
              }
              className="h-10 w-full rounded-md border border-slate-200"
            />
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            Shape
            <select
              value={state.settings.shape}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, shape: event.target.value },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            >
              {SHAPES.map((shape) => (
                <option key={shape} value={shape}>
                  {shape}
                </option>
              ))}
            </select>
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            ECC
            <select
              value={state.settings.ecc}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, ecc: event.target.value },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            >
              {ECC_ORDER.map((level) => (
                <option key={level} value={level}>
                  {level}
                </option>
              ))}
            </select>
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.exportSettings.quietZone}
            <input
              type="number"
              min="0"
              max="20"
              value={state.settings.quietZoneModules}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, quietZoneModules: Number(event.target.value || 0) },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            />
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.exportSettings.logoSize}
            <input
              type="number"
              min="0"
              max="40"
              value={state.settings.logoSizePercent}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, logoSizePercent: Number(event.target.value || 0) },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            />
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.exportSettings.logoPadding}
            <input
              type="number"
              min="0"
              max="12"
              value={state.settings.logoPaddingModules}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, logoPaddingModules: Number(event.target.value || 0) },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            />
          </label>

          <div className="sm:col-span-2 space-y-2">
            <p className="text-xs font-semibold text-slate-600">{logoUploadLabel}</p>
            <label className="block cursor-pointer rounded-2xl border border-dashed border-slate-300 bg-white p-3 text-center transition hover:border-indigo-200">
              <input
                type="file"
                accept="image/*"
                onChange={(event) => {
                  const file = event.target.files && event.target.files[0];
                  if (!file) {
                    setLogoUploadName('');
                    setState((prev) => ({ ...prev, logoDataUrl: '' }));
                    return;
                  }

                  setLogoUploadName(file.name);
                  const reader = new FileReader();
                  reader.onload = () => {
                    setState((prev) => ({
                      ...prev,
                      logoDataUrl: String(reader.result || ''),
                    }));
                  };
                  reader.readAsDataURL(file);
                }}
                className="sr-only"
              />
              <div className="rounded-xl bg-slate-50 px-4 py-4 text-xs text-slate-600">
                <p className="font-medium text-slate-700">{logoUploadHint}</p>
                <p className="mt-1 text-[11px] text-slate-500">{logoUploadMeta}</p>
              </div>
            </label>
          </div>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.exportSettings.format}
            <select
              value={state.settings.exportFormat}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, exportFormat: event.target.value },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            >
              <option value="svg">SVG</option>
              <option value="png">PNG</option>
              <option value="pdf">PDF</option>
            </select>
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.exportSettings.sizePx}
            <input
              type="number"
              min="128"
              max="4096"
              value={state.settings.sizePx}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, sizePx: Number(event.target.value || 1024) },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            />
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.exportSettings.dpi}
            <input
              type="number"
              min="72"
              max="1200"
              value={state.settings.dpi}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, dpi: Number(event.target.value || 300) },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            />
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.exportSettings.bg}
            <select
              value={state.settings.pngBackground}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, pngBackground: event.target.value },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            >
              <option value="solid">Solid</option>
              <option value="transparent">Transparent</option>
            </select>
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.exportSettings.pageSize}
            <select
              value={state.settings.pdfPageSize}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, pdfPageSize: event.target.value },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            >
              <option value="A4">A4</option>
              <option value="Letter">Letter</option>
              <option value="Custom">Custom</option>
            </select>
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.exportSettings.marginMm}
            <input
              type="number"
              min="0"
              max="50"
              value={state.settings.pdfMarginMm}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, pdfMarginMm: Number(event.target.value || 0) },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            />
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.exportSettings.bleedMm}
            <input
              type="number"
              min="0"
              max="20"
              value={state.settings.pdfBleedMm}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, pdfBleedMm: Number(event.target.value || 0) },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            />
          </label>

          <label className="inline-flex items-center gap-2 text-xs font-semibold text-slate-600">
            <input
              type="checkbox"
              checked={!!state.settings.pdfCropMarks}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, pdfCropMarks: event.target.checked },
                }))
              }
            />
            {pageT.exportSettings.cropMarks}
          </label>

          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.exportSettings.icsMode}
            <select
              value={state.settings.icsMode}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, icsMode: event.target.value },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            >
              <option value="raw">raw</option>
              <option value="data_uri">data: URI</option>
            </select>
          </label>
        </div>
      </div>
    );

    const dynamicPanel =
      state.mode === 'dynamic' ? (
        <div className="space-y-3 rounded-2xl border border-slate-200 bg-white/90 p-4">
          <h3 className="text-sm font-semibold text-slate-800">{pageT.dynamic.title}</h3>
          <div className="grid gap-3 sm:grid-cols-2">
            <label className="flex flex-col gap-2 text-sm text-slate-700">
              {pageT.dynamic.id}
              <input
                type="text"
                value={state.dynamic.id}
                onChange={(event) =>
                  setState((prev) => ({
                    ...prev,
                    dynamic: { ...prev.dynamic, id: event.target.value.trim() },
                  }))
                }
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>

            <label className="flex flex-col gap-2 text-sm text-slate-700">
              {pageT.dynamic.ttl}
              <input
                type="number"
                min=""
                value={state.dynamic.ttlDays}
                onChange={(event) =>
                  setState((prev) => ({
                    ...prev,
                    dynamic: { ...prev.dynamic, ttlDays: event.target.value },
                  }))
                }
                className="rounded-xl border border-slate-200 px-3 py-2"
              />
            </label>

            <label className="inline-flex items-center gap-2 text-sm text-slate-700 sm:col-span-2">
              <input
                type="checkbox"
                checked={!!state.dynamic.anonymizeIp}
                onChange={(event) =>
                  setState((prev) => ({
                    ...prev,
                    dynamic: { ...prev.dynamic, anonymizeIp: event.target.checked },
                  }))
                }
              />
              {pageT.dynamic.anonymize}
            </label>
          </div>

          <p className="text-xs text-slate-500">
            {pageT.dynamic.shortUrl}:{' '}
            <span className="font-semibold text-slate-700 break-all">
              {state.dynamic.shortUrl || 'https://getqt.io/q/{id}'}
            </span>
          </p>

          <div className="flex flex-wrap gap-2">
            <button
              type="button"
              onClick={onSaveDynamic}
              className="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white"
            >
              {pageT.dynamic.save}
            </button>
            <button
              type="button"
              onClick={onDisableDynamic}
              className="rounded-xl border border-amber-300 bg-amber-50 px-4 py-2 text-sm font-semibold text-amber-700"
              disabled={!state.dynamic.id}
            >
              {pageT.dynamic.disable}
            </button>
            <button
              type="button"
              onClick={onLoadStats}
              className="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700"
              disabled={!state.dynamic.id || dynamicStatsLoading}
            >
              {pageT.dynamic.loadStats}
            </button>
            <button
              type="button"
              onClick={onExportStatsCsv}
              className="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700"
              disabled={!state.dynamic.id}
            >
              {pageT.dynamic.exportCsv}
            </button>
          </div>
        </div>
      ) : null;

    const scanabilityPanel = (
      <div className="space-y-3 rounded-2xl border border-slate-200 bg-white/90 p-4">
        <h3 className="text-sm font-semibold text-slate-800">{pageT.scanability.title}</h3>

        <p className="text-xs text-slate-600">
          {pageT.scanability.contrast}: <strong>{contrast.toFixed(2)}:1</strong>
        </p>
        <p className="text-xs text-slate-600">
          {pageT.scanability.logoCoverage}: <strong>{logoCoverage.toFixed(2)}%</strong>
        </p>
        <p className="text-xs text-slate-600">
          {pageT.scanability.recommendedEcc}: <strong>{recommendedEcc}</strong>
        </p>

        <div className="grid gap-3 sm:grid-cols-2">
          <label className="flex flex-col gap-2 text-xs font-semibold text-slate-600">
            {pageT.scanability.threshold}
            <input
              type="number"
              min="1"
              max="21"
              step="0.1"
              value={state.settings.contrastThreshold}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, contrastThreshold: Number(event.target.value || 4.5) },
                }))
              }
              className="rounded-xl border border-slate-200 px-3 py-2"
            />
          </label>

          <label className="inline-flex items-center gap-2 text-xs font-semibold text-slate-600">
            <input
              type="checkbox"
              checked={!!state.settings.strictMode}
              onChange={(event) =>
                setState((prev) => ({
                  ...prev,
                  settings: { ...prev.settings, strictMode: event.target.checked },
                }))
              }
            />
            {pageT.scanability.strictMode}
          </label>
        </div>

        {contrast < Number(state.settings.contrastThreshold || 4.5) ? (
          <button
            type="button"
            onClick={onAutoFixContrast}
            className="rounded-xl border border-amber-300 bg-amber-50 px-4 py-2 text-xs font-semibold text-amber-700"
          >
            {pageT.scanability.autoFix}
          </button>
        ) : null}
      </div>
    );

    const batchPanel = showBatch ? (
      <div className="space-y-3 rounded-2xl border border-slate-200 bg-white/90 p-4">
        <h3 className="text-sm font-semibold text-slate-800">{pageT.batch.title}</h3>

        <label className="flex flex-col gap-2 text-sm text-slate-700">
          {pageT.batch.paste}
          <textarea
            value={csvText}
            onChange={(event) => setCsvText(event.target.value)}
            className="min-h-[120px] rounded-xl border border-slate-200 px-3 py-2"
            placeholder="header1,header2\nvalue1,value2"
          />
        </label>

        <div className="rounded-2xl border border-dashed border-slate-300 bg-white p-4 text-center">
          <label className="block cursor-pointer">
            <span className="text-sm font-semibold text-slate-700">{pageT.batch.upload}</span>
            <input
              type="file"
              accept=".csv,text/csv"
              className="sr-only"
              onChange={(event) => {
                const file = event.target.files && event.target.files[0];
                if (!file) {
                  setBatchUploadName('');
                  return;
                }

                setBatchUploadName(file.name);
                const reader = new FileReader();
                reader.onload = () => {
                  setCsvText(String(reader.result || ''));
                };
                reader.readAsText(file);
              }}
            />
            <div className="mt-3 rounded-xl bg-slate-50 px-4 py-5 text-sm text-slate-600">
              <p className="font-medium text-slate-700">{batchUploadHint}</p>
              <p className="mt-1 text-xs text-slate-500">{batchUploadMeta}</p>
            </div>
          </label>
        </div>

        <button
          type="button"
          onClick={onParseCsv}
          className="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700"
        >
          {pageT.batch.parse}
        </button>

        {csvData.header.length ? (
          <div className="space-y-2">
            <p className="text-xs font-semibold text-slate-600">Column mapping</p>
            {(REQUIRED_MAPPING_FIELDS[state.type] || []).map((field) => (
              <label key={field} className="flex items-center gap-2 text-xs text-slate-700">
                <span className="w-32 shrink-0">{field}</span>
                <select
                  value={mapping[field] || ''}
                  onChange={(event) => setMapping((prev) => ({ ...prev, [field]: event.target.value }))}
                  className="min-w-[180px] rounded-lg border border-slate-200 px-2 py-1"
                >
                  <option value="">(none)</option>
                  {csvData.header.map((column) => (
                    <option key={column} value={column}>
                      {column}
                    </option>
                  ))}
                </select>
              </label>
            ))}
          </div>
        ) : null}

        <div className="flex flex-wrap gap-2">
          <button
            type="button"
            onClick={() => setBatchMode('generate_valid')}
            className={`rounded-xl px-4 py-2 text-xs font-semibold ${
              batchMode === 'generate_valid'
                ? 'bg-indigo-600 text-white'
                : 'border border-slate-200 bg-white text-slate-700'
            }`}
          >
            {pageT.batch.generateValid}
          </button>
          <button
            type="button"
            onClick={() => setBatchMode('stop_first')}
            className={`rounded-xl px-4 py-2 text-xs font-semibold ${
              batchMode === 'stop_first'
                ? 'bg-indigo-600 text-white'
                : 'border border-slate-200 bg-white text-slate-700'
            }`}
          >
            {pageT.batch.stopFirst}
          </button>
        </div>

        <div className="flex flex-wrap gap-2">
          <button
            type="button"
            onClick={generateBatch}
            className="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white"
            disabled={batchProgress.active}
          >
            {pageT.batch.generate}
          </button>
          <button
            type="button"
            onClick={onCancelBatch}
            className="rounded-xl border border-amber-300 bg-amber-50 px-4 py-2 text-sm font-semibold text-amber-700"
            disabled={!batchProgress.active}
          >
            {pageT.batch.cancel}
          </button>
        </div>

        <div className="rounded-xl border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700">
          <p className="font-semibold">{pageT.batch.report}</p>
          <p>
            {pageT.csv.rows}: {batchReport.rows} | {pageT.csv.valid}: {batchReport.validRows.length} | {pageT.csv.errors}:{' '}
            {batchReport.errors.length}
          </p>
          {batchReport.errors.slice(0, 8).map((errorRow, index) => (
            <p key={`${errorRow.row}-${index}`} className="text-rose-600">
              Row {errorRow.row}: {errorRow.message}
            </p>
          ))}
          {batchProgress.active ? (
            <p className="mt-2 text-indigo-600">
              {batchProgress.processed}/{batchProgress.total}
            </p>
          ) : null}
        </div>
      </div>
    ) : null;

    const historyPanel = showHistory ? (
      <div className="space-y-2 rounded-2xl border border-slate-200 bg-white/90 p-4">
        {historyEntries.length ? (
          historyEntries.map((entry) => (
            <button
              key={entry.id}
              type="button"
              className="flex w-full items-center justify-between rounded-xl border border-slate-200 bg-white px-3 py-2 text-left text-xs text-slate-700"
              onClick={() => {
                const nextState = entry.state || DEFAULT_STATE;
                setState({
                  ...DEFAULT_STATE,
                  ...nextState,
                  fields: { ...DEFAULT_FIELDS, ...(nextState.fields || {}) },
                  settings: { ...DEFAULT_STATE.settings, ...(nextState.settings || {}) },
                  dynamic: { ...DEFAULT_STATE.dynamic, ...(nextState.dynamic || {}) },
                });
                setStatusMessage(`History restored: ${entry.label}`, 'text-emerald-600');
              }}
            >
              <span>{entry.label}</span>
              <span>{new Date(entry.timestamp).toLocaleString(language)}</span>
            </button>
          ))
        ) : (
          <p className="text-xs text-slate-500">{pageT.historyEmpty}</p>
        )}
        <button
          type="button"
          onClick={() => {
            historyStoreRef.current.clear();
            setHistoryEntries([]);
          }}
          className="rounded-xl border border-slate-200 bg-white px-3 py-2 text-xs font-semibold text-slate-700"
        >
          Clear history
        </button>
      </div>
    ) : null;

    const statsPanel = dynamicStats ? (
      <div className="space-y-3 rounded-2xl border border-slate-200 bg-white/90 p-4 text-xs text-slate-700">
        <p className="font-semibold">
          {pageT.stats.daily}: {dynamicStats.total_views || 0}
        </p>
        <div>
          <p className="font-semibold">{pageT.stats.daily}</p>
          <ul className="space-y-1">
            {(dynamicStats.daily_views || []).slice(-10).map((row) => (
              <li key={row.date}>
                {row.date}: {row.views}
              </li>
            ))}
          </ul>
        </div>
        <div>
          <p className="font-semibold">{pageT.stats.referrers}</p>
          <ul className="space-y-1">
            {(dynamicStats.top_referrers || []).slice(0, 6).map((row) => (
              <li key={row.source}>
                {row.source}: {row.views}
              </li>
            ))}
          </ul>
        </div>
        <div>
          <p className="font-semibold">{pageT.stats.platforms}</p>
          <ul className="space-y-1">
            {(dynamicStats.top_platforms || []).slice(0, 6).map((row) => (
              <li key={row.platform}>
                {row.platform}: {row.views}
              </li>
            ))}
          </ul>
        </div>
      </div>
    ) : null;

    return (
      <main className="mx-auto max-w-6xl space-y-8">
        <header className="qt-topbar space-y-4" data-animate>
          <div className="qt-topbar-row flex flex-wrap items-center justify-between gap-3">
            <a
              href="/"
              className="qt-brand inline-flex items-center"
              aria-label="Quick Tools home"
            >
              <img src="/assets/img/logo.png" alt="QuickTools logo" className="h-10 w-auto object-contain" />
            </a>
            <div className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white p-0.5 shadow-sm">
              {LANGUAGE_OPTIONS.map((option) => {
                const active = option.id === language;
                return (
                  <button
                    type="button"
                    key={option.id}
                    data-language={option.id}
                    data-language-reload="false"
                    onClick={() => setLanguage(option.id)}
                    className={`flex items-center gap-2 rounded-full px-2 py-1 text-xs font-semibold transition ${
                      active ? 'bg-indigo-50 text-indigo-600 shadow-sm' : 'bg-transparent text-slate-700 hover:text-slate-900'
                    }`}
                  >
                    <img src={option.flag} alt={`${option.label} flag`} className="h-4 w-4 rounded-full object-cover" />
                    <span className="sr-only">{option.label}</span>
                  </button>
                );
              })}
            </div>
          </div>

          <button
            type="button"
            className="qt-tool-menu-toggle"
            data-tool-menu-toggle
            aria-expanded={mobileMenuOpen ? 'true' : 'false'}
            aria-controls="qt-tool-grid-home"
            onClick={() => setMobileMenuOpen((prev) => !prev)}
          >
            <span>{pageT.navMenu}</span>
            <span className="qt-tool-menu-toggle-icon" aria-hidden="true">
              +
            </span>
          </button>

          <nav
            id="qt-tool-grid-home"
            className={`qt-tool-grid qt-tool-grid--collapsible flex flex-wrap items-stretch justify-center gap-3${mobileMenuOpen ? ' is-open' : ''}`}
            data-tool-menu
          >
            {toolNavItems.map((item) => {
              const active = item.key === 'home';
              return (
                <a
                  key={item.key}
                  href={item.href}
                  className={`qt-tool-link group relative flex w-full items-center gap-3 rounded-2xl border border-white/40 bg-white/80 px-3 py-3 text-left text-xs font-semibold text-slate-600 shadow-sm transition hover:border-indigo-200 hover:text-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-200 sm:w-[170px] ${
                    active ? 'qt-tool-link--active bg-indigo-600 text-white border-indigo-500 shadow-lg hover:text-white' : ''
                  }`}
                  aria-current={active ? 'page' : undefined}
                  onClick={() => setMobileMenuOpen(false)}
                >
                  <span className="qt-tool-icon-wrap inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-slate-100/80">
                    <img src={item.icon} alt="" className="qt-tool-icon h-9 w-9 object-contain" loading="lazy" />
                  </span>
                  <span className="text-xs font-semibold leading-tight sm:text-sm">{item.label}</span>
                </a>
              );
            })}
          </nav>
        </header>

        <section className="rounded-3xl border border-white/60 bg-white/80 p-6 shadow-glow" data-animate>
          <div className="space-y-2">
            <h1 className="text-3xl font-semibold text-slate-900 sm:text-4xl">{pageT.title}</h1>
            <p className="max-w-4xl text-sm text-slate-500 sm:text-base">{pageT.subtitle}</p>
          </div>
        </section>

        <div className="grid gap-8 lg:grid-cols-[minmax(360px,460px)_1fr]">
          <section className="space-y-4 rounded-3xl border border-white/60 bg-white/80 p-6 shadow-glow" data-animate>
            <div className="grid gap-3 sm:grid-cols-2">
              <label className="flex flex-col gap-2 text-sm text-slate-700">
                Mode
                <select
                  value={state.mode}
                  onChange={(event) =>
                    setState((prev) => ({
                      ...prev,
                      mode: event.target.value,
                    }))
                  }
                  className="rounded-xl border border-slate-200 px-3 py-2"
                >
                  <option value="static">{pageT.modeStatic}</option>
                  <option value="dynamic">{pageT.modeDynamic}</option>
                </select>
              </label>

              <label className="flex flex-col gap-2 text-sm text-slate-700">
                {pageT.type}
                <select
                  value={state.type}
                  onChange={(event) =>
                    setState((prev) => ({
                      ...prev,
                      type: event.target.value,
                    }))
                  }
                  className="rounded-xl border border-slate-200 px-3 py-2"
                >
                  {Object.keys(TYPE_FIELDS).map((key) => (
                    <option key={key} value={key}>
                      {pageT.templates[key] || key}
                    </option>
                  ))}
                </select>
              </label>
            </div>

            {renderTypeFields()}

            {templateErrors.length ? (
              <div className="rounded-xl border border-rose-200 bg-rose-50 p-3 text-xs text-rose-700">
                {templateErrors.map((errorText, index) => (
                  <p key={index}>{errorText}</p>
                ))}
              </div>
            ) : null}

            <label className="flex flex-col gap-2 text-sm text-slate-700">
              {pageT.payload}
              <textarea
                value={payload}
                readOnly
                className="min-h-[120px] rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs"
              />
            </label>

            <div className="flex flex-wrap gap-2">
              <button type="button" onClick={onCopyPayload} className="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white">
                {pageT.copyPayload}
              </button>
              <button type="button" onClick={onExport} className="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white">
                {pageT.export}
              </button>
              <button
                type="button"
                onClick={onReset}
                className="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700"
              >
                {pageT.reset}
              </button>
              <button
                type="button"
                onClick={onSwapColors}
                className="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700"
              >
                {pageT.swap}
              </button>
              <button
                type="button"
                onClick={() => setShowSettings((prev) => !prev)}
                className="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700"
              >
                {pageT.settings}
              </button>
              <button
                type="button"
                onClick={() => setShowHistory((prev) => !prev)}
                className="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700"
              >
                {pageT.history}
              </button>
              <button
                type="button"
                onClick={onCopyShareLink}
                className="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700"
              >
                {pageT.share}
              </button>
              <button
                type="button"
                onClick={() => setShowBatch((prev) => !prev)}
                className="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700"
              >
                CSV -> ZIP
              </button>
            </div>

            {showSettings ? settingsPanel : null}
            {scanabilityPanel}
            {dynamicPanel}
            {statsPanel}
            {batchPanel}
            {historyPanel}

            <p className={`text-xs sm:text-sm ${status.tone}`}>{status.message}</p>
          </section>

          <section className="space-y-4 rounded-3xl border border-white/60 bg-white/80 p-6 shadow-glow" data-animate>
            <h2 className="text-xl font-semibold text-slate-900">{pageT.preview}</h2>
            <div className="mx-auto w-full max-w-[420px] rounded-3xl border border-slate-200 bg-white p-4 shadow-sm sm:p-6">
              <div
                ref={qrContainerRef}
                className="mx-auto aspect-square w-full max-w-[320px] overflow-hidden rounded-2xl border border-slate-200 bg-white"
              />
            </div>
            <p className="text-xs text-slate-500 break-all">
              {state.mode === 'dynamic' ? state.dynamic.shortUrl || 'https://getqt.io/q/{id}' : payload}
            </p>
            {recaptchaSiteKey ? (
              <div className="space-y-2 rounded-xl border border-slate-200 bg-slate-50 p-3">
                <div ref={recaptchaContainerRef} />
                {recaptchaError ? <p className="text-xs font-semibold text-rose-600">{recaptchaError}</p> : null}
              </div>
            ) : null}
          </section>
        </div>
      </main>
    );
  };

  const mount = document.getElementById('app');
  if (!mount) {
    return;
  }
  const root = createRoot(mount);
  root.render(<App />);
})();
