import type { Context } from 'hono';
import { createRouter } from '../../create-router';
import { getUserSettings } from 'officerdb';
import {
  getGeneralChatSessionsCwd,
  listClaudePwds,
  listClaudeSessions,
  loadClaudeSession,
  loadClaudeSessionById,
  deleteClaudeSession,
  renameClaudeSession,
  loadBackgroundTask,
  claudeSessionContext,
} from './claude-sessions';
import {
  listOpenCodeSessions,
  loadOpenCodeSession,
  deleteOpenCodeSession,
  renameOpenCodeSession,
  isOpenCodeSessionId,
} from './opencode-sessions';
import { listChatModels } from './list-models';
import { logger } from './logger';
import { readSttConfig } from '../server-settings/stt';
import { transcribeAudio } from '../stt/transcribe';
import { registerAgentPanelRoutes } from './agent-panels-routes';

export const chatRouter = createRouter();

// The working directory a request operates on: an explicit ?cwd= (a chosen pwd), else the default
// general_chat_sessions dir. Claude groups sessions by cwd, so this selects which project group we read.
// (OpenCode sessions all live in the one fixed server and ignore cwd.)
const cwdOf = (ctx: Context, email: string): string => ctx.req.query('cwd')?.trim() || getGeneralChatSessionsCwd(email);

// GET /chat/pwds — the default /chat dir plus every directory that already has Claude sessions.
chatRouter.get('/pwds', (ctx) => {
  const email = ctx.get('user').email;
  return ctx.json({ pwds: listClaudePwds(email), default: getGeneralChatSessionsCwd(email) });
});

// GET /chat/sessions[?cwd=] — conversations for a working directory, merged across both harnesses
// (Claude transcripts + OpenCode's session store), newest first.
chatRouter.get('/sessions', async (ctx) => {
  const email = ctx.get('user').email;
  const cwd = cwdOf(ctx, email);
  const claude = listClaudeSessions(email, cwd).map((s) => ({ ...s, harness: 'claude' as const }));
  const opencode = await listOpenCodeSessions(cwd);
  const sessions = [...claude, ...opencode].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
  return ctx.json({ sessions });
});

// GET /chat/sessions/:id[?cwd=][&limit=&before=] — one conversation's transcript. Routes by harness
// (ses_ = OpenCode). Transcripts get extremely long, so the client opens at the tail and pages upward:
// `limit` caps how many messages come back and `before` (absolute index into the full transcript,
// exclusive) selects the window's upper bound. Absent params return the whole transcript (legacy). The
// response carries `total` (full length) and `offset` (absolute index of messages[0]) so the client knows
// where the window sits and whether older messages remain above it.
chatRouter.get('/sessions/:id', async (ctx) => {
  const email = ctx.get('user').email;
  const id = ctx.req.param('id');
  const cwd = cwdOf(ctx, email);
  // Fall back to a by-id scan when the (default) cwd doesn't hold it — a fresh /chat/<id> deep-link/refresh
  // doesn't know the session's cwd. The returned detail carries the real cwd for the client to scope the UI.
  const detail = isOpenCodeSessionId(id)
    ? await loadOpenCodeSession(id)
    : (loadClaudeSession(email, cwd, id) ?? loadClaudeSessionById(email, id));
  if (!detail) return ctx.text('Not found', 404);

  const total = detail.messages.length;
  const limitRaw = ctx.req.query('limit');
  const beforeRaw = ctx.req.query('before');
  const limit = limitRaw != null ? Math.max(0, parseInt(limitRaw, 10) || 0) : null;
  const end = beforeRaw != null ? Math.min(total, Math.max(0, parseInt(beforeRaw, 10) || 0)) : total;
  const start = limit != null ? Math.max(0, end - limit) : 0;
  const windowed = limit != null || beforeRaw != null;
  const messages = windowed ? detail.messages.slice(start, end) : detail.messages;

  // The title the list shows this session under, and how many `/clear` parts it spans. Resolved against
  // `detail.cwd` — the transcript's own directory — not the requested `cwd`, which on a deep link is
  // still the default group and holds none of this session's neighbours. OpenCode has no chains of its
  // own, so it gets neither rather than a fabricated answer.
  const context = isOpenCodeSessionId(id) ? null : claudeSessionContext(email, detail.cwd, id);

  return ctx.json({
    ...detail,
    messages,
    total,
    offset: windowed ? start : 0,
    title: context?.title ?? null,
    partCount: context?.partCount ?? 1,
  });
});

