/**
 * chat-svc — full chat logic from apps/frontend
 * API: POST /api/v1/chat (NDJSON stream), GET /health
 * Config/providers: GET/POST /api/v1/config, GET/POST/DELETE/PATCH /api/v1/providers, models CRUD
 * Messages сохраняются в library-svc (при auth), SQLite удалён
 */

import Fastify from 'fastify';
import multipart from '@fastify/multipart';
import { LibraryClient } from './lib/library-client.js';
import cors from '@fastify/cors';
import { z } from 'zod';
import ModelRegistry from './lib/models/registry.js';
import UploadManager from './lib/uploads/manager.js';
import configManager from './lib/config/index.js';
import { isEnvOnlyMode } from './lib/config/serverRegistry.js';
import { ConfigModelProvider } from './lib/config/types.js';
import SearchAgent from './lib/agents/search/index.js';
import SessionManager from './lib/session.js';
import { ChatTurnMessage } from './lib/types.js';
import { SearchSources } from './lib/agents/search/types.js';
import path from 'node:path';
import fs from 'node:fs';

const DATA_DIR = process.env.DATA_DIR
  ? path.resolve(process.cwd(), process.env.DATA_DIR)
  : process.cwd();

const dataDir = path.join(DATA_DIR, 'data');
if (!fs.existsSync(dataDir)) {
  fs.mkdirSync(dataDir, { recursive: true });
}
const configPath = path.join(dataDir, 'config.json');
if (!fs.existsSync(configPath)) {
  fs.writeFileSync(configPath, JSON.stringify({
    version: 1,
    setupComplete: false,
    preferences: {},
    personalization: {},
    modelProviders: [],
    search: { searxngURL: '', newsRegion: 'auto' },
  }, null, 2));
}

const PORT = parseInt(process.env.PORT ?? '3005', 10);
const MEMORY_SVC_URL = process.env.MEMORY_SVC_URL ?? '';
const LLM_SVC_URL = process.env.LLM_SVC_URL ?? '';

const messageSchema = z.object({
  messageId: z.string().min(1),
  chatId: z.string().min(1),
  content: z.string().min(1),
});

const answerModeEnum = z.enum(['standard', 'focus', 'academic', 'writing', 'travel', 'finance']);
const councilModelSchema = z.object({ providerId: z.string(), key: z.string() });
const bodySchema = z.object({
  message: messageSchema,
  optimizationMode: z.enum(['speed', 'balanced', 'quality']),
  sources: z.array(z.string()).optional().default([]),
  history: z.array(z.tuple([z.string(), z.string()])).optional().default([]),
  files: z.array(z.string()).optional().default([]),
  chatModel: z.object({ providerId: z.string(), key: z.string() }),
  embeddingModel: z.object({ providerId: z.string().optional().default(''), key: z.string().optional().default('') }).optional().default({ providerId: '', key: '' }),
  systemInstructions: z.string().nullable().optional().default(''),
  locale: z.string().optional(),
  answerMode: answerModeEnum.optional().default('standard'),
  responsePrefs: z
    .object({
      format: z.string().optional(),
      length: z.string().optional(),
      tone: z.string().optional(),
    })
    .optional(),
  learningMode: z.boolean().optional().default(false),
  /** Model Council (Max): 3 models in parallel → synthesis */
  modelCouncil: z.boolean().optional().default(false),
  councilModels: z.array(councilModelSchema).length(3).optional(),
});

type Body = z.infer<typeof bodySchema>;

const app = Fastify({ logger: true });
const corsOrigin = process.env.ALLOWED_ORIGINS
  ? process.env.ALLOWED_ORIGINS.split(',').map((s) => s.trim()).filter(Boolean)
  : true;
await app.register(cors, { origin: corsOrigin });
await app.register(multipart, { limits: { fileSize: 50 * 1024 * 1024 } });

app.get('/health', async () => ({ status: 'ok' }));
app.get('/ready', async () => ({ status: 'ready' }));

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\n' +
    'gooseek_up 1\n'
  );
});

/* --- Config --- */
app.get('/api/v1/config', async (_req, reply) => {
  try {
    const values = configManager.getCurrentConfig();
    const fields = configManager.getUIConfigSections();
    let modelProviders: ConfigModelProvider[];
    let envOnlyMode: boolean;
    if (LLM_SVC_URL) {
      const base = LLM_SVC_URL.replace(/\/$/, '');
      const res = await fetch(`${base}/api/v1/providers`, {
        signal: AbortSignal.timeout(5000),
      });
      if (!res.ok) throw new Error(`llm-svc fetch failed: ${res.status}`);
      const data = (await res.json()) as { providers: ConfigModelProvider[]; envOnlyMode?: boolean };
      modelProviders = data.providers ?? [];
      envOnlyMode = data.envOnlyMode ?? false;
      values.modelProviders = modelProviders;
    } else {
      const registry = new ModelRegistry();
      const providers = await registry.getActiveProviders();
      modelProviders = values.modelProviders.map((mp: ConfigModelProvider) => {
        const activeProvider = providers.find((p) => p.id === mp.id);
        return {
          ...mp,
          chatModels: activeProvider?.chatModels ?? mp.chatModels,
          embeddingModels: activeProvider?.embeddingModels ?? mp.embeddingModels,
        };
      });
      values.modelProviders = modelProviders;
      envOnlyMode = isEnvOnlyMode();
    }
    return reply.send({ values, fields, modelProviders, envOnlyMode });
  } catch (err) {
    app.log.error(err);
    return reply.status(500).send({ message: 'An error has occurred.' });
  }
});

