// Kai Dial v0.9 — canvas mesh 4K, dark theme, widgets, AI search, import/export, pin
const STORAGE_KEY = 'kai-dial-data';
const SCHEMA_VERSION = 11;
const DEFAULT_DATA = {
  activeGroup: 'cc',
  tileMode: 'favicon',
  tileSize: 'large',
  theme: 'auto',
  wallpaperSource: 'mesh',
  wallpaper: null,
  wallpaperDate: null,
  wallpaperUrl: null,
  wallpaperPaletteSeed: null,
  showClock: true,
  showWeather: true,
  weatherCity: null,
  weatherSource: 'open-meteo',
  clockTimezone: null,  // null = системный
  showMusicWidget: false,
  musicPlaylistUrl: null,
  musicAutostart: false,
  _musicAutostartedSession: false,
  searchEngine: 'google',
  lastModified: 0,
  version: SCHEMA_VERSION,
  groups: [],
  bookmarks: [],
};

let state = DEFAULT_DATA;

async function loadDefaultBookmarks() {
  try {
    const r = await fetch(chrome.runtime.getURL('newtab/default-bookmarks.json'));
    return await r.json();
  } catch {
    return { groups: [{ id: 'work', name: 'Работа' }], bookmarks: [] };
  }
}

// ═══════════════════════════════════════════════
// Storage
// ═══════════════════════════════════════════════

// Storage: local — мгновенный, sync — debounced chunked write для cross-device
const META_KEY = 'kai-dial-meta';
const CHUNK_PREFIX = 'kai-dial-chunk-';
const CHUNK_SIZE = 7000; // под лимит 8 КБ/ключ
const SYNC_DEBOUNCE_MS = 4000;

let _syncWriteTimer = null;
let _syncIncoming = false; // флаг чтобы не reentry при onChanged

async function syncWrite(d) {
  if (typeof chrome === 'undefined' || !chrome.storage?.sync) return;
  try {
    const json = JSON.stringify(d);
    const chunks = [];
    for (let i = 0; i < json.length; i += CHUNK_SIZE) chunks.push(json.slice(i, i + CHUNK_SIZE));
    const data = { [META_KEY]: { chunks: chunks.length, version: SCHEMA_VERSION, timestamp: d.lastModified || Date.now() } };
    chunks.forEach((c, i) => { data[CHUNK_PREFIX + i] = c; });
    // Чистим старые избыточные чанки
    const allKeys = await new Promise(r => chrome.storage.sync.get(null, r));
    const oldChunkKeys = Object.keys(allKeys).filter(k => k.startsWith(CHUNK_PREFIX) && parseInt(k.slice(CHUNK_PREFIX.length)) >= chunks.length);
    if (oldChunkKeys.length) await new Promise(r => chrome.storage.sync.remove(oldChunkKeys, r));
    await new Promise((res, rej) => chrome.storage.sync.set(data, () => {
      if (chrome.runtime.lastError) rej(chrome.runtime.lastError); else res();
    }));
    console.log('[Kai Dial] sync wrote', chunks.length, 'chunks (' + json.length + ' bytes)');
  } catch (err) {
    console.warn('[Kai Dial] sync write failed:', err.message || err);
  }
}

async function syncRead() {
  if (typeof chrome === 'undefined' || !chrome.storage?.sync) return null;
  try {
    const meta = await new Promise(r => chrome.storage.sync.get(META_KEY, x => r(x[META_KEY])));
    if (!meta || !meta.chunks) return null;
    const keys = Array.from({ length: meta.chunks }, (_, i) => CHUNK_PREFIX + i);
    const all = await new Promise(r => chrome.storage.sync.get(keys, r));
    const json = keys.map(k => all[k] || '').join('');
    if (!json) return null;
    const parsed = JSON.parse(json);
    parsed.lastModified = parsed.lastModified || meta.timestamp || 0;
    return parsed;
  } catch (err) {
    console.warn('[Kai Dial] sync read failed:', err.message || err);
    return null;
  }
}

function scheduleSyncWrite() {
  if (_syncIncoming) return; // не пишем то что только что прилетело
  if (_syncWriteTimer) clearTimeout(_syncWriteTimer);
  _syncWriteTimer = setTimeout(() => syncWrite(state), SYNC_DEBOUNCE_MS);
}

const storage = {
  async get() {
    if (typeof chrome !== 'undefined' && chrome.storage) {
      const local = await new Promise(r => chrome.storage.local.get(STORAGE_KEY, x => r(x[STORAGE_KEY])));
      const remote = await syncRead();
      // Берём более свежий
      if (remote && (!local || (remote.lastModified || 0) > (local.lastModified || 0))) {
        await new Promise(r => chrome.storage.local.set({ [STORAGE_KEY]: remote }, r));
        console.log('[Kai Dial] restored from sync (newer)');
        return remote;
      }
      // Старая миграция со sync (одним ключом)
      if (!local) {
        const old = await new Promise(r => chrome.storage.sync.get(STORAGE_KEY, x => r(x[STORAGE_KEY])));
        if (old) {
          await new Promise(r => chrome.storage.local.set({ [STORAGE_KEY]: old }, r));
          return old;
        }
      }
      return local || DEFAULT_DATA;
    }
    return JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null') || DEFAULT_DATA;
  },
  async set(d) {
    d.lastModified = Date.now();
    if (typeof chrome !== 'undefined' && chrome.storage) {
      await new Promise(r => chrome.storage.local.set({ [STORAGE_KEY]: d }, r));
      scheduleSyncWrite();
      return;
    }
    localStorage.setItem(STORAGE_KEY, JSON.stringify(d));
  },
};

// Throttled save: при множественных вызовах (загрузка иконок) пишем один раз
let _saveTimer = null;
let _savePending = false;
function save() {
  _savePending = true;
  if (_saveTimer) return;
  _saveTimer = setTimeout(async () => {
    _saveTimer = null;
    if (!_savePending) return;
    _savePending = false;
    await storage.set(state);
  }, 250);
}
// Immediate save для критических операций (drag, delete, group change)
async function saveNow() {
  if (_saveTimer) { clearTimeout(_saveTimer); _saveTimer = null; }
  _savePending = false;
  await storage.set(state);
}

// ═══════════════════════════════════════════════
// Icons — apple-touch-icon → Google sz=256 → DDG → favicon → Clearbit
// ═══════════════════════════════════════════════

// Simple Icons CDN — высококачественные SVG бренд-логотипы с official brand colors
const SI = c => `https://cdn.simpleicons.org/${c}`;
const CUSTOM_ICONS = {
  // Google products
  'console.cloud.google.com': SI('googlecloud/4285F4'),
  'cloud.google.com': SI('googlecloud/4285F4'),
  'mail.google.com': SI('gmail/EA4335'),
  'drive.google.com': SI('googledrive/4285F4'),
  'docs.google.com': SI('googledocs/4285F4'),
  'meet.google.com': SI('googlemeet/00897B'),
  'analytics.google.com': SI('googleanalytics/E37400'),
  'gemini.google.com': SI('googlegemini/8E75B2'),
  // AI
  'claude.ai': SI('anthropic/D97757'),
  'console.anthropic.com': SI('anthropic/D97757'),
  'chat.openai.com': SI('openai'),
  'chatgpt.com': SI('openai'),
  'openai.com': SI('openai'),
  'perplexity.ai': SI('perplexity/22B8CD'),
  'elevenlabs.io': SI('elevenlabs'),
  'openrouter.ai': SI('openrouter'),
  // Dev / cloud
  'github.com': SI('github'),
  'vercel.com': SI('vercel'),
  'netlify.com': SI('netlify/00C7B7'),
  'supabase.com': SI('supabase/3FCF8E'),
  'figma.com': SI('figma/F24E1E'),
  'notion.so': SI('notion'),
  'cloudflare.com': SI('cloudflare/F38020'),
  'dash.cloudflare.com': SI('cloudflare/F38020'),
  'one.dash.cloudflare.com': SI('cloudflare/F38020'),
  'collect-chemistry-anonymous-midi.trycloudflare.com': SI('cloudflare/F38020'),
  'chronicles-specials-distinction-think.trycloudflare.com': SI('cloudflare/F38020'),
  'deployment-parallel-pull-relaxation.trycloudflare.com': SI('cloudflare/F38020'),
  'console.apify.com': SI('apify/97D700'),
  // Соцсети
  'instagram.com': SI('instagram/E4405F'),
  'tiktok.com': SI('tiktok'),
  'linkedin.com': SI('linkedin/0A66C2'),
  'facebook.com': SI('facebook/1877F2'),
  'reddit.com': SI('reddit/FF4500'),
  'discord.com': SI('discord/5865F2'),
  'web.telegram.org': SI('telegram/26A5E4'),
  'web.whatsapp.com': SI('whatsapp/25D366'),
  'pinterest.com': SI('pinterest/BD081C'),
  'youtube.com': SI('youtube/FF0000'),
  'threads.com': SI('threads'),
  'x.com': SI('x'),
  'twitter.com': SI('x'),
  'music.youtube.com': SI('youtubemusic/FF0000'),
  // Фриланс / биржи
  'upwork.com': SI('upwork/6FDA44'),
  'fiverr.com': SI('fiverr/1DBF73'),
  'kwork.ru': 'https://kwork.ru/favicon.ico',
  // Криптобиржи
  'binance.com': SI('binance/F0B90B'),
  'bybit.com': SI('bybit/FFA500'),
  'mexc.com': SI('mexc'),
  'bitget.com': SI('bitget'),
  'okx.com': SI('okx'),
  // Финансы
  'monobank.ua': SI('monobank/000000'),
  'web.monobank.ua': SI('monobank/000000'),
  'portmone.com.ua': 'https://www.portmone.com.ua/favicon.ico',
  'wallet.tg': SI('telegram/26A5E4'),
};

// Pattern-based — для служебных subdomain'ов (Railway, Vercel, Cloudflare)
const HOST_PATTERNS = [
  { test: h => h.endsWith('.up.railway.app') || h === 'railway.app' || h === 'railway.com',
    icon: SI('railway') },
  { test: h => h.endsWith('.vercel.app'),
    icon: SI('vercel') },
  { test: h => h.endsWith('.trycloudflare.com') || h.endsWith('.cloudflare.com')
            || h.endsWith('.workers.dev') || h.endsWith('.pages.dev'),
    icon: SI('cloudflare/F38020') },
];

