// === DATABASE SYNC HANDLERS ===
const dbSyncHandler = (msg, sender, sendResponse) => {
  if (msg.target !== 'background') return false;

  if (msg.cmd === 'recordDb' || msg.cmd === 'checkBatchDb') {
    (async () => {
      try {
        const lGet = (key, fallback) => new Promise(r => chrome.storage.local.get({ [key]: fallback }, d => r(d[key])));
        const lSet = (key, val) => new Promise(r => chrome.storage.local.set({ [key]: val }, () => r()));
        let userAuth = await lGet('userAuth', null);
        const settings = await lGet('localSettings', {});
        const ownEmail = userAuth?.user?.email || null;
        const targetEmail = settings.syncEmail || ownEmail;

        if (!ownEmail || !userAuth?.token) {
          throw new Error("Silakan Login ulang di tab Sewa APIKey (Token tidak tersedia).");
        }

        const firebaseKey = "AIzaSyCNGYKxyqvqCrduHbl6BJ5a2AbBOJMsrz8";
        const projectId = "fast-proxy-ai";

        // Token Refresh Logic
        async function refreshIfNeeded(status) {
          if (status === 401 && userAuth.refreshToken) {
            const refreshRes = await fetch(`https://securetoken.googleapis.com/v1/token?key=${firebaseKey}`, {
              method: 'POST',
              headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
              body: `grant_type=refresh_token&refresh_token=${userAuth.refreshToken}`
            });
            if (refreshRes.ok) {
              const refreshData = await refreshRes.json();
              userAuth.token = refreshData.access_token;
              userAuth.refreshToken = refreshData.refresh_token;
              await lSet('userAuth', userAuth);
              return true;
            }
          }
          return false;
        }

        const baseUrl = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents`;

        if (msg.cmd === 'recordDb') {
          // WRITE: Write to shared team collection + own personal collection
          const teamId = settings.syncEmail ? settings.syncEmail.trim().toLowerCase() : null;

          async function doPatch(collectionPath) {
            const url = `${baseUrl}/${collectionPath}/${encodeURIComponent(msg.hash)}?key=${firebaseKey}`;
            return await fetch(url, {
              method: 'PATCH',
              headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${userAuth.token}` },
              body: JSON.stringify({
                fields: {
                  filename: { stringValue: msg.filename },
                  date: { stringValue: new Date().toISOString() },
                  hash: { stringValue: msg.hash },
                  uploadedBy: { stringValue: ownEmail }
                }
              })
            });
          }

          // 1. Write to shared team collection (all accounts read from here)
          if (teamId) {
            console.log(`[recordDb] Writing to team: teams/${teamId}/uploads/${msg.hash}`);
            let resTeam = await doPatch(`teams/${encodeURIComponent(teamId)}/uploads`);
            if (resTeam.status === 401 && await refreshIfNeeded(401)) {
              resTeam = await doPatch(`teams/${encodeURIComponent(teamId)}/uploads`);
            }
            if (!resTeam.ok) {
              const err = await resTeam.json().catch(() => ({}));
              console.warn(`[Team Write] FAILED: ${err?.error?.message || resTeam.status}`);
            } else {
              console.log(`[Team Write] SUCCESS: teams/${teamId}/uploads/${msg.hash}`);
            }
          } else {
            console.log(`[recordDb] No teamId set, skipping team write`);
          }

          // 2. Also write to own personal collection (backup)
          let resOwn = await doPatch(`users/${encodeURIComponent(ownEmail)}/uploads`);
          if (resOwn.status === 401 && await refreshIfNeeded(401)) {
            resOwn = await doPatch(`users/${encodeURIComponent(ownEmail)}/uploads`);
          }
          if (!resOwn.ok) {
            const err = await resOwn.json().catch(() => ({}));
            throw new Error(`DB Error (${resOwn.status}): ${err?.error?.message || 'Unauthorized/Forbidden'}`);
          }

          sendResponse({ success: true });
        } else {
          // READ: Check shared team collection first, then personal fallback
          const duplicates = [];
          const results = {};
          const teamId = settings.syncEmail ? settings.syncEmail.trim().toLowerCase() : null;

          async function batchCheck(collectionPath, hashes) {
            const documents = hashes.map(h =>
              `projects/${projectId}/databases/(default)/documents/${collectionPath}/${h}`
            );

            const chunks = [];
            for (let i = 0; i < documents.length; i += 100) {
              chunks.push(documents.slice(i, i + 100));
            }

            const found = {};

            for (const chunk of chunks) {
              let r = await fetch(`${baseUrl}:batchGet?key=${firebaseKey}`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${userAuth.token}` },
                body: JSON.stringify({ documents: chunk })
              });

              if (r.status === 401 && await refreshIfNeeded(401)) {
                r = await fetch(`${baseUrl}:batchGet?key=${firebaseKey}`, {
                  method: 'POST',
                  headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${userAuth.token}` },
                  body: JSON.stringify({ documents: chunk })
                });
              }

              if (!r.ok) {
                console.warn(`[batchGet] Error for ${collectionPath}: HTTP ${r.status}`);
                continue;
              }

              const batchResults = await r.json();
              for (const item of batchResults) {
                if (item.found) {
                  const pathParts = item.found.name.split('/');
                  const hash = decodeURIComponent(pathParts[pathParts.length - 1]);
                  found[hash] = item.found;
                }
              }
            }

            return found;
          }

          // 1. Check shared team collection (contains data from ALL team members)
          let teamFound = {};
          if (teamId) {
            console.log(`[checkBatchDb] Checking team: teams/${teamId}/uploads/ for ${msg.hashes.length} hashes`);
            teamFound = await batchCheck(`teams/${teamId}/uploads`, msg.hashes);
            console.log(`[checkBatchDb] Team results: ${Object.keys(teamFound).length} found`);
          } else {
            console.log(`[checkBatchDb] No teamId, skipping team check`);
          }

          // 2. Fallback: check own personal collection for hashes not in team
          const missingHashes = msg.hashes.filter(h => !teamFound[h]);
          let ownFound = {};
          if (missingHashes.length > 0) {
            ownFound = await batchCheck(`users/${ownEmail}/uploads`, missingHashes);
          }

          // Merge results
          for (const h of msg.hashes) {
            const doc = teamFound[h] || ownFound[h];
            if (doc) {
              const source = teamFound[h] ? `team:${teamId}` : ownEmail;
              duplicates.push(h);
              results[h] = {
                exists: true,
                source: source,
                data: {
                  lastSeen: doc.fields?.date?.stringValue || new Date().toISOString(),
                  filename: doc.fields?.filename?.stringValue,
                  uploadedBy: doc.fields?.uploadedBy?.stringValue || ownEmail
                }
              };
            }
          }

          console.log(`[batchGet] ${msg.hashes.length} hashes → ${duplicates.length} dups (team:${Object.keys(teamFound).length}, own:${Object.keys(ownFound).length})`);
          sendResponse({ success: true, duplicates, results });
        }
      } catch (e) {
        sendResponse({ success: false, error: e.message, duplicates: [], results: {} });
      }
    })();
    return true;
  }
  return false;
};
dbSyncHandler._isLocalAiHandler = true;
chrome.runtime.onMessage.addListener(dbSyncHandler);

