/**
 * quicproquo web client — main application.
 *
 * Connects the TS SDK, WASM crypto, IndexedDB store, and the DOM together.
 */

import { LocalStore } from "./store.js";
import type { StoredConversation, StoredMessage } from "./store.js";

// --- DOM references ---
const $ = <T extends HTMLElement>(id: string) =>
  document.getElementById(id) as T;

const loginScreen = $<HTMLDivElement>("login-screen");
const chatScreen = $<HTMLDivElement>("chat-screen");
const loginForm = $<HTMLFormElement>("login-form");
const serverInput = $<HTMLInputElement>("server-input");
const usernameInput = $<HTMLInputElement>("username-input");
const passwordInput = $<HTMLInputElement>("password-input");
const loginBtn = $<HTMLButtonElement>("login-btn");
const registerBtn = $<HTMLButtonElement>("register-btn");
const loginError = $<HTMLParagraphElement>("login-error");
const userDisplay = $<HTMLSpanElement>("user-display");
const logoutBtn = $<HTMLButtonElement>("logout-btn");
const conversationList = $<HTMLUListElement>("conversation-list");
const newDmBtn = $<HTMLButtonElement>("new-dm-btn");
const chatTitle = $<HTMLSpanElement>("chat-title");
const epochBadge = $<HTMLSpanElement>("epoch-badge");
const connBadge = $<HTMLSpanElement>("conn-badge");
const messagesDiv = $<HTMLDivElement>("messages");
const sendForm = $<HTMLFormElement>("send-form");
const msgInput = $<HTMLInputElement>("msg-input");

// --- App state ---
const store = new LocalStore();
let wsTransport: WebSocket | null = null;
let currentUser: string | null = null;
let conversations: StoredConversation[] = [];
let activeConvId: string | null = null;
let connected = false;

// --- Init ---
async function init() {
  await store.open();

  // Register service worker for PWA offline support.
  if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register("sw.js").catch(() => {
      // Service worker registration failed — non-critical.
    });
  }

  loginForm.addEventListener("submit", (e) => {
    e.preventDefault();
    handleLogin();
  });
  registerBtn.addEventListener("click", handleRegister);
  logoutBtn.addEventListener("click", handleLogout);
  sendForm.addEventListener("submit", (e) => {
    e.preventDefault();
    handleSend();
  });
  newDmBtn.addEventListener("click", handleNewDm);
}

// --- Auth ---
async function handleLogin() {
  const server = serverInput.value.trim();
  const username = usernameInput.value.trim();
  const password = passwordInput.value;

  if (!server || !username || !password) {
    showError("All fields are required");
    return;
  }

  loginBtn.disabled = true;
  hideError();

  try {
    await connectWS(server);
    await rpcCall("login", { username, password });
    currentUser = username;
    showChat();
  } catch (err: unknown) {
    showError(`Login failed: ${err instanceof Error ? err.message : String(err)}`);
  } finally {
    loginBtn.disabled = false;
  }
}

async function handleRegister() {
  const server = serverInput.value.trim();
  const username = usernameInput.value.trim();
  const password = passwordInput.value;

  if (!server || !username || !password) {
    showError("All fields are required");
    return;
  }

  registerBtn.disabled = true;
  hideError();

  try {
    await connectWS(server);
    await rpcCall("register", { username, password });
    await rpcCall("login", { username, password });
    currentUser = username;
    showChat();
  } catch (err: unknown) {
    showError(`Registration failed: ${err instanceof Error ? err.message : String(err)}`);
  } finally {
    registerBtn.disabled = false;
  }
}

function handleLogout() {
  currentUser = null;
  activeConvId = null;
  if (wsTransport) {
    wsTransport.close();
    wsTransport = null;
  }
  setConnected(false);
  loginScreen.hidden = false;
  chatScreen.hidden = true;
}

// --- WebSocket transport ---
let rpcId = 0;
const pendingCalls = new Map<
  number,
  { resolve: (v: unknown) => void; reject: (e: Error) => void }
>();

function connectWS(addr: string): Promise<void> {
  return new Promise((resolve, reject) => {
    if (wsTransport) {
      wsTransport.close();
    }

    const ws = new WebSocket(addr);
    const timeout = setTimeout(() => {
      reject(new Error("Connection timeout"));
      ws.close();
    }, 5000);

    ws.addEventListener("open", () => {
      clearTimeout(timeout);
      wsTransport = ws;
      setConnected(true);
      resolve();
    });

    ws.addEventListener("close", () => {
      clearTimeout(timeout);
      setConnected(false);
      rejectAllPending("Connection closed");
    });

    ws.addEventListener("error", () => {
      clearTimeout(timeout);
      reject(new Error("WebSocket error"));
    });

    ws.addEventListener("message", (ev) => {
      if (typeof ev.data === "string") {
        try {
          const resp = JSON.parse(ev.data);
          const pending = pendingCalls.get(resp.id);
          if (pending) {
            pendingCalls.delete(resp.id);
            if (resp.ok) {
              pending.resolve(resp.result);
            } else {
              pending.reject(new Error(resp.error ?? "RPC error"));
            }
          }
          // Handle server push events.
          if (resp.type === "message") {
            handleIncomingMessage(resp);
          }
        } catch {
          // Ignore malformed messages.
        }
      }
    });
  });
}