const configPostSchema = z.object({ key: z.string().min(1), value: z.any() });
app.post<{ Body: unknown }>('/api/v1/config', async (req, reply) => {
  const parsed = configPostSchema.safeParse(req.body);
  if (!parsed.success) {
    return reply.status(400).send({ message: 'Key and value are required.', error: parsed.error.issues });
  }
  try {
    configManager.updateConfig(parsed.data.key, parsed.data.value);
    return reply.send({ message: 'Config updated successfully.' });
  } catch (err) {
    app.log.error(err);
    return reply.status(500).send({ message: 'An error has occurred.' });
  }
});

const setupCompleteSchema = z.object({ complete: z.boolean() });
app.post<{ Body: unknown }>('/api/v1/config/setup-complete', async (req, reply) => {
  const parsed = setupCompleteSchema.safeParse(req.body);
  if (!parsed.success) {
    return reply.status(400).send({ message: 'complete (boolean) is required.', error: parsed.error.issues });
  }
  try {
    configManager.updateConfig('setupComplete', parsed.data.complete);
    return reply.send({ message: 'Setup marked as complete.' });
  } catch (err) {
    app.log.error(err);
    return reply.status(500).send({ message: 'An error has occurred.' });
  }
});

/* --- Providers: routed to llm-svc via api-gateway --- */

/* --- Uploads --- */
app.post('/api/v1/uploads', async (req, reply) => {
  try {
    let providerId = '';
    let modelKey = '';
    const fileParts: { buffer: Buffer; filename: string; mimetype: string }[] = [];

    const parts = req.parts();
    for await (const part of parts) {
      if (part.type === 'field') {
        const val = (part as { value?: string }).value ?? '';
        if (part.fieldname === 'embedding_model_provider_id') providerId = val;
        else if (part.fieldname === 'embedding_model_key') modelKey = val;
      } else if (part.type === 'file' && part.file) {
        const buffer = await part.toBuffer();
        fileParts.push({
          buffer,
          filename: part.filename ?? 'file',
          mimetype: part.mimetype ?? 'application/octet-stream',
        });
      }
    }

    if (!providerId || !modelKey) {
      return reply.status(400).send({ message: 'Missing embedding model or provider' });
    }
    if (fileParts.length === 0) {
      return reply.status(400).send({ message: 'No files uploaded' });
    }

    const registry = new ModelRegistry();
    const embedding = await registry.loadEmbeddingModel(providerId, modelKey);
    const manager = new UploadManager({ embeddingModel: embedding });

    const fileObjects = fileParts.map(
      (p) => new File([new Blob([p.buffer], { type: p.mimetype })], p.filename, { type: p.mimetype }),
    );
    const processed = await manager.processFiles(fileObjects);
    return reply.send({ files: processed });
  } catch (err) {
    app.log.error(err);
    return reply.status(500).send({ message: 'An error has occurred.' });
  }
});

