import path from 'node:path';
import os from 'node:os';
import fs from 'node:fs';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import Fastify from 'fastify';
import fastifyStatic from '@fastify/static';
import { GlmClient, Agent, buildTools, ApprovalManager, DEFAULT_SYSTEM_PROMPT } from '@naomi/core';
import { loadConfig, saveConfig } from './config.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PUBLIC_DIR = path.join(__dirname, '..', 'public');

export async function startUiServer({ port } = {}) {
  const config = loadConfig();
  const approvals = new ApprovalManager();
  const subscribers = new Set();
  const chats = new Map();
  const approvalChat = new Map();

  function broadcast(event) {
    const frame = `data: ${JSON.stringify(event)}\n\n`;
    for (const res of subscribers) res.raw.write(frame);
  }

  function buildAgent(chatId) {
    const cfg = loadConfig();
    if (!cfg.server || !cfg.apiKey) return null;
    const client = new GlmClient({
      baseUrl: `${cfg.server.replace(/\/+$/, '')}/v1`,
      apiKey: cfg.apiKey,
      model: cfg.model,
    });
    const home = os.homedir();
    const agent = new Agent({
      client,
      tools: buildTools(),
      approvals,
      policy: cfg.policy,
      systemPrompt: `${DEFAULT_SYSTEM_PROMPT}

Домашняя директория пользователя: ${home}. Рабочий стол: ${home}/Desktop.`,
    });
    const ev = (type, extra) => broadcast({ type, chatId, ...extra });
    agent.on('llm_start', ({ step }) => ev('status', { status: 'thinking', step }));
    agent.on('reasoning', ({ chunk }) => ev('reasoning', { chunk }));
    agent.on('delta', ({ chunk }) => ev('delta', { chunk }));
    agent.on('llm_done', ({ usage }) => ev('message_done', { usage }));
    agent.on('approval_wait', (info) => {
      approvalChat.set(info.id, chatId);
      ev('approval', { ...info });
    });
    agent.on('tool_start', ({ name, args, approved }) => ev('tool_start', { name, args, approved }));
    agent.on('tool_end', ({ name, result, approved }) => ev('tool_end', { name, result, approved }));
    agent.on('done', ({ text, aborted }) => {
      const chat = chats.get(chatId);
      if (chat && !aborted) chat.history.push({ role: 'assistant', text });
      ev('done', { text, aborted });
    });
    agent.on('error', ({ error }) => ev('error', { error: error.message }));
    return agent;
  }

  function getChat(chatId, replayHistory) {
    let chat = chats.get(chatId);
    if (!chat) {
      const agent = buildAgent(chatId);
      if (!agent) return null;
      chat = { id: chatId, agent, history: [], title: '', controller: null };
      chats.set(chatId, chat);
      if (Array.isArray(replayHistory)) {
        for (const m of replayHistory.slice(-30)) {
          if ((m.role === 'user' || m.role === 'assistant') && m.text) {
            chat.history.push({ role: m.role, text: m.text });
            agent.messages.push({ role: m.role, content: m.text });
          }
        }
      }
    }
    return chat;
  }

  function abortChat(chat) {
    if (chat?.controller) chat.controller.abort();
    approvals.resolveAll(false);
  }

  const app = Fastify({ logger: false });
  app.register(fastifyStatic, { root: PUBLIC_DIR, cacheControl: false });
  try {
    const { createRequire } = await import('node:module');
    const req = createRequire(import.meta.url);
    const katexDist = path.join(path.dirname(req.resolve('katex/package.json')), 'dist');
    await app.register(fastifyStatic, { root: katexDist, prefix: '/katex/', decorateReply: false });
  } catch {
    console.log('[naomi ui] KaTeX не найден — формулы будут показаны как текст');
  }
  app.addHook('onSend', async (req, reply, payload) => {
    if (/\.(html|js|css)$/i.test(req.url) || req.url === '/') {
      reply.header('cache-control', 'no-cache');
    }
    return payload;
  });

  // корневую страницу отдаём сами: fastify-static стримит мимо onSend-хука,
  // а здесь нужно дописывать ассетам версии по mtime (см. ниже)
  app.get('/', async (req, reply) => {
    let html;
    try {
      html = fs.readFileSync(path.join(PUBLIC_DIR, 'index.html'), 'utf8');
    } catch {
      return reply.code(500).send('index.html не найден');
    }
    // версионирование ассетов по mtime: после каждой правки CSS/JS браузер гарантированно
    // забирает свежий файл, а не держит старый из кэша (реальная боль при итерациях)
    for (const asset of ['style-base.css', 'style-agent.css', 'md-renderer.js', 'app.js']) {
      try {
        const mtime = fs.statSync(path.join(PUBLIC_DIR, asset)).mtimeMs;
        html = html.replace(new RegExp(`/${asset}(\\?v=\\d+)?`), `/${asset}?v=${mtime}`);
      } catch {}
    }
    reply.type('text/html; charset=utf-8');
    return html;
  });

  app.get('/api/stream', async (req, reply) => {
    reply.raw.writeHead(200, {
      'content-type': 'text/event-stream',
      'cache-control': 'no-cache',
      connection: 'keep-alive',
    });
    reply.raw.write(`data: ${JSON.stringify({ type: 'hello' })}\n\n`);
    subscribers.add(reply);
    const ping = setInterval(() => {
      try { reply.raw.write(': ping\n\n'); } catch {}
    }, 25000);
    req.raw.on('close', () => { clearInterval(ping); subscribers.delete(reply); });
    return reply;
  });

  // точная история чата с сервера — для восстановления после потери SSE
  app.get('/api/chats/:id/messages', async (req, reply) => {
    const chat = chats.get(req.params.id);
    if (!chat) return reply.code(404).send({ error: 'чат не найден' });
    return {
      busy: chat.agent.busy,
      history: chat.history.filter((m) => m.role === 'user' || m.role === 'assistant'),
    };
  });

  app.get('/api/state', async () => {
    const cfg = loadConfig();
    return {
      config: { ...cfg, apiKey: maskKey(cfg.apiKey) },
      name: process.env.NAOMI_USER_NAME ?? '',
      chats: [...chats.values()].map((c) => ({ id: c.id, title: c.title, messages: c.history.length })),
      busyChats: [...chats.entries()].filter(([, c]) => c.agent.busy).map(([id]) => id),
      busyAny: [...chats.values()].some((c) => c.agent.busy),
      pendingApprovals: [...approvals.pending.values()].map(({ info }) => ({
        ...info,
        chatId: approvalChat.get(info.id) ?? null,
      })),
    };
  });

  // полный ключ только для локальной панели настроек: сервер слушает 127.0.0.1,
  // наружу не торчит, а пользователь имеет право видеть свой ключ целиком
  app.get('/api/key', async () => ({ key: loadConfig().apiKey ?? '' }));

  app.get('/api/reg-config', async (req, reply) => {
    const cfg = loadConfig();
    try {
      const res = await fetch(`${cfg.server.replace(/\/+$/, '')}/health`, { signal: AbortSignal.timeout(8000) });
      const json = await res.json();
      const base = cfg.server.replace(/\/+$/, '');
      const oauth = json.oauth ?? {};
      return {
        enabled: !!json.registration?.enabled,
        trial_credits: json.registration?.trialCredits ?? 0,
        oauth_urls: {
          google: oauth.google ? `${base}/auth/google` : null,
          yandex: oauth.yandex ? `${base}/auth/yandex` : null,
          vk: oauth.vk ? `${base}/auth/vk` : null,
        },
      };
    } catch (err) {
      return reply.code(502).send({ error: 'шлюз недоступен' });
    }
  });

  for (const action of ['register', 'login', 'rotate', 'login_or_register']) {
    app.post(`/api/account/${action}`, async (req, reply) => {
      const cfg = loadConfig();
      try {
        const res = await fetch(`${cfg.server.replace(/\/+$/, '')}/v1/account/${action}`, {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify(req.body ?? {}),
          signal: AbortSignal.timeout(15000),
        });
        const json = await res.json();
        return reply.code(res.status).send(json);
      } catch (err) {
        return reply.code(502).send({ error: `шлюз недоступен: ${err.message}` });
      }
    });
  }

  app.post('/api/register', async (req, reply) => {
    const cfg = loadConfig();
    try {
      const res = await fetch(`${cfg.server.replace(/\/+$/, '')}/v1/register`, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ name: req.body?.name ?? '' }),
        signal: AbortSignal.timeout(15000),
      });
      const json = await res.json();
      return reply.code(res.status).send(json);
    } catch (err) {
      return reply.code(502).send({ error: `шлюз недоступен: ${err.message}` });
    }
  });

  app.post('/api/config', async (req, reply) => {
    const body = req.body ?? {};
    const patch = {};
    for (const field of ['server', 'apiKey', 'model', 'policy']) {
      if (typeof body[field] === 'string' && body[field].trim()) patch[field] = body[field].trim();
      if (body[field] === null && field !== 'policy') patch[field] = '';
    }
    if (!['safe', 'ask', 'auto'].includes(patch.policy ?? '')) delete patch.policy;
    const next = saveConfig(patch);
    if (body.resetSessions !== false && (patch.apiKey !== undefined || patch.server !== undefined)) {
      approvals.resolveAll(false);
      chats.clear();
    } else if (patch.policy) {
      for (const c of chats.values()) c.agent.setPolicy(patch.policy);
    }
    if (patch.model) {
      for (const c of chats.values()) {
        if (c.agent?.client?.model !== undefined) c.agent.client.model = patch.model;
      }
    }
    broadcast({ type: 'config_updated', config: { ...next, apiKey: maskKey(next.apiKey) } });
    return { ok: true, config: { ...next, apiKey: maskKey(next.apiKey) } };
  });

  app.post('/api/chats', async (req) => {
    const chatId = crypto.randomUUID();
    const chat = getChat(chatId, null);
    if (!chat) return { error: 'нет ключа' };
    chat.title = String(req.body?.title ?? 'Новый чат');
    return { id: chatId, title: chat.title };
  });

  app.post('/api/chats/:id/delete', async (req) => {
    const chat = chats.get(req.params.id);
    if (chat?.agent.busy) approvals.resolveAll(false);
    chats.delete(req.params.id);
    return { ok: true };
  });

  app.post('/api/chat', async (req, reply) => {
    const text = String(req.body?.text ?? '').trim();
    const chatId = String(req.body?.chatId ?? 'main');
    const resend = req.body?.resend === true;
    if (!text) return reply.code(400).send({ error: 'пустой запрос' });
    const chat = getChat(chatId, req.body?.history);
    if (!chat) {
      return reply.code(400).send({ error: 'Не активирован ключ NAOMI — нажмите «Войти» вверху слева.' });
    }
    if (chat.agent.busy) {
      if (!resend) return reply.code(409).send({ error: 'Агент занят предыдущим запросом' });
      chat.controller?.abort();
      approvals.resolveAll(false);
      await chat.running;
    }
    if (resend && Array.isArray(req.body?.history)) {
      chat.agent.reset();
      chat.history = [];
      for (const m of req.body.history.slice(-40)) {
        if ((m.role === 'user' || m.role === 'assistant') && m.text) {
          chat.history.push({ role: m.role, text: m.text });
          chat.agent.messages.push({ role: m.role, content: m.text });
        }
      }
    }
    chat.history.push({ role: 'user', text });
    if (!chat.title || chat.title === 'Новый чат') {
      chat.title = text.slice(0, 40);
    }
    broadcast({ type: 'user', chatId, text, resend });

    // изображения: текстовые модели не видят картинки (z.ai code 1210) — описываем их
    // vision-моделью glm-4.5v и вшиваем описания в промпт; фолбэк — мультимодальный
    // content-массив на случай, когда основная модель умеет зрение
    const images = Array.isArray(req.body?.images) ? req.body.images.filter((u) => typeof u === 'string' && u.startsWith('data:image/')) : [];
    let userContent = text;
    if (images.length) {
      broadcast({ type: 'status', chatId, status: 'processing', step: 0 });
      let described = false;
      try {
        const cfg0 = loadConfig();
        const describeOnce = async (url, tryIdx) => {
          const res = await fetch(`${cfg0.server.replace(/\/+$/, '')}/v1/chat/completions`, {
            method: 'POST',
            headers: { 'content-type': 'application/json', authorization: `Bearer ${cfg0.apiKey}` },
            body: JSON.stringify({
              model: 'glm-4.5v',
              stream: false,
              max_tokens: 900,
              thinking: { type: 'disabled' }, // описание не думает — быстрее и дешевле
              messages: [{
                role: 'user',
                content: [
                  { type: 'text', text: 'Опиши изображение детально и фактично для текстовой языковой модели: весь видимый текст ДОСЛОВНО, объекты и их действия, цвета, расположение элементов, характер изображения (скриншот/фото/схема). Без вступлений, только описание.' },
                  { type: 'image_url', image_url: { url } },
                ],
              }],
            }),
            signal: AbortSignal.timeout(45000),
          });
          // лимит/поломка апстрима — один повтор с паузой (шлюз дева 1 в эти моменты
          // может уводить запрос на текстовую модель, которая картинки не принимает)
          if (!res.ok && (res.status === 429 || res.status >= 500) && tryIdx === 0) {
            await new Promise((r) => setTimeout(r, 1500));
            return describeOnce(url, 1);
          }
          if (!res.ok) throw new Error(`vision ${res.status}`);
          const json = await res.json();
          const d = json.choices?.[0]?.message?.content;
          if (typeof d !== 'string' || !d.trim()) throw new Error('vision empty');
          return d.trim();
        };
        const descriptions = await Promise.all(images.map((url) => describeOnce(url, 0)));
        const blocks = images.map((_u, i) => `[Изображение ${i + 1}]\n${descriptions[i]}`);
        userContent = `${text}\n\n${blocks.join('\n\n')}\n\n(Ты не видишь изображения напрямую — выше детальные описания от vision-модели, опирайся на них.)`;
        described = true;
      } catch {
        // описатель недоступен — попробуем отдать картинки как есть (для мультимодальных моделей)
      }
      if (!described) {
        userContent = [{ type: 'text', text }, ...images.map((url) => ({ type: 'image_url', image_url: { url } }))];
      }
    }

    chat.controller = new AbortController();
    chat.running = chat.agent.run(userContent, { signal: chat.controller.signal }).catch(() => {});
    return { ok: true };
  });

  app.post('/api/stop', async (req) => {
    const chatId = String(req.body?.chatId ?? '');
    const chat = chats.get(chatId);
    if (chat?.controller) chat.controller.abort();
    approvals.resolveAll(false);
    broadcast({ type: 'stopped', chatId });
    return { ok: true };
  });

  app.post('/api/approve', async (req, reply) => {
    const { id, approved } = req.body ?? {};
    const ok = approvals.resolve(String(id), !!approved);
    approvalChat.delete(String(id));
    if (!ok) return reply.code(404).send({ error: 'запрос подтверждения не найден' });
    return { ok: true };
  });

  app.get('/api/balance', async (req, reply) => {
    const cfg = loadConfig();
    if (!cfg.apiKey) return reply.code(400).send({ error: 'ключ не настроен' });
    try {
      const res = await fetch(`${cfg.server.replace(/\/+$/, '')}/v1/balance`, {
        headers: { authorization: `Bearer ${cfg.apiKey}` },
        signal: AbortSignal.timeout(8000),
      });
      const json = await res.json();
      if (!res.ok) return reply.code(res.status).send(json);
      return json;
    } catch (err) {
      return reply.code(502).send({ error: `шлюз недоступен: ${err.message}` });
    }
  });

  app.all('/api/gw/*', async (req, reply) => {
    const cfg = loadConfig();
    if (!cfg.apiKey) return reply.code(400).send({ error: 'ключ не настроен' });
    const sub = req.url.replace(/^\/api\/gw\//, '').replace(/\?.*$/, '');
    const query = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
    try {
      const res = await fetch(`${cfg.server.replace(/\/+$/, '')}/v1/${sub}${query}`, {
        method: req.method,
        headers: {
          'content-type': 'application/json',
          authorization: `Bearer ${cfg.apiKey}`,
        },
        ...(req.method !== 'GET' && req.method !== 'HEAD' ? { body: JSON.stringify(req.body ?? {}) } : {}),
        signal: AbortSignal.timeout(20000),
      });
      const json = await res.json().catch(() => ({}));
      return reply.code(res.status).send(json);
    } catch (err) {
      return reply.code(502).send({ error: `шлюз недоступен: ${err.message}` });
    }
  });

  const uiPort = port ?? config.uiPort;
  app.listen({ port: uiPort, host: '127.0.0.1' }, (err) => {
    if (err) throw err;
    const url = `http://127.0.0.1:${uiPort}`;
    console.log(`\n  NAOMI запущен: ${url}\n`);
    if (!loadConfig().apiKey) {
      console.log('  Ключ ещё не активирован: откройте интерфейс → «Войти» → вставьте ключ naomi_…\n');
    }
    const opener = process.platform === 'win32' ? 'start'
      : process.platform === 'darwin' ? 'open' : 'xdg-open';
    import('node:child_process').then(({ spawn }) => {
      //ENOENT и прочие ошибки открытия браузера (headless) не должны ронять сервер
      try {
        const child = spawn(opener, [url], { detached: true, stdio: 'ignore' });
        child.on('error', () => {});
        child.unref();
      } catch {}
    });
  });

  const shutdown = () => {
    approvals.resolveAll(false);
    process.exit(0);
  };
  process.on('SIGINT', shutdown);
  process.on('SIGTERM', shutdown);
}

function maskKey(key) {
  if (!key) return '';
  if (key.length <= 14) return key;
  return `${key.slice(0, 10)}...${key.slice(-4)}`;
}
