/**
 * Telegram ↔ chat-standalone bridge.
 *
 * Each Telegram chat_id maps to one Forge chat session. The map is
 * persisted as JSON in <dataDir>/telegram-chat-sessions.json so a
 * conversation survives bot restarts.
 *
 * Per turn we:
 *   1. Send a "thinking…" placeholder to Telegram, remember its message_id
 *   2. Open SSE on chat-standalone, then POST the user text
 *   3. Stream agent events:
 *        - tool_use → edit placeholder with "⚙ running <tool>(args)…"
 *        - text_delta → buffer; throttled edit (every 2s) to show progress
 *        - turn_done → final edit with full assistant text (split if >MAX)
 *        - error → final edit with "✗ <error>"
 *
 * Telegram has a ~30 msg/sec API limit and edit rate-limits per chat;
 * we throttle edits to once per 2s during streaming.
 */

import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { getDataDir } from '../dirs';

const CHAT_PORT = Number(process.env.CHAT_PORT) || 8408;
const BASE = `http://127.0.0.1:${CHAT_PORT}`;
const MAX_TELEGRAM_MSG = 4000; // a bit under 4096 to allow markers
const EDIT_THROTTLE_MS = 2000;

// ─── Session map persistence ──────────────────────────────

interface SessionMap {
  /** telegram chat_id → forge session_id */
  sessions: Record<string, string>;
}

function mapFile(): string {
  return join(getDataDir(), 'telegram-chat-sessions.json');
}

function loadMap(): SessionMap {
  try {
    const f = mapFile();
    if (!existsSync(f)) return { sessions: {} };
    return JSON.parse(readFileSync(f, 'utf-8')) as SessionMap;
  } catch { return { sessions: {} }; }
}

function saveMap(m: SessionMap): void {
  try {
    mkdirSync(getDataDir(), { recursive: true });
    writeFileSync(mapFile(), JSON.stringify(m, null, 2));
  } catch (e) {
    console.error('[chat-tg] save map failed', e);
  }
}

export function getTelegramSession(telegramChatId: number): string | null {
  const m = loadMap();
  return m.sessions[String(telegramChatId)] || null;
}

export function setTelegramSession(telegramChatId: number, sessionId: string): void {
  const m = loadMap();
  m.sessions[String(telegramChatId)] = sessionId;
  saveMap(m);
}

export function clearTelegramSession(telegramChatId: number): void {
  const m = loadMap();
  delete m.sessions[String(telegramChatId)];
  saveMap(m);
}

// ─── chat-standalone client ───────────────────────────────

async function createSession(title: string): Promise<string> {
  const res = await fetch(`${BASE}/api/sessions`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ title }),
  });
  if (!res.ok) throw new Error(`create session: HTTP ${res.status}`);
  const j = await res.json();
  if (!j?.session?.id) throw new Error('create session: no id');
  return j.session.id as string;
}

async function ensureSession(telegramChatId: number): Promise<string> {
  const existing = getTelegramSession(telegramChatId);
  if (existing) {
    // Validate it still exists; if not, drop and recreate.
    try {
      const r = await fetch(`${BASE}/api/sessions/${encodeURIComponent(existing)}`);
      if (r.status === 404) {
        clearTelegramSession(telegramChatId);
      } else if (r.ok) {
        return existing;
      }
    } catch {/* fall through to recreate */}
  }
  const id = await createSession(`Telegram chat ${telegramChatId}`);
  setTelegramSession(telegramChatId, id);
  return id;
}

async function openSse(sessionId: string): Promise<ReadableStreamDefaultReader<Uint8Array>> {
  const res = await fetch(`${BASE}/api/sessions/${encodeURIComponent(sessionId)}/events`, {
    headers: { accept: 'text/event-stream' },
  });
  if (!res.ok || !res.body) throw new Error(`SSE open: HTTP ${res.status}`);
  const reader = res.body.getReader();
  await reader.read(); // wait for initial `: subscribed` comment
  return reader;
}

async function postMessage(sessionId: string, text: string): Promise<void> {
  const res = await fetch(`${BASE}/api/sessions/${encodeURIComponent(sessionId)}/messages`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ text }),
  });
  if (res.status !== 202 && !res.ok) {
    throw new Error(`POST message: HTTP ${res.status}`);
  }
}

// ─── Turn driver ──────────────────────────────────────────

export interface TelegramTurnCallbacks {
  /** Send placeholder; return message_id. Bot edits this with progress. */
  sendPlaceholder: (text: string) => Promise<number | null>;
  /** Edit a previously-sent placeholder. Failures are swallowed (rate-limit). */
  editPlaceholder: (messageId: number, text: string) => Promise<void>;
  /** Send a brand-new message (used for the final text when it splits). */
  sendNew: (text: string) => Promise<number | null>;
}