app.post<{ Body: unknown }>('/api/v1/chat', async (req, reply) => {
  const parseBody = bodySchema.safeParse(req.body);
  if (!parseBody.success) {
    return reply.status(400).send({ message: 'Invalid request body', error: parseBody.error.issues });
  }
  const body = parseBody.data as Body;
  if (body.message.content === '') {
    return reply.status(400).send({ message: 'Please provide a message to process' });
  }

  let memoryContext: string | undefined;
  const authHeader = req.headers.authorization;
  const useMemory = MEMORY_SVC_URL && authHeader && (body.optimizationMode === 'balanced' || body.optimizationMode === 'quality');
  if (useMemory) {
    try {
      const memRes = await fetch(`${MEMORY_SVC_URL.replace(/\/$/, '')}/api/v1/memory`, {
        headers: { Authorization: authHeader! },
        signal: AbortSignal.timeout(3000),
      });
      if (memRes.ok) {
        const memData = (await memRes.json()) as { items?: { key: string; value: string }[] };
        const items = memData.items ?? [];
        if (items.length > 0) {
          memoryContext = items.map((r) => `- ${r.key}: ${r.value}`).join('\n');
        }
      }
    } catch (err) {
      req.log.warn({ err }, 'Memory fetch failed');
    }
  }

  try {
    const registry = new ModelRegistry();
    const llm = await registry.loadChatModel(body.chatModel.providerId, body.chatModel.key);
    let embedding = null;
    if (body.embeddingModel?.providerId) {
      embedding = await registry.loadEmbeddingModel(body.embeddingModel.providerId, body.embeddingModel.key);
    }
    let councilLlms: [Awaited<ReturnType<ModelRegistry['loadChatModel']>>, Awaited<ReturnType<ModelRegistry['loadChatModel']>>, Awaited<ReturnType<ModelRegistry['loadChatModel']>>] | undefined;
    if (body.modelCouncil && body.councilModels && body.councilModels.length === 3) {
      try {
        councilLlms = [
          await registry.loadChatModel(body.councilModels[0].providerId, body.councilModels[0].key),
          await registry.loadChatModel(body.councilModels[1].providerId, body.councilModels[1].key),
          await registry.loadChatModel(body.councilModels[2].providerId, body.councilModels[2].key),
        ];
      } catch (councilErr) {
        req.log.warn({ err: councilErr }, 'Model Council: failed to load 3 models, falling back to single');
      }
    }
    const history: ChatTurnMessage[] = body.history.map((msg) =>
      msg[0] === 'human' ? { role: 'user' as const, content: msg[1] } : { role: 'assistant' as const, content: msg[1] });
    const libraryClient = LibraryClient.create(authHeader);
    const config = {
      llm,
      embedding,
      sources: body.sources as SearchSources[],
      mode: body.optimizationMode,
      fileIds: body.files,
      systemInstructions: body.systemInstructions || 'None',
      locale: body.locale ?? 'en',
      memoryContext,
      answerMode: body.answerMode,
      responsePrefs: body.responsePrefs,
      learningMode: body.learningMode,
      libraryClient,
      ...(councilLlms && { councilLlms }),
    };
    const agent = new SearchAgent();
    const session = SessionManager.createSession();
    const encoder = new TextEncoder();
    const stream = new ReadableStream({
      start(controller) {
        const disconnect = session.subscribe((event: string, data: unknown) => {
          const d = data as { type?: string; block?: unknown; blockId?: string; patch?: unknown; data?: unknown };
          if (event === 'data') {
            if (d.type === 'block') controller.enqueue(encoder.encode(JSON.stringify({ type: 'block', block: d.block }) + '\n'));
            else if (d.type === 'updateBlock') controller.enqueue(encoder.encode(JSON.stringify({ type: 'updateBlock', blockId: d.blockId, patch: d.patch }) + '\n'));
            else if (d.type === 'researchComplete') controller.enqueue(encoder.encode(JSON.stringify({ type: 'researchComplete' }) + '\n'));
          } else if (event === 'end') {
            controller.enqueue(encoder.encode(JSON.stringify({ type: 'messageEnd' }) + '\n'));
            controller.close();
            session.removeAllListeners();
          } else if (event === 'error') {
            controller.enqueue(encoder.encode(JSON.stringify({ type: 'error', data: d.data }) + '\n'));
            controller.close();
            session.removeAllListeners();
          }
        });
        agent.searchAsync(session, { chatHistory: history, followUp: body.message.content, chatId: body.message.chatId, messageId: body.message.messageId, config }).catch((err: Error) => {
          req.log.error(err);
          session.emit('error', { data: err?.message ?? 'Error during search.' });
        });
        req.raw.on?.('abort', () => { disconnect(); try { controller.close(); } catch {} });
      },
    });
    return reply.header('Content-Type', 'application/x-ndjson').header('Cache-Control', 'no-cache').send(stream);
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ message: 'An error occurred while processing chat request' });
  }
});

/**
 * Background Assistant (Max) — фоновые задачи
 * docs/architecture: 01-perplexity-analogue-design.md §5.15
 * stub: POST создаёт taskId, GET возвращает status
 */
const taskSchema = z.object({
  query: z.string().min(1),
  mode: z.enum(['quick', 'pro', 'deep']).optional().default('pro'),
  callback: z.enum(['push', 'poll']).optional().default('poll'),
});
app.post<{ Body: unknown }>('/api/v1/tasks', async (req, reply) => {
  const parsed = taskSchema.safeParse(req.body);
  if (!parsed.success) {
    return reply.status(400).send({ error: 'Invalid request', issues: parsed.error.issues });
  }
  const taskId = crypto.randomUUID();
  return reply.send({ taskId });
});
app.get<{ Params: { id: string } }>('/api/v1/tasks/:id', async (req, reply) => {
  return reply.send({
    taskId: req.params.id,
    status: 'pending',
    result: null,
    error: null,
    _stub: true,
    message: 'Background tasks will be implemented in a future release (Max).',
  });
});

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