function faviconUrls(url, customIcon) {
  if (customIcon) return [customIcon];
  try {
    const host = new URL(url).hostname.replace(/^www\./, '');
    if (CUSTOM_ICONS[host]) return [CUSTOM_ICONS[host]];
    for (const p of HOST_PATTERNS) {
      if (p.test(host)) return [p.icon, `https://www.google.com/s2/favicons?domain=${host}&sz=256`];
    }
    // Приоритет: Google s2 sz=256 (стабильное HD), DDG (бренд-логотипы), apple-touch, favicon.ico, Clearbit
    return [
      `https://www.google.com/s2/favicons?domain=${host}&sz=256`,
      `https://icons.duckduckgo.com/ip3/${host}.ico`,
      `https://${host}/apple-touch-icon.png`,
      `https://${host}/apple-touch-icon-precomposed.png`,
      `https://${host}/favicon.ico`,
      `https://logo.clearbit.com/${host}?size=512`,
    ];
  } catch { return []; }
}

function screenshotUrls(url) {
  return [
    `https://s.wordpress.com/mshots/v1/${encodeURIComponent(url)}?w=800&h=600`,
    `https://image.thum.io/get/width/800/crop/600/${url}`,
  ];
}

function tryImageChain(img, urls, onFallback, onSuccess) {
  let i = 0;
  img.onerror = () => {
    i++;
    if (i < urls.length) img.src = urls[i];
    else onFallback();
  };
  img.onload = () => {
    // Если иконка слишком мелкая (16x16) и есть ещё кандидаты — идём дальше за HD-вариантом
    if (img.naturalWidth && img.naturalWidth < 32 && i + 1 < urls.length) {
      i++;
      img.src = urls[i];
      return;
    }
    onSuccess?.(urls[i]);
  };
  img.src = urls[0];
}

function loadFavicon(img, b, onFallback) {
  const cached = b.iconCached;
  const fresh = faviconUrls(b.url, b.icon);
  const urls = cached ? [cached, ...fresh.filter(u => u !== cached)] : fresh;
  tryImageChain(img, urls, onFallback, (workingUrl) => {
    if (b.iconCached !== workingUrl) {
      b.iconCached = workingUrl;
      save();
    }
  });
}

function perTileMode(bm) { return bm.mode || state.tileMode; }

// ═══════════════════════════════════════════════
// Wallpaper — canvas mesh gradient 4K (всегда работает оффлайн)
// ═══════════════════════════════════════════════

function todayKey() {
  const d = new Date();
  return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}

function strHash(s) {
  let h = 0;
  for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
  return h;
}

function mulberry32(a) {
  return function() {
    a |= 0; a = a + 0x6D2B79F5 | 0;
    let t = a;
    t = Math.imul(t ^ t >>> 15, t | 1);
    t ^= t + Math.imul(t ^ t >>> 7, t | 61);
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}

const PALETTES_LIGHT = [
  { name: 'Lavender Dream', bg: '#f5f0ff', colors: ['#a78bfa', '#60a5fa', '#f472b6', '#fbbf24'] },
  { name: 'Sunset Coral',   bg: '#fff5ed', colors: ['#fb923c', '#f87171', '#fbbf24', '#f472b6'] },
  { name: 'Ocean Mint',     bg: '#ecfeff', colors: ['#22d3ee', '#34d399', '#60a5fa', '#a78bfa'] },
  { name: 'Berry Holo',     bg: '#fdf4ff', colors: ['#d946ef', '#8b5cf6', '#ec4899', '#f43f5e'] },
  { name: 'Peach Aurora',   bg: '#fff7ed', colors: ['#fdba74', '#fda4af', '#f0abfc', '#7dd3fc'] },
  { name: 'Sky Pastel',     bg: '#eff6ff', colors: ['#60a5fa', '#a78bfa', '#f472b6', '#34d399'] },
  { name: 'Iridescent',     bg: '#fafafa', colors: ['#a5f3fc', '#fda4af', '#c4b5fd', '#fde68a'] },
  { name: 'Rose Gold',      bg: '#fff1f2', colors: ['#fb7185', '#fbbf24', '#c084fc', '#f0abfc'] },
];

const PALETTES_DARK = [
  { name: 'Deep Aurora',  bg: '#0a0e1a', colors: ['#1e40af', '#7c3aed', '#be185d', '#0e7490'] },
  { name: 'Midnight',     bg: '#0c0a1a', colors: ['#4338ca', '#a21caf', '#831843', '#1e3a8a'] },
  { name: 'Cyber Neon',   bg: '#06070d', colors: ['#06b6d4', '#a855f7', '#ec4899', '#facc15'] },
  { name: 'Nebula',       bg: '#0a0014', colors: ['#7c3aed', '#db2777', '#0891b2', '#ea580c'] },
  { name: 'Forest Night', bg: '#0a1410', colors: ['#059669', '#0d9488', '#7c3aed', '#1e40af'] },
  { name: 'Volcanic',     bg: '#140a0a', colors: ['#dc2626', '#ea580c', '#a21caf', '#1e1b4b'] },
];

function pickPalette(seed) {
  const isDark = getActualTheme() === 'dark';
  const list = isDark ? PALETTES_DARK : PALETTES_LIGHT;
  return list[Math.abs(seed) % list.length];
}

// Рисует mesh gradient НАПРЯМУЮ в canvas элемент (без blob URL — надёжнее для extension page)
function drawMeshGradient(canvas, seed) {
  const rng = mulberry32(seed);
  const palette = pickPalette(seed);
  const w = canvas.width;
  const h = canvas.height;
  const ctx = canvas.getContext('2d');

  ctx.fillStyle = palette.bg;
  ctx.fillRect(0, 0, w, h);

  const blobCount = 9 + Math.floor(rng() * 4);
  for (let i = 0; i < blobCount; i++) {
    const x = rng() * w * 1.2 - w * 0.1;
    const y = rng() * h * 1.2 - h * 0.1;
    const r = (0.25 + rng() * 0.45) * Math.max(w, h);
    const color = palette.colors[Math.floor(rng() * palette.colors.length)];

    const grad = ctx.createRadialGradient(x, y, 0, x, y, r);
    const opacityHex = Math.floor(160 + rng() * 80).toString(16).padStart(2, '0');
    grad.addColorStop(0, color + opacityHex);
    grad.addColorStop(0.4, color + '55');
    grad.addColorStop(1, color + '00');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, w, h);
  }

  const overlay = ctx.createLinearGradient(0, 0, w, h);
  overlay.addColorStop(0, 'rgba(255,255,255,0.05)');
  overlay.addColorStop(1, 'rgba(0,0,0,0.10)');
  ctx.fillStyle = overlay;
  ctx.fillRect(0, 0, w, h);

  return palette.name;
}

const WALLPAPER_QUERIES = [
  'abstract+gradient+pastel',
  'abstract+aurora+borealis',
  'abstract+fluid+art',
  'abstract+iridescent+holographic',
  'mesh+gradient+minimal',
  'abstract+liquid+glass',
  'abstract+nebula+pastel',
  'abstract+wave+silk',
];

async function fetchWallhavenWallpaper(seed) {
  const queryIdx = Math.abs(seed) % WALLPAPER_QUERIES.length;
  const q = WALLPAPER_QUERIES[queryIdx];
  const apiUrl = `https://wallhaven.cc/api/v1/search?q=${q}&atleast=2560x1440&sorting=random&purity=100&categories=100`;
  try {
    const r = await fetch(apiUrl);
    if (!r.ok) throw new Error('http ' + r.status);
    const d = await r.json();
    const items = d.data || [];
    if (items.length > 0) {
      return items[Math.abs(seed) % items.length].path;
    }
  } catch (e) {
    console.warn('Wallhaven fetch failed:', e);
  }
  return null;
}

function paintCanvasImage(canvas, img) {
  const ctx = canvas.getContext('2d');
  // cover-fit
  const cw = canvas.width, ch = canvas.height;
  const ir = img.width / img.height;
  const cr = cw / ch;
  let dw, dh, dx, dy;
  if (ir > cr) { dh = ch; dw = ch * ir; dx = (cw - dw) / 2; dy = 0; }
  else { dw = cw; dh = cw / ir; dx = 0; dy = (ch - dh) / 2; }
  ctx.drawImage(img, dx, dy, dw, dh);
}

async function renderWallpaper() {
  const canvas = document.getElementById('wallpaper');
  if (!canvas || canvas.tagName !== 'CANVAS') {
    console.error('[Kai Dial] wallpaper canvas not found or wrong tag');
    return;
  }
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = Math.min(Math.round(window.innerWidth * dpr), 3840);
  canvas.height = Math.min(Math.round(window.innerHeight * dpr), 2160);

  // 1. Baseline mesh gradient — рисуем сразу (всегда работает)
  try {
    const seed = strHash(todayKey() + (getActualTheme() || ''));
    const name = drawMeshGradient(canvas, seed);
    console.log('[Kai Dial] mesh painted to canvas:', name, canvas.width, '×', canvas.height);
  } catch (e) {
    console.error('[Kai Dial] mesh draw failed:', e);
  }

  // 2. Кастомный URL — рисуем поверх
  if (state.wallpaperSource === 'custom' && state.wallpaper) {
    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.onload = () => paintCanvasImage(canvas, img);
    img.onerror = () => console.warn('[Kai Dial] custom wallpaper failed:', state.wallpaper);
    img.src = state.wallpaper;
    return;
  }

  // 3. Wallhaven — рисуем поверх mesh, если выбран
  if (state.wallpaperSource === 'wallhaven') {
    try {
      const today = todayKey();
      let url = (state.wallpaperDate === today) ? state.wallpaperUrl : null;
      if (!url) {
        url = await fetchWallhavenWallpaper(strHash(today));
        if (url) {
          state.wallpaperDate = today;
          state.wallpaperUrl = url;
          save();
        }
      }
      if (url) {
        const img = new Image();
        img.crossOrigin = 'anonymous';
        img.onload = () => {
          paintCanvasImage(canvas, img);
          console.log('[Kai Dial] wallhaven painted:', url);
        };
        img.onerror = () => console.warn('[Kai Dial] wallhaven image load failed:', url);
        img.src = url;
      }
    } catch (e) {
      console.warn('[Kai Dial] wallhaven block failed:', e);
    }
  }
}

