const app = document.getElementById("app");
const messengerExportUrl = "https://accountscenter.facebook.com/info_and_permissions/dyi";

const eyeIcon = `
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path d="M2.8 12s3.2-6 9.2-6 9.2 6 9.2 6-3.2 6-9.2 6-9.2-6-9.2-6Z" stroke="currentColor" stroke-width="2"/>
    <circle cx="12" cy="12" r="3" fill="currentColor"/>
  </svg>`;

const sourceInfo = {
  whatsapp: {
    label: "WhatsApp",
    tutorialUrl: "",
    fileLabel: "WhatsApp ZIP or TXT exports",
    uploadLabel: "Upload Files (.zip) or exported (.txt)",
    accept: "ZIP, TXT",
    description: "Best for real chat rhythm, quick replies, phrases and relationship style.",
    helpTabs: {
      ios: [
        "Open WhatsApp on iPhone.",
        "Open the exact chat you want to export.",
        "Tap the contact or group name at the top.",
        "Scroll down and tap Export Chat.",
        "Choose Without Media.",
        "Save or share the ZIP file to this computer, then upload the ZIP here.",
      ],
      android: [
        "Open WhatsApp on Android.",
        "Open the exact chat you want to export.",
        "Tap the contact or group name at the top, or use the three-dot menu > More.",
        "Tap Export chat.",
        "Choose Without media.",
        "Move the ZIP file to this computer, then upload the ZIP here.",
      ],
      info: [
        "WhatsApp usually creates a ZIP file with a TXT chat export inside.",
        "Memory Clone accepts the original ZIP or the TXT file after you extract it.",
        "Open the TXT if you want to check sender names. The cloned person is the sender whose writing style you want to copy.",
        "Upload several chats with different people if you want one clone to learn how that person writes across relationships.",
      ],
      phone: [
        "Fastest today: AirDrop, Nearby Share, USB cable, iCloud Drive, Google Drive, OneDrive or WhatsApp Web download.",
        "Best product path next: a QR code that opens a temporary local upload page on your phone while the desktop app is running.",
        "For release safety, keep the manual upload here until the QR uploader has password/token protection and clear local-network warnings.",
      ],
    },
  },
  messenger: {
    label: "Messenger / Facebook",
    tutorialUrl: "",
    fileLabel: "Messenger ZIP, HTML or JSON exports",
    accept: "ZIP, HTML, JSON",
    description: "Adds long-term Facebook and Messenger conversations to the clone memory.",
    instructions: [
      "Download your Facebook information from Meta Accounts Center: accountscenter.facebook.com/info_and_permissions/dyi.",
      "Choose Messages. HTML is fine, JSON is fine if Meta still offers it.",
      "Upload the whole Facebook ZIP or the message_*.html / message_*.json files inside it.",
      "Memory Clone will split the archive into separate Messenger conversations automatically.",
      "Messenger exports can take up to 24 hours for larger date ranges. You can upload it later.",
    ],
  },
  notes: {
    label: "Notes",
    tutorialUrl: "",
    fileLabel: "TXT notes",
    accept: "TXT",
    description: "Adds private notes, facts, stories and important background.",
    instructions: [
      "Create a plain TXT file with notes the clone should know.",
      "Use one topic per paragraph when possible.",
      "Upload one or more TXT files here.",
      "Notes stay only on this computer.",
    ],
  },
};

const futureSources = [
  "Discord",
  "Instagram",
  "Snapchat",
  "Telegram",
  "Signal",
  "TikTok",
  "Videos",
  "Photos",
  "Voice Recordings",
];

const ui = {
  setupPage: 1,
  activeCloneId: "",
  activeThreadId: "",
  activeTab: "chat",
  settingsDrafts: {},
  settingsDrafts: {},
  brainQuery: "",
  brainResults: [],
  brainPage: 0,
  brainContexts: {},
  brainMessage: "",
  wizard: null,
  notice: null,
  confirm: null,
  update: null,
  updateInstall: null,
  chatDraft: "",
  pendingThreadId: "",
  pendingMessages: [],
  showAllMessages: false,
  profileOpen: false,
  globalHandlersInstalled: false,
  networkOnline: navigator.onLine !== false,
};

let currentState = null;
let phoneUploadPoll = null;
let entitlementSyncTimer = null;
let entitlementSyncInFlight = false;
let lastEntitlementSyncAt = 0;
let settingsAutosaveTimer = null;
let settingsAutosaveQueue = Promise.resolve();

