// store.jsx — a tiny reactive layer over window.DATA + high-level actions that
// call the API and keep DATA in sync, then notify subscribers to re-render.
const _listeners = new Set();

window.Store = {
  emit() { _listeners.forEach((fn) => { try { fn(); } catch (e) { console.error(e); } }); },
  subscribe(fn) { _listeners.add(fn); return () => _listeners.delete(fn); },

  // recompute the nav badge (pending approvals) from conversations
  recountApprovals() {
    const n = (DATA.CONVERSATIONS || []).filter((c) => c.needsApproval).length;
    DATA.meta.pendingApprovals = n;
    return n;
  },
};

// Re-render hook: components that mutate shared data call useStore() and read DATA.
function useStore() {
  const [, force] = React.useReducer((x) => x + 1, 0);
  React.useEffect(() => Store.subscribe(force), []);
  return DATA;
}
window.useStore = useStore;

// ---- Actions ----------------------------------------------------------
// Each returns a promise; on success it patches DATA in place and emits.
window.Actions = {
  async refreshBootstrap() {
    const data = await API.bootstrap();
    Object.assign(DATA, data);
    window.LINK_TYPES = DATA.LINK_TYPES;
    Store.emit();
    return data;
  },

  // Lightweight inbox refresh for live polling — only re-renders when something
  // actually changed (new message / new draft / read state), so no flicker and
  // no wasted renders. Returns true if the inbox changed.
  async refreshConversations() {
    const list = await API.conversations.list();
    const sig = (arr) => (arr || []).map((c) => `${c.id}|${c.at}|${c.last}|${c.needsApproval ? 1 : 0}|${c.unread ? 1 : 0}|${c.agent}`).join("~");
    if (sig(list) === sig(DATA.CONVERSATIONS)) return false;
    DATA.CONVERSATIONS = list;
    Store.recountApprovals();
    Store.emit();
    return true;
  },

  // conversations / approvals
  async regenerate(convId) {
    const r = await API.conversations.regenerate(convId);
    patchConversation(r.conversation);
    Store.emit();
    return r; // { conversation, draft, workflow }
  },
  async approve(convId, text) {
    const r = await API.conversations.approve(convId, text);
    patchConversation(r.conversation);
    DATA.APPROVALS = r.approvals;
    DATA.meta.pendingApprovals = r.pendingApprovals;
    Store.emit();
    return r;
  },
  async reject(convId) {
    const r = await API.conversations.reject(convId);
    patchConversation(r.conversation);
    DATA.APPROVALS = r.approvals;
    DATA.meta.pendingApprovals = r.pendingApprovals;
    Store.emit();
    return r;
  },
  async sendManual(convId, text, as) {
    const r = await API.conversations.send(convId, text, as);
    patchConversation(r.conversation);
    Store.emit();
    return r;
  },
  async takeover(convId, taken) {
    const r = await API.conversations.takeover(convId, taken);
    patchConversation(r.conversation);
    Store.emit();
    return r;
  },

  // campaigns
  async createCampaign(body) {
    const c = await API.campaigns.create(body);
    DATA.CAMPAIGNS = [c, ...DATA.CAMPAIGNS];
    Store.emit();
    return c;
  },
  async updateCampaign(id, patch) {
    const c = await API.campaigns.update(id, patch);
    DATA.CAMPAIGNS = DATA.CAMPAIGNS.map((x) => (x.id === id ? c : x));
    Store.emit();
    return c;
  },
  async deleteCampaign(id) {
    await API.campaigns.remove(id);
    DATA.CAMPAIGNS = DATA.CAMPAIGNS.filter((x) => x.id !== id);
    // mirror the backend cascade locally: detach leads from the deleted campaign
    DATA.CONTACTS = DATA.CONTACTS.map((ct) => (ct.campaign === id ? { ...ct, campaign: "" } : ct));
    Store.emit();
  },

  // agents
  async createAgent(body) {
    const a = await API.agents.create(body);
    DATA.AGENTS = [...DATA.AGENTS, a];
    if (a.training) DATA.AGENT_TRAINING[a.id] = a.training;
    Store.emit();
    return a;
  },
  async updateAgent(id, patch) {
    const a = await API.agents.update(id, patch);
    DATA.AGENTS = DATA.AGENTS.map((x) => (x.id === id ? a : x));
    if (patch.training) DATA.AGENT_TRAINING[id] = patch.training;
    // mirror the backend rename cascade so every screen shows the new name now
    if (patch.name && a && a.name) {
      const sync = (arr) => (arr || []).map((r) => (r.agentId === id ? { ...r, agent: a.name } : r));
      DATA.CONTACTS = sync(DATA.CONTACTS);
      DATA.CAMPAIGNS = sync(DATA.CAMPAIGNS);
      DATA.CONVERSATIONS = sync(DATA.CONVERSATIONS);
    }
    Store.emit();
    return a;
  },
  async deleteAgent(id) {
    await API.agents.remove(id);
    DATA.AGENTS = DATA.AGENTS.filter((x) => x.id !== id);
    Store.emit();
  },

  // contacts / leads
  async importContacts(rows) {
    const r = await API.contacts.importRows({ rows });
    DATA.CONTACTS = [...(r.contacts || []), ...DATA.CONTACTS];
    Store.emit();
    return r; // { contacts, count }
  },
  async createLead(body) {
    const c = await API.contacts.create(body);
    DATA.CONTACTS = [c, ...DATA.CONTACTS];
    Store.emit();
    return c;
  },
  async updateLead(id, patch) {
    const c = await API.contacts.update(id, patch);
    DATA.CONTACTS = DATA.CONTACTS.map((x) => (x.id === id ? c : x));
    Store.emit();
    return c;
  },
  async deleteLead(id) {
    await API.contacts.remove(id);
    // Mirror the backend cascade locally so no screen shows dangling records.
    DATA.CONTACTS = DATA.CONTACTS.filter((x) => x.id !== id);
    DATA.CONVERSATIONS = (DATA.CONVERSATIONS || []).filter((c) => c.contactId !== id);
    DATA.FOLLOW_UPS = (DATA.FOLLOW_UPS || []).filter((f) => f.leadId !== id);
    DATA.ESCALATIONS = (DATA.ESCALATIONS || []).filter((e) => e.leadId !== id);
    DATA.CUSTOM_REQUESTS = (DATA.CUSTOM_REQUESTS || []).filter((r) => r.leadId !== id);
    if (DATA.WORKFLOW_STATUS) delete DATA.WORKFLOW_STATUS[id];
    Store.recountApprovals();
    Store.emit();
  },
  async addNote(id, text) {
    const r = await API.contacts.addNote(id, text);
    DATA.CONTACTS = DATA.CONTACTS.map((x) => (x.id === id ? { ...x, notes: r.notes } : x));
    Store.emit();
    return r;
  },

  // ---- Lead Intelligence: import / dedup / delete-all / analysis ----
  // Commit a previewed import (rows already tagged by the backend).
  async commitImport(rows, opts) {
    const r = await API.contacts.importCommit({ rows, ...(opts || {}) });
    DATA.CONTACTS = [...(r.created || []), ...DATA.CONTACTS];
    Store.emit();
    return r; // { created, count, skipped }
  },
  // Run Website Intelligence for a lead, then refresh the lead record so the
  // unified intelligence (intelligence.web, analysis, service) shows live.
  async analyzeWebsite(id) {
    // Phase 3 — async: enqueue the job (returns immediately, no nginx timeout) then
    // poll its progress to completion. The heavy analysis runs in a background worker.
    const enq = await API.contacts.analyzeWebsite(id);
    let job = null;
    for (let i = 0; i < 80; i++) {
      await new Promise((r) => setTimeout(r, 1500));
      try { job = (await API.contacts.websiteJob(id)).job; } catch { break; }
      if (job && (job.status === "completed" || job.status === "failed")) break;
    }
    try { const c = await API.contacts.get(id); DATA.CONTACTS = DATA.CONTACTS.map((x) => (x.id === id ? c : x)); } catch {}
    Store.emit();
    return { ok: job ? job.status === "completed" : !!enq.ok, job, error: (job && job.error) || null };
  },
  async analyzeMaps(id) {
    // Phase 7 — async: enqueue the Maps-analysis job then poll to completion (mirrors
    // analyzeWebsite). Honest: a "failed" job with no API key surfaces its reason.
    const enq = await API.contacts.analyzeMaps(id);
    let job = null;
    for (let i = 0; i < 40; i++) {
      await new Promise((r) => setTimeout(r, 1500));
      try { job = (await API.contacts.mapsJob(id)).job; } catch { break; }
      if (job && (job.status === "completed" || job.status === "failed")) break;
    }
    try { const c = await API.contacts.get(id); DATA.CONTACTS = DATA.CONTACTS.map((x) => (x.id === id ? c : x)); } catch {}
    Store.emit();
    return { ok: job ? job.status === "completed" : !!enq.ok, job, error: (job && job.error) || null };
  },
  // Phase 5 — Google Meet scheduler: set/update a lead's meeting, refresh the record.
  async setMeeting(id, body) {
    const r = await API.contacts.setMeeting(id, body);
    try { const c = await API.contacts.get(id); DATA.CONTACTS = DATA.CONTACTS.map((x) => (x.id === id ? c : x)); } catch {}
    Store.emit();
    return r; // { ok, meeting }
  },
  // Phase 8 — bulk website/Maps analysis (enqueue jobs for a set of contacts).
  async bulkAnalyze(body) {
    const r = await API.contacts.bulkAnalyze(body || {});
    try { DATA.CONTACTS = await API.contacts.list(); } catch {}
    Store.emit();
    return r; // { ok, website, maps, queued, skipped }
  },
  // Merge duplicate contacts (server keeps the best record + merges children),
  // then reload the canonical contacts list.
  async cleanDuplicates() {
    const r = await API.contacts.cleanDuplicates();
    try { DATA.CONTACTS = await API.contacts.list(); } catch {}
    Store.emit();
    return r; // { groupsFound, kept, removed, contactsAfter }
  },
  // Delete ALL contacts (gated). Spares agents/campaigns/lines/settings/keys.
  async deleteAllContacts(confirm) {
    const r = await API.contacts.deleteAll(confirm);
    if (r && r.ok) {
      DATA.CONTACTS = [];
      DATA.CONVERSATIONS = [];
      DATA.FOLLOW_UPS = [];
      DATA.ESCALATIONS = [];
      if (DATA.WORKFLOW_STATUS) Object.keys(DATA.WORKFLOW_STATUS).forEach((k) => delete DATA.WORKFLOW_STATUS[k]);
      Store.recountApprovals();
    }
    Store.emit();
    return r; // { ok, deleted, kept } | { ok:false, error }
  },

  // ---- Core Dynamic Workflow ----
  async runWorkflow(leadId, trigger) {
    const r = await API.workflow.run(leadId, trigger);
    applyWorkflowStatus(leadId, r);
    Store.emit();
    return r;
  },
  async runAgent(leadId, agentKey, runId) {
    const r = await API.workflow.runAgent(leadId, agentKey, runId);
    applyWorkflowStatus(leadId, r);
    Store.emit();
    return r;
  },
  async retryAgent(leadId, agentRunId) {
    const r = await API.workflow.retry(agentRunId);
    applyWorkflowStatus(leadId, r);
    Store.emit();
    return r;
  },
  async loadWorkflowStatus(leadId) {
    const r = await API.workflow.status(leadId);
    DATA.WORKFLOW_STATUS[leadId] = r;
    Store.emit();
    return r;
  },
  async generateWhatsapp(leadId) {
    return API.workflow.whatsapp(leadId);
  },
  async discoverLeads(body) {
    const r = await API.workflow.discover(body);
    DATA.CONTACTS = [...(r.leads || []), ...DATA.CONTACTS];
    Store.emit();
    return r; // { leads, count }
  },
  async refreshInsights() {
    const r = await API.workflow.insights();
    DATA.LEARNING = r.insight;
    Store.emit();
    return r.insight;
  },
  async analyzeInsights() {
    const r = await API.workflow.analyze();
    DATA.LEARNING = r.insight;
    Store.emit();
    return r.insight;
  },

  // ---- Knowledge Base ----
  async updateKnowledge(id, patch) {
    const item = await API.knowledge.update(id, patch);
    DATA.KNOWLEDGE = DATA.KNOWLEDGE.map((x) => (x.id === id ? item : x));
    Store.emit();
    return item;
  },

  // ---- Follow-ups ----
  async createFollowUp(body) {
    const f = await API.followups.create(body);
    DATA.FOLLOW_UPS = [f, ...(DATA.FOLLOW_UPS || [])];
    Store.emit();
    return f;
  },
  // Load the server-truth follow-ups for one lead and merge them into DATA,
  // replacing any cached entries for that lead. Used by the lead drawer.
  async loadFollowUps(leadId) {
    const list = await API.followups.list(leadId);
    const others = (DATA.FOLLOW_UPS || []).filter((f) => f.leadId !== leadId);
    DATA.FOLLOW_UPS = [...list, ...others];
    Store.emit();
    return list;
  },

  // sessions (WhatsApp lines)
  async createSession(body) {
    const s = await API.sessions.create(body);
    DATA.SESSIONS = [...(DATA.SESSIONS || []), s];
    Store.emit();
    return s;
  },
  async updateSession(id, patch) {
    const s = await API.sessions.update(id, patch);
    DATA.SESSIONS = (DATA.SESSIONS || []).map((x) => (x.id === id ? s : x));
    Store.emit();
    return s;
  },

  // settings
  async updateSettings(patch) {
    const s = await API.settings.update(patch);
    DATA.SETTINGS = s;
    Store.emit();
    return s;
  },
};