// === ORIGINAL BUNDLE (Un-minified, cleaned for Chrome Web Store) ===
(() => {
  "use strict";

  // Helper: set value in chrome.storage.local
  function localSet(key, value) {
    return new Promise(resolve => {
      const obj = {};
      obj[key] = value;
      chrome.storage.local.set(obj, () => resolve(value));
    });
  }

  // Helper: get value from chrome.storage.local
  function localGetSync(key, fallback = '') {
    return new Promise(resolve => {
      const obj = {};
      obj[key] = fallback;
      chrome.storage.local.get(obj, data => resolve(data[key]));
    });
  }

  // Helper: get value from chrome.storage.sync
  function syncGet(key, fallback = '') {
    return new Promise(resolve => {
      const obj = {};
      obj[key] = fallback;
      chrome.storage.sync.get(obj, data => resolve(data[key]));
    });
  }

  // === Action Click: Navigate to Adobe Stock uploads ===
  chrome.action.onClicked.addListener(tab => {
    const hostname = tab.url.toString().replace(/^https?:\/\//i, '').match(/^[^/:]+/)?.[0] || '';
    const lastSegment = tab.url.split('/').at(-1);
    const adobeHost = 'contributor.stock.adobe.com';
    const uploadsUrl = 'https://' + adobeHost + '/uploads';

    if (hostname !== adobeHost) {
      chrome.tabs.create({ url: uploadsUrl });
    } else if (lastSegment !== 'uploads') {
      chrome.tabs.update(tab.id, { url: uploadsUrl });
    }
  });

  // === Message Router ===
  chrome.runtime.onMessage.addListener((msg) => {
    // Skip messages handled by other listeners
    if (msg && ['localProcess', 'openLocalSetup', 'validateKey', 'nativeSync', 'recordDb', 'checkBatchDb'].includes(msg.cmd)) {
      return false;
    }

    (async () => {
      let keyword, currentTabId;

      switch (msg.cmd) {
        case 'openOffScreen':
          if (msg.target !== 'background') return;
          await handleOffscreenAuth();
          break;

        case 'open':
          chrome.tabs.query({ currentWindow: true, active: true }, tabs => {
            currentTabId = tabs[0].id;
            chrome.storage.local.set({ nowTabId: currentTabId });
          });
          switch (msg.type) {
            case 'original':
              keyword = msg.keyword.replace(' ', '%20');
              chrome.tabs.create({ url: 'https://imstocker.com/en/keyworder?extension=true&q=' + keyword });
              break;
            case 'ai':
            case 'title':
              chrome.tabs.create({ url: 'https://chatgpt.com/?extension=true' });
              break;
            case 'aiBard':
            case 'titleBard':
              chrome.tabs.create({ url: 'https://gemini.google.com/app?extension=true' });
              break;
            case 'downloadOffscreen':
              chrome.tabs.create({ url: chrome.runtime.getURL('downloadOffscreen.html') });
              break;
          }
          break;

        case 'uploadImg':
          if (msg.target !== 'background' || !msg.data) return;
          chrome.tabs.query({ currentWindow: true, active: true }, tabs => {
            chrome.tabs.sendMessage(tabs[0].id, { cmd: 'uploadImgContent', target: 'content', data: msg.data });
          });
          break;

        case 'fillResult':
        case 'fillHistoryResult':
          if (msg.target !== 'background' || !msg.data) return;
          chrome.tabs.query({ currentWindow: true, active: true }, tabs => {
            chrome.tabs.sendMessage(tabs[0].id, { cmd: msg.cmd, target: 'content', data: msg.data });
          });
          break;

        case 'copyKeyword':
          chrome.storage.local.get(['nowTabId'], data => {
            let pasteCmd = 'pasteKeywords';
            switch (msg.type) {
              case 'ai':
              case 'aiBard':
                pasteCmd = 'pasteKeywords';
                break;
              case 'title':
              case 'titleBard':
                pasteCmd = 'pasteTitle';
                break;
            }
            chrome.tabs.sendMessage(parseInt(data.nowTabId), { cmd: pasteCmd }, () => {
              chrome.tabs.query({ currentWindow: true, active: true }, tabs => {
                const activeTabId = tabs[0].id;
                chrome.tabs.remove(activeTabId);
                chrome.tabs.update(parseInt(data.nowTabId), { active: true });
              });
            });
          });
          break;

        case 'openLogin':
          keyword = msg.keyword.replace(' ', '%20');
          chrome.tabs.query({ currentWindow: true, active: true }, tabs => {
            chrome.tabs.update(tabs[0].id, { url: 'https://imstocker.com/en/keyworder?q=' + keyword });
          });
          break;

        case 'loginUser':
          if (msg.target !== 'background') return;
          await notifyLoginTab();
          break;

        case 'closeTab':
          if (msg.target !== 'background') return;
          chrome.storage.local.get('nowTabId', ({ nowTabId }) => {
            chrome.tabs.query({ active: true, currentWindow: true }, ([activeTab]) => {
              chrome.tabs.update(nowTabId, { active: true });
              chrome.tabs.remove(activeTab.id);
            });
          });
          break;

        case 'openDrawerFromBackground':
          if (msg.target !== 'background') return;
          chrome.tabs.query({ active: true, lastFocusedWindow: true }, async ([tab]) => {
            await localSet('loginTabID', tab.id);
            chrome.tabs.sendMessage(tab.id, { cmd: 'openDrawer' });
          });
          break;

        case 'openHistory':
          if (msg.target !== 'background') return;
          chrome.tabs.query({ currentWindow: true, active: true }, ([tab]) => {
            chrome.sidePanel.open({ tabId: tab.id });
          });
          break;

        case 'closeOffscreen':
          if (msg.target !== 'background') return;
          await closeOffscreenDoc();
          break;
      }
    })();
  });

  // === Adobe Stock tab update listener ===
  const uploadsRegex = /https:\/\/contributor\.stock\.adobe\.com\/(.+)\/upload[s]$/i;
  const adobeRegex = /https:\/\/contributor\.stock\.adobe\.com(.*)$/i;

  chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
    if (changeInfo.url) {
      if (!adobeRegex.test(changeInfo.url)) return;
      if (uploadsRegex.test(changeInfo.url)) {
        chrome.tabs.sendMessage(tabId, { cmd: 'createBtn' });
      } else {
        chrome.tabs.sendMessage(tabId, { cmd: 'dismissAllToast', target: 'content' });
      }
    }
  });

  // === Offscreen Document Management ===
  const OFFSCREEN_PATH = '/offscreen.html';
  let offscreenCreating = null;

  async function hasOffscreenDocument() {
    const contexts = await chrome.runtime.getContexts({
      contextTypes: ['OFFSCREEN_DOCUMENT'],
      documentUrls: [chrome.runtime.getURL(OFFSCREEN_PATH)]
    });
    return contexts.length > 0;
  }

  async function createOffscreenDoc(path) {
    if (await hasOffscreenDocument()) return;
    if (offscreenCreating) {
      await offscreenCreating;
    } else {
      offscreenCreating = chrome.offscreen.createDocument({
        url: chrome.runtime.getURL(path),
        reasons: [chrome.offscreen.Reason.DOM_SCRAPING],
        justification: 'authentication'
      });
      await offscreenCreating;
      offscreenCreating = null;
    }
  }

  async function closeOffscreenDoc() {
    if (await hasOffscreenDocument()) {
      await chrome.offscreen.closeDocument();
    }
  }

  // === Firebase Auth via Offscreen ===
  async function handleOffscreenAuth() {
    await createOffscreenDoc(OFFSCREEN_PATH);

    const authResult = await new Promise((resolve, reject) => {
      (async () => {
        const response = await chrome.runtime.sendMessage({ type: 'firebase-auth', target: 'offscreen' });
        if (response?.name === 'FirebaseError') {
          reject(response);
        } else {
          resolve(response);
        }
      })();
    }).then(async (result) => {
      await localSet('userAuth', result);
      const isNewUser = Math.abs(result.user.createdAt - result.user.lastLoginAt) < 10;
      if (isNewUser) {
        await notifyLoginTab(true);
        await trackEmailForAntiAbuse(result);
      } else {
        await notifyLoginTab();
      }
      return result;
    }).catch(err => {
      if (err.code !== 'auth/operation-not-allowed') {
        console.error('Firebase Auth Error', err.code, err.message);
        return err;
      }
      console.error('OAuth provider not enabled in Firebase console.');
    }).finally(() => {
      closeOffscreenDoc();
    });

    return authResult;
  }

  // === Anti-abuse: track multiple email registrations ===
  async function trackEmailForAntiAbuse(authData) {
    const emailArray = (await syncGet('emailArray')) || [];
    emailArray.push(authData.user.email);
    const uniqueEmails = new Set(emailArray);
    const emailList = Array.from(uniqueEmails);
    await new Promise(resolve => {
      const obj = {};
      obj.emailArray = emailList;
      chrome.storage.sync.set(obj, () => resolve(emailList));
    });
    // Log warning if too many accounts detected
    if (uniqueEmails.size > 2) {
      console.warn('[AntiAbuse] Multiple accounts detected:', uniqueEmails.size);
    }
  }

  // === Notify content script about login status ===
  async function notifyLoginTab(isNewUser = false) {
    const loginTabId = await localGetSync('loginTabID');
    if (loginTabId) {
      try {
        chrome.tabs.sendMessage(parseInt(loginTabId), { cmd: 'login', target: 'content', data: isNewUser });
      } catch (err) {
        console.log(err);
      }
    }
  }
})();