// ═══════════════════════════════════════════════
// Theme
// ═══════════════════════════════════════════════

function getActualTheme() {
  const t = state.theme || 'auto';
  if (t === 'auto') {
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  }
  return t;
}

function applyTheme() {
  document.documentElement.dataset.theme = getActualTheme();
}

function cycleTheme() {
  const order = ['auto', 'light', 'dark'];
  const cur = state.theme || 'auto';
  state.theme = order[(order.indexOf(cur) + 1) % order.length];
  /* canvas будет перерисован в renderWallpaper с новой темой */
  save();
  applyTheme();
  renderWallpaper();
}

// ═══════════════════════════════════════════════
// Widgets
// ═══════════════════════════════════════════════

let _clockInterval = null;

function renderClock() {
  const el = document.getElementById('clock');
  if (!el) return;
  if (!state.showClock) { el.style.display = 'none'; return; }
  el.style.display = '';
  const update = () => {
    const tz = state.clockTimezone || undefined;
    const d = new Date();
    const time = d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', timeZone: tz });
    const date = d.toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long', timeZone: tz });
    const tzLabel = tz ? ` <span class="clock-tz">${tz.split('/').pop().replace(/_/g, ' ')}</span>` : '';
    el.innerHTML = `<div class="clock-time">${time}</div><div class="clock-date">${date.charAt(0).toUpperCase() + date.slice(1)}${tzLabel}</div>`;
  };
  update();
  if (_clockInterval) clearInterval(_clockInterval);
  _clockInterval = setInterval(update, 30000);
  el.style.cursor = 'pointer';
  el.title = 'Правый клик — выбрать часовой пояс';
  el.oncontextmenu = e => { e.preventDefault(); showWidgetMenu('clock', e); };
}

const WEATHER_ICONS = {
  0: '☀️', 1: '🌤', 2: '⛅', 3: '☁️',
  45: '🌫', 48: '🌫',
  51: '🌦', 53: '🌦', 55: '🌧',
  56: '🌧', 57: '🌧',
  61: '🌧', 63: '🌧', 65: '🌧',
  66: '🌧', 67: '🌧',
  71: '🌨', 73: '🌨', 75: '❄️', 77: '❄️',
  80: '🌧', 81: '🌧', 82: '⛈',
  85: '🌨', 86: '🌨',
  95: '⛈', 96: '⛈', 99: '⛈',
};

// Геолокация по таймзоне (оффлайн, без API)
const TZ_TO_CITY = {
  'Europe/Kyiv':       { lat: 50.4501, lon: 30.5234, name: 'Киев' },
  'Europe/Kiev':       { lat: 50.4501, lon: 30.5234, name: 'Киев' },
  'Europe/Moscow':     { lat: 55.7558, lon: 37.6173, name: 'Москва' },
  'Europe/Minsk':      { lat: 53.9006, lon: 27.5590, name: 'Минск' },
  'Europe/Vilnius':    { lat: 54.6872, lon: 25.2797, name: 'Вильнюс' },
  'Europe/Riga':       { lat: 56.9496, lon: 24.1052, name: 'Рига' },
  'Europe/Tallinn':    { lat: 59.4370, lon: 24.7536, name: 'Таллин' },
  'Europe/Warsaw':     { lat: 52.2297, lon: 21.0122, name: 'Варшава' },
  'Europe/Berlin':     { lat: 52.5200, lon: 13.4050, name: 'Берлин' },
  'Europe/Paris':      { lat: 48.8566, lon: 2.3522,  name: 'Париж' },
  'Europe/London':     { lat: 51.5074, lon: -0.1278, name: 'Лондон' },
  'Europe/Madrid':     { lat: 40.4168, lon: -3.7038, name: 'Мадрид' },
  'Europe/Rome':       { lat: 41.9028, lon: 12.4964, name: 'Рим' },
  'Europe/Amsterdam':  { lat: 52.3676, lon: 4.9041,  name: 'Амстердам' },
  'Europe/Istanbul':   { lat: 41.0082, lon: 28.9784, name: 'Стамбул' },
  'Europe/Lisbon':     { lat: 38.7223, lon: -9.1393, name: 'Лиссабон' },
  'Asia/Tbilisi':      { lat: 41.7151, lon: 44.8271, name: 'Тбилиси' },
  'Asia/Yerevan':      { lat: 40.1792, lon: 44.4991, name: 'Ереван' },
  'Asia/Almaty':       { lat: 43.2220, lon: 76.8512, name: 'Алматы' },
  'Asia/Tashkent':     { lat: 41.2995, lon: 69.2401, name: 'Ташкент' },
  'Asia/Bangkok':      { lat: 13.7563, lon: 100.5018, name: 'Бангкок' },
  'Asia/Dubai':        { lat: 25.2048, lon: 55.2708, name: 'Дубай' },
  'Asia/Singapore':    { lat: 1.3521,  lon: 103.8198, name: 'Сингапур' },
  'America/New_York':  { lat: 40.7128, lon: -74.0060, name: 'Нью-Йорк' },
  'America/Los_Angeles': { lat: 34.0522, lon: -118.2437, name: 'Лос-Анджелес' },
  'America/Chicago':   { lat: 41.8781, lon: -87.6298, name: 'Чикаго' },
};

function detectCityByTimezone() {
  try {
    const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
    return TZ_TO_CITY[tz] || null;
  } catch { return null; }
}

async function fetchWeatherCurrent(city) {
  const { lat, lon } = city;
  const source = state.weatherSource || 'open-meteo';
  if (source === 'wttr') {
    const r = await fetch(`https://wttr.in/${lat},${lon}?format=j1`);
    const d = await r.json();
    const cur = d.current_condition[0];
    return { temp: parseInt(cur.temp_C), code: parseInt(cur.weatherCode) || 0 };
  }
  // open-meteo
  const r = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,weather_code&timezone=auto`);
  const d = await r.json();
  return { temp: Math.round(d.current.temperature_2m), code: d.current.weather_code };
}

async function renderWeather() {
  const el = document.getElementById('weather');
  if (!el) return;
  if (!state.showWeather) { el.style.display = 'none'; return; }
  el.style.display = '';
  el.style.cursor = 'pointer';
  el.title = 'Клик — прогноз на 7 дней. Правый клик — настройки';
  el.onclick = () => showWeatherForecast();
  el.oncontextmenu = e => { e.preventDefault(); showWidgetMenu('weather', e); };
  el.innerHTML = '<div class="weather-temp">…</div>';
  try {
    if (!state.weatherCity) {
      let city = detectCityByTimezone();
      if (!city) city = { lat: 50.4501, lon: 30.5234, name: 'Киев' };
      state.weatherCity = city;
      save();
    }
    const { name } = state.weatherCity;
    const { temp, code } = await fetchWeatherCurrent(state.weatherCity);
    const icon = WEATHER_ICONS[code] || '🌡';
    el.innerHTML = `<div class="weather-icon">${icon}</div><div class="weather-temp">${temp}°</div><div class="weather-city">${name}</div>`;
  } catch (e) {
    console.warn('[Kai Dial] weather error:', e);
    el.innerHTML = `<div class="weather-temp">🌡</div>`;
  }
}

// ═══════════════════════════════════════════════
// Groups bar — hover-scroll по краям
// ═══════════════════════════════════════════════

function initGroupsHoverScroll() {
  const wrap = document.querySelector('.groups-wrap');
  const bar = document.getElementById('groups');
  if (!wrap || !bar) return;
  let dir = 0;
  let raf = null;
  const tick = () => {
    if (!dir) { raf = null; return; }
    bar.scrollLeft += dir * 6;
    raf = requestAnimationFrame(tick);
  };
  wrap.addEventListener('mousemove', e => {
    const r = wrap.getBoundingClientRect();
    const x = e.clientX - r.left;
    const edge = 80;
    if (x < edge) dir = -1 * (1 - x / edge);
    else if (x > r.width - edge) dir = (1 - (r.width - x) / edge);
    else dir = 0;
    if (dir && !raf) raf = requestAnimationFrame(tick);
  });
  wrap.addEventListener('mouseleave', () => { dir = 0; });
  // wheel → горизонтальный скролл
  bar.addEventListener('wheel', e => {
    if (Math.abs(e.deltaX) < Math.abs(e.deltaY)) {
      e.preventDefault();
      bar.scrollLeft += e.deltaY;
    }
  }, { passive: false });
}

// ═══════════════════════════════════════════════
// Search engine toggle (Google / Perplexity)
// ═══════════════════════════════════════════════

const SEARCH_ENGINES = {
  google:     { label: 'G',  title: 'Google',     url: 'https://www.google.com/search?q=' },
  perplexity: { label: 'Px', title: 'Perplexity', url: 'https://www.perplexity.ai/search?q=' },
};

function applySearchEngineBadge() {
  const engine = SEARCH_ENGINES[state.searchEngine || 'google'] || SEARCH_ENGINES.google;
  const labelEl = document.getElementById('search-engine-label');
  const btn = document.getElementById('search-engine-btn');
  if (!labelEl || !btn) return;
  labelEl.textContent = engine.label;
  btn.title = `Поиск через ${engine.title} — клик для переключения`;
  btn.dataset.engine = state.searchEngine || 'google';
}

function cycleSearchEngine() {
  const ids = Object.keys(SEARCH_ENGINES);
  const cur = ids.indexOf(state.searchEngine || 'google');
  state.searchEngine = ids[(cur + 1) % ids.length];
  applySearchEngineBadge();
  save();
  document.getElementById('search').focus();
}

// ═══════════════════════════════════════════════
// Music widget — YouTube / YT Music embed (bottom bar)
// ═══════════════════════════════════════════════

// Принимает: полный URL YouTube/YT Music ИЛИ шорткат типа "LM", "WL", "RDxxx"
function parseYouTubeUrl(url) {
  if (!url) return null;
  const trimmed = url.trim();
  // Шорткаты — голые ID плейлистов
  if (/^(LM|WL|LL|HL)$/i.test(trimmed)) return { type: 'playlist', id: trimmed.toUpperCase() };
  if (/^(PL|RD|OL|UU|FL)[\w-]{10,}$/.test(trimmed)) return { type: 'playlist', id: trimmed };
  try {
    const u = new URL(trimmed);
    const list = u.searchParams.get('list');
    const v = u.searchParams.get('v');
    if (list && (!v || u.pathname.includes('playlist'))) return { type: 'playlist', id: list };
    if (v) return { type: 'video', id: v, list: list || null };
    if (u.hostname === 'youtu.be') return { type: 'video', id: u.pathname.slice(1) };
    const m = u.pathname.match(/\/embed\/([\w-]+)/);
    if (m) return { type: 'video', id: m[1] };
    return null;
  } catch { return null; }
}

// Гарантированно недоступные для embed плейлисты (требуют твою сессию):
// LM=Liked Music, WL=Watch Later, LL=Liked Videos, HL=History
// RD*/OL* — это публичные auto-generated radio/mix, они embed-ятся нормально.
function isPrivatePlaylist(parsed) {
  if (!parsed || parsed.type !== 'playlist') return false;
  return ['LM', 'WL', 'LL', 'HL'].includes(parsed.id);
}

function buildEmbedUrl(parsed) {
  if (!parsed) return '';
  const origin = encodeURIComponent(location.origin);
  const params = `enablejsapi=1&autoplay=0&modestbranding=1&rel=0&origin=${origin}`;
  if (parsed.type === 'playlist') {
    return `https://www.youtube.com/embed/videoseries?list=${parsed.id}&${params}`;
  }
  if (parsed.list) {
    return `https://www.youtube.com/embed/${parsed.id}?list=${parsed.list}&${params}`;
  }
  return `https://www.youtube.com/embed/${parsed.id}?${params}`;
}

