/**
 * library-svc — история тредов и сообщений для аккаунтов
 * docs/architecture: 01-perplexity-analogue-design.md §2.2.G, §5.6
 * API: GET/POST/DELETE /api/v1/library/threads
 *      GET /api/v1/library/threads/:id (thread + messages)
 *      POST/PATCH /api/v1/library/threads/:id/messages
 *      GET /api/v1/library/threads/:id/export?format=pdf|md
 */

import Fastify from 'fastify';
import cors from '@fastify/cors';
import { and, asc, desc, eq } from 'drizzle-orm';
import { db } from './db/index.js';
import { threads, threadMessages, type Block } from './db/schema.js';

const PORT = parseInt(process.env.PORT ?? '3009', 10);
const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL ?? '';

const app = Fastify({ logger: true });

await app.register(cors, { origin: true });

async function getUserIdFromToken(authHeader: string | undefined): Promise<string | null> {
  if (!authHeader?.startsWith('Bearer ')) return null;
  const token = authHeader.slice(7);
  if (!AUTH_SERVICE_URL) {
    return token ? 'guest' : null;
  }
  try {
    const res = await fetch(`${AUTH_SERVICE_URL.replace(/\/$/, '')}/api/auth/me`, {
      headers: { Authorization: `Bearer ${token}` },
      signal: AbortSignal.timeout(5000),
    });
    if (!res.ok) return null;
    const data = (await res.json()) as { id?: string };
    return data.id ?? null;
  } catch {
    return null;
  }
}

app.get('/health', async () => ({ status: 'ok' }));
app.get('/metrics', async (_req, reply) => {
  reply.header('Content-Type', 'text/plain; charset=utf-8');
  return reply.send('# HELP gooseek_up Service is up (1) or down (0)\n# TYPE gooseek_up gauge\ngooseek_up 1\n');
});
app.get('/ready', async () => ({ status: 'ready' }));

app.get('/api/v1/library/threads', async (req, reply) => {
  const userId = await getUserIdFromToken(req.headers.authorization);
  if (!userId) {
    return reply.status(401).send({ error: 'Authorization required' });
  }
  try {
    const list = await db
      .select()
      .from(threads)
      .where(eq(threads.userId, userId))
      .orderBy(desc(threads.createdAt));
    const chats = list.map((r) => ({
      id: r.id,
      title: r.title,
      createdAt: r.createdAt?.toISOString?.() ?? r.createdAt,
      sources: (r.sources as string[]) ?? [],
      files: (r.files as { fileId: string; name: string }[]) ?? [],
    }));
    return { chats };
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ error: 'Failed to fetch threads' });
  }
});

app.post<{
  Body: { id?: string; title: string; sources?: string[]; files?: { fileId: string; name: string }[] };
}>('/api/v1/library/threads', async (req, reply) => {
  const userId = await getUserIdFromToken(req.headers.authorization);
  if (!userId) {
    return reply.status(401).send({ error: 'Authorization required' });
  }
  const { id, title, sources = [], files = [] } = req.body ?? {};
  if (!title?.trim()) {
    return reply.status(400).send({ error: 'title is required' });
  }
  const threadId = id ?? crypto.randomUUID();
  try {
    const [inserted] = await db
      .insert(threads)
      .values({
        id: threadId,
        userId,
        title: title.trim(),
        sources,
        files,
      })
      .returning();
    return {
      id: inserted.id,
      title: inserted.title,
      createdAt: inserted.createdAt?.toISOString?.() ?? inserted.createdAt,
      sources: (inserted.sources as string[]) ?? [],
      files: (inserted.files as { fileId: string; name: string }[]) ?? [],
    };
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ error: 'Failed to save thread' });
  }
});

app.delete<{ Params: { id: string } }>('/api/v1/library/threads/:id', async (req, reply) => {
  const userId = await getUserIdFromToken(req.headers.authorization);
  if (!userId) {
    return reply.status(401).send({ error: 'Authorization required' });
  }
  const { id } = req.params;
  try {
    const [t] = await db.select({ id: threads.id }).from(threads).where(and(eq(threads.id, id), eq(threads.userId, userId)));
    if (!t) {
      return reply.status(404).send({ error: 'Thread not found' });
    }
    await db.delete(threads).where(eq(threads.id, id));
    return { ok: true };
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ error: 'Failed to delete thread' });
  }
});

