(() => {
  'use strict';

  const BOARD_TO_INDEX = {
    'img.2chan.net': 'https://tsumanne.net/si/indexes.php?format=json&sbmt=URL',
    'dat.2chan.net': 'https://tsumanne.net/sa/indexes.php?format=json&sbmt=URL',
    'may.2chan.net': 'https://tsumanne.net/my/indexes.php?format=json&sbmt=URL',
    'jun.2chan.net': 'https://tsumanne.net/tj/indexes.php?format=json&sbmt=URL'
  };
  const indexUrl = BOARD_TO_INDEX[location.hostname];
  if (!indexUrl) return;
  const inputUrl = new URL('input.php', indexUrl).href;
  const appendCategory = new URL('ac.php', indexUrl).href;
  const tagSearchUrl = new URL('indexes.php', indexUrl);
  tagSearchUrl.search = new URLSearchParams({
    format: 'json',
    sbmt: '\u2193\u591a'
  }).toString();

  const THREAD_PATHS = {
    'jun.2chan.net': /^\/30\/res\/(\d+)\.htm$/,
    default: /^\/b\/res\/(\d+)\.htm$/
  };
  const text = {
    registered: '[\u767b\u9332\u6e08]',
    register: '[\u767b\u9332]',
    checking: '[\u78ba\u8a8d\u4e2d]',
    failed: '[\u78ba\u8a8d\u5931\u6557]'
  };

  function canonicalUrl(value) {
    try {
      const url = new URL(value);
      return `${url.hostname.toLowerCase()}${url.pathname}`;
    } catch {
      return '';
    }
  }

  function threadLinks() {
    const links = [];
    const threadPath = THREAD_PATHS[location.hostname] || THREAD_PATHS.default;
    for (const anchor of document.querySelectorAll('a[href]')) {
      if (anchor.textContent.trim() !== '\u8fd4\u4fe1') continue;
      const url = new URL(anchor.href, location.href);
      if (url.hostname !== location.hostname || !threadPath.test(url.pathname)) continue;
      const key = canonicalUrl(url.href);
      links.push({
        url: url.href,
        key,
        insert: (node) => insertAfterClosingBracket(anchor, node)
      });
    }
    const currentUrl = new URL(location.href);
    if (threadPath.test(currentUrl.pathname)) {
      const container = document.querySelector('span.cntd');
      if (container) {
        links.push({
          url: currentUrl.href,
          key: canonicalUrl(currentUrl.href),
          insert: (node) => container.after(node)
        });
      }
    }
    return links;
  }

  function categories(value) {
    const list = Array.isArray(value) ? value : value == null ? [] : [value];
    return list.map((item) => {
      if (typeof item === 'string' || typeof item === 'number') return String(item);
      return item.name || item.title || item.category || '';
    }).filter(Boolean);
  }

  function logMap(logs) {
    const mapped = new Map();
    const add = (url, value) => {
      const key = canonicalUrl(url);
      if (key) mapped.set(key, {
        categories: categories(value?.category),
        id: value?.id,
        value
      });
    };
    if (Array.isArray(logs)) {
      for (const value of logs) {
        if (typeof value === 'string') add(value, {});
        else if (value && typeof value === 'object') add(value.url || value.href || value.source, value);
      }
    } else if (logs && typeof logs === 'object') {
      for (const [url, value] of Object.entries(logs)) {
        add(value?.url || value?.href || url, value || {});
      }
    }
    return mapped;
  }

  function insertAfterClosingBracket(anchor, element) {
    const sibling = anchor.nextSibling;
    if (sibling?.nodeType === Node.TEXT_NODE) {
      const closing = sibling.nodeValue.indexOf(']');
      if (closing !== -1) {
        const tail = sibling.splitText(closing + 1);
        tail.before(element);
        return;
      }
    }
    anchor.after(element);
  }

  function annotation(kind, labels = [], url = '', id = null) {
    const node = document.createElement(kind === 'register' ? 'button' : 'span');
    node.className = `tsumanne-addon__${kind}`;
    if (url) node.setAttribute('data-tsumanne-url', canonicalUrl(url));
    node.textContent = kind === 'register' ? text.register : text.registered;
    if (kind === 'registered' || labels.length) {
      const category = document.createElement('span');
      category.className = 'tsumanne-addon__categories';
      category.textContent = labels.length ? ` ${labels.map((label) => `[${label}]`).join(' ')}` : '';
      node.append(category);
    }
    if (kind === 'registered' && id != null) {
      const toggle = document.createElement('button');
      toggle.type = 'button';
      toggle.className = 'tsumanne-addon__category-toggle';
      toggle.textContent = '+';
      toggle.title = '\u30ab\u30c6\u30b4\u30ea\u3092\u8ffd\u52a0';
      toggle.addEventListener('click', () => toggleCategoryEditor(node, toggle, id, url, labels));
      node.append(toggle);
    }
    if (kind === 'register') {
      node.type = 'button';
      node.title = '\u300c\u300d\u30c3\u30c1\u30fc\u306b\u767b\u9332\u3057\u307e\u3059';
      node.addEventListener('click', () => register(url, node));
    }
    return node;
  }

  function categoryText(labels) {
    return ` ${labels.map((label) => `[${label}]`).join(' ')}`;
  }

  function updateCategoryDisplay(url, labels) {
    const key = canonicalUrl(url);
    for (const node of document.querySelectorAll(`[data-tsumanne-url="${key}"]`)) {
      let category = node.querySelector('.tsumanne-addon__categories');
      if (!category) {
        category = document.createElement('span');
        category.className = 'tsumanne-addon__categories';
        const toggle = node.querySelector('.tsumanne-addon__category-toggle');
        if (toggle) toggle.before(category);
        else node.append(category);
      }
      category.textContent = categoryText(labels);
    }
  }

  async function lookup(urls) {
    const form = new URLSearchParams();
    urls.forEach((url) => form.append('w[]', url));
    const response = await fetch(indexUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
      body: form,
      credentials: 'include'
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();
    return logMap(data.logs);
  }

  function messageFrom(data) {
    return Array.isArray(data?.messages) && data.messages.length ? String(data.messages[0]) : '';
  }

  function updateTagCandidates(input, datalist, state) {
    const value = input.value.trim();
    if (value.length < state.previousLength) datalist.replaceChildren();
    state.previousLength = value.length;
    clearTimeout(state.timer);
    if (!value) return;
    state.timer = setTimeout(async () => {
      const requestUrl = new URL(tagSearchUrl);
      requestUrl.searchParams.set('w', value);
      try {
        const response = await fetch(requestUrl, { credentials: 'include' });
        if (!response.ok) return;
        const data = await response.json();
        if (input.value.trim() !== value) return;
        datalist.replaceChildren(...(Array.isArray(data.tags) ? data.tags : [])
          .map((item) => item?.tag)
          .filter(Boolean)
          .map((tag) => {
            const option = document.createElement('option');
            option.value = tag;
            return option;
          }));
      } catch (error) {
        console.warn('[Tsumanne Link] tag lookup failed', error);
      }
    }, 180);
  }

  function toggleCategoryEditor(container, toggle, id, url, currentCategories) {
    const existing = container.querySelector('.tsumanne-addon__category-editor');
    if (existing) {
      existing.remove();
      toggle.textContent = '+';
      return;
    }
    const editor = document.createElement('span');
    editor.className = 'tsumanne-addon__category-editor';
    const datalistid = `tsumanne-datalist${id}`;
    const datalist = document.createElement('datalist');
    datalist.id = datalistid;
    editor.append(datalist);
    const input = document.createElement('input');
    input.type = 'text';
    input.placeholder = '\u8ffd\u52a0\u30ab\u30c6\u30b4\u30ea';
    input.setAttribute('list', datalistid);
    const tagState = { previousLength: 0, timer: null };
    input.addEventListener('input', (event) => {
      if (!event.isComposing) updateTagCandidates(input, datalist, tagState);
    });
    input.addEventListener('compositionend', () => updateTagCandidates(input, datalist, tagState));
    input.addEventListener('keydown', (e) => {
      if(e.key =='Enter') {
        addCategory(id, input, url, currentCategories, add);
      }
    });
    const add = document.createElement('button');
    add.type = 'button';
    add.textContent = '\u8ffd\u52a0';
    add.addEventListener('click', () => addCategory(id, input, url, currentCategories, add));
    editor.append(input, add);
    toggle.before(editor);
    toggle.textContent = '-';
    input.focus();
  }

  async function refreshAnnotation(url, fallbackCategories = [], fallbackId = null) {
    let labels = categories(fallbackCategories);
    try {
      const logs = await lookup([url]);
      const log = logs.get(canonicalUrl(url));
      if (log) {
        labels = log.categories;
        fallbackId = log.id;
      }
    } catch (error) {
      console.warn('[Tsumanne Link] refresh failed', error);
    }
    const key = canonicalUrl(url);
    for (const current of document.querySelectorAll(`[data-tsumanne-url="${key}"]`)) {
      current.replaceWith(annotation('registered', labels, url, fallbackId));
    }
  }

  async function addCategory(id, input, url, currentCategories, button) {
    const category = input.value.trim();
    if (!category) return;
    button.disabled = true;
    let response;
    let data = {};
    try {
      response = await fetch(appendCategory, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
        body: new URLSearchParams({ _id: String(id), category }),
        credentials: "include"
      });
      try { data = await response.json(); } catch {
        alert('JSON\u30d1\u30fc\u30b9\u30a8\u30e9\u30fc');
        return;
      }
      const message = messageFrom(data);
      if (!response.ok) {
        alert(`HTTP error ${response.status}\n${message}`);
        return;
      }
      if (!data.success) {
        alert(message || '\u767b\u9332\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f');
        return;
      }
      currentCategories.push(data.category);
      updateCategoryDisplay(url, currentCategories);
      input.value = '';
      document.getElementById(`tsumanne-datalist${id}`).replaceChildren();
    } catch (error) {
      console.warn('[Tsumanne Link] category update failed', error);
      alert('\u767b\u9332\u30ea\u30af\u30a8\u30b9\u30c8\u306b\u5931\u6557\u3057\u307e\u3057\u305f');
    } finally {
      if (button.isConnected) button.disabled = false;
    }
  }

  async function register(url, button) {
    button.disabled = true;
    button.textContent = '[\u767b\u9332\u4e2d]';
    let response;
    let data = {};
    try {
      response = await fetch(inputUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
        body: new URLSearchParams({ url }),
        credentials: "include"
      });
      try { data = await response.json(); } catch { 
        alert('JSON\u30d1\u30fc\u30b9\u30a8\u30e9\u30fc');
        return;
      }
      const message = messageFrom(data);
      if (!response.ok) {
        alert(`HTTP error ${response.status}\n${message}`);
        return;
      }
      if (!data.success) {
        alert(message || '\u767b\u9332\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f');
        return;
      }
      await refreshAnnotation(url, data.category);
    } catch (error) {
      console.warn('[Tsumanne Link]', error);
      alert('\u767b\u9332\u30ea\u30af\u30a8\u30b9\u30c8\u306b\u5931\u6557\u3057\u307e\u3057\u305f');
    } finally {
      if (button.isConnected) {
        button.disabled = false;
        button.textContent = text.register;
      }
    }
  }

  async function run() {
    const links = threadLinks();
    if (!links.length) return;
    const lookupUrls = [...new Set(links.map((link) => link.url))];
    let logs;
    try {
      logs = await lookup(lookupUrls);
    } catch (error) {
      console.warn('[Tsumanne Link]', error);
      return;
    }
    for (const link of links) {
      const log = logs.get(link.key);
      link.insert(log
        ? annotation('registered', log.categories, link.url, log.id)
        : annotation('register', [], link.url));
    }
  }

  run();
})();