const _yt = {
  ready: false,
  playing: false,
  currentTime: 0,
  duration: 0,
  videoData: null,
};

function ytSendCommand(func, args = []) {
  const iframe = document.getElementById('yt-player');
  if (!iframe?.contentWindow) return;
  iframe.contentWindow.postMessage(JSON.stringify({ event: 'command', func, args }), '*');
}

function ytStartListening() {
  const iframe = document.getElementById('yt-player');
  if (!iframe?.contentWindow) return;
  iframe.contentWindow.postMessage(JSON.stringify({ event: 'listening', id: 'kai-dial', channel: 'kai' }), '*');
}

function formatTime(s) {
  if (!s || !isFinite(s)) return '0:00';
  s = Math.floor(s);
  return `${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}`;
}

function updateMusicUI() {
  const titleEl = document.getElementById('m-title');
  const authorEl = document.getElementById('m-author');
  const coverEl = document.getElementById('m-cover');
  const timeEl = document.getElementById('m-time');
  const playBtn = document.getElementById('m-play');
  if (!titleEl) return;

  if (_yt.videoData) {
    titleEl.textContent = _yt.videoData.title || 'YouTube Music';
    authorEl.textContent = _yt.videoData.author || '';
    if (_yt.videoData.video_id) {
      coverEl.src = `https://i.ytimg.com/vi/${_yt.videoData.video_id}/mqdefault.jpg`;
    }
  }
  timeEl.textContent = `${formatTime(_yt.currentTime)} / ${formatTime(_yt.duration)}`;
  playBtn.innerHTML = _yt.playing
    ? '<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>'
    : '<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>';
}

let _ytTimeTimer = null;

function renderMusic() {
  const bar = document.getElementById('music-bar');
  if (!bar) return;
  if (!state.showMusicWidget || !state.musicPlaylistUrl) {
    bar.hidden = true;
    return;
  }
  bar.hidden = false;
  const parsed = parseYouTubeUrl(state.musicPlaylistUrl);
  const titleEl = document.getElementById('m-title');
  const authorEl = document.getElementById('m-author');
  const coverEl = document.getElementById('m-cover');
  const iframe = document.getElementById('yt-player');

  if (!parsed) {
    titleEl.textContent = 'YouTube Music';
    authorEl.textContent = 'Неверный URL — проверь настройки';
    coverEl.src = '';
    iframe.src = '';
    return;
  }
  if (isPrivatePlaylist(parsed)) {
    const labelMap = { LM: 'Liked Music', WL: 'Watch Later', LL: 'Liked Videos', HL: 'История' };
    const label = labelMap[parsed.id] || (parsed.id.startsWith('RD') ? 'Radio Mix' : 'Приватный плейлист');
    titleEl.textContent = `🎵 ${label}`;
    authorEl.textContent = 'Embed недоступен — нажми ▶ чтобы открыть в YT Music (pinned tab)';
    coverEl.src = '';
    iframe.src = '';
    return;
  }

  // Публичный плейлист или видео — грузим в скрытый iframe
  const newSrc = buildEmbedUrl(parsed);
  if (iframe.src !== newSrc) {
    _yt.ready = false;
    _yt.videoData = null;
    iframe.src = newSrc;
    iframe.onload = () => {
      // Handshake retry: первый listening event иногда теряется до того как iframe
      // успел подписаться на postMessage. Шлём 5 раз с возрастающим интервалом.
      let attempt = 0;
      const handshake = () => {
        ytStartListening();
        ytSendCommand('addEventListener', ['onReady']);
        ytSendCommand('addEventListener', ['onStateChange']);
        attempt++;
        if (attempt < 5 && !_yt.ready) {
          setTimeout(handshake, 500 + attempt * 300);
        }
      };
      handshake();
      if (_ytTimeTimer) clearInterval(_ytTimeTimer);
      _ytTimeTimer = setInterval(() => {
        if (_yt.ready) ytSendCommand('getCurrentTime');
      }, 1000);
    };
  }
  titleEl.textContent = 'Загрузка…';
  authorEl.textContent = '';
}

function toggleMusicPlay() {
  const parsed = parseYouTubeUrl(state.musicPlaylistUrl);
  if (!parsed) return;
  if (isPrivatePlaylist(parsed)) {
    openOrFocusYtMusicTab(state.musicPlaylistUrl);
    return;
  }
  if (!_yt.ready) return;
  ytSendCommand(_yt.playing ? 'pauseVideo' : 'playVideo');
}

// Открывает YT Music URL в pinned-вкладке (или фокусирует существующую).
// Дальше юзер управляет через Chrome media controls (иконка ноты в toolbar).
async function openOrFocusYtMusicTab(url) {
  if (!chrome?.tabs) {
    window.open(url, '_blank');
    return;
  }
  try {
    const existing = await new Promise(r => chrome.tabs.query({ url: 'https://music.youtube.com/*' }, r));
    if (existing.length > 0) {
      chrome.tabs.update(existing[0].id, { url, active: true });
    } else {
      chrome.tabs.create({ url, active: true, pinned: true });
    }
  } catch (e) {
    console.warn('[Kai Dial] tabs.query/create failed:', e);
    window.open(url, '_blank');
  }
}

// Старая реализация autoplay-injection — оставлена опционально через флаг state.musicAutostart,
// но по умолчанию выключена (хрупкая, ломается при смене UI YT Music).
async function autoplayPrivatePlaylist() {
  if (!chrome?.tabs || !chrome?.scripting) {
    openMusicInTab();
    return;
  }

  const titleEl = document.getElementById('m-title');
  const authorEl = document.getElementById('m-author');
  const setStatus = (t, a) => {
    if (titleEl) titleEl.textContent = t;
    if (authorEl) authorEl.textContent = a;
  };
  setStatus('🎵 Открываю YouTube Music…', 'Сейчас откроется вкладка, нажмётся play и вернётся обратно');

  const url = state.musicPlaylistUrl;
  const currentTab = await new Promise(r => chrome.tabs.getCurrent(r));

  // Проверяем уже открытые YT Music вкладки
  const existing = await new Promise(r => chrome.tabs.query({ url: 'https://music.youtube.com/*' }, r));
  let ytTab;
  if (existing.length > 0) {
    ytTab = existing[0];
    chrome.tabs.update(ytTab.id, { url, active: true });
  } else {
    ytTab = await new Promise(r => chrome.tabs.create({ url, active: true, pinned: true }, r));
  }

  // Ждём пока вкладка полностью загрузится (status === 'complete')
  await new Promise(resolve => {
    let resolved = false;
    const safetyTimer = setTimeout(() => { if (!resolved) { resolved = true; resolve(); } }, 8000);
    const listener = (tabId, info) => {
      if (tabId === ytTab.id && info.status === 'complete' && !resolved) {
        chrome.tabs.onUpdated.removeListener(listener);
        clearTimeout(safetyTimer);
        resolved = true;
        resolve();
      }
    };
    chrome.tabs.onUpdated.addListener(listener);
  });

  // +1.5 сек на render YT Music UI после load
  await new Promise(r => setTimeout(r, 1500));

  // Inject — clickPlay через deep DOM query (учитываем shadow roots)
  try {
    const results = await chrome.scripting.executeScript({
      target: { tabId: ytTab.id },
      func: kaiDialClickYtPlay,
    });
    console.log('[Kai Dial] injection result:', results);
  } catch (e) {
    console.warn('[Kai Dial] scripting.executeScript failed:', e);
  }

  // Возвращаемся обратно на newtab
  if (currentTab?.id) {
    chrome.tabs.update(currentTab.id, { active: true });
  }

  setStatus('▶ Liked Music — играет на YT Music', 'Управляй через Chrome media controls (иконка ноты в toolbar)');
}