function rpcCall(method: string, params: Record<string, unknown>): Promise<unknown> {
  if (!wsTransport || wsTransport.readyState !== WebSocket.OPEN) {
    return Promise.reject(new Error("Not connected"));
  }

  const id = ++rpcId;
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      pendingCalls.delete(id);
      reject(new Error(`RPC timeout: ${method}`));
    }, 10000);

    pendingCalls.set(id, {
      resolve: (v) => {
        clearTimeout(timer);
        resolve(v);
      },
      reject: (e) => {
        clearTimeout(timer);
        reject(e);
      },
    });

    wsTransport!.send(JSON.stringify({ id, method, params }));
  });
}

function rejectAllPending(reason: string) {
  for (const [id, p] of pendingCalls) {
    p.reject(new Error(reason));
    pendingCalls.delete(id);
  }
}

// --- UI updates ---
function setConnected(value: boolean) {
  connected = value;
  connBadge.textContent = value ? "Online" : "Offline";
  connBadge.className = value ? "online" : "offline";
}

function showError(msg: string) {
  loginError.textContent = msg;
  loginError.hidden = false;
}

function hideError() {
  loginError.hidden = true;
}

async function showChat() {
  loginScreen.hidden = true;
  chatScreen.hidden = false;
  userDisplay.textContent = currentUser ?? "";

  // Load conversations from IndexedDB.
  conversations = await store.listConversations();
  renderConversations();
}

function renderConversations() {
  conversationList.innerHTML = "";
  for (const conv of conversations) {
    const li = document.createElement("li");
    if (conv.id === activeConvId) li.classList.add("active");

    const nameSpan = document.createElement("span");
    nameSpan.className = "conv-name";
    nameSpan.textContent = conv.name;
    li.appendChild(nameSpan);

    if (conv.unread > 0) {
      const badge = document.createElement("span");
      badge.className = "unread-badge";
      badge.textContent = String(conv.unread);
      li.appendChild(badge);
    }

    li.addEventListener("click", () => selectConversation(conv.id));
    conversationList.appendChild(li);
  }
}

async function selectConversation(id: string) {
  activeConvId = id;
  const conv = conversations.find((c) => c.id === id);
  if (conv) {
    chatTitle.textContent = conv.name;
    conv.unread = 0;
    await store.saveConversation(conv);
  }
  renderConversations();
  await renderMessages();
}

async function renderMessages() {
  messagesDiv.innerHTML = "";
  if (!activeConvId) return;

  const msgs = await store.loadMessages(activeConvId);
  for (const msg of msgs) {
    appendMessageElement(msg);
  }
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

function appendMessageElement(msg: StoredMessage) {
  const div = document.createElement("div");
  div.className = `message${msg.isOutgoing ? " outgoing" : ""}`;

  const meta = document.createElement("div");
  meta.className = "meta";
  const time = new Date(msg.timestamp);
  const timeStr = `${String(time.getHours()).padStart(2, "0")}:${String(time.getMinutes()).padStart(2, "0")}`;
  meta.textContent = `${timeStr} ${msg.sender}`;

  const body = document.createElement("div");
  body.className = "body";
  body.textContent = msg.body;

  div.appendChild(meta);
  div.appendChild(body);
  messagesDiv.appendChild(div);
}

// --- Send ---
async function handleSend() {
  const text = msgInput.value.trim();
  if (!text || !activeConvId || !currentUser) return;

  msgInput.value = "";

  const msg: StoredMessage = {
    id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
    conversationId: activeConvId,
    sender: currentUser,
    body: text,
    timestamp: Date.now(),
    isOutgoing: true,
  };

  await store.saveMessage(msg);
  appendMessageElement(msg);
  messagesDiv.scrollTop = messagesDiv.scrollHeight;

  // Send via RPC (best effort).
  try {
    await rpcCall("send", {
      conversationId: activeConvId,
      body: text,
    });
  } catch {
    // Message saved locally even if RPC fails.
  }
}

// --- Receive ---
async function handleIncomingMessage(data: {
  conversationId?: string;
  sender?: string;
  body?: string;
}) {
  if (!data.conversationId || !data.body) return;

  const msg: StoredMessage = {
    id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
    conversationId: data.conversationId,
    sender: data.sender ?? "unknown",
    body: data.body,
    timestamp: Date.now(),
    isOutgoing: false,
  };

  await store.saveMessage(msg);

  // Update unread or display.
  if (data.conversationId === activeConvId) {
    appendMessageElement(msg);
    messagesDiv.scrollTop = messagesDiv.scrollHeight;
  } else {
    const conv = conversations.find((c) => c.id === data.conversationId);
    if (conv) {
      conv.unread += 1;
      await store.saveConversation(conv);
      renderConversations();
    }
  }
}

// --- New DM ---
async function handleNewDm() {
  const peerName = prompt("Enter username for DM:");
  if (!peerName) return;

  const convId = `dm-${[currentUser, peerName].sort().join("-")}`;

  // Check if already exists.
  if (conversations.find((c) => c.id === convId)) {
    await selectConversation(convId);
    return;
  }

  const conv: StoredConversation = {
    id: convId,
    name: peerName,
    unread: 0,
    lastActivity: Date.now(),
  };

  await store.saveConversation(conv);
  conversations.unshift(conv);
  renderConversations();
  await selectConversation(convId);
}

// --- Bootstrap ---
init();