interface AgentEvent { type: string; data?: any; message_id?: string }

function summarizeToolInput(input: unknown): string {
  try {
    const s = JSON.stringify(input ?? {});
    return s.length > 80 ? s.slice(0, 80) + '…' : s;
  } catch { return ''; }
}

/**
 * Run one chat turn against chat-standalone, streaming progress to Telegram.
 * Returns the final assistant text (joined if multi-block).
 */
export async function runTelegramTurn(args: {
  telegramChatId: number;
  userText: string;
  cb: TelegramTurnCallbacks;
}): Promise<{ ok: boolean; sessionId: string; finalText?: string; error?: string }> {
  const sessionId = await ensureSession(args.telegramChatId);

  const placeholderId = await args.cb.sendPlaceholder('🤖 thinking…');

  let reader: ReadableStreamDefaultReader<Uint8Array>;
  try {
    reader = await openSse(sessionId);
  } catch (e) {
    const msg = (e as Error).message;
    if (placeholderId) await args.cb.editPlaceholder(placeholderId, `✗ ${msg}`);
    return { ok: false, sessionId, error: msg };
  }

  try {
    await postMessage(sessionId, args.userText);
  } catch (e) {
    try { await reader.cancel(); } catch {}
    const msg = (e as Error).message;
    if (placeholderId) await args.cb.editPlaceholder(placeholderId, `✗ ${msg}`);
    return { ok: false, sessionId, error: msg };
  }

  // Streaming consume + throttled edits.
  const decoder = new TextDecoder();
  let buf = '';
  let assistantText = '';
  let currentTool: string | null = null;
  let lastEditAt = 0;
  let lastEditPayload = '';
  let errorMsg: string | null = null;

  async function maybeEdit(force = false) {
    if (!placeholderId) return;
    const now = Date.now();
    if (!force && now - lastEditAt < EDIT_THROTTLE_MS) return;
    let payload: string;
    if (currentTool) {
      payload = assistantText
        ? `${assistantText}\n\n⚙ ${currentTool}…`
        : `⚙ ${currentTool}…`;
    } else {
      payload = assistantText || '🤖 thinking…';
    }
    if (payload.length > MAX_TELEGRAM_MSG) payload = payload.slice(0, MAX_TELEGRAM_MSG) + '…';
    if (payload === lastEditPayload) return;
    lastEditPayload = payload;
    lastEditAt = now;
    await args.cb.editPlaceholder(placeholderId, payload);
  }

  outer:
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buf += decoder.decode(value, { stream: true });

    let idx: number;
    while ((idx = buf.indexOf('\n\n')) >= 0) {
      const frame = buf.slice(0, idx);
      buf = buf.slice(idx + 2);
      const dataLine = frame.split('\n').find((l) => l.startsWith('data: '));
      if (!dataLine) continue;
      let evt: AgentEvent;
      try { evt = JSON.parse(dataLine.slice(6)); } catch { continue; }

      switch (evt.type) {
        case 'text_delta': {
          assistantText += String(evt.data?.delta || '');
          void maybeEdit();
          break;
        }
        case 'tool_use': {
          const name = String(evt.data?.name || '?');
          currentTool = `${name}(${summarizeToolInput(evt.data?.input)})`;
          void maybeEdit(true);
          break;
        }
        case 'tool_result': {
          currentTool = null;
          void maybeEdit(true);
          break;
        }
        case 'turn_done': {
          break outer;
        }
        case 'error': {
          errorMsg = String(evt.data?.error || 'unknown');
          break outer;
        }
      }
    }
  }
  try { await reader.cancel(); } catch {}

  if (errorMsg) {
    if (placeholderId) await args.cb.editPlaceholder(placeholderId, `✗ ${errorMsg}`);
    return { ok: false, sessionId, error: errorMsg };
  }

  const finalText = assistantText.trim() || '(no reply)';

  // Final delivery: if it fits one message, edit the placeholder. Else
  // edit with the first slice and send follow-ups as new messages.
  if (placeholderId) {
    const first = finalText.length > MAX_TELEGRAM_MSG ? finalText.slice(0, MAX_TELEGRAM_MSG) : finalText;
    await args.cb.editPlaceholder(placeholderId, first);
    if (finalText.length > MAX_TELEGRAM_MSG) {
      for (let i = MAX_TELEGRAM_MSG; i < finalText.length; i += MAX_TELEGRAM_MSG) {
        await args.cb.sendNew(finalText.slice(i, i + MAX_TELEGRAM_MSG));
      }
    }
  } else {
    // Placeholder failed; send fresh
    for (let i = 0; i < finalText.length; i += MAX_TELEGRAM_MSG) {
      await args.cb.sendNew(finalText.slice(i, i + MAX_TELEGRAM_MSG));
    }
  }

  return { ok: true, sessionId, finalText };
}