// Запускается в контексте YT Music tab — ищет play button через shadow DOM
function kaiDialClickYtPlay() {
  const log = (...a) => console.log('[Kai Dial inject]', ...a);

  function deepQuery(selectors, root) {
    root = root || document;
    for (const sel of selectors) {
      try {
        const found = root.querySelector?.(sel);
        if (found) return found;
      } catch {}
    }
    const items = root.querySelectorAll?.('*') || [];
    for (const item of items) {
      if (item.shadowRoot) {
        const r = deepQuery(selectors, item.shadowRoot);
        if (r) return r;
      }
    }
    return null;
  }

  const PLAY_SELECTORS = [
    'ytmusic-play-button-renderer button',
    'ytmusic-play-button-renderer',
    '.play-button.ytmusic-shelf-renderer',
    '#play-pause-button',
    'tp-yt-paper-icon-button#play-pause-button',
    'button[aria-label="Воспроизвести"]',
    'button[aria-label="Play"]',
    'button[aria-label="Перемешать"]',
    'button[aria-label="Shuffle play"]',
    'button[aria-label*="Воспр" i]',
    'button[aria-label*="Play" i]',
  ];

  let attempts = 0;
  const tryPlay = () => {
    attempts++;
    const btn = deepQuery(PLAY_SELECTORS);
    if (btn) {
      const label = (btn.getAttribute('aria-label') || '').toLowerCase();
      if (label.includes('pause') || label.includes('пауза') || label.includes('приостан')) {
        log('already playing — pause button visible:', label);
        return;
      }
      log('clicking:', btn.tagName, label);
      btn.click();
      btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
      // Через 1 сек проверим что заиграло; если нет — попробуем space
      setTimeout(() => {
        const checkBtn = deepQuery(['#play-pause-button', 'tp-yt-paper-icon-button#play-pause-button']);
        const checkLabel = (checkBtn?.getAttribute('aria-label') || '').toLowerCase();
        if (!checkLabel.includes('pause') && !checkLabel.includes('пауза') && attempts < 20) {
          log('play did not start, retry:', attempts);
          tryPlay();
        } else {
          log('playing now');
        }
      }, 1200);
      return;
    }
    if (attempts < 20) {
      setTimeout(tryPlay, 700);
    } else {
      log('FAILED — could not find play button after 20 attempts. YT Music UI selectors might have changed.');
    }
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', tryPlay);
  } else {
    tryPlay();
  }
}

function nextTrack() {
  if (!_yt.ready) return;
  ytSendCommand('nextVideo');
}
function prevTrack() {
  if (!_yt.ready) return;
  ytSendCommand('previousVideo');
}

function openMusicInTab() {
  const url = state.musicPlaylistUrl;
  if (!url) return;
  try {
    const u = new URL(url);
    if (u.hostname === 'www.youtube.com' || u.hostname === 'youtube.com') {
      u.hostname = 'music.youtube.com';
      window.open(u.toString(), '_blank');
      return;
    }
  } catch {}
  window.open(url, '_blank');
}

function initMusicBar() {
  document.getElementById('m-play').onclick = toggleMusicPlay;
  document.getElementById('m-next').onclick = nextTrack;
  document.getElementById('m-prev').onclick = prevTrack;
  document.getElementById('m-open').onclick = openMusicInTab;
  document.getElementById('m-close').onclick = () => {
    state.showMusicWidget = false;
    save();
    renderMusic();
  };

  // Слушаем postMessage от YT iframe
  window.addEventListener('message', e => {
    if (typeof e.data !== 'string') return;
    if (!/youtube\.com/.test(e.origin)) return;
    let data;
    try { data = JSON.parse(e.data); } catch { return; }
    if (data.event === 'onReady' || data.event === 'initialDelivery') {
      _yt.ready = true;
      ytSendCommand('getDuration');
      ytSendCommand('getCurrentTime');
    }
    if (data.event === 'infoDelivery' || data.event === 'onStateChange') {
      const info = data.info;
      if (!info) return;
      if (info.videoData) _yt.videoData = info.videoData;
      if (typeof info.duration === 'number') _yt.duration = info.duration;
      if (typeof info.currentTime === 'number') _yt.currentTime = info.currentTime;
      if (typeof info.playerState === 'number') {
        _yt.playing = info.playerState === 1;
      }
      updateMusicUI();
    }
  });
}

// ═══════════════════════════════════════════════
// Widget context menu + dialogs (clock TZ, city, forecast, weather source)
// ═══════════════════════════════════════════════

function showWidgetMenu(type, e) {
  const menu = document.getElementById('context-menu');
  const items = type === 'clock'
    ? [{ act: 'clock-tz', label: 'Часовой пояс…' }, { act: 'clock-tz-auto', label: 'Сбросить (системный)' }]
    : [
        { act: 'weather-forecast', label: 'Прогноз на 7 дней' },
        { act: 'weather-city', label: 'Выбрать город…' },
        { act: 'weather-source', label: 'Источник погоды…' },
      ];
  menu.innerHTML = items.map(i => `<button data-act-w="${i.act}">${i.label}</button>`).join('');
  menu.classList.add('visible');
  const rect = menu.getBoundingClientRect();
  const x = Math.min(e.clientX, window.innerWidth - rect.width - 8);
  const y = Math.min(e.clientY, window.innerHeight - rect.height - 8);
  menu.style.left = x + 'px';
  menu.style.top = y + 'px';
  menu.querySelectorAll('button').forEach(btn => {
    btn.onclick = () => {
      menu.classList.remove('visible');
      const act = btn.dataset.actW;
      if (act === 'clock-tz') showTzPicker();
      else if (act === 'clock-tz-auto') { state.clockTimezone = null; save(); renderClock(); }
      else if (act === 'weather-forecast') showWeatherForecast();
      else if (act === 'weather-city') showCityPicker();
      else if (act === 'weather-source') showWeatherSource();
    };
  });
}