function escapeHtml(value) {
  return String(value ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

function escapeRegExp(value) {
  return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function brainSearchPhrase() {
  const query = String(ui.brainQuery || "").trim();
  // Date-only searches filter the index; they are not a phrase to highlight.
  if (!query
    || /^\d{4}-\d{2}-\d{2}$/.test(query)
    || /^\d{2}[./-]\d{2}[./-]\d{4}$/.test(query)
    || /^\d{1,2}\s+[^\d\s]+\s+\d{4}$/u.test(query)) return "";
  return query.length >= 2 ? query : "";
}

function renderMemoryText(text) {
  const raw = String(text || "").slice(0, 1200);
  const phrase = brainSearchPhrase();
  if (!phrase) return escapeHtml(raw);
  const phraseRe = new RegExp(escapeRegExp(phrase), "ig");
  return raw.split("\n").map((line) => {
    const match = line.match(/^(\[[^\]]+\]\s*)([^:]+)(:\s*)(.*)$/);
    const body = match ? match[4] : line;
    const bodyHasPhrase = body.toLowerCase().includes(phrase.toLowerCase());
    const escapedLine = escapeHtml(line).replace(phraseRe, (hit) => `<mark class="phrase-hit">${escapeHtml(hit)}</mark>`);
    if (!match || !bodyHasPhrase) return escapedLine;
    const sender = escapeHtml(match[2]);
    const highlightedSender = `<mark class="sender-hit">${sender}</mark>`;
    return `${escapeHtml(match[1])}${highlightedSender}${escapeHtml(match[3])}${escapeHtml(body).replace(phraseRe, (hit) => `<mark class="phrase-hit">${escapeHtml(hit)}</mark>`)}`;
  }).join("\n");
}

function memoryContextState(memoryId) {
  const existing = ui.brainContexts[memoryId] || {};
  return {
    previous: Array.isArray(existing.previous) ? existing.previous : (existing.previous ? [existing.previous] : []),
    next: Array.isArray(existing.next) ? existing.next : (existing.next ? [existing.next] : []),
    previousCursor: existing.previousCursor || memoryId,
    nextCursor: existing.nextCursor || memoryId,
    previousDone: Boolean(existing.previousDone),
    nextDone: Boolean(existing.nextDone),
  };
}

function renderMemoryContextList(items, label) {
  if (!items.length) return "";
  return items.map((item, index) => `
    <div class="memory-context">
      <b>${label} ${index + 1}</b>
      <p>${renderMemoryText(item.text || "")}</p>
    </div>
  `).join("");
}

function asList(value) {
  if (Array.isArray(value)) return value.map((item) => String(item || "").trim()).filter(Boolean);
  return String(value || "")
    .split(",")
    .map((item) => item.trim())
    .filter(Boolean);
}

function uniqueList(items) {
  const seen = new Set();
  const out = [];
  for (const item of items) {
    const clean = String(item || "").trim();
    const key = clean.toLowerCase();
    if (clean && !seen.has(key)) {
      out.push(clean);
      seen.add(key);
    }
  }
  return out;
}

function hasListItem(items, value) {
  const key = String(value || "").trim().toLowerCase();
  return asList(items).some((item) => item.toLowerCase() === key);
}

function formatNumber(value) {
  return Number(value || 0).toLocaleString();
}

function formatCredits(value) {
  const numeric = Number(value || 0);
  if (!Number.isFinite(numeric)) return "0";
  return numeric.toFixed(3).replace(/\.?0+$/, "");
}

function creditsLabel(entitlement) {
  const current = Number(entitlement?.creditsBalance || 0);
  const max = Number(entitlement?.monthlyTokenLimit || 0);
  if (max > 0) return `${formatCredits(current)} / ${formatCredits(max)}`;
  return formatCredits(current);
}

function isOffline() {
  return ui.networkOnline === false || navigator.onLine === false;
}

function creditsDisplay(entitlement) {
  return isOffline() ? "No Connection" : creditsLabel(entitlement);
}

function cloneSlotsDisplay(entitlement) {
  return isOffline() ? "No Connection" : formatNumber(entitlement?.maxClones || 0);
}

function serverActionBlockedMessage() {
  return "No internet connection. You can browse local memories and old chats, but creating clones, sending messages, deleting server clones and spending credits needs Memory Clone servers.";
}

function entitlementSignature(state) {
  const entitlement = state?.entitlement || {};
  return [
    entitlement.planName || "",
    entitlement.status || "",
    Number(entitlement.maxClones || 0),
    Number(entitlement.creditsBalance || 0),
    Number(entitlement.monthlyTokenLimit || 0),
    Number(entitlement.expiredClones || 0),
    Number(state?.availableCloneSlots || 0),
  ].join("|");
}

function canAutoSyncEntitlement() {
  if (!currentState?.authenticated || isOffline() || ui.update?.required) return false;
  if (entitlementSyncInFlight) return false;
  const active = document.activeElement;
  if (active && ["INPUT", "TEXTAREA", "SELECT"].includes(active.tagName || "")) return false;
  return true;
}

async function syncEntitlementIfNeeded(force = false) {
  if (!canAutoSyncEntitlement()) return;
  const now = Date.now();
  if (!force && now - lastEntitlementSyncAt < 30000) return;
  entitlementSyncInFlight = true;
  lastEntitlementSyncAt = now;
  const before = entitlementSignature(currentState);
  try {
    const result = await apiCall("sync_license");
    const after = entitlementSignature(result);
    currentState = result;
    if (after !== before) {
      routeState(result);
    }
  } catch (error) {
    if (error?.message && !String(error.message).toLowerCase().includes("log in first")) {
      console.warn("Plan sync failed", error);
    }
  } finally {
    entitlementSyncInFlight = false;
  }
}

function startEntitlementAutoSync() {
  if (entitlementSyncTimer) return;
  entitlementSyncTimer = window.setInterval(() => syncEntitlementIfNeeded(false), 30000);
}

function cloneBuildCreditLabel() {
  const files = rowsForCurrentImport();
  if (!files.length) return "Calculated after upload";
  return "Actual successful AI usage";
}

function importRows(state = currentState) {
  const rows = state?.importManifest?.files || [];
  return Array.isArray(rows) ? rows : [];
}

function newClientId(prefix = "draft") {
  if (window.crypto?.randomUUID) return `${prefix}-${window.crypto.randomUUID()}`;
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}

function currentImportScope() {
  const wizard = ui.wizard;
  if (!wizard) return { scopeId: "", cloneId: ui.activeCloneId || "" };
  if (wizard.mode === "append") return { scopeId: "", cloneId: wizard.cloneId || ui.activeCloneId || "" };
  return { scopeId: wizard.scopeId || "", cloneId: "" };
}

function rowsForCurrentImport() {
  const { scopeId, cloneId } = currentImportScope();
  return importRows().filter((row) => {
    const rowScope = String(row.scope_id || row.scopeId || "");
    const rowClone = String(row.clone_id || row.cloneId || "");
    if (scopeId) return rowScope === scopeId;
    if (cloneId) return rowClone === cloneId || (!rowScope && !rowClone);
    return !rowScope && !rowClone;
  });
}

function rowsForSource(source) {
  return rowsForCurrentImport().filter((row) => String(row.source || "").toLowerCase() === source);
}

function conversationKeyForRow(row) {
  return String(
    row?.conversation_key
    || row?.conversationKey
    || row?.thread_path
    || row?.threadPath
    || row?.stored
    || row?.original
    || ""
  ).trim();
}

function importConversationRows(rows) {
  const conversations = new Map();
  for (const row of rows || []) {
    const source = String(row?.source || "").toLowerCase();
    const key = `${source}:${conversationKeyForRow(row) || row?.stored || row?.original || Math.random()}`;
    let conversation = conversations.get(key);
    if (!conversation) {
      conversation = {
        ...row,
        storedFiles: [],
        originalFiles: [],
        detected_senders: [],
        contact_aliases: [],
        thread_title: String(row?.thread_title || row?.threadTitle || "").trim(),
        conversation_key: conversationKeyForRow(row),
      };
      conversations.set(key, conversation);
    }
    if (row?.stored && !conversation.storedFiles.includes(row.stored)) conversation.storedFiles.push(row.stored);
    if (row?.original && !conversation.originalFiles.includes(row.original)) conversation.originalFiles.push(row.original);
    conversation.detected_senders = uniqueList([
      ...conversation.detected_senders,
      ...asList(row?.detected_senders || row?.detectedSenders || ""),
    ]);
    conversation.contact_aliases = uniqueList([
      ...conversation.contact_aliases,
      ...asList(row?.contact_aliases || row?.contactAliases || ""),
    ]);
    if (!conversation.thread_title && (row?.thread_title || row?.threadTitle)) {
      conversation.thread_title = String(row.thread_title || row.threadTitle).trim();
    }
  }
  return Array.from(conversations.values()).map((conversation) => ({
    ...conversation,
    stored: conversation.storedFiles[0] || conversation.stored || "",
    original: conversation.thread_title || conversation.originalFiles[0] || conversation.original || "Imported conversation",
    partCount: conversation.storedFiles.length || 1,
  }));
}

function looseAliasKey(value) {
  return String(value || "")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/(.)\1{2,}/g, "$1$1")
    .replace(/[^a-z0-9]+/g, " ")
    .trim();
}

function aliasMatchesName(name, aliases) {
  const nameKey = looseAliasKey(name);
  if (!nameKey) return false;
  return asList(aliases).some((alias) => {
    const aliasKey = looseAliasKey(alias);
    if (!aliasKey) return false;
    const first = aliasKey.split(" ")[0] || aliasKey;
    return nameKey === aliasKey || nameKey === first || aliasKey.includes(nameKey) || nameKey.includes(first);
  });
}

function isPrivateNoteName(name) {
  return /^self[\s_-]*note$/i.test(String(name || "").trim());
}

function subjectAliasListForWizard(wizard) {
  return uniqueList([...asList(wizard?.subjectAliases), wizard?.cloneName].filter(Boolean));
}

function subjectAliasListForClone(clone) {
  return uniqueList([
    clone?.identitySubjectName,
    clone?.name,
    ...(clone?.subjectSenderAliases || []),
  ].filter(Boolean));
}

function isSubjectOrNoteName(name, subjectAliases) {
  return isPrivateNoteName(name) || aliasMatchesName(name, subjectAliases);
}

function suggestedContactForFile(file) {
  const wizard = ensureWizard();
  const storedFiles = file?.storedFiles?.length ? file.storedFiles : [file?.stored].filter(Boolean);
  const manual = storedFiles.map((stored) => wizard.fileContacts[stored]).find(Boolean);
  if (manual) return manual;
  const subjectAliases = subjectAliasListForWizard(wizard);
  const explicit = uniqueList([
    ...asList(file?.contact_aliases || ""),
    ...asList(file?.thread_title || ""),
  ]).filter((name) => !isSubjectOrNoteName(name, subjectAliases));
  if (explicit.length) return explicit.join(", ");
  const senders = uniqueList(asList(file?.detected_senders || file?.detectedSenders || ""));
  if (!senders.length) return "";
  const userAliases = asList(wizard.userAliases);
  const nonSubject = senders.filter((sender) => !aliasMatchesName(sender, subjectAliases));
  const userMatch = nonSubject.find((sender) => aliasMatchesName(sender, userAliases));
  if (userMatch) return userMatch;
  if (nonSubject.length === 1) return nonSubject[0];
  return "";
}

function captureScrollPositions() {
  return {
    viewKey: ui.renderedViewKey || "",
    windowY: window.scrollY || document.documentElement.scrollTop || 0,
    main: document.querySelector(".main-panel")?.scrollTop || 0,
    wizard: document.querySelector(".wizard-view")?.scrollTop || 0,
    messages: document.querySelector(".messages")?.scrollTop || 0,
  };
}

function restoreScrollPositions(snapshot) {
  window.requestAnimationFrame(() => {
    if (snapshot.windowY) window.scrollTo(0, snapshot.windowY);
    const main = document.querySelector(".main-panel");
    if (main) main.scrollTop = snapshot.main || 0;
    const wizard = document.querySelector(".wizard-view");
    if (wizard) wizard.scrollTop = snapshot.wizard || 0;
    const messages = document.querySelector(".messages");
    if (messages && snapshot.messages) messages.scrollTop = snapshot.messages;
  });
}

function subjectCandidatesForWizard() {
  const counts = new Map();
  const sources = new Map();
  for (const row of rowsForCurrentImport()) {
    for (const sender of asList(row.detected_senders || row.detectedSenders || "")) {
      const key = sender.toLowerCase();
      counts.set(sender, (counts.get(sender) || 0) + 1);
      if (!sources.has(key)) sources.set(key, new Set());
      if (row.source) sources.get(key).add(String(row.source));
    }
  }
  const stateCandidates = Array.isArray(currentState?.importSubjectCandidates) ? currentState.importSubjectCandidates : [];
  for (const candidate of stateCandidates) {
    const name = String(candidate.name || "").trim();
    if (!name) continue;
    counts.set(name, Math.max(counts.get(name) || 0, Number(candidate.fileCount || 0)));
    if (!sources.has(name.toLowerCase())) sources.set(name.toLowerCase(), new Set());
    for (const source of asList(candidate.sources || "")) sources.get(name.toLowerCase()).add(source);
  }
  return Array.from(counts.entries())
    .map(([name, fileCount]) => ({
      name,
      fileCount,
      sources: Array.from(sources.get(name.toLowerCase()) || []),
    }))
    .sort((a, b) => b.fileCount - a.fileCount || a.name.localeCompare(b.name))
    .slice(0, 12);
}

async function apiCall(name, ...args) {
  const controller = new AbortController();
  const timeoutMs = name === "select_import_files"
    ? 600000
    : name === "refine_clone_brain"
      ? 660000
      : 20000;
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetch(`/api/${encodeURIComponent(name)}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ args }),
      signal: controller.signal,
    });
    const payload = await response.json();
    if (!response.ok || !payload.ok) {
      const apiError = new Error(payload.message || "Memory Clone could not complete this action.");
      apiError.payload = payload;
      throw apiError;
    }
    return payload;
  } catch (error) {
    if (error.name === "AbortError") {
      throw new Error("Memory Clone took too long to respond. Please try again.");
    }
    if (error instanceof TypeError && navigator.onLine === false) {
      ui.networkOnline = false;
      throw new Error(serverActionBlockedMessage());
    }
    throw error;
  } finally {
    clearTimeout(timeout);
  }
}

function renderLoading(message = "Preparing your private clone workspace...", progress = 20) {
  app.innerHTML = `
    <section class="loading-screen">
      <div class="loading-card">
        <div class="ring"></div>
        <h1>Loading Memory Clone</h1>
        <p>${escapeHtml(message)}</p>
        <strong>${Math.max(0, Math.min(100, Number(progress || 0)))}%</strong>
        <div class="progress"><span style="width:${Math.max(4, Math.min(100, Number(progress || 0)))}%"></span></div>
      </div>
    </section>`;
}

function renderLogin(state = {}) {
  const loginMessage = state.message && state.message !== "Ready" ? state.message : "";
  app.innerHTML = `
    <section class="login-screen">
      <div class="login-wrap">
        <header class="login-brand">
          <span class="brand-logo"></span>
          <div>
            <h1>Memory Clone</h1>
            <p>Your memories. Your voice. Your clone.</p>
          </div>
        </header>

        <form class="login-card" id="loginForm">
          <h2>Welcome back</h2>
          <p class="sub">Log in to access your clones and memories.</p>

          <label for="email">Email</label>
          <div class="field">
            <input id="email" type="email" autocomplete="email" value="${escapeHtml(state.email || state.rememberedEmail || "")}" placeholder="Enter your email" />
          </div>

          <label for="password">Password</label>
          <div class="field password-field">
            <input id="password" type="password" autocomplete="current-password" placeholder="Enter your password" />
            <button class="eye-btn" id="togglePassword" type="button" aria-label="Show password">${eyeIcon}</button>
          </div>

          <div class="login-actions-row">
            <label class="check">
              <input id="remember" type="checkbox" checked />
              <span>Remember me</span>
            </label>
            <a href="#" data-open="https://memoryclone.com/login.php">Forgot password?</a>
          </div>

          <button class="primary-btn full" id="loginButton" type="submit">
            <span>Log In</span>
          </button>
          <div class="status" id="loginStatus">${escapeHtml(loginMessage)}</div>

          <div class="divider"><span></span><em>or continue with</em><span></span></div>
          <a class="secondary-btn full" href="#" data-open="https://memoryclone.com/register.php">Create Account</a>
        </form>

        <p class="privacy-note">
          Your original imported chat files stay on this computer. Read the
          <a href="#" data-open="https://memoryclone.com/privacy.php">Privacy Policy</a>.
        </p>
      </div>
    </section>`;

  bindExternalLinks();
  document.getElementById("togglePassword").addEventListener("click", () => {
    const input = document.getElementById("password");
    input.type = input.type === "password" ? "text" : "password";
  });
  document.getElementById("loginForm").addEventListener("submit", handleLogin);
}

async function handleLogin(event) {
  event.preventDefault();
  const button = document.getElementById("loginButton");
  const status = document.getElementById("loginStatus");
  button.disabled = true;
  status.textContent = "Signing in securely...";
  try {
    const email = document.getElementById("email").value;
    const password = document.getElementById("password").value;
    const remember = document.getElementById("remember").checked;
    const result = await apiCall("login", email, password, remember);
    ui.profileOpen = false;
    if (ui.notice?.actionType === "logout") ui.notice = null;
    routeState(result);
  } catch (error) {
    status.textContent = error.message || "We could not sign you in. Please try again.";
  } finally {
    button.disabled = false;
  }
}

function renderSetup(state) {
  const entitlement = state.entitlement || {};
  const profile = state.profile || {};
  const displayName = profile.displayName || state.email?.split("@")[0] || "";
  const aliases = (profile.relationshipAliases || []).join(", ");
  const hasClonePlan = String(entitlement.status || "") === "active" && Number(entitlement.maxClones || 0) > 0;

  app.innerHTML = `
    <section class="app-screen setup-screen">
      <div class="setup-card">
        <header class="setup-brand">
          <span class="brand-logo"></span>
          <div>
            <h1>Memory Clone</h1>
            <p>Your memories. Your voice. Your clone.</p>
          </div>
        </header>

        <div class="setup-topline">
          <span>Clones Available: ${cloneSlotsDisplay(entitlement)}</span>
          <span>Talking Credits: ${creditsDisplay(entitlement)}</span>
        </div>

        <div class="step-indicator">
          <span>Welcome Setup</span>
          <strong>${ui.setupPage}/2</strong>
        </div>

        ${ui.setupPage === 1 ? `
          <div class="setup-page">
            <h2>Welcome Setup</h2>
            <p>Setup your relation with the clone. Are you a Mother, Yourself, Sister or a Lover?</p>
            ${renderAlphaNote()}
            <div class="license-card">
              <h3>My Licenses</h3>
              <div><span>Clones Available</span><strong>${cloneSlotsDisplay(entitlement)}</strong></div>
              <div><span>Talking Credits</span><strong>${creditsDisplay(entitlement)}</strong></div>
              ${hasClonePlan ? `<p class="ok-text">Your account is ready to create a clone.</p>` : `
                <p>You need an active clone plan before creating and talking with a clone.</p>
                <button class="plan-btn" data-open="${escapeHtml(state.checkoutUrl || "https://memoryclone.com/pricing.php")}">View Plans</button>
              `}
            </div>
            <div class="setup-nav">
              <span></span>
              <button class="primary-btn" id="setupNext" type="button">Next</button>
            </div>
          </div>` : `
          <form class="setup-page" id="setupForm">
            <h2>Who are you for this clone?</h2>
            <p>Enter how this clone should recognize you in conversations: your name, relationship, nickname, or words this person used for you.</p>
            <label for="displayName">Your Display Name</label>
            <input id="displayName" value="${escapeHtml(displayName)}" />
            <label for="aliases">How should the clone call you? <small>Comma separated</small></label>
            <input id="aliases" value="${escapeHtml(aliases)}" placeholder="Your name, Mom, Sister, Love, Honey" />
            <label class="check policy-check">
              <input id="privacyOk" type="checkbox" />
              <span>I understand the <a href="#" data-open="https://memoryclone.com/privacy.php">Privacy Policy</a>. Original chat exports and the local memory index stay on this computer; secure AI analysis is always separately explained and confirmed before selected evidence is sent.</span>
            </label>
            <div class="status" id="setupStatus"></div>
            <div class="setup-nav">
              <button class="secondary-btn" id="setupPrev" type="button">Previous</button>
              <button class="primary-btn" id="setupSave" type="submit">Finish Setup</button>
            </div>
          </form>`}
      </div>
    </section>`;

  bindExternalLinks();
  document.getElementById("setupNext")?.addEventListener("click", () => {
    ui.setupPage = 2;
    renderSetup(state);
  });
  document.getElementById("setupPrev")?.addEventListener("click", () => {
    ui.setupPage = 1;
    renderSetup(state);
  });
  document.getElementById("setupForm")?.addEventListener("submit", handleSetup);
}

function renderAlphaNote() {
  return `
    <aside class="alpha-note">
      <strong>Built independently. Tested with you.</strong>
      <p>Memory Clone is an alpha product built by one independent developer, not a billion-dollar team. Thank you for testing it. I am improving every release to make this experience feel truly special.</p>
      <small>Coming soon: Instagram, voice, video, photos, Discord, copy chats and more import platforms.</small>
    </aside>`;
}

async function handleSetup(event) {
  event.preventDefault();
  const status = document.getElementById("setupStatus");
  status.textContent = "Saving setup...";
  try {
    const result = await apiCall(
      "save_setup",
      document.getElementById("displayName").value,
      document.getElementById("aliases").value,
      document.getElementById("privacyOk").checked,
    );
    routeState(result);
  } catch (error) {
    status.textContent = error.message || "Please confirm your privacy settings before continuing.";
  }
}

function ensureActiveClone(state) {
  const clones = state.clones || [];
  const activeClones = clones.filter((clone) => !clone.archived);
  if (!ui.activeCloneId && clones.length) ui.activeCloneId = (activeClones[0] || clones[0]).id;
  if (ui.activeCloneId && !clones.some((clone) => clone.id === ui.activeCloneId)) {
    ui.activeCloneId = (activeClones[0] || clones[0])?.id || "";
  }
  return clones.find((clone) => clone.id === ui.activeCloneId) || activeClones[0] || clones[0] || null;
}

function hasAvailableCloneSlot(state) {
  const entitlement = state.entitlement || {};
  return !isOffline() && String(entitlement.status || "") === "active" && Number(state.availableCloneSlots || 0) > 0;
}

function ensureWizard(mode = ui.wizard?.mode || "create", cloneId = ui.wizard?.cloneId || "") {
  if (!ui.wizard || ui.wizard.mode !== mode || (mode === "append" && ui.wizard.cloneId !== cloneId)) {
    ui.wizard = {
      mode,
      cloneId,
      scopeId: mode === "create" ? newClientId("clone-draft") : "",
      step: "sources",
      selectedSources: ["whatsapp"],
      sourceIndex: 0,
      cloneName: "",
      subjectAliases: "",
      userAliases: (currentState?.profile?.relationshipAliases || []).join(", "),
      identityMode: "standard",
      fileContacts: {},
      cloudAnalysisConsent: false,
      isBuilding: false,
      helpTab: "ios",
      phoneUpload: null,
      status: "",
    };
  }
  return ui.wizard;
}

function renderApp(state) {
  const scrollSnapshot = captureScrollPositions();
  currentState = state;
  const clone = ensureActiveClone(state);
  const clones = state.clones || [];
  const hasActiveClone = Number(state.activeCloneCount || 0) > 0;
  const needsPlan = clones.length === 0 && !hasAvailableCloneSlot(state);
  const shouldShowWizard = !needsPlan && (Boolean(ui.wizard) || (!hasActiveClone && clones.length === 0));
  if (shouldShowWizard) ensureWizard(clones.length === 0 ? "create" : ui.wizard?.mode || "create", ui.wizard?.cloneId || clone?.id || "");
  const viewKey = needsPlan
    ? "plan"
    : shouldShowWizard
      ? `wizard:${ui.wizard?.step || ""}`
      : `workspace:${ui.activeCloneId || ""}:${ui.activeThreadId || ""}:${ui.activeTab || "chat"}`;

  app.innerHTML = `
    <section class="app-screen">
      <div class="app-shell">
        ${renderSidebar(state, clone)}
        <main class="main-panel">
          ${needsPlan ? renderPlanRequired(state) : shouldShowWizard ? renderWizard(state) : renderCloneWorkspace(state, clone)}
        </main>
        ${renderNotice()}
        ${renderConfirm()}
      </div>
    </section>`;

  bindAppEvents();
  bindExternalLinks();
  installGlobalHandlers();
  if (scrollSnapshot.viewKey && scrollSnapshot.viewKey === viewKey) restoreScrollPositions(scrollSnapshot);
  else if (!shouldShowWizard && ui.activeTab === "chat") scrollChatToBottom();
  ui.renderedViewKey = viewKey;
  syncPhoneUploadPolling();
}

function installGlobalHandlers() {
  if (ui.globalHandlersInstalled) return;
  ui.globalHandlersInstalled = true;
  document.addEventListener("click", (event) => {
    if (!ui.profileOpen) return;
    const target = event.target;
    if (target?.closest?.(".profile-menu")) return;
    ui.profileOpen = false;
    renderApp(currentState);
  });
}

function closeProfileMenu() {
  ui.profileOpen = false;
}

function scrollChatToBottom() {
  window.requestAnimationFrame(() => {
    const messages = document.querySelector(".messages");
    if (messages) messages.scrollTop = messages.scrollHeight;
  });
}

function syncPhoneUploadPolling() {
  const active = Boolean(ui.wizard?.phoneUpload?.token);
  if (active && !phoneUploadPoll) {
    phoneUploadPoll = window.setInterval(async () => {
      const token = ui.wizard?.phoneUpload?.token;
      if (!token) return;
      try {
        const result = await apiCall("phone_upload_status", token);
        currentState = result.state || currentState;
        const uploaded = Number(result.uploaded || 0);
        if (uploaded > 0) {
          const requestedSource = ui.wizard.phoneUpload?.source || "";
          const detectedSources = [...new Set(
            (result.detectedSources || []).filter((item) => ["whatsapp", "messenger", "notes"].includes(item))
          )];
          for (const detectedSource of detectedSources) {
            if (!ui.wizard.selectedSources.includes(detectedSource)) {
              ui.wizard.selectedSources.push(detectedSource);
            }
          }
          ui.wizard.phoneUpload = null;
          if (detectedSources.length === 1 && detectedSources[0] !== requestedSource) {
            ui.wizard.step = `upload:${detectedSources[0]}`;
            const label = SOURCE_INFO[detectedSources[0]]?.label || "imported";
            ui.wizard.status = `Detected ${label} data and added ${formatNumber(uploaded)} conversation file(s) there.`;
          } else {
            ui.wizard.status = `Received ${formatNumber(uploaded)} file(s) from your phone.`;
          }
          apiCall("stop_phone_upload", token).catch(() => {});
          renderApp(currentState);
        }
      } catch (error) {
        const payload = error.payload || {};
        if (payload.expired) {
          currentState = payload.state || currentState;
          ui.wizard.phoneUpload = null;
          ui.wizard.status = "Phone upload link expired. Start a new QR upload when you need it.";
          renderApp(currentState);
        }
      }
    }, 3000);
  } else if (!active && phoneUploadPoll) {
    window.clearInterval(phoneUploadPoll);
    phoneUploadPoll = null;
  }
}

function stopPhoneUploadPolling() {
  if (!phoneUploadPoll) return;
  window.clearInterval(phoneUploadPoll);
  phoneUploadPoll = null;
}

function closeActivePhoneUpload(message = "") {
  const token = ui.wizard?.phoneUpload?.token || "";
  if (ui.wizard?.phoneUpload) {
    ui.wizard.phoneUpload = null;
    if (message) ui.wizard.status = message;
  }
  stopPhoneUploadPolling();
  if (token) apiCall("stop_phone_upload", token).catch(() => {});
}

function renderNotice() {
  if (!ui.notice) return "";
  const updateStatus = ui.notice.actionType === "update" ? ui.updateInstall || {} : {};
  const updateProgress = Math.max(0, Math.min(100, Number(updateStatus.progress || 0)));
  const updateBusy = ["downloading", "launching"].includes(updateStatus.state);
  return `
    <div class="modal-backdrop">
      <section class="modal-card">
        <button class="notice-close" id="noticeClose" type="button" aria-label="Close">&times;</button>
        <strong>${escapeHtml(ui.notice.title || "Memory Clone")}</strong>
        <p>${escapeHtml(ui.notice.message || "")}</p>
        ${updateStatus.message ? `
          <div class="update-progress">
            <div class="progress"><span style="width:${updateProgress}%"></span></div>
            <small>${escapeHtml(updateStatus.message)}</small>
          </div>
        ` : ""}
        <div class="modal-actions">
          ${ui.notice.actionUrl ? `<button class="plan-btn notice-action" data-open="${escapeHtml(ui.notice.actionUrl)}">${escapeHtml(ui.notice.actionLabel || "View Plans")}</button>` : ""}
          ${ui.notice.actionType ? `<button class="primary-btn" id="noticeAction" type="button" ${updateBusy ? "disabled" : ""}>${escapeHtml(updateBusy ? "Updating..." : ui.notice.actionLabel || "Continue")}</button>` : ""}
          <button class="secondary-btn" id="noticeDismiss" type="button">Close</button>
        </div>
      </section>
    </div>`;
}

function renderUpdateRequired(update) {
  const latest = update?.latestVersion || "latest";
  const notes = update?.releaseNotes || "Download the newest Memory Clone build to continue.";
  const status = ui.updateInstall || {};
  const busy = ["downloading", "launching"].includes(status.state);
  const progress = Math.max(0, Math.min(100, Number(status.progress || 0)));
  return `
    <section class="update-required-screen">
      <div class="update-required-card">
        <div class="brand-row">
          <span class="app-logo"></span>
          <div>
            <h1>Memory Clone update required</h1>
            <p>Your app version is no longer supported.</p>
          </div>
        </div>
        <div class="setup-stats">
          <span>Installed: ${escapeHtml(update?.currentVersion || "unknown")}</span>
          <span>Latest: ${escapeHtml(latest)}</span>
        </div>
        <p>${escapeHtml(notes)}</p>
        ${status.message ? `
          <div class="update-progress">
            <div class="progress"><span style="width:${progress}%"></span></div>
            <small>${escapeHtml(status.message)}</small>
          </div>
        ` : ""}
        <div class="form-actions">
          <button class="primary-btn" id="updateNowBtn" type="button" ${busy ? "disabled" : ""}>${busy ? "Updating..." : "Update Now"}</button>
        </div>
      </div>
    </section>`;
}

async function handleUpdateNow() {
  const update = ui.update || {};
  const downloadUrl = update.downloadUrl || "https://memoryclone.com/downloads/MemoryClone-Setup.exe";
  try {
    const started = await apiCall("start_update_download", downloadUrl);
    ui.updateInstall = started.updateStatus || { state: "downloading", progress: 0, message: "Downloading update..." };
    routeState(currentState || { authenticated: false });
    const timer = window.setInterval(async () => {
      try {
        const statusResult = await apiCall("update_download_status");
        ui.updateInstall = statusResult.updateStatus || ui.updateInstall;
        routeState(currentState || { authenticated: false });
        if (ui.updateInstall?.state === "downloaded") {
          window.clearInterval(timer);
          ui.updateInstall.message = "Starting installer...";
          routeState(currentState || { authenticated: false });
          await apiCall("install_downloaded_update");
        } else if (ui.updateInstall?.state === "error") {
          window.clearInterval(timer);
        }
      } catch (error) {
        window.clearInterval(timer);
        ui.updateInstall = { state: "error", progress: 0, message: error.message || "Could not update Memory Clone." };
        routeState(currentState || { authenticated: false });
      }
    }, 500);
  } catch (error) {
    ui.updateInstall = { state: "error", progress: 0, message: error.message || "Could not start update." };
    routeState(currentState || { authenticated: false });
  }
}

function showNotice(title, message, actionLabel = "", actionUrl = "", actionType = "") {
  ui.notice = { title, message, actionLabel, actionUrl, actionType };
  renderApp(currentState);
}

function friendlyApiError(error, fallback = "Something went wrong. Please try again.") {
  const raw = String(error?.message || "").toLowerCase();
  if (raw.includes("quota") || raw.includes("billing") || raw.includes("insufficient")) {
    return "The AI service is temporarily unavailable. Your message was not charged. Please try again shortly.";
  }
  if (raw.includes("rate") || raw.includes("too many")) {
    return "Too many requests were sent at once. Please wait a moment and try again.";
  }
  if (raw.includes("network") || raw.includes("connection") || raw.includes("fetch")) {
    return "Memory Clone could not reach the secure service. Check your internet connection and try again.";
  }
  return fallback;
}

function friendlyStatusError(message, fallback = "Something went wrong. Please try again.") {
  return friendlyApiError({ message }, fallback);
}

function renderConfirm() {
  if (!ui.confirm) return "";
  return `
    <div class="modal-backdrop">
      <section class="modal-card">
        <strong>${escapeHtml(ui.confirm.title || "Are you sure?")}</strong>
        <p>${escapeHtml(ui.confirm.message || "")}</p>
        <div class="modal-actions">
          <button class="secondary-btn" id="confirmCancel" type="button">${escapeHtml(ui.confirm.cancelLabel || "Cancel")}</button>
          <button class="${ui.confirm.danger ? "danger-btn" : "primary-btn"}" id="confirmAccept" type="button">${escapeHtml(ui.confirm.confirmLabel || "Confirm")}</button>
        </div>
      </section>
    </div>`;
}

function askConfirm(options) {
  return new Promise((resolve) => {
    ui.confirm = { ...options, resolve };
    renderApp(currentState);
  });
}

function clearConfirm(value) {
  const pending = ui.confirm;
  ui.confirm = null;
  renderApp(currentState);
  if (pending?.resolve) pending.resolve(value);
}

function handleAuthError(error, title = "Session expired") {
  const message = error?.message || "";
  const authExpired = Boolean(error?.payload?.authExpired)
    || message.toLowerCase().includes("log in again")
    || message.toLowerCase().includes("bearer token")
    || message.toLowerCase().includes("expired session");
  if (!authExpired) return false;
  showNotice(
    title,
    "Your secure login session is no longer connected to this app. Please log in again, then retry this action.",
    "Log in again",
    "",
    "logout",
  );
  return true;
}

function renderPlanRequired(state) {
  const checkoutUrl = state.checkoutUrl || "https://memoryclone.com/pricing.php";
  return `
    <section class="plan-required">
      <div class="plan-card-large">
        <span class="eyebrow">Create your first clone</span>
        <h1>Buy a clone plan to start.</h1>
        <p>Your account is signed in, but it does not have an active clone slot yet. Buy the starter clone plan on memoryclone.com, then come back here and sign in again.</p>
        <div class="plan-actions">
          <button class="plan-btn" data-open="${escapeHtml(checkoutUrl)}">View Plans</button>
          <button class="secondary-btn" id="logoutBtn" type="button">Log out</button>
        </div>
      </div>
    </section>`;
}

function renderSidebar(state, activeClone) {
  const entitlement = state.entitlement || {};
  const clones = state.clones || [];
  const canCreate = hasAvailableCloneSlot(state);
  const email = state.email || state.profile?.email || "";

  return `
    <aside class="sidebar">
      <div class="side-brand">
        <span class="mini-logo"></span>
        <strong>Memory Clone</strong>
        <span class="app-version">v${escapeHtml(state.appVersion || "")}</span>
        ${ui.update?.available ? `
          <button class="update-icon-btn ${ui.update.required ? "required" : ""}" id="updateIconBtn" type="button" title="Download Memory Clone update" aria-label="Download Memory Clone update">
            <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <path d="M12 3v11m0 0 4-4m-4 4-4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
              <path d="M5 17v2h14v-2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
            </svg>
          </button>
        ` : ""}
      </div>

      <div class="side-section side-section-head">
        <span>Clones</span>
      </div>
      <button class="create-clone-btn ${canCreate ? "" : "muted"}" id="newCloneBtn" type="button">Create New Clone</button>

      <div class="clone-list">
        ${clones.length ? clones.map((clone) => renderCloneNavItem(state, clone, activeClone)).join("") : `
          <div class="empty-clones">
            <strong>No active clones yet</strong>
            <span>Set up your first clone on the right.</span>
          </div>`}
      </div>

      <div class="account-dock">
        <div class="usage-card ${isOffline() ? "offline" : ""}">
          <strong>${escapeHtml(email || "Signed in")}</strong>
          <span>Clones Available: ${cloneSlotsDisplay(entitlement)}</span>
          <span>Talking Credits: ${creditsDisplay(entitlement)}</span>
          ${isOffline() ? `<small>No Connection: local memories only.</small>` : ""}
          <button class="plan-btn compact-plan" data-open="${escapeHtml(state.checkoutUrl || "https://memoryclone.com/pricing.php")}">View Plans</button>
        </div>
        <div class="profile-menu ${ui.profileOpen ? "open" : ""}">
          <button class="profile-summary" id="profileToggle" type="button">Profile & Settings</button>
          <div class="profile-popover">
          ${activeClone ? `
            <button data-tab="settings" class="menu-row">Clone Settings</button>
            <button data-tab="brain" class="menu-row">Usage & Memory Search</button>
            <button data-tab="map" class="menu-row">Clone Map</button>
          ` : ""}
          <button data-open="https://memoryclone.com/privacy.php" class="menu-row">Privacy Policy</button>
          <button id="logoutBtn" class="menu-row danger">Log out</button>
          </div>
        </div>
      </div>
    </aside>`;
}

function renderCloneNavItem(state, clone, activeClone) {
  const isActive = activeClone?.id === clone.id;
  return `
      <div class="clone-nav-block ${isActive ? "active" : ""} ${clone.archived ? "archived" : ""}">
        <button class="clone-item compact ${isActive ? "active" : ""}" data-clone-id="${escapeHtml(clone.id)}">
        <span class="avatar tiny">${escapeHtml((clone.name || "C").slice(0, 1).toUpperCase())}</span>
        <strong>${escapeHtml(clone.name)}</strong>
          <i class="${clone.archived ? "archived" : ""}" title="${clone.archived ? "Archived" : "Active"}"></i>
        </button>
      ${isActive ? renderThreadList(state, clone) : ""}
    </div>`;
}

function renderThreadList(state, activeClone) {
  const threads = state.threadListsByClone?.[activeClone.id] || [];
  if (!threads.length) return "";
  if (!ui.activeThreadId || !threads.some((thread) => thread.id === ui.activeThreadId)) {
    ui.activeThreadId = threads[0].id;
  }
  return `
    <div class="threads-head">
      <span>Threads</span>
      ${activeClone.archived ? "" : '<button class="round-plus" id="newThreadBtn" title="New chat thread">+</button>'}
    </div>
    <div class="thread-list">
      ${threads.map((thread) => `
        <div class="thread-row">
          <button class="thread-item ${ui.activeThreadId === thread.id ? "active" : ""}" data-thread-id="${escapeHtml(thread.id)}">
            <span>${escapeHtml(thread.title || "New chat")}</span>
            <small>${formatNumber(thread.messageCount || 0)} messages</small>
          </button>
          ${activeClone.archived ? "" : `<button class="thread-delete" data-delete-thread="${escapeHtml(thread.id)}" title="Delete this chat thread" type="button">&times;</button>`}
        </div>
      `).join("")}
    </div>`;
}

function renderCloneWorkspace(state, clone) {
  if (!clone) return renderWizard(state);
  return `
    <header class="workspace-head">
      <div class="clone-heading">
        <span class="avatar large">${escapeHtml((clone.name || "C").slice(0, 1).toUpperCase())}</span>
        <div>
          <h1>${escapeHtml(clone.name)}</h1>
          <p>${escapeHtml(clone.description || "Your Memory Clone")}</p>
        </div>
      </div>
      <nav class="tabs">
        <button class="${ui.activeTab === "chat" ? "active" : ""}" data-tab="chat">Clone Chat</button>
        <button class="${ui.activeTab === "brain" ? "active" : ""}" data-tab="brain">Clone Brain</button>
        <button class="${ui.activeTab === "map" ? "active" : ""}" data-tab="map">Clone Map</button>
        <button class="${ui.activeTab === "settings" ? "active" : ""}" data-tab="settings">Clone Settings</button>
      </nav>
    </header>
    ${ui.activeTab === "brain" ? renderBrain(state, clone) : ui.activeTab === "map" ? renderCloneMap(state, clone) : ui.activeTab === "settings" ? renderSettings(state, clone) : renderChat(state, clone)}
  `;
}

function getActiveThread(state, clone) {
  const threads = state.threadListsByClone?.[clone.id] || [];
  if (!ui.activeThreadId || !threads.some((thread) => thread.id === ui.activeThreadId)) {
    ui.activeThreadId = threads[0]?.id || "";
  }
  return state.threadMessagesById?.[ui.activeThreadId] || state.threadsByClone?.[clone.id] || state.activeThread || { messages: [] };
}

function renderChat(state, clone) {
  const entitlement = state.entitlement || {};
  const thread = getActiveThread(state, clone);
  const canChat = !clone.archived && !isOffline() && String(entitlement.status || "") === "active" && Number(entitlement.creditsBalance || 0) > 0;
  const chatPlaceholder = clone.archived ? "This archived clone is read-only." : isOffline() ? "No connection. Local chat history is read-only." : canChat ? "Type a message..." : "Add Talking Credits to keep chatting.";
  const baseMessages = thread.messages || [];
  const pending = ui.pendingThreadId === ui.activeThreadId ? ui.pendingMessages : [];
  const messages = [...baseMessages, ...pending];
  const hiddenCount = Math.max(0, messages.length - 20);
  const visibleMessages = ui.showAllMessages || !hiddenCount ? messages : messages.slice(-20);
  return `
    <section class="chat-view">
      <div class="day-pill">Today</div>
      <div class="messages">
        ${clone.archived ? `<div class="chat-empty archived-clone-note"><h2>${escapeHtml(clone.name)} is archived</h2><p>All local chats, memories and the Clone Map are still available to read. Reactivate this clone to continue chatting.</p><button class="primary-btn" id="reactivateCloneBtn" type="button">Reactivate Clone</button></div>` : ""}
        ${hiddenCount ? `
          <button class="history-toggle" id="toggleHistory" type="button">${ui.showAllMessages ? "Show latest only" : `Show ${formatNumber(hiddenCount)} earlier messages`}</button>
        ` : ""}
        ${messages.length ? visibleMessages.map((message) => `
          <div class="message ${message.role === "user" ? "user" : "clone"}">
            <div class="bubble">${message.typing ? `<span class="typing-dots"><i></i><i></i><i></i></span>` : escapeHtml(message.content)}</div>
          </div>
        `).join("") : `
          <div class="chat-empty">
            <h2>Start talking with ${escapeHtml(clone.name)}</h2>
            <p>The clone uses the local memory index on this computer and answers through the secured Memory Clone server.</p>
          </div>`}
      </div>
      <form class="composer" id="chatForm">
        <button type="button" class="mic-btn mic-disabled" title="Voice messages are coming in the next update." aria-label="Voice messages are coming in the next update.">🎙</button>
        <input id="chatInput" value="${escapeHtml(ui.chatDraft)}" placeholder="${escapeHtml(chatPlaceholder)}" ${canChat ? "" : "disabled"} />
        <button class="send-btn" type="submit" ${canChat ? "" : "disabled"}>Send</button>
        <div class="status composer-status" id="chatStatus"></div>
      </form>
    </section>`;
}

function renderBrain(state, clone) {
  const pageSize = 5;
  const total = ui.brainResults.length;
  const maxPage = Math.max(0, Math.ceil(total / pageSize) - 1);
  ui.brainPage = Math.max(0, Math.min(ui.brainPage || 0, maxPage));
  const start = ui.brainPage * pageSize;
  const visible = ui.brainResults.slice(start, start + pageSize);
  return `
    <section class="brain-view">
      <div class="panel-title with-action">
        <div>
          <span>Clone Brain</span>
          <h2>Search memories, dates and phrases</h2>
          <p>Search the local memory index created from imported chats and notes.</p>
        </div>
        <button class="secondary-btn compact-back" type="button" data-back-chat>Back to Chat</button>
      </div>
      <form class="brain-search" id="brainForm">
        <input id="brainQuery" value="${escapeHtml(ui.brainQuery)}" placeholder="Search a date, person, phrase, or memory..." />
        <button class="primary-btn" type="submit">Search</button>
      </form>
      <div class="status">${escapeHtml(ui.brainMessage)}</div>
      ${total > pageSize ? `
        <div class="brain-pager">
          <button class="secondary-btn" id="brainPrevPage" type="button" ${ui.brainPage <= 0 ? "disabled" : ""}>Previous</button>
          <span>Showing ${formatNumber(start + 1)}-${formatNumber(Math.min(start + pageSize, total))} of ${formatNumber(total)}</span>
          <button class="secondary-btn" id="brainNextPage" type="button" ${ui.brainPage >= maxPage ? "disabled" : ""}>Next</button>
        </div>
      ` : ""}
      <div class="memory-results">
        ${visible.length ? visible.map((memory, index) => {
          const context = memoryContextState(memory.id);
          const previousDisabled = context.previousDone || context.previous.length >= 10;
          const nextDisabled = context.nextDone || context.next.length >= 10;
          return `
          <article class="memory-card">
            <strong>${escapeHtml(memory.start_dt || "Memory")} ${memory.end_dt ? "&rarr; " + escapeHtml(memory.end_dt) : ""}</strong>
            ${renderMemoryContextList([...context.previous].reverse(), "Previous")}
            <p>${renderMemoryText(memory.text || "")}</p>
            ${renderMemoryContextList(context.next, "Next")}
            <small>${escapeHtml((memory.contacts || []).join(", "))}</small>
            <div class="memory-card-actions">
              <button class="secondary-btn" data-memory-context="previous" data-memory-id="${escapeHtml(memory.id || "")}" type="button" ${previousDisabled ? "disabled" : ""}>Show previous</button>
              <span class="memory-step-count">${formatNumber(context.previous.length)}/10 back</span>
              <button class="secondary-btn" data-memory-context="next" data-memory-id="${escapeHtml(memory.id || "")}" type="button" ${nextDisabled ? "disabled" : ""}>Show next</button>
              <span class="memory-step-count">${formatNumber(context.next.length)}/10 next</span>
            </div>
          </article>
        `}).join("") : `<div class="empty-state">No search results yet. Try a date like 2024-04-04 or a phrase from the chats.</div>`}
      </div>
    </section>`;
}

function scoreBar(label, value) {
  const numeric = Math.max(0, Math.min(1, Number(value || 0)));
  return `
    <div class="map-score">
      <span>${escapeHtml(label)}</span>
      <i><b style="width:${Math.round(numeric * 100)}%"></b></i>
      <strong>${Math.round(numeric * 100)}%</strong>
    </div>`;
}

function renderCloneMap(state, clone) {
  const map = state.cloneMapsById?.[clone.id] || {};
  const quality = map.quality || {};
  const subjectAliases = subjectAliasListForClone(clone);
  const people = (map.people || []).filter((person) => !isSubjectOrNoteName(person?.name, subjectAliases));
  const relationships = (map.relationships || []).filter((rel) => !isSubjectOrNoteName(rel?.contactName, subjectAliases));
  const reactions = (map.reactionPatterns || []).filter((item) => !asList(item?.contacts || "").some((name) => isPrivateNoteName(name)));
  const emotions = (map.emotionalMap || []).filter((item) => !isSubjectOrNoteName(item?.contactName, subjectAliases));
  const evidence = quality.evidence || {};
  const evidenceWarnings = Array.isArray(evidence.warnings) ? evidence.warnings : [];
  if (!Object.keys(map).length) {
    return `
      <section class="map-view">
        <div class="panel-title">
          <span>Clone Map</span>
          <h2>No clone brain map yet</h2>
          <p>Create or rebuild this clone from imported memories to see people, relationships, reactions and emotional patterns.</p>
        </div>
      </section>`;
  }
  return `
    <section class="map-view">
      <div class="panel-title">
        <span>Clone Map</span>
        <h2>${escapeHtml(clone.name)} brain overview</h2>
        <p>A private local map of the clone's learned identity, relationships, behavior and emotional signals.</p>
      </div>

      <div class="map-grid map-stats">
        <article><span>Mode</span><strong>${escapeHtml(map.mode || "standard")}</strong></article>
        <article><span>Messages</span><strong>${formatNumber(quality.total_messages || 0)}</strong></article>
        <article><span>People</span><strong>${formatNumber(people.length)}</strong></article>
        <article><span>Relationships</span><strong>${formatNumber(relationships.length)}</strong></article>
        <article><span>Reactions</span><strong>${formatNumber(quality.reaction_pattern_count || reactions.reduce((sum, item) => sum + Number(item.count || 0), 0))}</strong></article>
        <article><span>Emotions</span><strong>${formatNumber(quality.emotional_episode_count || emotions.length)}</strong></article>
      </div>

      <section class="map-panel evidence-panel">
        <div class="map-row-head"><h3>Evidence Coverage</h3><strong>${escapeHtml(String(evidence.status || "unknown").replace(/^./, (char) => char.toUpperCase()))}</strong></div>
        <p>${formatNumber(evidence.subject_message_count || quality.subject_message_count || 0)} messages from the cloned person, ${formatNumber(evidence.strongest_relationship_message_count || 0)} in the strongest relationship, ${formatNumber(evidence.reaction_count || quality.reaction_pattern_count || 0)} observed reaction pairs.</p>
        ${evidenceWarnings.length ? `<small>${escapeHtml(evidenceWarnings.slice(0, 2).join(" "))}</small>` : `<small>Coverage is based on the local conversations selected for this clone.</small>`}
      </section>

      <div class="map-columns">
        <section class="map-panel">
          <h3>People</h3>
          ${people.length ? people.slice(0, 8).map((person) => `
            <article class="map-person">
              <div><strong>${escapeHtml(person.name)}</strong><span>${formatNumber(person.messageCount || 0)} messages</span></div>
              <small>${escapeHtml((person.contacts || []).slice(0, 4).join(", ") || "No contacts mapped")}</small>
            </article>
          `).join("") : `<p class="map-muted">No people detected yet.</p>`}
        </section>

        <section class="map-panel">
          <h3>Relationships</h3>
          ${relationships.length ? relationships.slice(0, 6).map((rel) => `
            <article class="map-relationship">
              <div class="map-row-head"><strong>${escapeHtml(rel.contactName)}</strong><span>${formatNumber(rel.subjectMessageCount || 0)} / ${formatNumber(rel.partnerMessageCount || 0)}</span></div>
              ${scoreBar("warmth", rel.warmthScore)}
              ${scoreBar("humor", rel.humorScore)}
              ${scoreBar("affection", rel.affectionScore)}
              ${scoreBar("care", rel.careScore)}
              ${rel.topics?.length ? `<small>${escapeHtml(rel.topics.slice(0, 6).join(", "))}</small>` : ""}
            </article>
          `).join("") : `<p class="map-muted">No relationships detected yet.</p>`}
        </section>
      </div>

      <div class="map-columns">
        <section class="map-panel">
          <h3>Reaction Patterns</h3>
          ${reactions.length ? reactions.slice(0, 10).map((item) => `
            <article class="map-reaction">
              <strong>${escapeHtml(item.trigger)}</strong>
              <span>${formatNumber(item.count || 0)} examples · max intensity ${formatNumber(item.maxIntensity || 0)}</span>
              <small>${escapeHtml((item.contacts || []).slice(0, 5).join(", ") || "general")}</small>
            </article>
          `).join("") : `<p class="map-muted">No reaction patterns detected yet.</p>`}
        </section>

        <section class="map-panel">
          <h3>Emotional Map</h3>
          ${emotions.length ? emotions.slice(0, 12).map((item) => `
            <article class="map-emotion">
              <strong>${escapeHtml(item.emotion)}</strong>
              <span>${escapeHtml(item.contactName || "Unknown")} · intensity ${formatNumber(item.intensity || 0)}</span>
              <small>${escapeHtml(item.startDt || "")}</small>
            </article>
          `).join("") : `<p class="map-muted">No emotional episodes detected yet.</p>`}
        </section>
      </div>

    </section>`;
}

function renderSettings(state, clone) {
  const draft = ui.settingsDrafts[clone.id] || {};
  const draftValue = (key, fallback) => Object.prototype.hasOwnProperty.call(draft, key) ? draft[key] : fallback;
  const profileAliases = draftValue("user_aliases", (clone.interlocutorAliases || state.profile?.relationshipAliases || []).join(", "));
  const cloneName = draftValue("name", clone.name || "");
  const subjectName = draftValue("identity_subject_name", clone.identitySubjectName || clone.name || "");
  const subjectAliases = draftValue("subject_aliases", (clone.subjectSenderAliases || []).join(", "));
  const identityMode = draftValue("identity_mode", clone.identityMode || "standard");
  const allowContactMixing = draftValue("allow_contact_mixing", Boolean(clone.allowContactMixing));
  const contextMessages = draftValue("chat_context_messages", clone.chatContextMessages || 10);
  const styleDateFrom = draftValue("style_date_from", clone.styleDateFrom || "");
  const styleDateTo = draftValue("style_date_to", clone.styleDateTo || "");
  const knownPeople = (clone.assignedContacts || []).filter((name) => !isSubjectOrNoteName(name, subjectAliasListForClone(clone)));
  const hasBlueprint = Boolean(clone.refinement?.blueprint);
  return `
    <section class="settings-view">
      <div class="panel-title with-action">
        <div>
          <span>Clone Settings</span>
          <h2>Keep ${escapeHtml(clone.name)} accurate</h2>
          <p>Keep identity details accurate. Personality uses local memories, writing style and the Persona Blueprint.</p>
        </div>
        <button class="secondary-btn compact-back" type="button" data-back-chat>Back to Chat</button>
      </div>
      <form class="settings-grid" id="settingsForm">
        <label>Clone name<input id="settingsName" value="${escapeHtml(cloneName)}" ${clone.archived ? "disabled" : ""} /></label>
        <label>Person being cloned<input id="settingsSubject" value="${escapeHtml(subjectName)}" ${clone.archived ? "disabled" : ""} /></label>
        <label>Sender aliases for this person<input id="settingsSubjectAliases" value="${escapeHtml(subjectAliases)}" ${clone.archived ? "disabled" : ""} /></label>
        <label>How should the clone recognize you?<input id="settingsUserAliases" value="${escapeHtml(profileAliases)}" ${clone.archived ? "disabled" : ""} /></label>
        <label class="settings-readonly">Imported conversations<input id="settingsContacts" value="${escapeHtml(knownPeople.join(", "))}" readonly /><small>Each group remains one conversation. Its participants are listed in Clone Map.</small></label>
        <label class="wide settings-mode-field">Clone realism mode
          <select id="settingsIdentityMode" ${clone.archived ? "disabled" : ""}>
            <option value="standard" ${identityMode === "standard" ? "selected" : ""}>Standard Clone - full fidelity foundation</option>
            <option value="deep" ${identityMode !== "standard" ? "selected" : ""}>Deep Clone - expanded evidence pass</option>
          </select>
        </label>
        <label class="check wide"><input id="settingsMixing" type="checkbox" ${allowContactMixing ? "checked" : ""} ${clone.archived ? "disabled" : ""} /> <span>Allow memory search across all imported chats when needed.</span></label>
        <label>Recent chat context
          <input id="settingsContextMessages" type="number" min="1" max="20" value="${escapeHtml(contextMessages)}" ${clone.archived ? "disabled" : ""} />
          <small>Messages kept in the immediate conversation context. Default: 10.</small>
        </label>
        <div class="settings-period">
          <label>Style from<input id="settingsStyleFrom" type="date" value="${escapeHtml(styleDateFrom)}" ${clone.archived ? "disabled" : ""} /></label>
          <label>Style to<input id="settingsStyleTo" type="date" value="${escapeHtml(styleDateTo)}" ${clone.archived ? "disabled" : ""} /></label>
        </div>
        <div class="settings-blueprint wide">
          <strong>${hasBlueprint ? "Persona Blueprint is current" : "Persona Blueprint has not been built yet"}</strong>
          <span>${hasBlueprint ? "Rebuild after changing identity, memories or realism mode." : "Build it to apply the clone's full identity and style analysis."}</span>
        </div>
        ${clone.archived ? `<div class="settings-blueprint wide"><strong>This clone is archived and read-only.</strong><span>Reactivate it from Clone Chat to change settings, add memories or continue the conversation.</span></div>` : `<div class="settings-actions">
          <button class="primary-btn" type="submit">Save Settings</button>
          <button class="secondary-btn" type="button" id="rebuildBlueprintBtn">${hasBlueprint ? "Rebuild Persona Blueprint" : "Build Persona Blueprint"}</button>
          <button class="plan-btn" type="button" id="addMemoriesBtn">Add More Memories</button>
          <button class="danger-btn" type="button" id="deleteCloneBtn">Archive Clone</button>
        </div>`}
        <div class="status settings-save-status" id="settingsStatus"></div>
      </form>
    </section>`;
}

function renderWizard(state) {
  const wizard = ensureWizard();
  const steps = wizardSteps(wizard);
  const currentIndex = steps.findIndex((step) => step.key === wizard.step);
  const progress = Math.round(((currentIndex + 1) / steps.length) * 100);
  return `
    <section class="wizard-view">
      <div class="wizard-head with-action">
        <div>
          <span>${wizard.mode === "append" ? "Add memories" : "Setup your first clone"}</span>
          <h1>${wizard.mode === "append" ? "Improve Clone Memory" : "Import memories, then start talking."}</h1>
          <p>Choose the exports you want to use. Memory Clone keeps imported files only on this computer and builds a local memory index for the clone.</p>
        </div>
        ${state.clones?.length ? `<button class="secondary-btn compact-back" type="button" data-back-chat>Back to Chat</button>` : ""}
      </div>
      <div class="wizard-card">
        ${wizard.step === "sources" ? renderSourceStep(wizard) : wizard.step === "identity" ? renderIdentityStep(wizard, state) : wizard.step === "review" ? renderReviewStep(wizard, state) : renderUploadStep(wizard)}
      </div>
      <div class="wizard-bottom">
        <div class="progress slim"><span style="width:${progress}%"></span></div>
        <strong>${currentIndex + 1}/${steps.length}</strong>
      </div>
    </section>`;
}

function wizardSteps(wizard) {
  const sourceSteps = wizard.selectedSources.map((source) => ({ key: `upload:${source}`, source }));
  return wizard.mode === "append"
    ? [{ key: "sources" }, ...sourceSteps, { key: "review" }]
    : [{ key: "sources" }, { key: "identity" }, ...sourceSteps, { key: "review" }];
}

function renderSourceStep(wizard) {
  const selected = new Set(wizard.selectedSources);
  return `
    <div class="wizard-section">
      <span class="eyebrow">Step 1</span>
      <h2>Which services do you want to use?</h2>
      <p>Select the exports Memory Clone should learn from first. Upload steps follow the order you select them. You can add more chats later.</p>
      ${renderAlphaNote()}
      <div class="source-grid">
        ${Object.entries(sourceInfo).map(([key, info]) => `
          <button class="source-card ${selected.has(key) ? "selected" : ""}" data-source-toggle="${key}">
            <strong>${info.label}</strong>
            <span>${info.description}</span>
            <em>${selected.has(key) ? "Selected" : "Click to select"}</em>
          </button>
        `).join("")}
        ${futureSources.map((name) => `
          <button class="source-card disabled" disabled>
            <strong>${escapeHtml(name)} <small>v2</small></strong>
          </button>
        `).join("")}
      </div>
      <div class="wizard-actions">
        <span class="status">${escapeHtml(wizard.status || "")}</span>
        <button class="primary-btn" id="wizardNext" type="button">Build Clone Memory</button>
      </div>
    </div>`;
}

function renderIdentityStep(wizard, state) {
  const profileAliases = wizard.userAliases || (state.profile?.relationshipAliases || []).join(", ");
  const candidates = subjectCandidatesForWizard();
  const selectedAliases = asList(wizard.subjectAliases);
  return `
    <div class="wizard-section narrow">
      <span class="eyebrow">Clone identity</span>
      <h2>Who are you cloning?</h2>
      <p>Choose or type every name, nickname and account name that belongs to the person you want to clone. This keeps writing styles from different people separated.</p>
      ${candidates.length ? `
        <div class="sender-picker prominent">
          <strong>Detected people in your uploads</strong>
          <span>You can select more than one name if they are the same person.</span>
          <div>
            ${candidates.map((candidate) => `<button class="${hasListItem(selectedAliases, candidate.name) || wizard.cloneName === candidate.name ? "active" : ""}" data-use-sender="${escapeHtml(candidate.name)}" type="button">${escapeHtml(candidate.name)} <small>${formatNumber(candidate.fileCount)}</small></button>`).join("")}
          </div>
        </div>
      ` : ""}
      <label>Clone display name<input id="wizardCloneName" value="${escapeHtml(wizard.cloneName)}" placeholder="Mom, Dad, Grandma Rose, Alex" /></label>
      <label>How this person is named in chats <small>comma separated</small><input id="wizardSubjectAliases" value="${escapeHtml(wizard.subjectAliases)}" placeholder="Name, Last Name, nickname, contact name" /></label>
      <label>Who are you? How should the clone recognize you? <small>comma separated</small><input id="wizardUserAliases" value="${escapeHtml(profileAliases)}" placeholder="Your name, Mom, Sister, Love, Honey, Baby" /></label>
      <div class="identity-mode-grid">
        <button class="identity-mode-card ${wizard.identityMode === "standard" ? "selected" : ""}" data-identity-mode="standard" type="button">
          <strong>Standard Clone</strong>
          <span>Full fidelity foundation. Builds Style DNA, relationship matching, reaction patterns and Persona Blueprint.</span>
        </button>
        <button class="identity-mode-card ${wizard.identityMode !== "standard" ? "selected" : ""}" data-identity-mode="deep" type="button">
          <strong>Deep Clone</strong>
          <span>Expanded evidence pass for larger archives and more calibration examples. Standard remains complete on its own.</span>
        </button>
      </div>
      <div class="status">${escapeHtml(wizard.status || "")}</div>
      <div class="wizard-actions split">
        <button class="secondary-btn" id="wizardBack" type="button">Previous</button>
        <button class="primary-btn" id="wizardNext" type="button">Next</button>
      </div>
    </div>`;
}

function renderUploadStep(wizard) {
  const source = wizard.step.split(":")[1];
  const info = sourceInfo[source];
  const sourceRows = rowsForSource(source);
  const files = importConversationRows(sourceRows);
  const helpTab = wizard.helpTab || "ios";
  const helpItems = info.helpTabs?.[helpTab] || info.instructions || [];
  const helpTabs = info.helpTabs
    ? { ...info.helpTabs, ...(info.tutorialUrl ? { video: [] } : {}) }
    : (info.tutorialUrl ? { video: [] } : null);
  const detectedSenders = uniqueList(files.flatMap((file) => asList(file.detected_senders || file.detectedSenders || ""))).slice(0, 8);
  const phoneUpload = wizard.phoneUpload?.source === source ? wizard.phoneUpload : null;
  const hasFiles = files.length > 0;
  const statusText = files.length && String(wizard.status || "").toLowerCase().includes("no files selected")
    ? "You can upload more chats."
    : wizard.status || "";
  return `
    <div class="wizard-section">
      <span class="eyebrow">${escapeHtml(info.label)}</span>
      <h2>Upload ${escapeHtml(info.fileLabel)}</h2>
      <p>These files teach the clone how the person wrote, remembered, reacted and talked with different people.</p>
      ${helpTabs ? `
        <div class="help-tabs">
          ${Object.keys(helpTabs).map((key) => `<button class="${helpTab === key ? "active" : ""}" data-help-tab="${key}" type="button">${key === "ios" ? "iOS" : key === "android" ? "Android" : key === "phone" ? "Phone transfer" : key === "video" ? "Video Tutorial" : "Information"}</button>`).join("")}
        </div>
      ` : ""}
      ${helpTab === "video" && info.tutorialUrl ? `
        <div class="tutorial-panel">
          <button class="secondary-btn" data-open="${escapeHtml(info.tutorialUrl)}" type="button">Open video tutorial</button>
        </div>
      ` : `
        <ol class="instructions">
          ${helpItems.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}
        </ol>
      `}
      <button class="secondary-btn upload-wide" data-upload-source="${source}" type="button">${escapeHtml(info.uploadLabel || "Upload Files")}</button>
      <div class="or-divider"><span></span><em>OR</em><span></span></div>
      <div class="phone-upload-card">
        <div>
          <strong>Send directly from your phone</strong>
          <span>Scan the QR code, choose one or more exported files, and they appear here automatically.</span>
          <small>Phone and computer must be on the same Wi-Fi.</small>
        </div>
        ${phoneUpload ? `
          <div class="phone-upload-live">
            <img src="${escapeHtml(phoneUpload.qrUrl)}" alt="Phone upload QR code" />
            <div>
              <span>Open on phone:</span>
              <a href="${escapeHtml(phoneUpload.uploadUrl)}" target="_blank" rel="noreferrer">${escapeHtml(phoneUpload.uploadUrl)}</a>
              <small>Private local link. Expires automatically.</small>
              <button class="secondary-btn" id="stopPhoneUpload" type="button">Close phone upload</button>
            </div>
          </div>
        ` : `<button class="secondary-btn" id="startPhoneUpload" data-phone-source="${source}" type="button">Show QR upload</button>`}
      </div>
      <div class="file-list">
        ${files.length ? `
          <p class="import-summary">Received ${formatNumber(sourceRows.length)} file${sourceRows.length === 1 ? "" : "s"}. Detected ${formatNumber(files.length)} conversation${files.length === 1 ? "" : "s"}.</p>
          <details class="detected-conversations" ${files.length <= 4 ? "open" : ""}>
            <summary>Detected conversations (${formatNumber(files.length)})</summary>
            <div>
              ${files.map((file) => `<span>${escapeHtml(file.original || file.stored)}${file.partCount > 1 ? ` (${file.partCount} export parts)` : ""}</span>`).join("")}
            </div>
          </details>
        ` : ""}
        ${files.length && source !== "notes" ? `
          <p class="contact-hint">For each uploaded chat, enter who the cloned person was talking with. Examples: Name, Last Name, Mom, Love, Honey, Baby. Do not enter the cloned person's own name here.</p>
        ` : ""}
        ${files.length ? files.map((file) => `
          <div class="file-row">
            <div>
              <strong>${escapeHtml(file.original || file.stored)}${file.partCount > 1 ? ` (${file.partCount} export parts)` : ""}</strong>
              <span>${escapeHtml(file.partCount > 1 ? "Combined automatically from the same conversation." : file.stored)}</span>
            </div>
            ${source === "notes" ? `<em>Notes</em>` : `
              <input data-contact-files="${escapeHtml((file.storedFiles || [file.stored]).join("|"))}" value="${escapeHtml(suggestedContactForFile(file))}" placeholder="Contact names: Name, Mom, Love, Honey" />
            `}
            <button class="file-remove" data-remove-imports="${escapeHtml((file.storedFiles || [file.stored]).join("|"))}" title="Remove this conversation" type="button">&times;</button>
          </div>
        `).join("") : `<div class="empty-state">No ${escapeHtml(info.label)} files uploaded yet.</div>`}
      </div>
      ${detectedSenders.length && source !== "notes" ? `
        <div class="sender-picker">
          <strong>Who are you cloning?</strong>
          <span>Pick every sender name that belongs to the cloned person.</span>
          <div>
            ${detectedSenders.map((sender) => `<button class="${hasListItem(wizard.subjectAliases, sender) ? "active" : ""}" data-use-sender="${escapeHtml(sender)}" type="button">${escapeHtml(sender)}</button>`).join("")}
          </div>
        </div>
      ` : ""}
      <p class="help-text">You can add more chats later and improve the clone's memory and personality.</p>
      <div class="status">${escapeHtml(statusText)}</div>
      <div class="wizard-actions split">
        <button class="secondary-btn" id="wizardBack" type="button">Previous</button>
        <div class="wizard-next-group">
          ${!hasFiles ? `<button class="link-btn" id="uploadLater" type="button">Upload later</button>` : ""}
          <button class="primary-btn" id="wizardNext" type="button" ${hasFiles ? "" : "disabled"}>Next</button>
        </div>
      </div>
    </div>`;
}

function renderReviewStep(wizard) {
  const files = importConversationRows(rowsForCurrentImport().filter((file) => wizard.selectedSources.includes(file.source)));
  const candidates = wizard.mode === "create" ? subjectCandidatesForWizard() : [];
  const selectedAliases = asList(wizard.subjectAliases);
  return `
    <div class="wizard-section narrow">
      <span class="eyebrow">Final step</span>
      <h2>${wizard.mode === "append" ? "Install new memories" : "Create clone and install memories"}</h2>
      <p>Memory Clone first builds the local memory index, then creates a Persona Blueprint from selected evidence before opening the clone chat.</p>
      ${candidates.length ? `
        <div class="sender-picker prominent">
          <strong>Person to clone</strong>
          <span>Confirm this before building the clone brain. You can select more than one name if they are the same person.</span>
          <div>
            ${candidates.map((candidate) => `<button class="${hasListItem(selectedAliases, candidate.name) || wizard.cloneName === candidate.name ? "active" : ""}" data-use-sender="${escapeHtml(candidate.name)}" type="button">${escapeHtml(candidate.name)} <small>${formatNumber(candidate.fileCount)}</small></button>`).join("")}
          </div>
        </div>
      ` : ""}
      <div class="review-box">
        <div><span>Selected sources</span><strong>${wizard.selectedSources.map((source) => sourceInfo[source].label).join(", ")}</strong></div>
        <div><span>Uploaded files</span><strong>${files.length}</strong></div>
        <div><span>Persona Blueprint</span><strong>${cloneBuildCreditLabel()}</strong></div>
        ${wizard.mode === "create" ? `<div><span>Clone</span><strong>${escapeHtml(wizard.cloneName || "Not set")}</strong></div>` : ""}
      </div>
      <label class="check policy-check clone-build-consent">
        <input id="cloudAnalysisConsent" type="checkbox" ${wizard.cloudAnalysisConsent ? "checked" : ""} />
        <span>I understand that original export files stay on this computer. To build this clone's Persona Blueprint, Memory Clone will securely send a limited evidence package of selected chat excerpts and local style analysis to its AI build service. The build is charged only after a successful response.</span>
      </label>
      <div class="import-progress" id="importProgress">
        ${renderImportProgress(currentState?.importStatus)}
      </div>
      <div class="status">${escapeHtml(wizard.status || "")}</div>
      <div class="wizard-actions split">
        <button class="secondary-btn" id="wizardBack" type="button" ${wizard.isBuilding ? "disabled" : ""}>Previous</button>
        <button class="primary-btn" id="wizardBuild" type="button" ${(!wizard.cloudAnalysisConsent || wizard.isBuilding) ? "disabled" : ""}>${wizard.isBuilding ? "Building Persona Blueprint..." : wizard.mode === "append" ? "Install Memories" : "Create Clone"}</button>
      </div>
    </div>`;
}

function renderImportProgress(status = {}) {
  const progress = Number(status?.progress || 0);
  const state = String(status?.state || "idle");
  const stage = status?.stage || (state === "idle" ? "Waiting for files" : state);
  return `
    <div class="progress slim"><span style="width:${Math.max(0, Math.min(100, progress))}%"></span></div>
    <div class="progress-meta">
      <strong>${escapeHtml(stage)}</strong>
      <span>${formatNumber(progress)}%</span>
    </div>
    ${status?.error ? `<p class="error-text">${escapeHtml(friendlyStatusError(status.error, "The import could not finish. Please try again."))}</p>` : ""}
  `;
}

function bindAppEvents() {
  document.getElementById("noticeClose")?.addEventListener("click", () => {
    closeProfileMenu();
    ui.notice = null;
    renderApp(currentState);
  });
  document.getElementById("noticeDismiss")?.addEventListener("click", () => {
    closeProfileMenu();
    ui.notice = null;
    renderApp(currentState);
  });
  document.getElementById("noticeAction")?.addEventListener("click", async () => {
    if (ui.notice?.actionType === "logout") {
      closeActivePhoneUpload();
      routeState(await apiCall("logout"));
    } else if (ui.notice?.actionType === "update") {
      await handleUpdateNow();
    }
  });
  document.getElementById("confirmCancel")?.addEventListener("click", () => clearConfirm(false));
  document.getElementById("confirmAccept")?.addEventListener("click", () => clearConfirm(true));
  document.querySelectorAll("#logoutBtn, [data-action='logout']").forEach((button) => {
    button.addEventListener("click", async () => {
      closeActivePhoneUpload();
      routeState(await apiCall("logout"));
    });
  });
  document.getElementById("newCloneBtn")?.addEventListener("click", () => {
    if (isOffline()) {
      closeProfileMenu();
      showNotice("No Connection", serverActionBlockedMessage());
      return;
    }
    if (!hasAvailableCloneSlot(currentState)) {
      closeProfileMenu();
      showNotice(
        "No Clone Spots available",
        "Your current plan has no unused clone spots. Buy another clone spot if you want to clone another person.",
        "View Plans",
        currentState.checkoutUrl || "https://memoryclone.com/pricing.php",
      );
      return;
    }
    closeProfileMenu();
    closeActivePhoneUpload();
    const oldScopeId = ui.wizard?.mode === "create" ? ui.wizard.scopeId : "";
    ui.wizard = null;
    ensureWizard("create");
    if (oldScopeId) apiCall("reset_import_draft", oldScopeId).then((result) => {
      currentState = result.state || currentState;
      renderApp(currentState);
    }).catch(() => {});
    renderApp(currentState);
  });
  document.getElementById("newThreadBtn")?.addEventListener("click", (event) => {
    closeProfileMenu();
    handleNewThread(event);
  });
  document.getElementById("profileToggle")?.addEventListener("click", () => {
    ui.profileOpen = !ui.profileOpen;
    renderApp(currentState);
  });
  document.getElementById("updateIconBtn")?.addEventListener("click", () => {
    closeProfileMenu();
    handleUpdateNow();
  });
  document.querySelectorAll("[data-clone-id]").forEach((button) => {
    button.addEventListener("click", () => {
      closeProfileMenu();
      ui.activeCloneId = button.dataset.cloneId;
      ui.activeThreadId = "";
      ui.activeTab = "chat";
      closeActivePhoneUpload();
      ui.wizard = null;
      renderApp(currentState);
    });
  });
  document.querySelectorAll("[data-thread-id]").forEach((button) => {
    button.addEventListener("click", () => {
      closeProfileMenu();
      ui.activeThreadId = button.dataset.threadId;
      ui.activeTab = "chat";
      closeActivePhoneUpload();
      ui.wizard = null;
      renderApp(currentState);
    });
  });
  document.querySelectorAll("[data-delete-thread]").forEach((button) => {
    button.addEventListener("click", () => {
      closeProfileMenu();
      handleDeleteThread(button.dataset.deleteThread);
    });
  });
  document.querySelectorAll("[data-tab]").forEach((button) => {
    button.addEventListener("click", () => {
      closeProfileMenu();
      ui.activeTab = button.dataset.tab;
      closeActivePhoneUpload();
      ui.wizard = null;
      renderApp(currentState);
    });
  });
  document.querySelectorAll("[data-source-toggle]").forEach((button) => {
    button.addEventListener("click", () => {
      closeProfileMenu();
      toggleSource(button.dataset.sourceToggle);
    });
  });
  document.querySelectorAll("[data-upload-source]").forEach((button) => {
    button.addEventListener("click", () => {
      closeProfileMenu();
      handleSelectImportFiles(button.dataset.uploadSource);
    });
  });
  document.getElementById("startPhoneUpload")?.addEventListener("click", (event) => {
    closeProfileMenu();
    handleStartPhoneUpload(event.currentTarget.dataset.phoneSource || "whatsapp");
  });
  document.getElementById("stopPhoneUpload")?.addEventListener("click", () => {
    closeProfileMenu();
    handleStopPhoneUpload();
  });
  document.querySelectorAll("[data-remove-imports], [data-remove-import]").forEach((button) => {
    button.addEventListener("click", () => {
      closeProfileMenu();
      handleRemoveImportFiles(String(button.dataset.removeImports || button.dataset.removeImport || "").split("|").filter(Boolean));
    });
  });
  document.querySelectorAll("[data-help-tab]").forEach((button) => {
    button.addEventListener("click", () => {
      const wizard = ensureWizard();
      saveIdentityFields();
      saveContactFields();
      wizard.helpTab = button.dataset.helpTab || "ios";
      renderApp(currentState);
    });
  });
  document.querySelectorAll("[data-use-sender]").forEach((button) => {
    button.addEventListener("click", () => {
      saveIdentityFields();
      saveContactFields();
      toggleSelectedSender(button.dataset.useSender || "");
      renderApp(currentState);
    });
  });
  document.querySelectorAll("[data-identity-mode]").forEach((button) => {
    button.addEventListener("click", () => {
      const wizard = ensureWizard();
      saveIdentityFields();
      saveContactFields();
      wizard.identityMode = button.dataset.identityMode === "standard" ? "standard" : "deep";
      renderApp(currentState);
    });
  });
  document.querySelectorAll("[data-contact-files], [data-contact-file]").forEach((input) => {
    input.addEventListener("input", () => {
      saveContactValue(input.dataset.contactFiles || input.dataset.contactFile || "", input.value);
    });
  });
  document.getElementById("wizardNext")?.addEventListener("click", () => { closeProfileMenu(); wizardNext(); });
  document.getElementById("uploadLater")?.addEventListener("click", () => { closeProfileMenu(); wizardNext({ allowEmptyUpload: true }); });
  document.getElementById("wizardBack")?.addEventListener("click", () => { closeProfileMenu(); wizardBack(); });
  document.getElementById("wizardBuild")?.addEventListener("click", () => { closeProfileMenu(); closeActivePhoneUpload(); handleBuildWizard(); });
  document.getElementById("cloudAnalysisConsent")?.addEventListener("change", (event) => {
    const wizard = ensureWizard();
    wizard.cloudAnalysisConsent = Boolean(event.target.checked);
    const buildButton = document.getElementById("wizardBuild");
    if (buildButton) buildButton.disabled = !wizard.cloudAnalysisConsent || Boolean(wizard.isBuilding);
  });
  document.getElementById("chatForm")?.addEventListener("submit", handleSendMessage);
  document.getElementById("toggleHistory")?.addEventListener("click", () => {
    ui.showAllMessages = !ui.showAllMessages;
    renderApp(currentState);
  });
  document.getElementById("chatInput")?.addEventListener("input", (event) => {
    ui.chatDraft = event.target.value;
  });
  document.getElementById("brainForm")?.addEventListener("submit", handleBrainSearch);
  document.getElementById("brainPrevPage")?.addEventListener("click", () => {
    ui.brainPage = Math.max(0, ui.brainPage - 1);
    renderApp(currentState);
  });
  document.getElementById("brainNextPage")?.addEventListener("click", () => {
    ui.brainPage += 1;
    renderApp(currentState);
  });
  document.querySelectorAll("[data-memory-context]").forEach((button) => {
    button.addEventListener("click", () => handleMemoryContext(button.dataset.memoryId, button.dataset.memoryContext));
  });
  document.querySelectorAll("[data-back-chat]").forEach((button) => {
    button.addEventListener("click", () => {
      closeProfileMenu();
      closeActivePhoneUpload();
      ui.wizard = null;
      ui.activeTab = "chat";
      renderApp(currentState);
    });
  });
  document.getElementById("settingsForm")?.addEventListener("submit", handleSettingsSave);
  document.querySelectorAll("#settingsForm input:not([readonly]), #settingsForm select").forEach((control) => {
    const eventName = control.matches('select, input[type="checkbox"], input[type="date"], input[type="number"]') ? "change" : "input";
    control.addEventListener(eventName, scheduleSettingsAutosave);
    control.addEventListener("blur", () => {
      captureSettingsDraft();
      window.clearTimeout(settingsAutosaveTimer);
      queueSettingsAutosave();
    });
  });
  document.getElementById("deleteCloneBtn")?.addEventListener("click", () => { closeProfileMenu(); handleDeleteClone(); });
  document.getElementById("reactivateCloneBtn")?.addEventListener("click", () => { closeProfileMenu(); handleReactivateClone(); });
  document.getElementById("addMemoriesBtn")?.addEventListener("click", () => {
    closeProfileMenu();
    closeActivePhoneUpload();
    ui.wizard = null;
    ensureWizard("append", ui.activeCloneId);
    renderApp(currentState);
  });
  document.getElementById("rebuildBlueprintBtn")?.addEventListener("click", () => { closeProfileMenu(); handleRebuildPersonaBlueprint(); });
}

function toggleSource(source) {
  const wizard = ensureWizard();
  closeActivePhoneUpload();
  const selected = new Set(wizard.selectedSources);
  selected.has(source) ? selected.delete(source) : selected.add(source);
  wizard.selectedSources = Array.from(selected);
  wizard.status = "";
  renderApp(currentState);
}

function saveIdentityFields() {
  const wizard = ensureWizard();
  const cloneName = document.getElementById("wizardCloneName");
  const subject = document.getElementById("wizardSubjectAliases");
  const user = document.getElementById("wizardUserAliases");
  const cloudConsent = document.getElementById("cloudAnalysisConsent");
  if (cloneName) wizard.cloneName = cloneName.value;
  if (subject) wizard.subjectAliases = subject.value;
  if (user) wizard.userAliases = user.value;
  if (cloudConsent) wizard.cloudAnalysisConsent = Boolean(cloudConsent.checked);
}

function saveContactFields() {
  document.querySelectorAll("[data-contact-files], [data-contact-file]").forEach((input) => {
    saveContactValue(input.dataset.contactFiles || input.dataset.contactFile || "", input.value);
  });
}

function saveContactValue(storedFiles, value) {
  const wizard = ensureWizard();
  String(storedFiles || "").split("|").filter(Boolean).forEach((stored) => {
    wizard.fileContacts[stored] = value;
  });
}

function toggleSelectedSender(sender) {
  const wizard = ensureWizard();
  const clean = String(sender || "").trim();
  if (!clean) return;
  const aliases = asList(wizard.subjectAliases);
  const exists = aliases.some((item) => item.toLowerCase() === clean.toLowerCase());
  wizard.subjectAliases = uniqueList(exists ? aliases.filter((item) => item.toLowerCase() !== clean.toLowerCase()) : [...aliases, clean]).join(", ");
  if (!wizard.cloneName.trim()) wizard.cloneName = clean;
}

function wizardNext(options = {}) {
  const wizard = ensureWizard();
  saveIdentityFields();
  saveContactFields();
  closeActivePhoneUpload();
  if (wizard.step === "sources") {
    if (!wizard.selectedSources.length) {
      wizard.status = "Choose at least one source first.";
      renderApp(currentState);
      return;
    }
    wizard.step = wizard.mode === "append" ? `upload:${wizard.selectedSources[0]}` : "identity";
  } else if (wizard.step === "identity") {
    if (!wizard.cloneName.trim()) {
      wizard.status = "Please enter the clone display name.";
      renderApp(currentState);
      return;
    }
    if (!asList(wizard.subjectAliases).length) wizard.subjectAliases = wizard.cloneName;
    wizard.step = `upload:${wizard.selectedSources[0]}`;
  } else if (wizard.step.startsWith("upload:")) {
    const source = wizard.step.split(":")[1];
    const files = rowsForSource(source);
    if (!files.length && !options.allowEmptyUpload) {
      wizard.status = `Please upload at least one ${sourceInfo[source].label} file or go back and unselect this source.`;
      renderApp(currentState);
      return;
    }
    const index = wizard.selectedSources.indexOf(source);
    wizard.step = index < wizard.selectedSources.length - 1 ? `upload:${wizard.selectedSources[index + 1]}` : "review";
  }
  wizard.status = "";
  renderApp(currentState);
}

function wizardBack() {
  const wizard = ensureWizard();
  saveIdentityFields();
  saveContactFields();
  closeActivePhoneUpload();
  if (wizard.step === "identity") {
    wizard.step = "sources";
  } else if (wizard.step.startsWith("upload:")) {
    const source = wizard.step.split(":")[1];
    const index = wizard.selectedSources.indexOf(source);
    if (index > 0) wizard.step = `upload:${wizard.selectedSources[index - 1]}`;
    else wizard.step = wizard.mode === "append" ? "sources" : "identity";
  } else if (wizard.step === "review") {
    wizard.step = `upload:${wizard.selectedSources[wizard.selectedSources.length - 1]}`;
  }
  wizard.status = "";
  renderApp(currentState);
}

async function handleSelectImportFiles(source) {
  const wizard = ensureWizard();
  wizard.status = "Choose files in the system dialog...";
  renderApp(currentState);
  try {
    const scope = currentImportScope();
    const result = await apiCall("select_import_files", source, scope.scopeId, scope.cloneId);
    currentState = result.state || result;
    const importedFiles = result.files || [];
    for (const file of importedFiles) {
      if (file?.stored && !wizard.fileContacts[file.stored]) wizard.fileContacts[file.stored] = "";
    }
    const detectedSources = [...new Set(
      [
        ...(result.detectedSources || []),
        ...importedFiles.map((file) => file?.source),
      ].filter((item) => ["whatsapp", "messenger", "notes"].includes(item))
    )];
    for (const detectedSource of detectedSources) {
      if (!wizard.selectedSources.includes(detectedSource)) {
        wizard.selectedSources.push(detectedSource);
      }
    }
    if (detectedSources.length === 1 && detectedSources[0] !== source) {
      wizard.step = `upload:${detectedSources[0]}`;
    }
    wizard.status = result.message || "Files added.";
    renderApp(currentState);
  } catch (error) {
    const hasFiles = rowsForSource(source).length > 0;
    const message = friendlyStatusError(error.message, "Could not add files.");
    wizard.status = hasFiles && message.toLowerCase().includes("no files selected")
      ? "You can upload more chats."
      : message;
    renderApp(currentState);
  }
}

async function handleStartPhoneUpload(source) {
  const wizard = ensureWizard();
  wizard.status = "Creating private phone upload link...";
  renderApp(currentState);
  try {
    const scope = currentImportScope();
    const result = await apiCall("start_phone_upload", source, scope.scopeId, scope.cloneId);
    wizard.phoneUpload = { source, ...result };
    wizard.status = "Scan the QR code with your phone and upload the exported file.";
    renderApp(currentState);
  } catch (error) {
    wizard.status = friendlyStatusError(error.message, "Could not start phone upload.");
    renderApp(currentState);
  }
}

async function handleStopPhoneUpload() {
  const wizard = ensureWizard();
  const token = wizard.phoneUpload?.token || "";
  wizard.phoneUpload = null;
  wizard.status = "Phone upload closed.";
  renderApp(currentState);
  if (!token) return;
  try {
    const result = await apiCall("stop_phone_upload", token);
    currentState = result.state || currentState;
  } catch {
    // Closing the local UI is enough; expired server-side links are ignored.
  }
  renderApp(currentState);
}

async function handleRemoveImportFile(stored) {
  return handleRemoveImportFiles([stored]);
}

async function handleRemoveImportFiles(storedFiles) {
  const wizard = ensureWizard();
  wizard.status = "Removing file...";
  try {
    let result = null;
    for (const stored of storedFiles) {
      result = await apiCall("remove_import_file", stored);
      delete wizard.fileContacts[stored];
    }
    currentState = result?.state || result || currentState;
    wizard.status = result?.message || "Conversation removed.";
  } catch (error) {
    wizard.status = friendlyStatusError(error.message, "Could not remove this file.");
  }
  renderApp(currentState);
}

async function handleNewThread() {
  if (!ui.activeCloneId) return;
  try {
    const result = await apiCall("create_chat_thread", ui.activeCloneId, "New chat");
    currentState = result.state || result;
    ui.activeThreadId = result.threadId || "";
    ui.activeTab = "chat";
    renderApp(currentState);
  } catch (error) {
    showNotice("Could not start a new chat", friendlyApiError(error, "Please try again."));
  }
}

async function handleDeleteThread(threadId) {
  const confirmed = await askConfirm({
    title: "Delete this chat?",
    message: "Memory Clone will create a local backup first. This chat thread will disappear from the app.",
    confirmLabel: "Delete chat",
    danger: true,
  });
  if (!confirmed) return;
  try {
    const result = await apiCall("delete_chat_thread", threadId, true);
    currentState = result.state || result;
    ui.activeThreadId = "";
    renderApp(currentState);
  } catch (error) {
    showNotice("Could not delete chat", friendlyApiError(error, "Please try again."));
  }
}

async function handleDeleteClone() {
  if (isOffline()) {
    showNotice("No Connection", serverActionBlockedMessage());
    return;
  }
  const clone = (currentState?.clones || []).find((item) => item.id === ui.activeCloneId);
  if (!clone) return;
  const confirmed = await askConfirm({
    title: `Archive ${clone.name}?`,
    message: "This releases the active clone slot but keeps every local chat, memory and Clone Map available here in read-only mode. You can reactivate it later when a clone slot is free.",
    confirmLabel: "Archive clone",
    danger: true,
  });
  if (!confirmed) return;
  try {
    const result = await apiCall("delete_clone", clone.id, true);
    currentState = result.state || result;
    ui.activeCloneId = "";
    ui.activeThreadId = "";
    ui.activeTab = "chat";
    ui.wizard = null;
    showNotice("Clone archived", result.message || "This clone is now read-only and its active clone slot is available again.");
  } catch (error) {
    if (handleAuthError(error, "Could not archive clone")) return;
    showNotice("Could not archive clone", friendlyApiError(error, "Please try again."));
  }
}

async function handleReactivateClone() {
  if (isOffline()) {
    showNotice("No Connection", serverActionBlockedMessage());
    return;
  }
  const clone = (currentState?.clones || []).find((item) => item.id === ui.activeCloneId && item.archived);
  if (!clone) return;
  try {
    const result = await apiCall("reactivate_clone", clone.id);
    currentState = result.state || result;
    ui.activeTab = "chat";
    showNotice("Clone reactivated", result.message || "Your previous chats and memories are ready to use.");
  } catch (error) {
    if (handleAuthError(error, "Could not reactivate clone")) return;
    showNotice("Could not reactivate clone", friendlyApiError(error, "Buy another clone slot or archive an active clone first."));
  }
}

async function handleBuildWizard() {
  if (isOffline()) {
    const wizard = ensureWizard();
    wizard.status = serverActionBlockedMessage();
    renderApp(currentState);
    return;
  }
  const wizard = ensureWizard();
  if (wizard.isBuilding) return;
  saveIdentityFields();
  saveContactFields();
  const selectedFiles = rowsForCurrentImport().filter((file) => wizard.selectedSources.includes(file.source));
  if (!selectedFiles.length) {
    wizard.status = "Please upload at least one file before building clone memory.";
    renderApp(currentState);
    return;
  }
  if (wizard.mode === "create" && !wizard.cloneName.trim()) {
    wizard.status = "Please enter the clone display name.";
    renderApp(currentState);
    return;
  }
  if (!wizard.cloudAnalysisConsent) {
    wizard.status = "Confirm secure AI analysis before building the Persona Blueprint.";
    renderApp(currentState);
    return;
  }

  wizard.isBuilding = true;
  wizard.status = "Creating local clone profile...";
  renderApp(currentState);
  try {
    let state = currentState;
    if (wizard.mode === "create") {
      state = await apiCall("create_clone_from_wizard", {
        clone_name: wizard.cloneName,
        scope_id: wizard.scopeId,
        subject_aliases: asList(wizard.subjectAliases || wizard.cloneName),
        user_aliases: asList(wizard.userAliases),
        identity_mode: wizard.identityMode === "standard" ? "standard" : "deep",
        file_contacts: wizard.fileContacts,
      });
      currentState = state;
      const clone = (state.clones || [])[state.clones.length - 1];
      if (clone?.id) ui.activeCloneId = clone.id;
    } else {
      state = await apiCall("save_import_contacts", ui.activeCloneId, {
        clone_id: ui.activeCloneId,
        file_contacts: wizard.fileContacts,
      });
      currentState = state;
    }
    wizard.status = "Installing memories locally...";
    renderApp(currentState);
    await apiCall("start_import", wizard.selectedSources, "", ui.activeCloneId);
    await watchImportProgress();
  } catch (error) {
    wizard.isBuilding = false;
    if (handleAuthError(error, "Could not create clone")) return;
    wizard.status = friendlyStatusError(error.message, "Could not build clone memory.");
    renderApp(currentState);
  }
}

async function watchImportProgress() {
  const wizard = ensureWizard();
  for (;;) {
    await new Promise((resolve) => setTimeout(resolve, 900));
    const state = await apiCall("app_state");
    currentState = state;
    const status = state.importStatus || {};
    const progressBox = document.getElementById("importProgress");
    if (progressBox) progressBox.innerHTML = renderImportProgress(status);
    if (status.state === "done") {
        wizard.status = "Building the Persona Blueprint from selected evidence. Large imports can take several minutes.";
      renderApp(state);
      const refinement = await apiCall("refine_clone_brain", ui.activeCloneId, true);
      currentState = refinement.state || currentState;
      wizard.status = "Your clone has been created. Opening chat...";
      wizard.isBuilding = false;
      ui.wizard = null;
      ui.activeTab = "chat";
      routeState(currentState);
      return;
    }
    if (status.state === "error") {
      wizard.isBuilding = false;
      wizard.status = friendlyStatusError(status.error, "Memory import failed.");
      renderApp(state);
      return;
    }
  }
}

async function handleSendMessage(event) {
  event.preventDefault();
  if (isOffline()) {
    showNotice("No Connection", serverActionBlockedMessage());
    return;
  }
  const cloneId = ui.activeCloneId;
  const input = document.getElementById("chatInput");
  const status = document.getElementById("chatStatus");
  const text = input.value.trim();
  if (!text) return;
  const rawDraft = input.value;
  ui.chatDraft = "";
  ui.pendingThreadId = ui.activeThreadId;
  ui.pendingMessages = [
    { role: "user", content: text },
    { role: "assistant", content: "", typing: true },
  ];
  renderApp(currentState);
  try {
    const result = await apiCall("send_message", cloneId, text, ui.activeThreadId);
    ui.pendingThreadId = "";
    ui.pendingMessages = [];
    routeState(result);
  } catch (error) {
    ui.pendingThreadId = "";
    ui.pendingMessages = [];
    ui.chatDraft = rawDraft;
    const message = error.message || "Message failed.";
    if (handleAuthError(error, "Message not sent")) return;
    renderApp(currentState);
    showNotice("Message not sent", friendlyApiError(error, "Your message could not be sent. Please try again."));
  } finally {
    const nextInput = document.getElementById("chatInput");
    if (nextInput) nextInput.focus();
  }
}

async function handleBrainSearch(event) {
  event.preventDefault();
  const input = document.getElementById("brainQuery");
  ui.brainQuery = input.value.trim();
  ui.brainMessage = "Searching local clone memory...";
  // Never leave a previous query visible while the local index is being searched.
  ui.brainResults = [];
  ui.brainPage = 0;
  ui.brainContexts = {};
  renderApp(currentState);
  try {
    const result = await apiCall("search_memories", ui.activeCloneId, ui.brainQuery);
    ui.brainResults = result.memories || [];
    const total = Number(result.total || ui.brainResults.length || 0);
    ui.brainMessage = total ? `${formatNumber(total)} memory result(s). Showing 5 at a time.` : "No matching memories found.";
    renderApp(currentState);
  } catch (error) {
    ui.brainResults = [];
    ui.brainPage = 0;
    ui.brainContexts = {};
    ui.brainMessage = friendlyStatusError(error.message, "Memory search failed.");
    renderApp(currentState);
  }
}

async function handleMemoryContext(memoryId, direction) {
  if (!memoryId || !direction) return;
  const brainView = document.querySelector(".brain-view");
  const scrollTop = brainView ? brainView.scrollTop : 0;
  const restoreBrainScroll = () => window.requestAnimationFrame(() => {
    const nextBrain = document.querySelector(".brain-view");
    if (nextBrain) nextBrain.scrollTop = scrollTop;
  });
  const context = memoryContextState(memoryId);
  if ((direction === "previous" && (context.previousDone || context.previous.length >= 10))
    || (direction === "next" && (context.nextDone || context.next.length >= 10))) {
    return;
  }
  const cursorKey = direction === "previous" ? "previousCursor" : "nextCursor";
  const cursorId = context[cursorKey] || memoryId;
  ui.brainMessage = "Loading nearby messages...";
  renderApp(currentState);
  restoreBrainScroll();
  try {
    const result = await apiCall("memory_context", ui.activeCloneId, cursorId, direction, ui.brainQuery);
    const nextContext = memoryContextState(memoryId);
    const memory = result.memory || null;
    if (memory) {
      nextContext[direction] = [...nextContext[direction], memory].slice(0, 10);
      nextContext[cursorKey] = memory.id || cursorId;
    }
    if (result.exhausted || nextContext[direction].length >= 10) {
      nextContext[`${direction}Done`] = true;
    }
    ui.brainContexts[memoryId] = nextContext;
    ui.brainMessage = "Memory context loaded.";
  } catch (error) {
    if (error.payload?.exhausted) {
      const nextContext = memoryContextState(memoryId);
      nextContext[`${direction}Done`] = true;
      ui.brainContexts[memoryId] = nextContext;
    }
    ui.brainMessage = friendlyStatusError(error.message, "Could not load nearby messages.");
  }
  renderApp(currentState);
  restoreBrainScroll();
}

function settingsPayload(autosave = false) {
  return {
    name: document.getElementById("settingsName")?.value || "",
    identity_subject_name: document.getElementById("settingsSubject")?.value || "",
    subject_aliases: document.getElementById("settingsSubjectAliases")?.value || "",
    user_aliases: document.getElementById("settingsUserAliases")?.value || "",
    identity_mode: document.getElementById("settingsIdentityMode")?.value || "standard",
    allow_contact_mixing: Boolean(document.getElementById("settingsMixing")?.checked),
    chat_context_messages: Number(document.getElementById("settingsContextMessages")?.value || 10),
    style_date_from: document.getElementById("settingsStyleFrom")?.value || "",
    style_date_to: document.getElementById("settingsStyleTo")?.value || "",
    autosave,
  };
}

function captureSettingsDraft() {
  if (!ui.activeCloneId || !document.getElementById("settingsForm")) return;
  const payload = settingsPayload();
  delete payload.autosave;
  ui.settingsDrafts[ui.activeCloneId] = payload;
}

async function persistSettings({ autosave = false, cloneId = ui.activeCloneId, payload: suppliedPayload = null } = {}) {
  if (!cloneId) return;
  if (!suppliedPayload) captureSettingsDraft();
  const payload = { ...(suppliedPayload || ui.settingsDrafts[cloneId] || settingsPayload()), autosave };
  const status = document.getElementById("settingsStatus");
  if (status) status.textContent = autosave ? "Saving..." : "Saving clone settings...";
  try {
    const result = await apiCall("save_clone_settings", cloneId, payload);
    if (autosave) {
      const clone = currentState?.clones?.find((item) => item.id === cloneId);
      if (clone && result.clone) Object.assign(clone, result.clone);
      if (status) status.textContent = "Saved";
      return;
    }
    delete ui.settingsDrafts[cloneId];
    routeState(result);
    const nextStatus = document.getElementById("settingsStatus");
    if (nextStatus) nextStatus.textContent = "Settings saved.";
  } catch (error) {
    if (status) status.textContent = "";
    if (!handleAuthError(error, "Settings not saved")) {
      showNotice("Settings not saved", friendlyApiError(error, "Please try again."));
    }
  }
}

function queueSettingsAutosave() {
  const cloneId = ui.activeCloneId;
  if (!cloneId) return Promise.resolve();
  captureSettingsDraft();
  const payload = { ...(ui.settingsDrafts[cloneId] || settingsPayload()) };
  // Save snapshots in order so a slower earlier request cannot overwrite a newer edit.
  settingsAutosaveQueue = settingsAutosaveQueue
    .catch(() => undefined)
    .then(() => persistSettings({ autosave: true, cloneId, payload }));
  return settingsAutosaveQueue;
}

function scheduleSettingsAutosave() {
  captureSettingsDraft();
  window.clearTimeout(settingsAutosaveTimer);
  settingsAutosaveTimer = window.setTimeout(() => queueSettingsAutosave(), 650);
}

async function handleSettingsSave(event) {
  event.preventDefault();
  window.clearTimeout(settingsAutosaveTimer);
  await settingsAutosaveQueue.catch(() => undefined);
  await persistSettings({ autosave: false });
}

async function handleRebuildPersonaBlueprint() {
  if (isOffline()) {
    showNotice("No connection", serverActionBlockedMessage());
    return;
  }
  const accepted = await askConfirm({
    title: "Build Persona Blueprint",
    message: "Original export files remain on this computer. Memory Clone will send a limited evidence package of selected excerpts and local style analysis to its secure AI build service. Credits are charged only after a successful build.",
    confirmLabel: "Build Blueprint",
  });
  if (!accepted) return;
  const status = document.getElementById("settingsStatus");
    if (status) status.textContent = "Building Persona Blueprint. Large imports can take several minutes...";
  try {
    const result = await apiCall("refine_clone_brain", ui.activeCloneId, true);
    routeState(result.state || currentState);
    showNotice("Persona Blueprint ready", result.message || "Your clone now uses the refreshed evidence-based Persona Blueprint.");
  } catch (error) {
    if (!handleAuthError(error, "Persona Blueprint not built")) {
      showNotice("Persona Blueprint not built", friendlyApiError(error, "Please try again."));
    }
  }
}

function bindExternalLinks() {
  document.querySelectorAll("[data-open]").forEach((el) => {
    el.addEventListener("click", async (event) => {
      event.preventDefault();
      const url = el.getAttribute("data-open");
      try {
        await apiCall("open_url", url);
      } catch (error) {
        console.error(error);
      }
    });
  });
}

function bindUpdateEvents() {
  document.getElementById("updateNowBtn")?.addEventListener("click", handleUpdateNow);
}

function routeState(state) {
  currentState = state;
  if (ui.update?.required) {
    app.innerHTML = renderUpdateRequired(ui.update);
    bindExternalLinks();
    bindUpdateEvents();
    return;
  }
  if (!state.authenticated) {
    ui.profileOpen = false;
    stopPhoneUploadPolling();
    renderLogin(state);
    return;
  }
  if (ui.notice?.actionType === "logout") ui.notice = null;
  if (!state.profile?.onboardingCompleted) {
    ui.profileOpen = false;
    stopPhoneUploadPolling();
    renderSetup(state);
    return;
  }
  renderApp(state);
}

async function boot() {
  ui.networkOnline = navigator.onLine !== false;
  window.addEventListener("online", () => {
    ui.networkOnline = true;
    if (currentState) renderApp(currentState);
    syncEntitlementIfNeeded(true);
  });
  window.addEventListener("offline", () => {
    ui.networkOnline = false;
    if (currentState) renderApp(currentState);
  });
  window.addEventListener("focus", () => syncEntitlementIfNeeded(true));
  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "visible") syncEntitlementIfNeeded(true);
  });
  startEntitlementAutoSync();
  renderLoading("Preparing your private clone workspace...", 25);
  try {
    await fetch("/api/health", { cache: "no-store" });
    renderLoading("Checking Memory Clone version...", 48);
    const updateResult = await apiCall("check_update");
    ui.update = updateResult.update || null;
    if (ui.update?.required) {
      routeState({ authenticated: false, message: "Update required." });
      return;
    }
    if (ui.update?.available) {
      ui.notice = {
        title: `Memory Clone ${ui.update.latestVersion || ""} is available`.trim(),
        message: ui.update.releaseNotes || "A new Memory Clone update is ready.",
        actionLabel: "Update Now",
        actionType: "update",
      };
    }
    renderLoading("Connecting Memory Clone securely...", 72);
    const state = await apiCall("app_state");
    renderLoading("Ready.", 100);
    setTimeout(() => routeState(state), 150);
  } catch (error) {
    renderLogin({ message: error.message || "Memory Clone could not start." });
  }
}

document.addEventListener("DOMContentLoaded", boot);
