// AI dialogue manager — hybrid: topic tree drives structure, LLM fills lines.
//
// Architecture:
//   - Tree (window.TopicTree) defines node hierarchy + per-node guidance
//   - User input first matched against current node's children's triggers
//     • If match → next node = matched child; LLM generates content for it
//     • If no match → stay at current node; LLM generates a free-form
//       response that stays scoped to current topic
//   - LLM ALWAYS generates: char1 line, char2 line, image_query
//   - Menu options ALWAYS come from the tree (never the LLM)
//   - Menu title comes from the destination node's title
//
// This means: the kiosk has predictable, QA-able branches BUT every line
// feels fresh and can adapt to user phrasing.

const DialogueManager = (() => {

  function buildSystem({ char1, char2 }) {
    const c1 = char1 || { name: 'Wira', style: 'calm, warm, formal Malay with Perak dialect flavour', personality: 'Main host. Authoritative but welcoming. 1-2 short sentences per turn.' };
    const c2 = char2 || { name: 'Manja', style: 'cheerful, energetic, casual Malay with Terengganu dialect flavour', personality: 'Sidekick co-host. Builds on Wira with enthusiasm. Short punchy lines.' };
    return `You are scripting a dual-host conversation for METASPEAK, an interactive voice-driven Malaysian tourism + culture kiosk.

CRITICAL LANGUAGE RULE — ABSOLUTE:
- BOTH characters speak BAHASA MALAYSIA (Malay), with light English code-switching only for proper nouns, brands, place names.
- NEVER write full English sentences. NEVER write "Welcome to METASPEAK" — write "Selamat datang ke METASPEAK" instead.
- Casual/rojak (Malay+English mixed) is fine and preferred ("weh", "best gila", "memang power"). 100% English is FORBIDDEN.

Two hosts — STAY IN CHARACTER:
- ${c1.name.toUpperCase()}: ${c1.style}. ${c1.personality}
- ${c2.name.toUpperCase()}: ${c2.style}. ${c2.personality}

CRITICAL CONTENT RULE:
- Follow the "Topic guidance" message provided by the user EXACTLY. The guidance defines tone, style, and what to mention. Do not deviate or substitute generic content.
- Topic guidance overrides everything else. If guidance says "Manja react dulu", Manja's line MUST be the reactive one.

Output a JSON object EXACTLY:
{
  "char1": "<${c1.name}'s line in Bahasa, 1-2 short sentences, < 25 words. DO NOT prefix with the speaker's name.>",
  "char2": "<${c2.name}'s line in Bahasa, 1-2 short sentences, < 25 words, builds on ${c1.name}. DO NOT prefix with the speaker's name.>",
  "image_query": "<2-3 word visual search query — MUST be specific Malaysia term. Use the topic node's keywords. Examples: 'Langkawi Sky Bridge', 'Penang asam laksa', 'Petronas Twin Towers'. NEVER 'world travel landmarks' or 'Malaysia tourism' — too generic.>"
}

Rules:
- Snappy dialogue, like a Malaysian travel TV show. Heavy on local flavour.
- DO NOT include menu options, widget tags, emoji, brackets like [smile], or any markup.
- DO NOT include the speaker's own name in their dialogue line. WRONG: "Wira: Wah, selamat datang!" CORRECT: "Wah, selamat datang!". The kiosk UI already labels who is speaking.
- image_query: ALWAYS use the topic node's specific Malaysian keywords (langkawi, penang, kl, kinabalu, etc.). Never generic English travel terms.
- Return ONLY JSON, no prose, no markdown fences.`;
  }

  async function callAnthropic({ apiKey, system, userPrompt }) {
    const r = await fetch('https://api.anthropic.com/v1/messages', {
      method: 'POST',
      headers: {
        'x-api-key': apiKey,
        'anthropic-version': '2023-06-01',
        'anthropic-dangerous-direct-browser-access': 'true',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'claude-haiku-4-5',
        max_tokens: 512,
        system,
        messages: [{ role: 'user', content: userPrompt }],
      }),
    });
    if (!r.ok) {
      const errTxt = await r.text();
      throw new Error(`Anthropic ${r.status}: ${errTxt}`);
    }
    const j = await r.json();
    return j.content?.[0]?.text || '';
  }

  /** Resolve which node we should be at given current node + user input.
   *  Three cases:
   *    1) User clicked a menu button → input matches an option title exactly → traverse there
   *    2) User said something matching a child's triggers → traverse there
   *    3) User said something off-script → stay at current node, free-form response
   */
  function resolveNextNode(currentNodeId, userInput) {
    const tree = window.TopicTree;
    if (!tree) return { nodeId: currentNodeId, freeForm: true };

    const cur = tree.getNode(currentNodeId) || tree.getRoot();
    if (!cur) return { nodeId: 'root', freeForm: false };

    // Special case: user typed back/home/exit
    if (/\b(back|main menu|home|root|exit|kembali)\b/i.test(userInput || '')) {
      return { nodeId: 'root', freeForm: false };
    }

    // Terminal nodes — after speaking once, return to root for any new input
    if (cur.terminal) {
      // Try matching from root
      const fromRoot = window.TopicTree.matchInput('root', userInput);
      return { nodeId: fromRoot || 'root', freeForm: !fromRoot };
    }

    // Try matching against children
    if (cur.options?.length) {
      const lower = (userInput || '').trim().toLowerCase();
      // Exact title match (button click usually arrives as the option label)
      for (const childId of cur.options) {
        const child = tree.getNode(childId);
        if (child?.title && child.title.toLowerCase() === lower) {
          return { nodeId: childId, freeForm: false };
        }
      }
      // Trigger keyword match
      const matched = tree.matchInput(currentNodeId, userInput);
      if (matched) return { nodeId: matched, freeForm: false };
    }

    // Try matching from root (user might have jumped topics)
    const fromRoot = window.TopicTree.matchInput('root', userInput);
    if (fromRoot && fromRoot !== currentNodeId) {
      return { nodeId: fromRoot, freeForm: false };
    }

    // Free-form within current topic
    return { nodeId: currentNodeId, freeForm: true };
  }

  /** Build the user prompt that gets sent to Claude for THIS beat. */
  function buildBeatPrompt({ nodeId, freeForm, userInput, history, char1, char2 }) {
    const tree = window.TopicTree;
    const node = tree?.getNode(nodeId) || tree?.getRoot() || { id: 'root', title: 'Welcome', context: '' };
    const c1n = char1?.name || 'Wira';
    const c2n = char2?.name || 'Manja';

    const recentBeats = (history || []).slice(-6).map(h => ({
      speaker: h.role === 'char1' ? c1n : h.role === 'char2' ? c2n : h.role,
      text: h.text,
    }));

    const parts = [];
    parts.push(`Current topic: ${node.title}`);
    if (node.context) parts.push(`Topic guidance:\n${node.context}`);

    if (recentBeats.length) {
      parts.push(`Recent dialogue:\n${recentBeats.map(b => `${b.speaker}: ${b.text}`).join('\n')}`);
    }

    if (userInput) {
      parts.push(`User just said: "${userInput}"`);
    }

    if (freeForm) {
      parts.push(`The user's input did NOT match any sub-topic. Generate a brief response that stays on the current topic ("${node.title}") and gently steers them toward picking from the available sub-options. Do not invent new sub-topics not already in the tree.`);
    } else if (node.terminal) {
      parts.push(`This is a terminal sub-topic — the user has drilled in fully. Give a richer, more detailed response (still concise: 1-2 sentences each character). After speaking, the kiosk will return to the parent menu.`);
    } else if (node.intro) {
      parts.push(`This is the welcome/intro beat. Greet warmly, briefly introduce yourselves, and invite the user to pick a category.`);
    } else {
      parts.push(`Generate a brief intro to "${node.title}" — what makes it interesting, no specifics yet (those come in sub-topics). Keep it engaging, set up the menu of sub-options the kiosk will show next.`);
    }

    parts.push(`Generate the next dialogue beat as JSON. Do NOT include menu options or widgets — only char1, char2, and image_query.`);

    return parts.join('\n\n');
  }

  /** Decorate the LLM's content output with menu options + metadata from the tree. */
  function decorateBeat({ llmContent, nodeId, freeForm }) {
    const tree = window.TopicTree;
    const node = tree?.getNode(nodeId) || tree?.getRoot();
    const beat = {
      char1: llmContent.char1 || '...',
      char2: llmContent.char2 || '...',
      image_query: llmContent.image_query || node?.title || 'Malaysia',
    };

    // Menu derivation:
    if (node?.options?.length) {
      const optionTitles = node.options
        .map(id => tree.getNode(id)?.title)
        .filter(Boolean);
      beat.menu_title = node.title || 'Pick an option';
      beat.menu_options = optionTitles;
    } else if (node?.terminal) {
      // Terminal — back-to-parent only. The app shows just a "Back" button.
      beat.menu_title = '';
      beat.menu_options = [];
    } else {
      // Fallback (shouldn't happen with a well-formed tree)
      beat.menu_title = node?.title || 'What now?';
      beat.menu_options = [];
    }

    // Always allow back navigation except at root
    beat.include_back = (nodeId !== 'root');
    // Pass the node id + title forward so the app can track current
    // position and log telemetry. _matched indicates whether the input
    // resolved to a tree node (vs free-form fallback to current node).
    beat._nodeId = nodeId;
    beat._nodeTitle = node?.title || nodeId;
    beat._matched = !freeForm;
    return beat;
  }

  async function generate({ userInput, history, currentNode, claudeApiKey, char1, char2 }) {
    // 1. Resolve which node we should be at
    const { nodeId, freeForm } = resolveNextNode(currentNode || 'root', userInput);

    // 2. Build prompt
    const userPrompt = buildBeatPrompt({
      nodeId, freeForm, userInput, history, char1, char2,
    });
    const system = buildSystem({ char1, char2 });

    // 3. Call LLM
    try {
      let resp;
      if (claudeApiKey) {
        resp = await callAnthropic({ apiKey: claudeApiKey, system, userPrompt });
      } else if (window.claude?.complete) {
        resp = await window.claude.complete({
          messages: [{ role: 'user', content: system + '\n\n' + userPrompt }],
        });
      } else {
        throw new Error('No Claude API available — provide claudeApiKey in settings');
      }
      let txt = resp.trim().replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '');
      const start = txt.indexOf('{'), end = txt.lastIndexOf('}');
      if (start >= 0 && end > start) txt = txt.slice(start, end + 1);
      const llmContent = JSON.parse(txt);
      return decorateBeat({ llmContent, nodeId, freeForm });
    } catch (e) {
      console.warn('Dialogue gen failed', e);
      // Fallback — still respect the tree, just use static lines
      return decorateBeat({
        llmContent: {
          char1: "Maaf, sambungan saya terganggu sebentar.",
          char2: "Cuba pilih dari menu di bawah ye!",
          image_query: window.TopicTree?.getNode(nodeId)?.title || 'Malaysia',
        },
        nodeId,
        freeForm: true,
      });
    }
  }

  /** Generate the welcome/root beat. Called once at session start. */
  async function rootBeat({ claudeApiKey, char1, char2 } = {}) {
    const tree = window.TopicTree;
    if (!tree) {
      const c1n = char1?.name || 'Wira';
      const c2n = char2?.name || 'Manja';
      return {
        char1: `Selamat datang ke METASPEAK! Saya ${c1n}.`,
        char2: `Saya ${c2n}! Jom kita explore Malaysia bersama!`,
        image_query: 'Malaysia tourism',
        menu_title: 'Pick a category to explore',
        menu_options: ['Beach Holiday', 'City Escape', 'Nature Adventure', 'Food and Dining', 'Cultural and Heritage'],
        include_back: false,
        _nodeId: 'root',
      };
    }

    try {
      const system = buildSystem({ char1, char2 });
      const userPrompt = buildBeatPrompt({
        nodeId: 'root', freeForm: false, userInput: '', history: [], char1, char2,
      });
      let resp;
      if (claudeApiKey) {
        resp = await callAnthropic({ apiKey: claudeApiKey, system, userPrompt });
      } else if (window.claude?.complete) {
        resp = await window.claude.complete({
          messages: [{ role: 'user', content: system + '\n\n' + userPrompt }],
        });
      } else {
        throw new Error('No Claude API available');
      }
      let txt = resp.trim().replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '');
      const start = txt.indexOf('{'), end = txt.lastIndexOf('}');
      if (start >= 0 && end > start) txt = txt.slice(start, end + 1);
      const llmContent = JSON.parse(txt);
      return decorateBeat({ llmContent, nodeId: 'root', freeForm: false });
    } catch (e) {
      console.warn('Root beat gen failed, using static fallback', e);
      const c1n = char1?.name || 'Wira';
      const c2n = char2?.name || 'Manja';
      return decorateBeat({
        llmContent: {
          char1: `Selamat datang ke METASPEAK! Saya ${c1n}.`,
          char2: `Saya ${c2n}! Jom kita explore Malaysia!`,
          image_query: 'Malaysia tourism',
        },
        nodeId: 'root',
        freeForm: false,
      });
    }
  }

  return { generate, rootBeat };
})();

window.DialogueManager = DialogueManager;