// DELETE /chat/sessions/:id[?cwd=] — remove a conversation from the owning harness's store. For a
// Claude `/clear` chain that is every part of it: the list shows the chain as one conversation, so
// deleting it deletes one conversation.
chatRouter.delete('/sessions/:id', async (ctx) => {
  const email = ctx.get('user').email;
  const id = ctx.req.param('id');
  const cwd = cwdOf(ctx, email);
  const ok = isOpenCodeSessionId(id) ? await deleteOpenCodeSession(id) : deleteClaudeSession(email, cwd, id);
  if (!ok) return ctx.text('Not found', 404);
  return ctx.json({ ok: true });
});

// PATCH /chat/sessions/:id/title[?cwd=] — rename in the owning harness's store.
chatRouter.patch('/sessions/:id/title', async (ctx) => {
  const email = ctx.get('user').email;
  const id = ctx.req.param('id');
  const cwd = cwdOf(ctx, email);
  const { title } = await ctx.req.json<{ title?: string }>();
  if (!title?.trim()) return ctx.text('title is required', 400);
  const ok = isOpenCodeSessionId(id)
    ? await renameOpenCodeSession(id, title.trim())
    : renameClaudeSession(email, cwd, id, title.trim());
  if (!ok) return ctx.text('Not found', 404);
  return ctx.json({ ok: true });
});

// GET /chat/tasks/:id — what a background task is doing right now: a subagent's own trace, or the tail of
// a backgrounded shell's log. Polled by the tray above the chat input while the task is running.
//
// A task that has not written anything yet answers 200 with `{ kind: 'pending' }`, not 404. The tray asks
// the moment `task:started` arrives, which is routinely before the file exists, and a 404 there would be
// an error state for the most ordinary thing that can happen.
chatRouter.get('/tasks/:id', (ctx) => {
  const email = ctx.get('user').email;
  const detail = loadBackgroundTask(email, ctx.req.param('id'));
  return ctx.json(detail ?? { kind: 'pending' });
});

// GET /chat/models — Claude tiers only (the runner is the `claude` CLI).
chatRouter.get('/models', async (ctx: Context) => {
  try {
    const models = await listChatModels();
    const providerNames: Record<string, string> = { 'claude-code': 'Claude Code', opencode: 'OpenCode Zen' };
    return ctx.json({ models, providerNames, hostHome: process.env.HOME ?? '' });
  } catch (err) {
    logger.error('Failed to list models', { error: String(err) });
    return ctx.json({ models: [], providerNames: {} });
  }
});

// POST /chat/stt — proxy an audio clip to the configured Whisper server.
chatRouter.post('/stt', async (ctx: Context) => {
  const sttConfig = await readSttConfig();
  if (!sttConfig?.url) {
    return ctx.json({ error: 'Whisper not configured — set it up in Settings → Speech to Text' }, 400);
  }

  const user = ctx.get('user');
  const body = await ctx.req.parseBody();
  const file = body['file'];
  if (!file || !(file instanceof File)) {
    return ctx.json({ error: 'file is required' }, 400);
  }

  const settings = (await getUserSettings(user.id)) as { languages?: { spoken?: string[] } };
  const spokenLanguages = settings.languages?.spoken ?? [];

  try {
    const result = await transcribeAudio({ file, whisperUrl: sttConfig.url, spokenLanguages });
    return ctx.json(result);
  } catch (err) {
    logger.error('STT proxy failed', { error: String(err) });
    return ctx.json({ error: err instanceof Error ? err.message : 'Failed to reach Whisper server' }, 502);
  }
});

// The agent address book — naming a chat panel so other agents on the same dashboard can reach it.
// Lives on this router because it is the same authority `chat` already grants: creating and naming
// Claude sessions. See servers/api/chat/agent-panels-routes.ts and docs/agent-coordination.md.
registerAgentPanelRoutes(chatRouter);