/** GET thread by id with messages */
app.get<{ Params: { id: string } }>('/api/v1/library/threads/:id', async (req, reply) => {
  const userId = await getUserIdFromToken(req.headers.authorization);
  if (!userId) {
    return reply.status(401).send({ error: 'Authorization required' });
  }
  const { id } = req.params;
  try {
    const [t] = await db.select().from(threads).where(and(eq(threads.id, id), eq(threads.userId, userId)));
    if (!t) {
      return reply.status(404).send({ error: 'Thread not found' });
    }
    const msgList = await db
      .select()
      .from(threadMessages)
      .where(eq(threadMessages.threadId, id))
      .orderBy(asc(threadMessages.id));
    const messages = msgList.map((m) => ({
      messageId: m.messageId,
      chatId: id,
      backendId: m.backendId,
      query: m.query,
      createdAt: m.createdAt?.toISOString?.() ?? m.createdAt,
      responseBlocks: (m.responseBlocks ?? []) as Block[],
      status: m.status,
    }));
    return {
      chat: {
        id: t.id,
        title: t.title,
        createdAt: t.createdAt?.toISOString?.() ?? t.createdAt,
        sources: (t.sources as string[]) ?? [],
        files: (t.files as { fileId: string; name: string }[]) ?? [],
      },
      messages,
    };
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ error: 'Failed to fetch thread' });
  }
});

/** POST upsert message (для chat-svc при стриме). Если thread не существует — создаётся с title=query. */
app.post<{
  Params: { id: string };
  Body: {
    messageId: string;
    query: string;
    backendId?: string;
    responseBlocks?: Block[];
    status?: string;
    sources?: string[];
    files?: { fileId: string; name: string }[];
  };
}>('/api/v1/library/threads/:id/messages', async (req, reply) => {
  const userId = await getUserIdFromToken(req.headers.authorization);
  if (!userId) {
    return reply.status(401).send({ error: 'Authorization required' });
  }
  const { id } = req.params;
  const {
    messageId,
    query,
    backendId = '',
    responseBlocks = [],
    status = 'answering',
    sources = [],
    files = [],
  } = req.body ?? {};
  if (!messageId?.trim() || !query?.trim()) {
    return reply.status(400).send({ error: 'messageId and query are required' });
  }
  try {
    let [t] = await db.select({ id: threads.id }).from(threads).where(and(eq(threads.id, id), eq(threads.userId, userId)));
    if (!t) {
      await db.insert(threads).values({
        id,
        userId,
        title: query.length > 100 ? query.slice(0, 100) + '...' : query,
        sources,
        files,
      });
      t = { id };
    }
    const statusVal = (['answering', 'completed', 'error'] as const).includes(status as 'answering' | 'completed' | 'error') ? (status as 'answering' | 'completed' | 'error') : 'answering';
    const [existing] = await db.select().from(threadMessages).where(and(eq(threadMessages.threadId, t.id), eq(threadMessages.messageId, messageId)));
    if (existing) {
      await db
        .update(threadMessages)
        .set({ backendId, responseBlocks, status: statusVal })
        .where(eq(threadMessages.id, existing.id));
    } else {
      await db.insert(threadMessages).values({
        threadId: t.id,
        messageId,
        backendId,
        query,
        responseBlocks,
        status: statusVal,
      });
    }
    return { ok: true };
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ error: 'Failed to save message' });
  }
});