async function showCityPicker() {
  const dlg = document.getElementById('city-picker-dialog');
  const input = document.getElementById('city-search');
  const results = document.getElementById('city-results');
  input.value = state.weatherCity?.name || '';
  results.innerHTML = '';
  let timer = null;
  input.oninput = () => {
    if (timer) clearTimeout(timer);
    timer = setTimeout(async () => {
      const q = input.value.trim();
      if (q.length < 2) { results.innerHTML = ''; return; }
      try {
        const r = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(q)}&count=8&language=ru`);
        const d = await r.json();
        const items = d.results || [];
        if (!items.length) { results.innerHTML = '<div class="hint">Ничего не найдено</div>'; return; }
        results.innerHTML = items.map((c, i) => `
          <div class="item" data-i="${i}">
            <div>
              <div>${c.name}</div>
              <div class="meta">${[c.admin1, c.country].filter(Boolean).join(', ')}</div>
            </div>
            <div class="meta">${c.latitude.toFixed(2)}, ${c.longitude.toFixed(2)}</div>
          </div>
        `).join('');
        results.querySelectorAll('.item').forEach(el => {
          el.onclick = async () => {
            const c = items[parseInt(el.dataset.i)];
            state.weatherCity = { lat: c.latitude, lon: c.longitude, name: c.name };
            await save();
            dlg.close();
            renderWeather();
          };
        });
      } catch (e) {
        results.innerHTML = '<div class="hint">Ошибка поиска: ' + (e.message || e) + '</div>';
      }
    }, 300);
  };
  document.getElementById('city-cancel').onclick = () => dlg.close();
  dlg.showModal();
  setTimeout(() => input.focus(), 50);
}

function showTzPicker() {
  const dlg = document.getElementById('tz-picker-dialog');
  const input = document.getElementById('tz-search');
  const results = document.getElementById('tz-results');
  let allTz = [];
  try { allTz = Intl.supportedValuesOf('timeZone'); } catch { allTz = Object.keys(TZ_TO_CITY); }
  const current = state.clockTimezone;

  const renderList = (filter = '') => {
    const f = filter.toLowerCase();
    const list = (f ? allTz.filter(t => t.toLowerCase().includes(f)) : allTz).slice(0, 100);
    results.innerHTML = list.map(t => {
      const offset = (() => {
        try {
          const d = new Date();
          const opts = { timeZone: t, timeZoneName: 'shortOffset' };
          const parts = new Intl.DateTimeFormat('en-US', opts).formatToParts(d);
          return parts.find(p => p.type === 'timeZoneName')?.value || '';
        } catch { return ''; }
      })();
      return `<div class="item ${t === current ? 'active' : ''}" data-tz="${t}">
        <div>${t.replace(/_/g, ' ')}</div>
        <div class="meta">${offset}</div>
      </div>`;
    }).join('');
    results.querySelectorAll('.item').forEach(el => {
      el.onclick = () => {
        state.clockTimezone = el.dataset.tz;
        save();
        dlg.close();
        renderClock();
      };
    });
  };
  renderList(input.value);
  input.oninput = () => renderList(input.value);
  document.getElementById('tz-auto').onclick = () => {
    state.clockTimezone = null;
    save();
    dlg.close();
    renderClock();
  };
  document.getElementById('tz-cancel').onclick = () => dlg.close();
  dlg.showModal();
  setTimeout(() => input.focus(), 50);
}

async function fetchWeatherForecast(city) {
  const { lat, lon } = city;
  const source = state.weatherSource || 'open-meteo';
  if (source === 'wttr') {
    const r = await fetch(`https://wttr.in/${lat},${lon}?format=j1`);
    const d = await r.json();
    return d.weather.slice(0, 7).map(day => ({
      date: day.date,
      hi: parseInt(day.maxtempC),
      lo: parseInt(day.mintempC),
      code: parseInt(day.hourly[4]?.weatherCode) || 0,
      desc: day.hourly[4]?.weatherDesc?.[0]?.value || '',
    }));
  }
  // open-meteo
  const r = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&daily=temperature_2m_max,temperature_2m_min,weather_code&timezone=auto&forecast_days=7`);
  const d = await r.json();
  return d.daily.time.map((t, i) => ({
    date: t,
    hi: Math.round(d.daily.temperature_2m_max[i]),
    lo: Math.round(d.daily.temperature_2m_min[i]),
    code: d.daily.weather_code[i],
    desc: '',
  }));
}

const WEATHER_DESC = {
  0: 'Ясно', 1: 'Преимущественно ясно', 2: 'Переменная облачность', 3: 'Облачно',
  45: 'Туман', 48: 'Изморозь',
  51: 'Лёгкая морось', 53: 'Морось', 55: 'Сильная морось',
  61: 'Лёгкий дождь', 63: 'Дождь', 65: 'Сильный дождь',
  71: 'Лёгкий снег', 73: 'Снег', 75: 'Сильный снег', 77: 'Снежная крупа',
  80: 'Ливень', 81: 'Ливень', 82: 'Сильный ливень',
  85: 'Снегопад', 86: 'Сильный снегопад',
  95: 'Гроза', 96: 'Гроза с градом', 99: 'Сильная гроза',
};

async function showWeatherForecast() {
  const dlg = document.getElementById('forecast-dialog');
  const list = document.getElementById('forecast-list');
  const title = document.getElementById('forecast-title');
  if (!state.weatherCity) { alert('Сначала выбери город'); return; }
  title.textContent = `Прогноз: ${state.weatherCity.name}`;
  list.innerHTML = '<div class="hint">Загрузка…</div>';
  document.getElementById('forecast-close').onclick = () => dlg.close();
  dlg.showModal();
  try {
    const days = await fetchWeatherForecast(state.weatherCity);
    list.innerHTML = days.map(d => {
      const dt = new Date(d.date);
      const day = dt.toLocaleDateString('ru-RU', { weekday: 'short', day: 'numeric', month: 'short' });
      const desc = d.desc || WEATHER_DESC[d.code] || '';
      const icon = WEATHER_ICONS[d.code] || '🌡';
      return `<div class="forecast-day">
        <div class="date">${day.charAt(0).toUpperCase() + day.slice(1)}</div>
        <div class="icon">${icon}</div>
        <div class="desc">${desc}</div>
        <div class="hi">${d.hi}°</div>
        <div class="lo">${d.lo}°</div>
      </div>`;
    }).join('');
  } catch (e) {
    list.innerHTML = '<div class="hint">Ошибка: ' + (e.message || e) + '</div>';
  }
}

function showWeatherSource() {
  const dlg = document.getElementById('weather-source-dialog');
  document.getElementById('ws-source').value = state.weatherSource || 'open-meteo';
  document.getElementById('ws-cancel').onclick = () => dlg.close();
  document.getElementById('weather-source-form').onsubmit = async e => {
    e.preventDefault();
    state.weatherSource = document.getElementById('ws-source').value;
    await save();
    dlg.close();
    renderWeather();
  };
  dlg.showModal();
}

// ═══════════════════════════════════════════════
// AI Search via Chrome Built-in (Gemini Nano)
// ═══════════════════════════════════════════════

let _aiAvailable = false;
async function checkAIAvailable() {
  try {
    if (typeof window.LanguageModel !== 'undefined') {
      const avail = await window.LanguageModel.availability();
      _aiAvailable = avail === 'available' || avail === 'readily';
    } else if (window.ai?.languageModel) {
      const cap = await window.ai.languageModel.capabilities();
      _aiAvailable = cap.available === 'readily';
    }
  } catch { _aiAvailable = false; }
  document.getElementById('ai-btn').hidden = !_aiAvailable;
  document.getElementById('ai-badge').hidden = !_aiAvailable;
  return _aiAvailable;
}

async function aiSearch(query, bookmarks) {
  if (!_aiAvailable) return null;
  try {
    const ai = window.LanguageModel || window.ai.languageModel;
    const session = await ai.create({
      systemPrompt: 'You help search bookmarks. Return ONLY a JSON array of bookmark indices most relevant to the query, ordered by relevance. Max 12 results. No explanations, just the array.',
      outputLanguage: 'en',
      expectedOutputs: [{ type: 'text', languages: ['en'] }],
    });
    const list = bookmarks.map((b, i) => `${i}: ${b.title} | ${b.url}`).join('\n');
    const prompt = `Query: "${query}"\nBookmarks:\n${list}\n\nReturn JSON array of indices:`;
    const response = await session.prompt(prompt);
    session.destroy?.();
    const m = response.match(/\[[\d,\s]*\]/);
    return m ? JSON.parse(m[0]) : null;
  } catch (e) {
    console.warn('AI search failed:', e);
    return null;
  }
}

// ═══════════════════════════════════════════════
// Animations — FLIP, drag, crossfade
// ═══════════════════════════════════════════════

const SPRING = 'cubic-bezier(0.32, 0.72, 0, 1)';
const SPRING_BOUNCE = 'cubic-bezier(0.34, 1.56, 0.64, 1)';

function snapshotPositions(container) {
  const rects = new Map();
  for (const child of container.children) {
    const id = child.dataset.id || child.dataset.gid;
    if (id) rects.set(id, child.getBoundingClientRect());
  }
  return rects;
}

function playFLIP(container, oldRects, opts = {}) {
  const moveDuration = opts.duration ?? 380;
  const newDuration = opts.newDuration ?? 360;
  for (const child of container.children) {
    const id = child.dataset.id || child.dataset.gid;
    if (!id) continue;
    const oldR = oldRects.get(id);
    if (!oldR) {
      child.animate([
        { opacity: 0, transform: 'scale(0.82) translateY(12px)' },
        { opacity: 1, transform: 'scale(1) translateY(0)' }
      ], { duration: newDuration, easing: SPRING_BOUNCE, fill: 'backwards' });
      continue;
    }
    const newR = child.getBoundingClientRect();
    const dx = oldR.left - newR.left;
    const dy = oldR.top - newR.top;
    if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) continue;
    child.animate([
      { transform: `translate(${dx}px, ${dy}px)`, easing: SPRING },
      { transform: 'translate(0, 0)' }
    ], { duration: moveDuration, easing: SPRING });
  }
}

async function rerenderGroupsAnimated() {
  const el = document.getElementById('groups');
  const old = snapshotPositions(el);
  renderGroups();
  playFLIP(el, old);
}

async function rerenderGridAnimated(opts = {}) {
  const el = document.getElementById('grid');
  const old = snapshotPositions(el);
  await renderGrid();
  playFLIP(el, old, opts);
}

async function switchToGroup(newId) {
  if (newId === state.activeGroup) return;
  const grid = document.getElementById('grid');

  grid.classList.add('transitioning'); // включает will-change
  grid.classList.add('fading-out');
  await new Promise(r => setTimeout(r, 90));

  state.activeGroup = newId;
  saveNow();
  await renderGrid();
  rerenderGroupsAnimated();

  grid.classList.remove('fading-out');
  grid.classList.add('fading-in', 'no-transition');
  void grid.offsetHeight;
  grid.classList.remove('no-transition');
  void grid.offsetHeight;
  grid.classList.remove('fading-in');
  // снимаем will-change через 200мс после окончания transition (200мс = duration in CSS)
  setTimeout(() => grid.classList.remove('transitioning'), 240);
}

async function removeBookmarkAnimated(bm) {
  const tile = document.querySelector(`.tile[data-id="${bm.id}"]`);
  if (tile) {
    await tile.animate([
      { opacity: 1, transform: 'scale(1)' },
      { opacity: 0, transform: 'scale(0.78) translateY(6px)' }
    ], { duration: 220, easing: 'cubic-bezier(0.4, 0, 1, 1)', fill: 'forwards' }).finished;
  }
  state.bookmarks = state.bookmarks.filter(b => b.id !== bm.id);
  await save();
  await rerenderGridAnimated();
}

// ═══════════════════════════════════════════════
// Render
// ═══════════════════════════════════════════════

let dragGroupId = null;
let dragTileId = null;

function renderGroups() {
  const el = document.getElementById('groups');
  el.innerHTML = '';
  for (const g of state.groups) {
    const btn = document.createElement('button');
    btn.className = 'group-btn' + (g.id === state.activeGroup ? ' active' : '');
    btn.textContent = g.name;
    btn.draggable = true;
    btn.dataset.gid = g.id;
    btn.onclick = () => switchToGroup(g.id);
    btn.ondblclick = () => renameGroup(g);
    btn.oncontextmenu = e => { e.preventDefault(); showGroupMenu(g, e); };
    btn.ondragstart = e => { dragGroupId = g.id; btn.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; };
    btn.ondragend = () => { btn.classList.remove('dragging'); dragGroupId = null; document.querySelectorAll('.group-btn').forEach(b => b.classList.remove('drag-over', 'drag-tile-over')); };
    btn.ondragover = e => {
      if (dragGroupId && dragGroupId !== g.id) {
        e.preventDefault();
        btn.classList.add('drag-over');
      } else if (dragTileId) {
        e.preventDefault();
        btn.classList.add('drag-tile-over');
      }
    };
    btn.ondragleave = () => btn.classList.remove('drag-over', 'drag-tile-over');
    btn.ondrop = async e => {
      e.preventDefault();
      btn.classList.remove('drag-over', 'drag-tile-over');
      if (dragGroupId && dragGroupId !== g.id) {
        // reorder группы
        const groupsEl = document.getElementById('groups');
        const old = snapshotPositions(groupsEl);
        const fromIdx = state.groups.findIndex(x => x.id === dragGroupId);
        const toIdx = state.groups.findIndex(x => x.id === g.id);
        if (fromIdx === -1 || toIdx === -1) return;
        const [moved] = state.groups.splice(fromIdx, 1);
        state.groups.splice(toIdx, 0, moved);
        await save();
        renderGroups();
        playFLIP(groupsEl, old);
      } else if (dragTileId) {
        // тайл переместить в эту группу
        const bm = state.bookmarks.find(x => x.id === dragTileId);
        if (!bm || bm.group === g.id) return;
        bm.group = g.id;
        await save();
        // визуальный feedback на группе
        btn.animate([
          { transform: 'scale(1)' },
          { transform: 'scale(1.12)' },
          { transform: 'scale(1)' }
        ], { duration: 320, easing: SPRING_BOUNCE });
        await rerenderGridAnimated();
      }
    };
    el.appendChild(btn);
  }
  const addBtn = document.createElement('button');
  addBtn.className = 'group-btn add';
  addBtn.textContent = '+ группа';
  addBtn.onclick = addGroup;
  el.appendChild(addBtn);
}

let _searchAbort = null;
async function renderGrid() {
  const el = document.getElementById('grid');
  const empty = document.getElementById('empty-hint');
  const search = document.getElementById('search').value.trim().toLowerCase();
  el.innerHTML = '';
  el.className = 'grid size-' + state.tileSize;

  let filtered;
  if (search) {
    // text search
    const textMatch = state.bookmarks
      .map((b, idx) => ({ b, idx }))
      .filter(({ b }) => (b.title + ' ' + b.url).toLowerCase().includes(search));

    if (textMatch.length === 0 && _aiAvailable && search.length > 3) {
      // AI fallback for natural questions
      if (_searchAbort) _searchAbort.abort();
      _searchAbort = new AbortController();
      const indices = await aiSearch(search, state.bookmarks);
      if (indices && Array.isArray(indices)) {
        filtered = indices.map(i => state.bookmarks[i]).filter(Boolean);
      } else {
        filtered = [];
      }
    } else {
      filtered = textMatch.map(x => x.b);
    }
  } else {
    filtered = state.bookmarks.filter(b => b.group === state.activeGroup);
    // pinned первыми
    filtered.sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0));
  }

  empty.hidden = filtered.length > 0;

  for (const b of filtered) {
    const mode = perTileMode(b);
    const tile = document.createElement('a');
    tile.className = 'tile' + (mode === 'screenshot' ? ' tile-screenshot' : '') + (b.pinned ? ' pinned' : '');
    tile.href = b.url;
    tile.dataset.id = b.id;

    if (b.pinned) {
      const pinIcon = document.createElement('div');
      pinIcon.className = 'pin-icon';
      pinIcon.textContent = '📌';
      tile.appendChild(pinIcon);
    }

    if (mode === 'screenshot') {
      const loading = document.createElement('div');
      loading.className = 'screenshot-loading';
      loading.textContent = 'Загрузка…';
      tile.appendChild(loading);

      const img = document.createElement('img');
      img.className = 'screenshot-img';
      img.loading = 'lazy';
      img.onload = () => { loading.remove(); };
      tryImageChain(img, screenshotUrls(b.url), () => {
        loading.remove();
        const fav = document.createElement('img');
        fav.className = 'favi';
        loadFavicon(fav, b, () => {
          fav.remove();
          const fb = document.createElement('div');
          fb.className = 'favi-fallback';
          fb.textContent = (b.title[0] || '?').toUpperCase();
          tile.prepend(fb);
        });
        tile.prepend(fav);
      });
      tile.appendChild(img);
      const title = document.createElement('div');
      title.className = 'title';
      title.textContent = b.title;
      tile.appendChild(title);
    } else {
      const img = document.createElement('img');
      img.className = 'favi';
      loadFavicon(img, b, () => {
        img.remove();
        const fb = document.createElement('div');
        fb.className = 'favi-fallback';
        fb.textContent = (b.title[0] || '?').toUpperCase();
        tile.prepend(fb);
      });
      tile.appendChild(img);
      const title = document.createElement('div');
      title.className = 'title';
      title.textContent = b.title;
      tile.appendChild(title);
    }

    tile.oncontextmenu = e => { e.preventDefault(); showContextMenu(e, b); };

    // drag & drop тайлов внутри grid
    if (!search) { // не разрешаем drag в режиме поиска
      tile.draggable = true;
      tile.addEventListener('dragstart', e => {
        dragTileId = b.id;
        tile.classList.add('dragging');
        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData('text/plain', b.id);
      });
      tile.addEventListener('dragend', () => {
        dragTileId = null;
        tile.classList.remove('dragging');
        document.querySelectorAll('.tile.drag-target').forEach(t => t.classList.remove('drag-target'));
      });
      tile.addEventListener('dragover', e => {
        if (!dragTileId || dragTileId === b.id) return;
        e.preventDefault();
        e.dataTransfer.dropEffect = 'move';
        tile.classList.add('drag-target');
      });
      tile.addEventListener('dragleave', () => tile.classList.remove('drag-target'));
      tile.addEventListener('drop', async e => {
        e.preventDefault();
        tile.classList.remove('drag-target');
        if (!dragTileId || dragTileId === b.id) return;
        const fromIdx = state.bookmarks.findIndex(x => x.id === dragTileId);
        const toIdx = state.bookmarks.findIndex(x => x.id === b.id);
        if (fromIdx === -1 || toIdx === -1) return;
        const moved = state.bookmarks[fromIdx];
        moved.group = b.group;
        state.bookmarks.splice(fromIdx, 1);
        const newToIdx = state.bookmarks.findIndex(x => x.id === b.id);
        state.bookmarks.splice(newToIdx, 0, moved);
        await save();
        await rerenderGridAnimated();
      });
    }

    el.appendChild(tile);
  }
}

function render() {
  applyTheme();
  renderWallpaper();
  renderGroups();
  renderGrid();
  renderClock();
  renderWeather();
  renderMusic();
}

// ═══════════════════════════════════════════════
// Context menu / Dialogs
// ═══════════════════════════════════════════════

let currentContextBm = null;
function showContextMenu(e, bookmark) {
  currentContextBm = bookmark;
  const menu = document.getElementById('context-menu');
  // восстанавливаем стандартное меню (на случай если showGroupMenu заменил innerHTML)
  menu.innerHTML = `
    <button data-act="open">Открыть</button>
    <button data-act="open-bg">В новой вкладке</button>
    <div class="sep"></div>
    <button data-act="pin">${bookmark.pinned ? 'Открепить' : 'Закрепить'}</button>
    <button data-act="toggle-view">Иконка ↔ Скриншот</button>
    <button data-act="edit">Редактировать</button>
    <div class="sep"></div>
    <button data-act="delete" class="danger">Удалить</button>
  `;
  menu.classList.add('visible');
  const rect = menu.getBoundingClientRect();
  const x = Math.min(e.clientX, window.innerWidth - rect.width - 8);
  const y = Math.min(e.clientY, window.innerHeight - rect.height - 8);
  menu.style.left = x + 'px';
  menu.style.top = y + 'px';
}
function hideContextMenu() { document.getElementById('context-menu').classList.remove('visible'); currentContextBm = null; }

function showDialog(bookmark = null) {
  const dialog = document.getElementById('dialog');
  document.getElementById('form-title').textContent = bookmark ? 'Редактировать' : 'Новая закладка';
  document.getElementById('f-title').value = bookmark?.title || '';
  document.getElementById('f-url').value = bookmark?.url || '';
  document.getElementById('f-icon').value = bookmark?.icon || '';
  const gSel = document.getElementById('f-group');
  gSel.innerHTML = '';
  for (const g of state.groups) {
    const opt = document.createElement('option');
    opt.value = g.id; opt.textContent = g.name;
    if (g.id === (bookmark?.group || state.activeGroup)) opt.selected = true;
    gSel.appendChild(opt);
  }
  dialog.showModal();
  document.getElementById('f-title').focus();
  document.getElementById('form').onsubmit = async e => {
    e.preventDefault();
    const data = {
      title: document.getElementById('f-title').value,
      url: document.getElementById('f-url').value,
      group: gSel.value,
      icon: document.getElementById('f-icon').value.trim() || null,
    };
    if (bookmark) Object.assign(bookmark, data);
    else state.bookmarks.push({ id: Date.now() + '-' + Math.random().toString(36).slice(2,7), ...data });
    await save();
    await rerenderGridAnimated();
    dialog.close();
  };
}

function addGroup() {
  const name = prompt('Название новой группы?');
  if (!name) return;
  const id = name.toLowerCase().replace(/[^a-zа-я0-9]/gi, '') + Date.now().toString(36);
  state.groups.push({ id, name });
  state.activeGroup = id;
  save(); render();
}
function renameGroup(g) {
  const name = prompt('Новое название:', g.name);
  if (!name || name === g.name) return;
  g.name = name;
  save(); render();
}
function showGroupMenu(g, e) {
  const menu = document.getElementById('context-menu');
  menu.innerHTML = `
    <button data-act-g="rename">Переименовать</button>
    <button data-act-g="delete" class="danger">Удалить группу</button>
  `;
  menu.classList.add('visible');
  const x = Math.min(e.clientX, window.innerWidth - 240);
  const y = Math.min(e.clientY, window.innerHeight - 100);
  menu.style.left = x + 'px';
  menu.style.top = y + 'px';
  menu.querySelectorAll('button').forEach(btn => {
    btn.onclick = () => {
      const act = btn.dataset.actG;
      menu.classList.remove('visible');
      if (act === 'rename') renameGroup(g);
      else if (act === 'delete') deleteGroup(g);
    };
  });
}
function deleteGroup(g) {
  if (state.groups.length <= 1) return alert('Нельзя удалить последнюю группу');
  if (!confirm(`Удалить "${g.name}" и все закладки в ней?`)) return;
  state.bookmarks = state.bookmarks.filter(b => b.group !== g.id);
  state.groups = state.groups.filter(x => x.id !== g.id);
  if (state.activeGroup === g.id) state.activeGroup = state.groups[0].id;
  save(); render();
}

// ═══════════════════════════════════════════════
// Settings
// ═══════════════════════════════════════════════

function showSettings() {
  document.getElementById('s-theme').value = state.theme || 'auto';
  document.getElementById('s-search-engine').value = state.searchEngine || 'google';
  document.getElementById('s-wallpaper-source').value = state.wallpaperSource || 'mesh';
  document.getElementById('s-wallpaper').value = state.wallpaper || '';
  document.getElementById('s-show-clock').checked = !!state.showClock;
  document.getElementById('s-show-weather').checked = !!state.showWeather;
  document.getElementById('s-show-music').checked = !!state.showMusicWidget;
  document.getElementById('s-music-url').value = state.musicPlaylistUrl || '';
  document.getElementById('s-tile-size').value = state.tileSize;
  document.getElementById('s-tile-mode').value = state.tileMode;
  toggleWallpaperUrlVisibility();
  document.getElementById('settings-dialog').showModal();
}

function toggleWallpaperUrlVisibility() {
  const src = document.getElementById('s-wallpaper-source').value;
  document.getElementById('s-wallpaper-url-wrap').style.display = src === 'custom' ? '' : 'none';
}

// ═══════════════════════════════════════════════
// Import / Export
// ═══════════════════════════════════════════════

function exportJSON() {
  const data = JSON.stringify({ groups: state.groups, bookmarks: state.bookmarks, version: SCHEMA_VERSION }, null, 2);
  const blob = new Blob([data], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `kai-dial-${todayKey()}.json`;
  a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

async function importJSONFile(file) {
  try {
    const text = await file.text();
    const parsed = JSON.parse(text);
    if (!parsed.bookmarks || !Array.isArray(parsed.bookmarks)) throw new Error('invalid format');
    const existingUrls = new Set(state.bookmarks.map(b => b.url));
    const existingGroupIds = new Set(state.groups.map(g => g.id));
    let added = 0, skipped = 0;
    for (const g of (parsed.groups || [])) {
      if (!existingGroupIds.has(g.id)) state.groups.push(g);
    }
    for (const b of parsed.bookmarks) {
      if (existingUrls.has(b.url)) { skipped++; continue; }
      state.bookmarks.push({ id: Date.now() + '-' + Math.random().toString(36).slice(2,7), ...b });
      added++;
    }
    await save();
    render();
    alert(`Импортировано: ${added}, пропущено (дубликаты): ${skipped}`);
  } catch (e) {
    alert('Ошибка импорта: ' + e.message);
  }
}

async function importChromeBookmarks() {
  if (!chrome.bookmarks?.getTree) {
    alert('Chrome bookmarks API недоступен');
    return;
  }
  const tree = await new Promise(r => chrome.bookmarks.getTree(r));
  const groupId = 'chrome' + Date.now().toString(36);
  const groupName = 'Chrome закладки';
  const flat = [];
  function walk(node) {
    if (node.url) flat.push({ title: node.title || node.url, url: node.url });
    if (node.children) node.children.forEach(walk);
  }
  tree.forEach(walk);
  if (flat.length === 0) { alert('Закладок не найдено'); return; }
  if (!confirm(`Импортировать ${flat.length} закладок из Chrome в новую группу "${groupName}"?`)) return;

  state.groups.push({ id: groupId, name: groupName });
  const existingUrls = new Set(state.bookmarks.map(b => b.url));
  let added = 0;
  for (const b of flat) {
    if (existingUrls.has(b.url)) continue;
    state.bookmarks.push({
      id: Date.now() + '-' + Math.random().toString(36).slice(2,7),
      title: b.title, url: b.url, group: groupId,
    });
    added++;
  }
  state.activeGroup = groupId;
  await save();
  render();
  alert(`Импортировано: ${added} закладок`);
}

// ═══════════════════════════════════════════════
// Init
// ═══════════════════════════════════════════════

(async () => {
  state = await storage.get();
  // Defaults для пропущенных полей
  state.tileSize ??= 'large';
  state.tileMode ??= 'favicon';
  state.theme ??= 'auto';
  state.wallpaperSource ??= 'mesh';
  state.showClock ??= true;
  state.showWeather ??= true;

  // Миграция v9: удаляем группы 'cf' и 'fun'
  if ((state.version || 0) < 9 && state.groups && state.bookmarks) {
    state.groups = state.groups.filter(g => g.id !== 'cf' && g.id !== 'fun');
    state.bookmarks = state.bookmarks.filter(b => b.group !== 'cf' && b.group !== 'fun');
    if (state.activeGroup === 'cf' || state.activeGroup === 'fun') {
      state.activeGroup = state.groups[0]?.id || 'cc';
    }
  }

  // Миграция v10: сброс iconCached — массовый upgrade на Simple Icons
  if ((state.version || 0) < 10 && state.bookmarks) {
    state.bookmarks.forEach(b => { delete b.iconCached; });
  }

  // Миграция: версия < SCHEMA_VERSION → подгружаем дефолтные группы/закладки (без дублирования)
  if ((state.version || 0) < SCHEMA_VERSION || !state.bookmarks || state.bookmarks.length === 0) {
    const defaults = await loadDefaultBookmarks();
    const existing = state.bookmarks || [];
    const existingUrls = new Set(existing.map(b => b.url));
    const existingGroupIds = new Set((state.groups || []).map(g => g.id));

    state.groups = [...(state.groups || [])];
    for (const g of defaults.groups) {
      if (!existingGroupIds.has(g.id)) state.groups.push(g);
    }
    state.bookmarks = [...existing];
    for (const b of defaults.bookmarks) {
      if (!existingUrls.has(b.url)) {
        state.bookmarks.push({ id: Date.now() + '-' + Math.random().toString(36).slice(2,7), ...b });
      }
    }
    if (!state.activeGroup || !state.groups.find(g => g.id === state.activeGroup)) {
      state.activeGroup = state.groups[0]?.id || 'cc';
    }
    state.version = SCHEMA_VERSION;
    // сбросить кеш иконок чтобы пересчитать с новыми pattern overrides
    state.bookmarks.forEach(b => { delete b.iconCached; });
    await saveNow();
  }

  applyTheme();
  await checkAIAvailable();
  render();

  // Cross-device sync: реагируем когда другое устройство обновило данные
  if (chrome?.storage?.onChanged) {
    chrome.storage.onChanged.addListener(async (changes, area) => {
      if (area !== 'sync') return;
      const touched = Object.keys(changes).some(k => k === META_KEY || k.startsWith(CHUNK_PREFIX));
      if (!touched) return;
      const remote = await syncRead();
      if (!remote) return;
      if ((remote.lastModified || 0) <= (state.lastModified || 0)) return;
      console.log('[Kai Dial] sync incoming from another device');
      _syncIncoming = true;
      state = remote;
      await new Promise(r => chrome.storage.local.set({ [STORAGE_KEY]: state }, r));
      render();
      setTimeout(() => { _syncIncoming = false; }, 1000);
    });
  }

  // Реакция на смену системной темы
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
    if ((state.theme || 'auto') === 'auto') {
      /* canvas перерисовывается заново при renderWallpaper */
      applyTheme();
      renderWallpaper();
    }
  });

  document.getElementById('add-btn').onclick = () => showDialog();
  document.getElementById('f-cancel').onclick = () => document.getElementById('dialog').close();
  document.getElementById('search').oninput = renderGrid;
  document.getElementById('search').addEventListener('keydown', e => {
    if (e.key === 'Enter') {
      const q = e.target.value.trim();
      if (!q) return;
      e.preventDefault();
      const url = SEARCH_ENGINES[state.searchEngine || 'google'].url + encodeURIComponent(q);
      window.location.href = url;
    }
  });
  applySearchEngineBadge();
  document.getElementById('search-engine-btn').onclick = cycleSearchEngine;
  document.getElementById('settings-btn').onclick = showSettings;
  document.getElementById('theme-btn').onclick = cycleTheme;
  document.getElementById('s-close').onclick = () => document.getElementById('settings-dialog').close();
  document.getElementById('s-wallpaper-source').onchange = toggleWallpaperUrlVisibility;
  document.getElementById('s-default-bg').onclick = () => {
    state.wallpaper = null;
    state.wallpaperDate = null;
    state.wallpaperUrl = null;
    state.wallpaperSource = 'mesh';
    document.getElementById('s-wallpaper').value = '';
    document.getElementById('s-wallpaper-source').value = 'mesh';
    toggleWallpaperUrlVisibility();
    /* canvas перерисовывается заново при renderWallpaper */
    save(); renderWallpaper();
  };
  document.getElementById('s-export').onclick = exportJSON;
  document.getElementById('s-import').onclick = () => document.getElementById('s-import-file').click();
  document.getElementById('s-import-file').onchange = e => {
    const file = e.target.files?.[0];
    if (file) importJSONFile(file);
    e.target.value = '';
  };
  document.getElementById('s-import-chrome').onclick = importChromeBookmarks;
  document.getElementById('s-reset-icons').onclick = async () => {
    state.bookmarks.forEach(b => { delete b.iconCached; });
    await saveNow();
    render();
    alert('Кеш иконок сброшен — иконки сейчас перезагрузятся');
  };

  document.getElementById('settings-form').onsubmit = async e => {
    e.preventDefault();
    state.theme = document.getElementById('s-theme').value;
    state.searchEngine = document.getElementById('s-search-engine').value || 'google';
    applySearchEngineBadge();
    const newSource = document.getElementById('s-wallpaper-source').value;
    const sourceChanged = newSource !== state.wallpaperSource;
    state.wallpaperSource = newSource;
    const w = document.getElementById('s-wallpaper').value.trim();
    state.wallpaper = w || null;
    if (sourceChanged) { state.wallpaperDate = null; state.wallpaperUrl = null; }
    state.showClock = document.getElementById('s-show-clock').checked;
    state.showWeather = document.getElementById('s-show-weather').checked;
    state.showMusicWidget = document.getElementById('s-show-music').checked;
    state.musicPlaylistUrl = document.getElementById('s-music-url').value.trim() || null;
    state.tileSize = document.getElementById('s-tile-size').value;
    state.tileMode = document.getElementById('s-tile-mode').value;
    await save();
    render();
    document.getElementById('settings-dialog').close();
  };

  // AI-поиск кнопка - триггерит повторный рендер с force AI
  document.getElementById('ai-btn').onclick = () => {
    document.getElementById('search').focus();
  };

  // Music widget кнопки
  initMusicBar();

  // Shortcuts в настройках музыки — заполняют URL поле и включают плеер
  document.querySelectorAll('.music-shortcut').forEach(btn => {
    btn.onclick = () => {
      document.getElementById('s-music-url').value = btn.dataset.url;
      document.getElementById('s-show-music').checked = true;
    };
  });

  // Auto-scroll groups-bar при наведении на края
  initGroupsHoverScroll();

  document.getElementById('context-menu').addEventListener('click', async e => {
    const btn = e.target.closest('button');
    if (!btn) return;
    if (btn.dataset.actG) return; // group menu - обрабатывается отдельно
    if (!currentContextBm) return;
    const act = btn.dataset.act;
    const bm = currentContextBm;
    if (act === 'open') window.location.href = bm.url;
    else if (act === 'open-bg') window.open(bm.url, '_blank');
    else if (act === 'pin') {
      bm.pinned = !bm.pinned;
      await save();
      await rerenderGridAnimated();
    }
    else if (act === 'toggle-view') {
      const current = perTileMode(bm);
      bm.mode = current === 'favicon' ? 'screenshot' : 'favicon';
      await save();
      await rerenderGridAnimated();
    }
    else if (act === 'edit') showDialog(bm);
    else if (act === 'delete') {
      if (confirm('Удалить закладку "' + bm.title + '"?')) {
        await removeBookmarkAnimated(bm);
      }
    }
    hideContextMenu();
  });
  document.addEventListener('click', e => {
    if (!e.target.closest('#context-menu') && !e.target.closest('.tile') && !e.target.closest('.group-btn')) hideContextMenu();
  });
  document.addEventListener('scroll', hideContextMenu);

  document.addEventListener('keydown', e => {
    if (e.key === 'Escape') hideContextMenu();
    if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); document.getElementById('search').focus(); }
    if (e.key >= '1' && e.key <= '9' && !e.ctrlKey && !e.metaKey && document.activeElement.tagName !== 'INPUT') {
      const idx = parseInt(e.key) - 1;
      const list = state.bookmarks
        .filter(b => b.group === state.activeGroup)
        .sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0));
      if (list[idx]) window.location.href = list[idx].url;
    }
  });
})();