// === LOCAL AI HANDLER (REVERTED MODELS) ===
(function () {
  const GEMINI_MODEL = 'gemini-2.5-flash';
  const GROQ_MODEL = 'meta-llama/llama-4-scout-17b-16e-instruct';
  const GEMINI_BASE = 'https://generativelanguage.googleapis.com/v1beta/models';
  const GROQ_BASE = 'https://api.groq.com/openai/v1/chat/completions';

  function lGet(k, f = '') { return new Promise(r => { const o = {}; o[k] = f; chrome.storage.local.get(o, d => r(d[k])) }) }
  function lSet(k, v) { return new Promise(r => { const o = {}; o[k] = v; chrome.storage.local.set(o, () => r(v)) }) }

  function buildKwPrompt(name, maxKw = 49, lang = 'id', hasImage = false, concept = '') {
    const langMap = { 'en': 'English', 'id': 'Indonesian', 'th': 'Thai', 'ja': 'Japanese', 'ko': 'Korean', 'zh': 'Chinese', 'es': 'Spanish' };
    const langName = langMap[lang] || 'English';
    const ln = lang !== 'en' ? ` Respond in ${langName} language.` : '';
    const cn = concept ? ` Focus on the concept/topic: "${concept}". Make sure keywords are relevant to this concept.` : '';
    
    // Calculate keyword layer distribution
    const primaryCount = Math.round(maxKw * 0.20);
    const secondaryCount = Math.round(maxKw * 0.20);
    const contextCount = Math.round(maxKw * 0.20);
    const styleCount = Math.round(maxKw * 0.17);
    const conceptualCount = maxKw - primaryCount - secondaryCount - contextCount - styleCount;
    
    const layeredRule = `
=== KEYWORD RULES (LAYERED SEO STRATEGY) ===
- Generate EXACTLY ${maxKw} unique ${langName} keyword tags, separated by commas only. No numbering, no extra text.
- LANGUAGE: All keywords must be in ${langName}. Do NOT mix languages.
- Layer keywords by purpose in this exact order:
  1. PRIMARY (first ~${primaryCount} tags): Exact subject descriptors — what IS in the image (nouns)
  2. SECONDARY (next ~${secondaryCount} tags): Actions, states, emotions visible or implied
  3. CONTEXT (next ~${contextCount} tags): Setting, location, environment, time of day
  4. STYLE (next ~${styleCount} tags): Lighting, visual technique, camera angles, color palette
  5. CONCEPTUAL (remaining ~${conceptualCount} tags): Abstract themes, broad concepts, utility (e.g., lifestyle, technology)
- FORMAT: Use singular nouns and infinitive verbs. NO MERGING words improperly. Use proper spaces.
- NO PEOPLE RULE: If the image has NO people, you MUST include the tags for "no people" and "nobody" in ${langName}.`;

    if (hasImage) return `(important! please answer without conversation with me.) Analyze this image carefully. Generate EXACTLY ${maxKw} unique ${langName} keyword tags (not more, not less, exactly ${maxKw} tags) that accurately describe what you see in this image. Include objects, actions, emotions, colors, settings, concepts, and styles visible in the image.${cn}${layeredRule} Separate with commas only, no numbering, no extra text. Arrange by relevance. The result must be a string of exactly ${maxKw} comma-separated tags only.${ln}`;
    return `(important! please answer without conversation with me.) Use your stock photos websites knowledge and imagine the sentences "${name}" as a picture and make EXACTLY ${maxKw} unique ${langName} tags (not more, not less, exactly ${maxKw} tags) which the most relate to (also include main words of sentence) for describe this picture.${cn}${layeredRule} No order number or no any sign in front of tags, just use comma sign to separate tags, no other sentence just the tags only, arrange tags by most popular tags first, no image or no link or addition information sentences at the end. The result must be a string with exactly ${maxKw} comma-separated tags.${ln}`;
  }

  function buildTitlePrompt(ts, ks, lang = 'id', concept = '', minTL = 70) {
    const langMap = { 'en': 'English', 'id': 'Indonesian', 'th': 'Thai', 'ja': 'Japanese', 'ko': 'Korean', 'zh': 'Chinese', 'es': 'Spanish' };
    const langName = langMap[lang] || 'English';
    const ln = lang !== 'en' ? ` Respond in ${langName} language.` : '';
    const cn = concept ? ` The title must strongly relate to the concept: "${concept}".` : '';
    // Calculate targeted optimal length
    let targetMax = 200;
    let targetMin = minTL || 70;

    return `(important! answer ONLY the title string, no other text, no quotes, no explanation.)

You are an Adobe Stock SEO title expert. Create ONE highly descriptive, search-optimized title for a stock photo in ${langName} language.

Context clues: "${ts}"
Keyword clues: "${ks}"
${cn}

=== TITLE RULES (BUYER-INTENT SEO) ===
- LENGTH: MUST be ${targetMin}–${targetMax} characters. Aim close to ${targetMax}. Use the full character budget.
- BUYER INTENT: Think like a stock photo BUYER. What search query would lead them to this image? Front-load the most searchable terms at the start of the title.
- STRUCTURE: [Main Subject] + [Action/State] + [Setting/Context] + [Mood/Atmosphere] + [Visual Style]
- FORMAT: No quotes, no periods at the end, no colons, no special characters. Sentence case or Title Case.
- NATURAL LANGUAGE: Flowing descriptive phrases, NOT a comma-separated keyword list.
- FORBIDDEN: No brand names, no camera specs (Canon, 85mm, 4k), no real people's names, no meta words (stock photo, image, wallpaper).
- Output ONLY the title string, nothing else.${ln}`;
  }

  function buildCombinedPrompt(maxKw = 49, minTL = 70, lang = 'en', hasImage = false, imgName = '', concept = '') {
    const langMap = { 'en': 'English', 'id': 'Indonesian', 'th': 'Thai', 'ja': 'Japanese', 'ko': 'Korean', 'zh': 'Chinese', 'es': 'Spanish' };
    const langName = langMap[lang] || 'English';
    const ln = lang !== 'en' ? ` All output text must be in ${langName} language.` : '';
    const cn = concept ? `\nFOCUS CONCEPT: "${concept}" — title and keywords must strongly revolve around this topic.` : '';
    const imageCtx = hasImage
      ? 'Analyze this image carefully and precisely. Identify: main subject(s), actions/poses, environment/setting, lighting quality, color palette, mood/atmosphere, and visual style (photographic vs artistic).'
      : `Imagine the description "${imgName}" as a real stock photo. Visualize specific details: the subject, setting, atmosphere, lighting, colors, composition, and style.`;

    // Calculate targeted optimal length
    let targetMax = 200;
    let targetMin = minTL || 70;

    // Calculate keyword layer distribution
    const primaryCount = Math.round(maxKw * 0.20);
    const secondaryCount = Math.round(maxKw * 0.20);
    const contextCount = Math.round(maxKw * 0.20);
    const styleCount = Math.round(maxKw * 0.17);
    const conceptualCount = maxKw - primaryCount - secondaryCount - contextCount - styleCount;

    return `(important! Follow the format EXACTLY. No conversation, no explanation.)

${imageCtx}${cn}

You are a top-performing Adobe Stock contributor and SEO expert. Generate a title, keywords, category, and file type for this stock image in ${langName} language in a SINGLE response.${ln}

You MUST format your response EXACTLY like this (including the labels):
TITLE: [your title here]
KEYWORDS: [comma-separated keywords here]
CATEGORY: [one category from the list below]
FILE_TYPE: [Photo or Illustration]

=== FILE_TYPE RULES (CRITICAL) ===
- PHOTO: ONLY for realistic images that look like they were taken with a physical camera. Natural lighting, real human skin textures, real-world physics, no digital manipulation artifacts.
- ILLUSTRATION: For EVERYTHING ELSE, including: 3D renders, vector art, digital paintings, abstract backgrounds, AI-generated art, composites, or anything with surreal/non-photographic qualities. If in doubt, choose Illustration.

=== CATEGORY (Choose exactly ONE) ===
Animals, Buildings and Architecture, Business, Drinks, The Environment, States of Mind, Food, Graphic Resources, Hobbies and Leisure, Industry, Landscape, Lifestyle, People, Plants and Flowers, Culture and Religion, Science, Social Issues, Sports, Technology, Transport, Travel

Category decision guide:
- Real people prominent → People or Lifestyle
- Building/interior focus → Buildings and Architecture
- Close-up nature → Plants and Flowers or Animals
- Wide nature scene → Landscape or The Environment
- Commercial/work context → Business or Technology
- Food close-up → Food; Beverages → Drinks

=== TITLE RULES (BUYER-INTENT SEO) ===
- LENGTH: MUST be ${targetMin}–${targetMax} characters. Aim close to ${targetMax}. Use the full character budget.
- BUYER INTENT: Think like a stock photo BUYER. What search query would lead them to this image? Front-load the most searchable terms at the start of the title.
- STRUCTURE: [Main Subject] + [Action/State] + [Setting/Context] + [Mood/Atmosphere] + [Visual Style]
  Example: "Young professional woman working on laptop in bright modern home office with warm natural morning light and minimalist decor"
- FORMAT: No quotes, no periods at the end, no colons, no special characters. Sentence case or Title Case.
- NATURAL LANGUAGE: Flowing descriptive phrases, NOT a comma-separated keyword list.
- FORBIDDEN: No brand names, no camera specs (Canon, 85mm, 4k), no real people's names, no meta words (stock photo, image, wallpaper).

=== KEYWORD RULES (LAYERED SEO STRATEGY) ===
- Generate EXACTLY ${maxKw} unique ${langName} keyword tags, separated by commas only. No numbering, no extra text.
- LANGUAGE: All keywords must be in ${langName}. Do NOT mix languages.
- Layer keywords by purpose in this exact order:
  1. PRIMARY (first ~${primaryCount} tags): Exact subject descriptors — what IS in the image (nouns)
  2. SECONDARY (next ~${secondaryCount} tags): Actions, states, emotions visible or implied
  3. CONTEXT (next ~${contextCount} tags): Setting, environment, location type, time of day
  4. STYLE (next ~${styleCount} tags): Visual style, mood, lighting, color palette, composition
  5. CONCEPTUAL (last ~${conceptualCount} tags): Abstract concepts, use-cases, themes a buyer might search
- MIX: 60-70% single-word tags, 30-40% high-quality 2-3 word phrases.
- FORMAT: Singular nouns, infinitive verbs. Proper spaces between words.
- TITLE REINFORCEMENT: Key descriptive terms from the title MUST also appear in the keyword list.
- NO PEOPLE RULE: If NO people are visible, include "no people" and "nobody" tags.
- NO FILLER: Do NOT use meta words (stock photo, image, picture, wallpaper, illustration, vector, clipart, icon, graphic, screenshot).
- NO FILLER ADJECTIVES: Do NOT use generic filler (beautiful, amazing, stunning, wonderful, nice, good, great, best, perfect, awesome, lovely, pretty, gorgeous).
- NO BRANDS/CAMERA: No brand names, camera models, lens specs, or resolution specs.
- NO SYNONYM STACKING: Max 2 words from any synonym group (e.g., dog/canine/pup/puppy → pick only 2 most popular).

Output ONLY the four lines starting with TITLE:, KEYWORDS:, CATEGORY:, and FILE_TYPE: — absolutely nothing else.`;
  }



  function parseCombinedResponse(raw) {
    const titleMatch = raw.match(/TITLE:\s*(.+)/i);
    const kwMatch = raw.match(/KEYWORDS:\s*(.+)/i);
    const catMatch = raw.match(/CATEGORY:\s*(.+)/i);
    const ftMatch = raw.match(/FILE_TYPE:\s*(.+)/i);

    if (titleMatch && kwMatch) {
      return {
        title: titleMatch[1].trim(),
        keywords: kwMatch[1].trim(),
        category: catMatch ? catMatch[1].trim() : '',
        fileType: ftMatch ? ftMatch[1].trim() : ''
      };
    }
    // Fallback: try splitting by newline
    const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
    if (lines.length >= 2) {
      const res = {
        title: lines[0].replace(/^TITLE:\s*/i, ''),
        keywords: lines.slice(1).join(', ').replace(/^KEYWORDS:\s*/i, ''),
        category: '',
        fileType: ''
      };
      lines.forEach(l => {
        if (l.toUpperCase().startsWith('CATEGORY:')) res.category = l.replace(/^CATEGORY:\s*/i, '').trim();
        if (l.toUpperCase().startsWith('FILE_TYPE:')) res.fileType = l.replace(/^FILE_TYPE:\s*/i, '').trim();
      });
      return res;
    }
    return null;
  }

  async function callGemini(key, prompt, imgB64) {
    const parts = [];
    if (imgB64) { const m = imgB64.match(/^data:(.+?);base64,(.+)$/); if (m) parts.push({ inline_data: { mime_type: m[1], data: m[2] } }); }
    parts.push({ text: prompt });
    const r = await fetch(`${GEMINI_BASE}/${GEMINI_MODEL}:generateContent?key=${key}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts }], generationConfig: { temperature: 0.8, maxOutputTokens: 1500 }, safetySettings: [{ category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_NONE' }, { category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_NONE' }, { category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_NONE' }, { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_NONE' }] }) });
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw { code: r.status, message: e?.error?.message || `HTTP ${r.status}`, retryable: r.status === 429 || r.status >= 500 }; }
    const d = await r.json(); const t = d?.candidates?.[0]?.content?.parts?.[0]?.text; if (!t) throw { code: 0, message: 'Empty Gemini response', retryable: false }; return t.trim();
  }

  async function callGroq(key, prompt, imgB64) {
    const messages = [];
    if (imgB64) {
      messages.push({
        role: 'user', content: [
          { type: 'image_url', image_url: { url: imgB64 } },
          { type: 'text', text: prompt }
        ]
      });
    } else {
      messages.push({ role: 'user', content: prompt });
    }
    const r = await fetch(GROQ_BASE, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` }, body: JSON.stringify({ model: GROQ_MODEL, messages, temperature: 0.8, max_tokens: 1500 }) });
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw { code: r.status, message: e?.error?.message || `HTTP ${r.status}`, retryable: r.status === 429 || r.status >= 500 }; }
    const d = await r.json(); const t = d?.choices?.[0]?.message?.content; if (!t) throw { code: 0, message: 'Empty Groq response', retryable: false }; return t.trim();
  }

  async function callMistral(key, prompt, imgB64) {
    const messages = [];
    if (imgB64) {
      messages.push({
        role: 'user', content: [
          { type: 'image_url', image_url: { url: imgB64 } },
          { type: 'text', text: prompt }
        ]
      });
    } else {
      messages.push({ role: 'user', content: prompt });
    }
    const model = imgB64 ? 'pixtral-12b-2409' : 'mistral-tiny-latest';
    const r = await fetch('https://api.mistral.ai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` }, body: JSON.stringify({ model: model, messages, temperature: 0.8, max_tokens: 1500 }) });
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw { code: r.status, message: e?.error?.message || `HTTP ${r.status}`, retryable: r.status === 429 || r.status >= 500 }; }
    const d = await r.json(); const t = d?.choices?.[0]?.message?.content; if (!t) throw { code: 0, message: 'Empty Mistral response', retryable: false }; return t.trim();
  }

  async function callWithRotation(provider, prompt, imgB64) {
    const keys = await lGet(`localKeys_${provider}`, []);
    if (!keys || !keys.length) throw new Error(`Kunci API ${provider} belum dikonfigurasi.`);
    let idx = ((await lGet('localKeyIndex', {}))[provider] || 0) % keys.length;
    let last = null;
    for (let i = 0; i < keys.length; i++) {
      const ci = (idx + i) % keys.length;
      try {
        const res = provider === 'gemini' ? await callGemini(keys[ci], prompt, imgB64) :
          provider === 'groq' ? await callGroq(keys[ci], prompt, imgB64) :
            await callMistral(keys[ci], prompt, imgB64);
        const id = await lGet('localKeyIndex', {}); id[provider] = (ci + 1) % keys.length; await lSet('localKeyIndex', id);
        return res;
      } catch (e) { last = e; console.warn(`[Local] Key#${ci + 1} ${provider} failed:`, e.message); if (!e.retryable && e.code !== 429 && e.code !== 401) throw e; }
    }
    throw new Error(`Semua kunci ${provider} gagal: ${last?.message}`);
  }

  const localAiHandler = (msg, sender, sendResponse) => {
    if (msg.cmd === 'localProcess' && msg.target === 'background') {
      (async () => {
        try {
          const settings = await lGet('localSettings', {});
          const provider = await lGet('localProvider', 'gemini');
          const imgName = await lGet('imgName', '');
          const titleSuggest = await lGet('titleSuggest', '');
          const keySuggest = await lGet('keySuggest', '');
          const concept = settings.keyConcept || '';
          const minTL = settings.minTitleLength || 70;
          let prompt, imgB64 = msg.imageBase64 || null;

          if (!imgB64 && !msg.isTest) {
            throw new Error("Gambar tidak ditemukan. Proses dibatalkan karena ekstensi wajib menggunakan Image-to-Text metadata.");
          }

          if (msg.searchType === 'localAutoCombined') {
            // === COMBINED: Title + Keywords in ONE request ===
            const hasImage = !!imgB64;
            const maxKw = settings.maxKeywords || 35;
            prompt = buildCombinedPrompt(maxKw, minTL, settings.language || 'en', hasImage, imgName, concept);
          } else if (msg.searchType === 'localAi' || msg.searchType === 'localAiImg' || msg.searchType === 'proxy') {
            const hasImage = !!imgB64;
            prompt = buildKwPrompt(imgName, settings.maxKeywords || 35, settings.language || 'en', hasImage, concept);
          } else if (msg.searchType === 'localAutoTitle') {
            const ln = (settings.language || 'en') !== 'en' ? ` Respond in ${settings.language || 'en'} language.` : '';
            const cn = concept ? ` The title must strongly relate to the concept/topic: "${concept}".` : '';
            let targetMin = minTL || 70;
            let maxTL = 200;
            prompt = imgB64
              ? `(important! answer ONLY the title string, no other text, no quotes, no explanation.)

You are an Adobe Stock SEO title expert. Analyze this image carefully and create ONE highly descriptive, search-optimized title.
${cn}

=== TITLE RULES (BUYER-INTENT SEO) ===
- LENGTH: MUST be ${targetMin}–${maxTL} characters. Aim close to ${maxTL}. Use the full character budget.
- BUYER INTENT: Think like a stock photo BUYER. What search query would lead them to this image? Front-load the most searchable terms at the start of the title.
- STRUCTURE: [Main Subject] + [Action/State] + [Setting/Context] + [Mood/Atmosphere] + [Visual Style]
- FORMAT: No quotes, no periods at the end, no colons, no special characters. Sentence case or Title Case.
- NATURAL LANGUAGE: Flowing descriptive phrases, NOT a comma-separated keyword list.
- FORBIDDEN: No brand names, no camera specs (Canon, 85mm, 4k), no real people's names, no meta words (stock photo, image, wallpaper).
- Output ONLY the title string, nothing else.${ln}`
              : `(important! answer ONLY the title string, no other text, no quotes, no explanation.)

You are an Adobe Stock SEO title expert. Create ONE highly descriptive, search-optimized title for a stock photo described as: "${imgName}"
${cn}

=== TITLE RULES (BUYER-INTENT SEO) ===
- LENGTH: MUST be ${targetMin}–${maxTL} characters. Aim close to ${maxTL}. Use the full character budget.
- BUYER INTENT: Think like a stock photo BUYER. What search query would lead them to this image? Front-load the most searchable terms at the start of the title.
- STRUCTURE: [Main Subject] + [Action/State] + [Setting/Context] + [Mood/Atmosphere] + [Visual Style]
- FORMAT: No quotes, no periods at the end, no colons, no special characters. Sentence case or Title Case.
- NATURAL LANGUAGE: Flowing descriptive phrases, NOT a comma-separated keyword list.
- FORBIDDEN: No brand names, no camera specs (Canon, 85mm, 4k), no real people's names, no meta words (stock photo, image, wallpaper).
- Output ONLY the title string, nothing else.${ln}`;
          } else {
            prompt = buildTitlePrompt(titleSuggest, keySuggest, settings.language || 'en', concept, minTL);
          }
          let result;
          const apiBackend = await lGet('localApiBackend', 'proxy');
          if (apiBackend === 'proxy' || msg.searchType === 'proxy') {
            // === MULTI-SERVER PROXY WITH RANDOM SELECTION & FAILOVER ===
            let servers = await lGet('aiProxyServers', []);

            // Backward compatibility: jika array kosong, coba fallback ke single URL/token lama
            if (!servers || servers.length === 0) {
              const legacyUrl = await lGet('aiProxyUrl', '');
              const legacyToken = await lGet('aiProxyToken', '');
              if (legacyUrl && legacyToken) {
                servers = [{ id: 'legacy', url: legacyUrl, token: legacyToken }];
              }
            }

            if (!servers || servers.length === 0) {
              throw new Error("Sewa APIKey AI belum aktif. Silakan login dan sewa di menu Settings.");
            }

            // Shuffle servers secara random (Fisher-Yates) agar load terdistribusi merata
            const shuffled = [...servers];
            for (let i = shuffled.length - 1; i > 0; i--) {
              const j = Math.floor(Math.random() * (i + 1));
              [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
            }

            const bodyPayload = { prompt };
            if (imgB64) {
              const b64 = imgB64.includes('base64,') ? imgB64.split('base64,')[1] : imgB64;
              bodyPayload.image = { base64: b64, mime: "image/jpeg" };
            }
            const bodyStr = JSON.stringify(bodyPayload);

            const TIMEOUT_MS = 20000; // 20 detik timeout per server
            let lastError = null;
            let success = false;

            for (let round = 1; round <= 3; round++) {
              console.log(`[Proxy] Memulai percobaan ronde ke-${round}/3...`);
              for (const server of shuffled) {
                const controller = new AbortController();
                const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);

                try {
                  console.log(`[Proxy] Mencoba server: ${server.id} (${server.url}) [Ronde ${round}]`);
                  const proxyReq = await fetch(server.url, {
                    method: "POST",
                    headers: { "Authorization": `Bearer ${server.token}`, "Content-Type": "application/json" },
                    body: bodyStr,
                    signal: controller.signal
                  });
                  clearTimeout(timer);

                  const proxyRes = await proxyReq.json().catch(() => ({}));
                  if (!proxyReq.ok || !proxyRes.ok) {
                    throw new Error(proxyRes?.error?.message || `Proxy error: ${proxyReq.status}`);
                  }

                  console.log(`[Proxy] Sukses dari server: ${server.id}`);
                  result = proxyRes.result;
                  success = true;
                  break; // Keluar dari loop server
                } catch (e) {
                  clearTimeout(timer);
                  const isTimeout = e.name === 'AbortError';
                  lastError = isTimeout ? new Error(`Server ${server.id} timeout (>${TIMEOUT_MS / 1000}s)`) : e;
                  console.warn(`[Proxy] Server ${server.id} gagal: ${lastError.message}. Mencoba server lain...`);
                }
              }
              if (success) {
                break; // Keluar dari loop ronde jika sukses
              }
              if (round < 3) {
                console.log(`[Proxy] Semua server gagal di ronde ${round}. Menunggu 1.5 detik sebelum ronde berikutnya...`);
                await new Promise(resolve => setTimeout(resolve, 1500));
              }
            }

            if (!success) {
              throw new Error(`Semua ${shuffled.length} proxy server gagal setelah 3 ronde percobaan. Error terakhir: ${lastError?.message}`);
            }
          } else {
            result = await callWithRotation(provider, prompt, imgB64);
          }

          // Parse combined response if applicable
          if (msg.searchType === 'localAutoCombined') {
            const parsed = parseCombinedResponse(result);
            if (parsed) {
              sendResponse({
                success: true,
                title: parsed.title,
                keywords: parsed.keywords,
                category: parsed.category,
                fileType: parsed.fileType
              });
            } else {
              sendResponse({ success: false, error: 'Gagal parsing respons AI. Format tidak sesuai.' });
            }
          } else {
            sendResponse({ success: true, result });
          }
        } catch (e) { sendResponse({ success: false, error: e.message || String(e) }); }
      })();
      return true;
    }

    if (msg.cmd === 'openLocalSetup') {
      chrome.tabs.create({ url: chrome.runtime.getURL('localSetup.html') });
      sendResponse({ success: true });
      return true;
    }

    if (msg.cmd === 'validateKey') {
      (async () => {
        try {
          if (msg.provider === 'gemini') {
            const url = `${GEMINI_BASE}/${GEMINI_MODEL}:generateContent?key=${msg.apiKey}`;
            const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: 'Say ok' }] }], generationConfig: { maxOutputTokens: 5 } }) });
            if (r.ok) sendResponse({ valid: true }); else { const e = await r.json().catch(() => ({})); sendResponse({ valid: false, error: e?.error?.message || `HTTP ${r.status}` }); }
          } else if (msg.provider === 'groq') {
            const r = await fetch(GROQ_BASE, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${msg.apiKey}` }, body: JSON.stringify({ model: GROQ_MODEL, messages: [{ role: 'user', content: 'Say ok' }], max_tokens: 5 }) });
            if (r.ok) sendResponse({ valid: true }); else { const e = await r.json().catch(() => ({})); sendResponse({ valid: false, error: e?.error?.message || `HTTP ${r.status}` }); }
          } else if (msg.provider === 'mistral') {
            const r = await fetch('https://api.mistral.ai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${msg.apiKey}` }, body: JSON.stringify({ model: 'mistral-tiny-latest', messages: [{ role: 'user', content: 'Say ok' }], max_tokens: 5 }) });
            if (r.ok) sendResponse({ valid: true }); else { const e = await r.json().catch(() => ({})); sendResponse({ valid: false, error: e?.error?.message || `HTTP ${r.status}` }); }
          }
        } catch (e) { sendResponse({ valid: false, error: e.message }); }
      })();
      return true;
    }

    if (msg.cmd === 'nativeSync' && msg.target === 'background') {
      // Stub out nativeSync since we removed the local tracking system
      sendResponse({ success: false, error: 'Local tracking system is deprecated.' });
      return true;
    }



    return false;
  };
  localAiHandler._isLocalAiHandler = true;
  chrome.runtime.onMessage.addListener(localAiHandler);
})();