/** PATCH update message (responseBlocks, status) */
app.patch<{
  Params: { id: string; messageId: string };
  Body: { responseBlocks?: Block[]; status?: string };
}>('/api/v1/library/threads/:id/messages/:messageId', async (req, reply) => {
  const userId = await getUserIdFromToken(req.headers.authorization);
  if (!userId) {
    return reply.status(401).send({ error: 'Authorization required' });
  }
  const { id, messageId } = req.params;
  const body = req.body as { responseBlocks?: Block[]; status?: string } | undefined;
  if (!body?.responseBlocks && !body?.status) {
    return reply.status(400).send({ error: 'responseBlocks or status required' });
  }
  try {
    const [t] = await db.select({ id: threads.id }).from(threads).where(and(eq(threads.id, id), eq(threads.userId, userId)));
    if (!t) {
      return reply.status(404).send({ error: 'Thread not found' });
    }
    const [m] = await db.select().from(threadMessages).where(and(eq(threadMessages.threadId, id), eq(threadMessages.messageId, messageId)));
    if (!m) {
      return reply.status(404).send({ error: 'Message not found' });
    }
    const updates: { responseBlocks?: Block[]; status?: 'answering' | 'completed' | 'error' } = {};
    if (body.responseBlocks !== undefined) updates.responseBlocks = body.responseBlocks;
    if (body.status !== undefined && ['answering', 'completed', 'error'].includes(body.status)) {
      updates.status = body.status as 'answering' | 'completed' | 'error';
    }
    await db.update(threadMessages).set(updates).where(eq(threadMessages.id, m.id));
    return { ok: true };
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ error: 'Failed to update message' });
  }
});

/** GET export thread to PDF/MD */
const CREATE_SVC_URL = process.env.CREATE_SVC_URL ?? 'http://localhost:3011';
function extractTextFromBlocks(blocks: Block[]): string {
  const textBlock = blocks?.find((b) => b.type === 'text');
  if (textBlock && 'data' in textBlock && typeof textBlock.data === 'string') {
    return textBlock.data;
  }
  return '';
}
app.get<{ Params: { id: string }; Querystring: { format?: string } }>('/api/v1/library/threads/:id/export', async (req, reply) => {
  const userId = await getUserIdFromToken(req.headers.authorization);
  if (!userId) {
    return reply.status(401).send({ error: 'Authorization required' });
  }
  const { id } = req.params;
  const format = ((req.query?.format ?? 'pdf') as string).toLowerCase();
  if (format !== 'pdf' && format !== 'md') {
    return reply.status(400).send({ error: 'Invalid format. Use pdf or md.' });
  }
  try {
    const [t] = await db.select().from(threads).where(and(eq(threads.id, id), eq(threads.userId, userId)));
    if (!t) {
      return reply.status(404).send({ error: 'Thread not found' });
    }
    const msgList = await db.select().from(threadMessages).where(eq(threadMessages.threadId, id)).orderBy(asc(threadMessages.id));
    const sections = msgList.map((m) => {
      const blocks = (m.responseBlocks ?? []) as Block[];
      const text = extractTextFromBlocks(blocks);
      return {
        query: m.query ?? '',
        createdAt: m.createdAt?.toISOString?.() ?? m.createdAt,
        parsedTextBlocks: text ? [text] : [],
        responseBlocks: blocks.map((b) => ({ type: b.type, data: 'data' in b ? (b as { data?: unknown }).data : undefined })),
      };
    });
    if (sections.length === 0) {
      return reply.status(400).send({ error: 'Thread has no messages to export' });
    }
    const res = await fetch(`${CREATE_SVC_URL.replace(/\/$/, '')}/api/v1/export`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        format,
        title: t.title ?? `chat-${id.slice(0, 8)}`,
        sections,
      }),
      signal: AbortSignal.timeout(60000),
    });
    if (!res.ok) {
      const err = await res.json().catch(() => ({}));
      return reply.status(res.status).send(err);
    }
    const blob = await res.blob();
    const buf = Buffer.from(await blob.arrayBuffer());
    const contentType = res.headers.get('Content-Type') ?? 'application/octet-stream';
    const disposition =
      res.headers.get('Content-Disposition') ??
      `attachment; filename="${(t.title ?? 'chat').replace(/[^a-zA-Z0-9-_]/g, '_')}.${format}"`;
    return reply.header('Content-Type', contentType).header('Content-Disposition', disposition).send(buf);
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ error: 'Export failed' });
  }
});

try {
  await app.listen({ port: PORT, host: '0.0.0.0' });
  console.log(`library-svc listening on :${PORT}`);
} catch (err) {
  console.error(err);
  process.exit(1);
}
