/* =============================================================
   chat.jsx — local chat storage (localStorage) + thread UI
   Future: replace ChatStore with REST/WS-backed implementation.
   ============================================================= */
const CHAT_KEY = "prdt.chat.v1";
const CHAT_AUTHOR_KEY = "prdt.chat.author.v1";
const CHAT_READ_KEY = "prdt.chat.read.v1";
const CHAT_SCOPE_ALIASES = {
  program: "global",
  global: "global",
  cr: "change-request",
  "change-request": "change-request",
  plan: "migration-plan",
  "migration-plan": "migration-plan",
};
const CHAT_ALLOWED_SCOPES = new Set(["global", "region", "country", "site", "link", "change-request", "migration-plan"]);

function normalizeChatScopeKind(scopeKind) {
  const raw = String(scopeKind || "global").trim();
  const normalized = CHAT_SCOPE_ALIASES[raw] || raw;
  return CHAT_ALLOWED_SCOPES.has(normalized) ? normalized : "global";
}

function normalizeChatScopeId(scopeKind, scopeId) {
  return normalizeChatScopeKind(scopeKind) === "global" ? null : String(scopeId || "").trim();
}

function chatThreadId(scopeKind, scopeId) {
  const kind = normalizeChatScopeKind(scopeKind);
  const id = normalizeChatScopeId(kind, scopeId);
  return `${kind}:${id || ""}`;
}

function sanitizeChatText(value) {
  return String(value || "")
    .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")
    .replace(/\r\n?/g, "\n")
    .trim()
    .slice(0, 5000);
}

const ChatStore = {
  _cache: null,
  _readCache: null,
  _load() {
    if (this._cache) return this._cache;
    try {
      const raw = localStorage.getItem(CHAT_KEY);
      this._cache = raw ? JSON.parse(raw) : [];
    } catch (e) {
      this._cache = [];
    }
    return this._cache;
  },
  _loadReads() {
    if (this._readCache) return this._readCache;
    try {
      const raw = localStorage.getItem(CHAT_READ_KEY);
      this._readCache = raw ? JSON.parse(raw) : {};
    } catch (e) {
      this._readCache = {};
    }
    return this._readCache;
  },
  _save() {
    localStorage.setItem(CHAT_KEY, JSON.stringify(this._cache || []));
  },
  _saveReads() {
    localStorage.setItem(CHAT_READ_KEY, JSON.stringify(this._readCache || {}));
  },
  all() { return this._load().slice(); },
  for(scopeKind, scopeId) {
    const kind = normalizeChatScopeKind(scopeKind);
    const id = normalizeChatScopeId(kind, scopeId);
    return this._load().filter(
      (m) => normalizeChatScopeKind(m.scopeKind) === kind && (id == null || m.scopeId === id)
    );
  },
  listThreads(filter = {}) {
    const threads = new Map();
    for (const msg of this._load()) {
      const scopeKind = normalizeChatScopeKind(msg.scopeKind);
      const scopeId = normalizeChatScopeId(scopeKind, msg.scopeId);
      if (filter.scopeKind && normalizeChatScopeKind(filter.scopeKind) !== scopeKind) continue;
      if (filter.scopeId !== undefined && filter.scopeId !== "" && String(filter.scopeId || "") !== String(scopeId || "")) continue;
      const id = chatThreadId(scopeKind, scopeId);
      if (!threads.has(id)) threads.set(id, { id, scopeKind, scopeId, title: this.threadTitle(scopeKind, scopeId), count: 0, lastMessageAt: 0 });
      const thread = threads.get(id);
      thread.count++;
      thread.lastMessageAt = Math.max(thread.lastMessageAt, msg.ts || 0);
    }
    return [...threads.values()].sort((a, b) => (b.lastMessageAt || 0) - (a.lastMessageAt || 0));
  },
  createThread(input = {}) {
    const scopeKind = normalizeChatScopeKind(input.scopeKind || input.scope || "global");
    const scopeId = normalizeChatScopeId(scopeKind, input.scopeId || input.idRef || input.id);
    return { id: chatThreadId(scopeKind, scopeId), scopeKind, scopeId, title: input.title || this.threadTitle(scopeKind, scopeId) };
  },
  getOrCreateThread(scope = {}) {
    return this.createThread(scope);
  },
  threadTitle(scopeKind, scopeId) {
    const kind = normalizeChatScopeKind(scopeKind);
    const id = normalizeChatScopeId(kind, scopeId);
    if (kind === "global") return "Global project thread";
    return `${kind[0].toUpperCase() + kind.slice(1)} - ${id || "unscoped"}`;
  },
  listMessages(threadId, cursor) {
    const [scopeKind, ...rest] = String(threadId || "global:").split(":");
    const rows = this.for(scopeKind, rest.join(":") || null).sort((a, b) => (a.ts || 0) - (b.ts || 0));
    return { items: rows, cursor: null, total: rows.length };
  },
  sendMessage(threadId, body = {}) {
    const [scopeKind, ...rest] = String(threadId || "global:").split(":");
    return this.add({
      scopeKind,
      scopeId: rest.join(":") || null,
      author: body.author || body.authorEmail || this.getAuthor(),
      text: body.text || body.body || "",
      parentId: body.parentId || null,
    });
  },
  editMessage(id, body = {}) {
    this._load();
    const msg = this._cache.find((m) => m.id === id);
    if (!msg) return null;
    msg.text = sanitizeChatText(body.text || body.body || msg.text);
    msg.editedAt = Date.now();
    this._save();
    this.notify({ type: "chat.messageEdited", id });
    return msg;
  },
  markThreadRead(threadId) {
    this._loadReads();
    this._readCache[threadId] = Date.now();
    this._saveReads();
    return { ok: true, threadId };
  },
  add(msg) {
    this._load();
    const scopeKind = normalizeChatScopeKind(msg.scopeKind);
    const scopeId = normalizeChatScopeId(scopeKind, msg.scopeId);
    const full = {
      id: "m_" + Math.random().toString(36).slice(2, 10) + Date.now().toString(36),
      ts: Date.now(),
      ...msg,
      scopeKind,
      scopeId,
      text: sanitizeChatText(msg.text || msg.body || ""),
    };
    if (!full.text) return null;
    this._cache.push(full);
    this._save();
    return full;
  },
  ingestRemoteMessage(payload) {
    const source = payload && (payload.message || payload.payload || payload);
    if (!source) return null;
    this._load();
    const id = source.id || source.messageId || ("remote_" + Math.random().toString(36).slice(2));
    if (this._cache.some((m) => m.id === id)) return null;
    const scopeKind = normalizeChatScopeKind(source.scopeKind || source.scope || "global");
    const scopeId = normalizeChatScopeId(scopeKind, source.scopeId || source.idRef || source.threadScopeId);
    const full = {
      id,
      ts: source.ts || Date.parse(source.createdAt || source.occurredAt || "") || Date.now(),
      scopeKind,
      scopeId,
      author: source.author || source.authorEmail || source.actorEmail || "Remote user",
      text: sanitizeChatText(source.text || source.bodySanitized || source.body || ""),
      remote: true,
      rowVersion: source.rowVersion || null,
    };
    if (!full.text) return null;
    this._cache.push(full);
    this._save();
    this.notify({ type: "chat.messageAdded", payload: full });
    return full;
  },
  remove(id) {
    this._cache = this._load().filter((m) => m.id !== id);
    this._save();
  },
  getAuthor() {
    return localStorage.getItem(CHAT_AUTHOR_KEY) || "Management";
  },
  setAuthor(a) {
    localStorage.setItem(CHAT_AUTHOR_KEY, a);
  },
  // Subscription
  _subs: new Set(),
  subscribe(fn) { this._subs.add(fn); return () => this._subs.delete(fn); },
  notify(event) { this._subs.forEach((fn) => fn(event)); },
};