// Cache a workflow status response and reflect it on the lead record so the
// CRM list/drawer stay in sync with what the engine persisted.
function applyWorkflowStatus(leadId, status) {
  if (!status) return;
  DATA.WORKFLOW_STATUS[leadId] = status;
  const fr = status.run?.finalRecommendation;
  const stage = fr?.stage;
  if (fr) {
    DATA.CONTACTS = DATA.CONTACTS.map((c) =>
      c.id === leadId
        ? { ...c, stage: stage || c.stage, reco: fr.summary || c.reco }
        : c
    );
  }
  if (Array.isArray(status.followUps)) {
    const others = (DATA.FOLLOW_UPS || []).filter((f) => f.leadId !== leadId);
    DATA.FOLLOW_UPS = [...status.followUps, ...others];
  }
}
window.applyWorkflowStatus = applyWorkflowStatus;

function patchConversation(conv) {
  if (!conv) return;
  // update the inbox summary entry
  const summary = {
    id: conv.id, contactId: conv.contactId, name: conv.name, person: conv.person,
    agent: conv.agent, color: conv.color, status: conv.status, unread: conv.unread,
    hot: conv.hot, at: conv.at, needsApproval: Boolean(conv.pendingDraft),
    last: lastText(conv),
  };
  const i = DATA.CONVERSATIONS.findIndex((c) => c.id === conv.id);
  if (i >= 0) DATA.CONVERSATIONS[i] = { ...DATA.CONVERSATIONS[i], ...summary };
  Store.recountApprovals();
}

function lastText(conv) {
  const m = (conv.messages || [])[(conv.messages || []).length - 1];
  if (!m) return "";
  return m.from === "you" ? "You: " + m.text : m.text;
}