// Wrap add/remove to notify subs
const _origAdd = ChatStore.add.bind(ChatStore);
ChatStore.add = (m) => { const r = _origAdd(m); ChatStore.notify(); return r; };
const _origRemove = ChatStore.remove.bind(ChatStore);
ChatStore.remove = (id) => { _origRemove(id); ChatStore.notify(); };

if (window.PMClient && window.PMClient.kind === "azure" && window.PMClient.subscribe) {
  window.PMClient.subscribe((event) => {
    const type = event && (event.type || event.name);
    if (type !== "chat.messageAdded") return;
    ChatStore.ingestRemoteMessage(event.payload || event);
  });
}

// P7/B7: in server (azure) mode, persist chat send/delete through PMClient so messages
// are shared across users; the standalone localStorage path (ChatStore.add/remove) is kept
// for the offline demo. Gated on PMClient.kind === "azure".
function isServerChat() {
  return !!(window.PMClient && window.PMClient.kind === "azure");
}
async function sendChatMessageToServer({ scopeKind, scopeId, author, text }) {
  const client = window.PMClient;
  // Resolve (or create) the thread for this scope, then POST the message.
  // POST /api/chat/threads/lookup returns a WriteResult(Ok, RowVersion, CorrelationId)
  // (ChatThreadLookupAsync, EfServices.cs) — the thread GUID is carried in `correlationId`,
  // NOT `id`. The LocalClient path instead returns a thread object with `.id`, so we accept both.
  const thread = await client.getOrCreateChatThread({ scopeKind, scopeId: scopeId || null });
  const threadId = thread && (thread.id || thread.threadId || thread.Id || thread.correlationId || (thread.thread && thread.thread.id));
  if (!threadId) throw new Error("chat: could not resolve thread for scope");
  const res = await client.sendChatMessage(threadId, { body: text });
  // Optimistic local insert so the author sees their message immediately; the realtime
  // echo (chat.messageAdded) is deduped by id in ingestRemoteMessage. Other users receive
  // it via the program-group broadcast wired in CollaborationRoutes (RealtimeGroups.Program).
  // The message POST also returns WriteResult, so the server message GUID is in `correlationId`
  // — use it as the optimistic id so any future echo carrying that id de-dupes instead of duplicating.
  ChatStore.ingestRemoteMessage({
    id: (res && (res.id || res.messageId || res.correlationId)) || undefined,
    scopeKind,
    scopeId: scopeId || null,
    author,
    text,
    ts: Date.now(),
    rowVersion: res && res.rowVersion,
  });
  return res;
}

function useChatMessages(scopeKind, scopeId) {
  const [, force] = React.useState(0);
  React.useEffect(() => ChatStore.subscribe(() => force((n) => n + 1)), []);
  return ChatStore.for(scopeKind, scopeId);
}

function ChatThread({ scopeKind, scopeId, title }) {
  const messages = useChatMessages(scopeKind, scopeId);
  const [text, setText] = React.useState("");
  const [author, setAuthor] = React.useState(ChatStore.getAuthor());
  const bodyRef = React.useRef(null);

  React.useEffect(() => {
    if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
  }, [messages.length]);

  const send = () => {
    const t = text.trim();
    if (!t) return;
    ChatStore.setAuthor(author);
    if (isServerChat()) {
      sendChatMessageToServer({ scopeKind, scopeId: scopeId || null, author, text: t })
        .catch((e) => { try { console.warn("chat send failed", e); } catch (_) {} });
    } else {
      ChatStore.add({ scopeKind, scopeId: scopeId || null, author, text: t });
    }
    setText("");
  };

  const fmtTs = (ts) => {
    const d = new Date(ts);
    const today = new Date();
    const sameDay = d.toDateString() === today.toDateString();
    if (sameDay) return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
    return d.toLocaleDateString([], { year: "numeric", month: "short", day: "numeric" }) + " · " +
      d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
  };

  const initials = (name) =>
    (name || "").split(/\s+/).filter(Boolean).slice(0, 2).map((s) => s[0]).join("").toUpperCase() || "M";

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
      {title && (
        <div className="chat-scope-bar">
          <Glyph name="chat" size={13}/>
          <span style={{color: "var(--ink-700)"}}>{title}</span>
          <span style={{marginLeft: "auto", color: "var(--ink-500)"}}>{messages.length} message{messages.length === 1 ? "" : "s"}</span>
        </div>
      )}
      <div className="chat-thread" ref={bodyRef} style={{ flex: 1, overflow: "auto" }}>
        {messages.length === 0 && (
          <div className="chat-empty">
            <Glyph name="chat" size={22}/>
            <div style={{marginTop: 8}}>No messages in this thread yet.</div>
            <div style={{fontSize: 12, marginTop: 4}}>Ask a question or post an update — it stays linked to this scope.</div>
          </div>
        )}
        {messages.map((m) => (
          <div className="chat-msg" key={m.id}>
            <div className="avatar">{initials(m.author)}</div>
            <div className="body">
              <div className="meta">
                <span className="author">{m.author}</span>
                <span> · {fmtTs(m.ts)}</span>
                <button className="btn ghost" style={{ marginLeft: 6, padding: "0 4px", fontSize: 11 }} onClick={() => {
                  if (!window.confirm("Delete this message?")) return;
                  ChatStore.remove(m.id); // optimistic local removal; backend is source of truth in server mode
                  if (isServerChat()) {
                    Promise.resolve(window.PMClient.deleteChatMessage(m.id))
                      .catch((e) => { try { console.warn("chat delete failed", e); } catch (_) {} });
                  }
                }}>delete</button>
              </div>
              <div className="text">{m.text}</div>
            </div>
          </div>
        ))}
      </div>
      <div className="chat-input">
        <input
          type="text"
          value={author}
          onChange={(e) => setAuthor(e.target.value)}
          placeholder="Your name"
          style={{
            width: 110,
            border: "1px solid var(--line)",
            background: "var(--surface)",
            borderRadius: 8,
            padding: "8px 10px",
            fontSize: 12,
            outline: "none",
          }}
        />
        <textarea
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="Write a message…"
          onKeyDown={(e) => {
            if ((e.metaKey || e.ctrlKey) && e.key === "Enter") send();
          }}
        />
        <button className="btn primary" onClick={send} style={{height: 38}} title="Send (⌘/Ctrl+Enter)">
          <Glyph name="send" size={13}/> Send
        </button>
      </div>
    </div>
  );
}

Object.assign(window, { ChatStore, ChatThread, useChatMessages });
