import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import Fastify from 'fastify';
import cors from '@fastify/cors';
import { createDb } from './db.js';
import { Billing } from './billing.js';
import { RateLimiter } from './ratelimit.js';
import { loadPlugins, runHook } from './plugins.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');

function loadEnv() {
  const envPath = path.join(ROOT, '.env');
  if (fs.existsSync(envPath)) {
    for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
      const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
      if (match && process.env[match[1]] === undefined) {
        process.env[match[1]] = match[2].replace(/^["']|["']$/g, '');
      }
    }
  }
  const num = (name, def) => Number(process.env[name] ?? def);
  const str = (name, def) => (process.env[name] ?? def).trim();
  return {
    port: num('PORT', 8080),
    dataDir: path.resolve(ROOT, str('DATA_DIR', 'data')),
    databaseUrl: str('DATABASE_URL', ''),
    glmBaseUrl: str('GLM_BASE_URL', 'https://open.bigmodel.cn/api/paas/v4').replace(/\/+$/, ''),
    glmApiKey: str('GLM_API_KEY', ''),
    // glm-4.5v (vision-описатель картинок из local-server) доступен всегда,
    // независимо от MODELS в env — иначе фотки не работают там, где env не расширили
    models: [...new Set([...str('MODELS', 'glm-4.6,glm-4.5,glm-4.5-air').split(',').map((m) => m.trim()).filter(Boolean), 'glm-4.5v'])],
    thinkingOff: str('THINKING_OFF', 'glm-4.5-flash,glm-4.7-flash').split(',').map((m) => m.trim()).filter(Boolean),
    modelRates: Object.fromEntries(
      str('MODEL_RATES', 'glm-4.5-flash:2,glm-4.7-flash:2,glm-5.3-flash:3.5,glm-5.3:18,glm-4.6:9')
        .split(',').map((pair) => pair.split(':').map((x) => x.trim()))
        .filter(([m, k]) => m && Number(k) > 0)
    ),
    adminKey: str('ADMIN_KEY', ''),
    billing: new Billing({
      perRequestMilli: num('BILL_PER_REQUEST', 0),
      perMillionInMilli: num('BILL_PER_1M_INPUT', 15000),
      perMillionOutMilli: num('BILL_PER_1M_OUTPUT', 45000),
    }),
    ratePerMinute: num('RATE_PER_MINUTE', 30),
    paymentInfo: str('PAYMENT_INFO', 'Реквизиты для оплаты уточните у владельца NAOMI.'),
    priceRate: num('PRICE_RATE', 1),
    // пакеты вида "500:500,1500:1350" — кредиты:цена в ₽; цена опциональна (тогда кредиты × PRICE_RATE);
    // скидка на объём = «подписка» в духе study24: клиент предоплачивает токены дешевле
    packs: str('TOPUP_PACKS', '100,500,1000').split(',').map((p) => p.trim().split(':')).filter(([c]) => Number(c) > 0)
      .map(([c, price]) => ({ credits: Number(c), price: Number(price) > 0 ? Number(price) : Number(c) * num('PRICE_RATE', 1) })),
    yookassaShopId: str('YOOKASSA_SHOP_ID', ''),
    yookassaSecretKey: str('YOOKASSA_SECRET_KEY', ''),
    currency: str('PAYMENT_CURRENCY', 'RUB'),
    registration: {
      enabled: str('REGISTRATION_ENABLED', 'false') === 'true',
      trialCredits: num('REG_TRIAL_CREDITS', 10),
      perIpPerDay: num('REG_PER_IP_PER_DAY', 3),
    },
    google: {
      clientId: str('GOOGLE_CLIENT_ID', ''),
      clientSecret: str('GOOGLE_CLIENT_SECRET', ''),
    },
    yandex: {
      clientId: str('YANDEX_CLIENT_ID', ''),
      clientSecret: str('YANDEX_CLIENT_SECRET', ''),
    },
    vk: {
      clientId: str('VK_CLIENT_ID', ''),
      clientSecret: str('VK_CLIENT_SECRET', ''),
    },
  };
}

const config = loadEnv();

if (!config.glmApiKey) {
  console.error('[NAOMI gateway] Ошибка: не задан GLM_API_KEY. Скопируйте .env.example в .env и заполните.');
  process.exit(1);
}

if (!config.adminKey) {
  const adminKeyFile = path.join(config.dataDir, 'admin.key');
  if (fs.existsSync(adminKeyFile)) {
    config.adminKey = fs.readFileSync(adminKeyFile, 'utf8').trim();
  } else {
    config.adminKey = crypto.randomBytes(24).toString('hex');
    fs.mkdirSync(config.dataDir, { recursive: true });
    fs.writeFileSync(adminKeyFile, config.adminKey, { mode: 0o600 });
    console.log(`[NAOMI gateway] Сгенерирован ключ администратора: ${config.adminKey}\n               (сохранён в ${adminKeyFile})`);
  }
}

const db = await createDb({ databaseUrl: config.databaseUrl || undefined, dataDir: config.dataDir });
db.rollupUsage().catch((err) => console.error('[NAOMI gateway] rollup:', err.message));
setInterval(() => db.rollupUsage().catch(() => {}), 3600_000).unref();
const limiter = new RateLimiter({ requestsPerMinute: config.ratePerMinute });
const plugins = await loadPlugins(path.join(ROOT, 'plugins'));
if (plugins.length) console.log(`[NAOMI gateway] Плагины: ${plugins.map((p) => p.name).join(', ')}`);

/* ─── Здоровье бесплатных моделей ───
   У z.ai скорость бесплатных flash-моделей непредсказуемо скачет: одна и та же
   модель может отвечать за 0,5с и за минуту в разные часы. Фоновый замер знает,
   какая живая и насколько шустрая, и «Эконом» ездит по самой быстрой.
   Интервал 15 минут: у бесплатного тира есть лимит запросов, частый зонд
   съедает квоту (наблюдаемо ловили 429 «rate limit» от собственного же замера) */
const FREE_MODELS = ['glm-4.5-flash', 'glm-4.7-flash'];
const PROBE_INTERVAL_MS = Number(process.env.PROBE_INTERVAL_MS ?? 900_000);
const PROBE_TIMEOUT_MS = 45_000; // пережидаем и медленный старт (TTFT 24–30с), не только живой
const PROBE_FRESH_MS = 20 * 60_000; // данные старше 20 минут не доверяем
const FIRST_BYTE_GUARD_MS = Number(process.env.FIRST_BYTE_GUARD_MS ?? 8_000); // зависший апстрим переключаем; env позволяет пережидать деградацию провайдера без правки кода
const probeState = new Map(); // model -> { ttftMs, ok, at }
let econNow = ''; // текущая самая быстрая бесплатная (для лога смены)

async function probeModel(model) {
  const t0 = Date.now();
  try {
    const res = await fetch(`${config.glmBaseUrl}/chat/completions`, {
      method: 'POST',
      headers: { 'content-type': 'application/json', authorization: `Bearer ${config.glmApiKey}` },
      body: JSON.stringify({
        model, stream: true, max_tokens: 4,
        messages: [{ role: 'user', content: 'пинг' }],
        ...(config.thinkingOff.includes(model) ? { thinking: { type: 'disabled' } } : {}),
      }),
      signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
    });
    if (!res.ok) {
      // 429 «перегружен» — модель сейчас не служит: помечаем недоступной,
      // маршрут уйдёт на другую (если та жива), замер через цикл обновит
      probeState.set(model, { ttftMs: Infinity, ok: false, at: Date.now() });
      return;
    }
    const reader = res.body.getReader();
    await reader.read(); // первый чанк = время до первого токена
    reader.cancel().catch(() => {});
    probeState.set(model, { ttftMs: Date.now() - t0, ok: true, at: Date.now() });
  } catch {
    probeState.set(model, { ttftMs: Infinity, ok: false, at: Date.now() });
  }
}

async function probeFreeModels() {
  for (const model of FREE_MODELS) {
    if (config.models.includes(model)) await probeModel(model);
  }
  const fastest = econRoute(FREE_MODELS[0]);
  if (fastest !== econNow) {
    econNow = fastest;
    const p = probeState.get(econNow);
    console.log(`[NAOMI gateway] Эконом → ${econNow} (${p?.ok ? Math.round(p.ttftMs) + 'мс' : 'замер не прошёл, ждём следующий'})`);
  }
}

// маршрут «Эконома»: среди свежих успешных замеров берём самую быструю,
// иначе — как просил клиент (модель из бесплатного тира)
function econRoute(preferred) {
  if (!FREE_MODELS.includes(preferred) || !config.models.includes(preferred)) return preferred;
  const now = Date.now();
  const scored = FREE_MODELS
    .filter((m) => m !== preferred && config.models.includes(m))
    .map((m) => ({ m, p: probeState.get(m) }))
    .filter(({ p }) => p?.ok && now - p.at < PROBE_FRESH_MS && p.ttftMs < 5000);
  if (scored.length && probeState.get(preferred)?.ttftMs > scored[0].p.ttftMs * 2) {
    return scored[0].m; // заметно быстрее — едем туда
  }
  return preferred;
}

if (config.models.some((m) => FREE_MODELS.includes(m))) {
  probeFreeModels();
  setInterval(probeFreeModels, PROBE_INTERVAL_MS).unref();
}

// зависание апстрима до заголовков: abort и переход к следующей модели из цепочки
function fetchWithHeaderGuard(url, opts, ms) {
  if (!ms) return fetch(url, opts);
  const ac = new AbortController();
  const timer = setTimeout(() => ac.abort(), ms);
  return fetch(url, { ...opts, signal: ac.signal }).finally(() => clearTimeout(timer));
}

const app = Fastify({ logger: false, bodyLimit: 10 * 1024 * 1024, trustProxy: true });
const oauthRedirectUri = (req, provider) => {
  const proto = req.headers['x-forwarded-proto'] ?? 'http';
  return `${proto}://${req.headers.host}/auth/${provider}/callback`;
};
await app.register(cors, { origin: true });

async function authKey(req, reply) {
  const header = req.headers.authorization ?? '';
  const key = header.startsWith('Bearer ') ? header.slice(7).trim() : '';
  if (!key.startsWith('naomi_')) {
    reply.code(401).send({ error: { message: 'Нужен ключ вида naomi_... в Authorization: Bearer' } });
    return null;
  }
  const row = await db.getKey(key);
  if (!row) {
    reply.code(401).send({ error: { message: 'Ключ не найден' } });
    return null;
  }
  if (row.blocked) {
    reply.code(403).send({ error: { message: 'Ключ заблокирован' } });
    return null;
  }
  if (row.balance_milli <= 0) {
    reply.code(402).send({ error: { message: 'Баланс исчерпан, пополните ключ', code: 'insufficient_funds' } });
    return null;
  }
  if (!limiter.allow(key)) {
    reply.code(429).send({ error: { message: 'Слишком много запросов, подождите минуту' } });
    return null;
  }
  return row;
}

function authAdmin(req, reply) {
  const header = req.headers.authorization ?? '';
  const token = header.startsWith('Bearer ') ? header.slice(7).trim() : '';
  if (token !== config.adminKey) {
    reply.code(401).send({ error: { message: 'Неверный админ-ключ' } });
    return false;
  }
  return true;
}

app.get('/v1/balance', async (req, reply) => {
  const row = await authKey(req, reply);
  if (!row) return;
  const since = Date.now() - 30 * 24 * 3600 * 1000;
  const usage = await db.usageByKey(row.id, since);
  return {
    label: row.label,
    balance: row.balance_milli / 1000,
    models: config.models,
    usage_30d: {
      requests: usage.requests ?? 0,
      prompt_tokens: usage.prompt_tokens ?? 0,
      completion_tokens: usage.completion_tokens ?? 0,
      spent: (usage.cost_milli ?? 0) / 1000,
    },
  };
});

app.post('/v1/chat/completions', async (req, reply) => {
  const row = await authKey(req, reply);
  if (!row) return;

  const body = req.body ?? {};
  if (!Array.isArray(body.messages) || !body.messages.length) {
    return reply.code(400).send({ error: { message: 'messages обязателен' } });
  }
  if (body.model && !config.models.includes(body.model)) {
    return reply.code(400).send({ error: { message: `Модель недоступна. Доступны: ${config.models.join(', ')}` } });
  }
  body.model = body.model || config.models[0];

  try {
    await runHook(plugins, 'onRequest', { key: row.key, body });
  } catch (rejected) {
    return reply.code(rejected.statusCode ?? 403).send({ error: { message: rejected.message ?? 'отклонено плагином' } });
  }

  const upstreamBody = { ...body };
  const isStream = upstreamBody.stream === true;
  if (isStream) upstreamBody.stream_options = { include_usage: true };
  // «Эконом» едет по самой быстрой бесплатной модели по последнему замеру
  const routed = econRoute(body.model);
  upstreamBody.model = routed;
  if (config.thinkingOff.includes(routed)) upstreamBody.thinking = { type: 'disabled' };
  delete upstreamBody.naomi_session;

  // при перегрузе модели (429/5xx/зависание): для бесплатного тира — максимум
  // две попытки (два зависания по 8с ≈ 17с, вписываемся в бюджет 10–25с),
  // для платных — как раньше, три ретрая + фолбэк по цепочке
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  const primary = routed;
  const freeTier = FREE_MODELS.includes(body.model);
  const freeAlternate = freeTier
    ? FREE_MODELS.find((m) => m !== primary && config.models.includes(m))
    : undefined;
  // адаптивный гарант: если последний замер знает, что модель живая, но медленная
  // (деградация бесплатного тира, TTFT 24–30с), — пережидаем её и отвечаем, а не 503;
  // фолбэки остаются на базовом гарде, чтобы не тянуть две медленные попытки подряд
  const probeOfPrimary = probeState.get(primary);
  const primaryGuard = freeTier
    ? Math.min(45_000, Math.max(FIRST_BYTE_GUARD_MS, (probeOfPrimary?.ok ? probeOfPrimary.ttftMs : 0) + 10_000))
    : 0;
  // альтернатива идёт дважды: 429 «перегружен» у z.ai часто разовый,
  // повтор после секундной паузы спасает (429 отвечает мгновенно, бюджет не рвёт)
  const fallbackChain = freeTier
    ? [primary, ...(freeAlternate ? [freeAlternate, freeAlternate] : [primary])]
    : [primary, primary, primary, ...config.models.filter((m) => m !== primary)];
  let upstream;
  let lastStatus = 0;
  let lastErrText = '';
  for (let i = 0; i < fallbackChain.length; i++) {
    const candidate = fallbackChain[i];
    if (i > 0 && candidate === fallbackChain[i - 1]) await sleep(900);
    const attemptBody = { ...upstreamBody, model: candidate };
    if (config.thinkingOff.includes(candidate)) attemptBody.thinking = { type: 'disabled' };
    try {
      upstream = await fetchWithHeaderGuard(`${config.glmBaseUrl}/chat/completions`, {
        method: 'POST',
        headers: {
          'content-type': 'application/json',
          authorization: `Bearer ${config.glmApiKey}`,
        },
        body: JSON.stringify(attemptBody),
      }, i === 0 ? primaryGuard : Math.min(primaryGuard, FIRST_BYTE_GUARD_MS));
    } catch (err) {
      req.log.error(err);
      lastStatus = 502;
      lastErrText = 'GLM недоступен';
      continue;
    }
    if (upstream.ok || (upstream.status !== 429 && upstream.status < 500)) break;
    lastStatus = upstream.status;
    lastErrText = (await upstream.text().catch(() => '')).slice(0, 300);
    upstream = null;
    // «нет средств на провайдере» — НЕ подменяем модель молча: клиент должен знать,
    // что выбранный уровень недоступен (иначе платит ×4 за бесплатный эконом)
    if (lastErrText.includes('"code":"1113"') || /Insufficient balance/i.test(lastErrText)) {
      return reply.code(503).send({
        error: {
          message: 'Уровень временно недоступен — выберите «Эконом» в селекторе «Уровень»',
          code: 'level_unavailable',
        },
      });
    }
  }
  if (!upstream) {
    // обе бесплатные молчат: пользователь получает понятное «повторите позже»,
    // а не сырой текст ошибки; код level_unavailable включает дружелюбную ветку UI
    if (freeTier) {
      return reply.code(503).send({
        error: {
          message: 'Модели «Эконома» временно не отвечают — провайдер перегружен, повторите попытку позже',
          code: 'level_unavailable',
        },
      });
    }
    return reply.code(lastStatus === 429 ? 429 : 502)
      .send({ error: { message: `GLM: ${lastErrText || 'нет ответа'}` } });
  }

  if (!upstream.ok) {
    const text = await upstream.text().catch(() => '');
    return reply.code(upstream.status === 401 ? 502 : upstream.status)
      .send({ error: { message: `GLM: ${text.slice(0, 500)}` } });
  }

  let usage = null;

  if (isStream) {
    reply.raw.writeHead(200, {
      'content-type': 'text/event-stream',
      'cache-control': 'no-cache',
      connection: 'keep-alive',
    });
    const reader = upstream.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      reply.raw.write(value);
      buffer += decoder.decode(value, { stream: true });
      let idx;
      while ((idx = buffer.indexOf('\n')) >= 0) {
        const line = buffer.slice(0, idx).trim();
        buffer = buffer.slice(idx + 1);
        if (!line.startsWith('data:')) continue;
        const data = line.slice(5).trim();
        if (!data || data === '[DONE]') continue;
        try {
          const evt = JSON.parse(data);
          if (evt.usage) usage = evt.usage;
        } catch {}
      }
    }
    reply.raw.end();
  } else {
    const json = await upstream.json();
    usage = json.usage ?? null;
    reply.header('content-type', 'application/json');
    reply.send(JSON.stringify(json));
  }

  let promptTokens = usage?.prompt_tokens ?? 0;
  const completionTokens = usage?.completion_tokens ?? 0;
  if (!promptTokens && !completionTokens) {
    promptTokens = config.billing.estimateUsageFromBody(body.messages);
  }
  const rateMultiplier = Number(config.modelRates[body.model] ?? 1);
  const costMilli = Math.round(config.billing.costFor({ promptTokens, completionTokens }) * rateMultiplier);
  await db.charge(row.id, { model: body.model, promptTokens, completionTokens, costMilli });

  try {
    await runHook(plugins, 'onResponse', {
      key: row.key,
      body,
      usage: { promptTokens, completionTokens },
      costMilli,
    });
  } catch {}

  return reply;
});

app.get('/admin/keys', async (req, reply) => {
  if (!authAdmin(req, reply)) return;
  const keys = await db.listKeys();
  const withUsage = await Promise.all(keys.map(async (k) => ({
    ...k,
    balance: k.balance_milli / 1000,
    balance_milli: undefined,
    blocked: !!k.blocked,
    spent_30d: ((await db.usageByKey(k.id, Date.now() - 30 * 24 * 3600 * 1000)).cost_milli ?? 0) / 1000,
  })));
  return { keys: withUsage };
});

app.post('/admin/keys', async (req, reply) => {
  if (!authAdmin(req, reply)) return;
  const { label = '', balance = 0 } = req.body ?? {};
  const key = `naomi_${crypto.randomBytes(20).toString('hex')}`;
  const row = await db.createKey({ key, label: String(label), balanceMilli: Number(balance) * 1000 });
  return { key: row.key, label: row.label, balance: row.balance_milli / 1000 };
});

app.post('/admin/keys/:key/:action', async (req, reply) => {
  if (!authAdmin(req, reply)) return;
  const row = await db.getKey(req.params.key);
  if (!row) return reply.code(404).send({ error: { message: 'Ключ не найден' } });
  const { action } = req.params;
  if (action === 'topup') {
    const { amount = 0 } = req.body ?? {};
    await db.topUp(row.id, Number(amount) * 1000);
  } else if (action === 'block' || action === 'unblock') {
    await db.setBlocked(row.id, action === 'block');
  } else if (action === 'label') {
    await db.setLabel(row.id, String(req.body?.label ?? ''));
  } else {
    return reply.code(400).send({ error: { message: 'Действия: topup, block, unblock, label' } });
  }
  const updated = await db.getKey(row.key);
  return { key: updated.key, label: updated.label, balance: updated.balance_milli / 1000, blocked: !!updated.blocked };
});

app.get('/admin/usage', async (req, reply) => {
  if (!authAdmin(req, reply)) return;
  const days = Number(req.query?.days ?? 30);
  const since = Date.now() - days * 24 * 3600 * 1000;
  const [stats, keys] = await Promise.all([db.usageTotal(since), db.listKeys()]);
  return {
    days,
    requests: stats.requests ?? 0,
    prompt_tokens: stats.prompt_tokens ?? 0,
    completion_tokens: stats.completion_tokens ?? 0,
    earned: (stats.cost_milli ?? 0) / 1000,
    keys_total: keys.length,
  };
});

/* ─── Пополнение баланса ─── */
const yookassaEnabled = !!(config.yookassaShopId && config.yookassaSecretKey);
const googleEnabled = !!(config.google.clientId && config.google.clientSecret);
const oauthStatus = () => ({
  google: googleEnabled,
  yandex: !!(config.yandex.clientId && config.yandex.clientSecret),
  vk: !!(config.vk.clientId && config.vk.clientSecret),
});

function hashPassword(password) {
  const salt = crypto.randomBytes(16).toString('hex');
  const hash = crypto.scryptSync(String(password), salt, 32).toString('hex');
  return `${salt}:${hash}`;
}

function verifyPassword(password, stored) {
  const [salt, hash] = String(stored ?? '').split(':');
  if (!salt || !hash) return false;
  const check = crypto.scryptSync(String(password), salt, 32).toString('hex');
  return crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(check, 'hex'));
}

async function issueKeyForAccount(account, trialCredits) {
  const oldKey = await db.getKeyById(account.key_id);
  const balanceMilli = oldKey?.balance_milli ?? trialCredits * 1000;
  const key = `naomi_${crypto.randomBytes(20).toString('hex')}`;
  const row = await db.createKey({ key, label: account.email, balanceMilli });
  if (oldKey) await db.deleteKeyRow(oldKey.id);
  await db.setAccountKey(account.id, row.id);
  return { key: row.key, balance: row.balance_milli / 1000 };
}

function normalizePhone(raw) {
  const digits = String(raw ?? '').replace(/[^\d]/g, '');
  if (!digits) return '';
  const d = digits.length === 11 && digits[0] === '8' ? `7${digits.slice(1)}` : digits;
  return d.length === 11 ? `+${d}` : '';
}

app.post('/v1/account/register', async (req, reply) => {
  const email = String(req.body?.email ?? '').trim().toLowerCase();
  const phone = normalizePhone(req.body?.phone);
  const name = String(req.body?.name ?? '').slice(0, 40);
  const password = String(req.body?.password ?? '');
  if (!email && !phone) {
    return reply.code(400).send({ error: { message: 'Укажите email или телефон' } });
  }
  if (email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
    return reply.code(400).send({ error: { message: 'Введите корректный email' } });
  }
  if (req.body?.phone && !phone) {
    return reply.code(400).send({ error: { message: 'Телефон в формате +7 999 123-45-67' } });
  }
  if (password.length < 6) {
    return reply.code(400).send({ error: { message: 'Пароль минимум 6 символов' } });
  }
  if (email && await db.getAccountByEmail(email)) {
    return reply.code(409).send({ error: { message: 'Аккаунт с таким email уже есть — войдите' } });
  }
  if (phone && await db.getAccountByPhone(phone)) {
    return reply.code(409).send({ error: { message: 'Аккаунт с таким телефоном уже есть — войдите' } });
  }
  const label = name || email || phone;
  const key = `naomi_${crypto.randomBytes(20).toString('hex')}`;
  const keyRow = await db.createKey({ key, label, balanceMilli: config.registration.trialCredits * 1000 });
  const account = await db.createAccount({ email, phone, passwordHash: hashPassword(password), keyId: keyRow.id });
  console.log(`[NAOMI gateway] Аккаунт: ${email || phone} → создан (+${config.registration.trialCredits}₡)`);
  return { key: keyRow.key, balance: keyRow.balance_milli / 1000, email: account.email, phone: account.phone, name, created: true };
});

app.post('/v1/account/login', async (req, reply) => {
  const loginId = String(req.body?.login ?? req.body?.email ?? '').trim().toLowerCase();
  const account = (await db.getAccountByLogin(loginId)) ?? (await db.getAccountByLogin(normalizePhone(loginId)));
  if (!account || !verifyPassword(req.body?.password, account.password_hash)) {
    return reply.code(401).send({ error: { message: 'Неверный email/телефон или пароль' } });
  }
  const keyRow = await db.getKeyById(account.key_id);
  if (!keyRow || keyRow.blocked) {
    const fresh = await issueKeyForAccount(account, config.registration.trialCredits);
    return { ...fresh, email: account.email, phone: account.phone };
  }
  return { key: keyRow.key, balance: keyRow.balance_milli / 1000, email: account.email, phone: account.phone };
});

app.post('/v1/account/rotate', async (req, reply) => {
  const loginId = String(req.body?.login ?? req.body?.email ?? '').trim().toLowerCase();
  const account = (await db.getAccountByLogin(loginId)) ?? (await db.getAccountByLogin(normalizePhone(loginId)));
  if (!account || !verifyPassword(req.body?.password, account.password_hash)) {
    return reply.code(401).send({ error: { message: 'Неверный email/телефон или пароль' } });
  }
  const result = await issueKeyForAccount(account, config.registration.trialCredits);
  console.log(`[NAOMI gateway] Аккаунт ${account.email || account.phone}: ключ перевыпущен (старый удалён)`);
  return { ...result, rotated: true, email: account.email, phone: account.phone };
});

const oauthProviders = {
  google: {
    enabled: googleEnabled,
    authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
    scope: 'openid email',
    async exchange(code, redirectUri) {
      const res = await fetch('https://oauth2.googleapis.com/token', {
        method: 'POST',
        headers: { 'content-type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
          code, client_id: config.google.clientId, client_secret: config.google.clientSecret,
          redirect_uri: redirectUri, grant_type: 'authorization_code',
        }),
        signal: AbortSignal.timeout(15000),
      });
      const tokens = await res.json();
      if (!tokens.id_token) throw new Error(tokens.error_description ?? 'Google: нет токена');
      const payload = JSON.parse(Buffer.from(tokens.id_token.split('.')[1], 'base64url').toString('utf8'));
      return { sub: String(payload.sub), email: (payload.email ?? '').toLowerCase() };
    },
  },
  yandex: {
    enabled: !!(config.yandex.clientId && config.yandex.clientSecret),
    authUrl: 'https://oauth.yandex.ru/authorize',
    scope: 'login:info login:email',
    async exchange(code, redirectUri) {
      const res = await fetch('https://oauth.yandex.ru/token', {
        method: 'POST',
        headers: { 'content-type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
          code, client_id: config.yandex.clientId, client_secret: config.yandex.clientSecret,
          grant_type: 'authorization_code',
        }),
        signal: AbortSignal.timeout(15000),
      });
      const tokens = await res.json();
      if (!tokens.access_token) throw new Error(tokens.error_description ?? 'Яндекс: нет токена');
      const info = await (await fetch('https://login.yandex.ru/info?format=json', {
        headers: { authorization: `OAuth ${tokens.access_token}` },
        signal: AbortSignal.timeout(15000),
      })).json();
      return { sub: String(info.id), email: (info.default_email ?? `${info.login}@yandex.ru`).toLowerCase() };
    },
  },
  vk: {
    enabled: !!(config.vk.clientId && config.vk.clientSecret),
    authUrl: 'https://oauth.vk.com/authorize',
    scope: 'email',
    async exchange(code, redirectUri) {
      const url = new URL('https://oauth.vk.com/access_token');
      url.searchParams.set('client_id', config.vk.clientId);
      url.searchParams.set('client_secret', config.vk.clientSecret);
      url.searchParams.set('redirect_uri', redirectUri);
      url.searchParams.set('code', code);
      const tokens = await (await fetch(url, { signal: AbortSignal.timeout(15000) })).json();
      if (!tokens.user_id) throw new Error(tokens.error_description ?? 'VK: нет токена');
      return { sub: String(tokens.user_id), email: (tokens.email ?? '').toLowerCase() };
    },
  },
};

const anyOauth = Object.values(oauthProviders).some((p) => p.enabled);

if (anyOauth) {
  for (const [name, provider] of Object.entries(oauthProviders)) {
    if (!provider.enabled) continue;
    app.get(`/auth/${name}`, async (req, reply) => {
      const origin = String(req.query?.origin ?? '').replace(/[^a-z0-9:/.-]/gi, '');
      const url = new URL(provider.authUrl);
      url.searchParams.set('client_id', config[name].clientId);
      url.searchParams.set('redirect_uri', oauthRedirectUri(req, name));
      url.searchParams.set('response_type', 'code');
      url.searchParams.set('scope', provider.scope);
      url.searchParams.set('state', origin);
      // без этого Google молча использует прошлую сессию — пользователь не может
      // зайти в другой аккаунт; форсируем экран выбора аккаунта
      if (name === 'google') url.searchParams.set('prompt', 'select_account');
      if (name === 'yandex') url.searchParams.set('force_confirm', 'true');
      return reply.redirect(url.toString(), 302);
    });

    app.get(`/auth/${name}/callback`, async (req, reply) => {
      const origin = String(req.query?.state ?? '').replace(/[^a-z0-9:/.-]/gi, '');
      try {
        const { sub, email } = await provider.exchange(String(req.query?.code ?? ''), oauthRedirectUri(req, name));
        let account = await db.getAccountBySub(name, sub) ?? (email ? await db.getAccountByEmail(email) : undefined);
        if (!account) {
          const key = `naomi_${crypto.randomBytes(20).toString('hex')}`;
          const keyRow = await db.createKey({ key, label: email || `${name}_${sub}`, balanceMilli: config.registration.trialCredits * 1000 });
          account = await db.createAccount({ email, keyId: keyRow.id });
          await db.setAccountSub(account.id, name, sub);
        } else if (!(await db.getAccountBySub(name, sub))) {
          await db.setAccountSub(account.id, name, sub);
        }
        const keyRow = await db.getKeyById(account.key_id);
        const key = keyRow ? keyRow.key : (await issueKeyForAccount(account, config.registration.trialCredits)).key;
        console.log(`[NAOMI gateway] OAuth ${name}: ${email || sub} → вход`);
        return reply.redirect(`${origin}/#key=${key}`, 302);
      } catch (err) {
        return reply.redirect(`${origin}/?login_error=${encodeURIComponent(err.message)}`, 302);
      }
    });
  }
}

app.get('/v1/payments/config', async (req, reply) => {
  const row = await authKey(req, reply);
  if (!row) return;
  return {
    methods: yookassaEnabled ? ['yookassa', 'manual'] : ['manual'],
    payment_info: config.paymentInfo,
    price_rate: config.priceRate,
    currency: config.currency,
    packs: config.packs.map((p) => ({ credits: p.credits, price: p.price.toFixed(0) })),
  };
});

app.post('/v1/topup/request', async (req, reply) => {
  const row = await authKey(req, reply);
  if (!row) return;
  const credits = Number(req.body?.credits ?? 0);
  const pack = config.packs.find((p) => p.credits === credits);
  if (!pack) {
    return reply.code(400).send({ error: { message: `Доступные пакеты: ${config.packs.map((p) => p.credits).join(', ')} кредитов` } });
  }
  const id = await db.createTopup({ keyId: row.id, amountMilli: credits * 1000, method: 'manual' });
  return { id, credits, price: pack.price, payment_info: config.paymentInfo };
});

app.get('/v1/topup/status', async (req, reply) => {
  const row = await authKey(req, reply);
  if (!row) return;
  const t = await db.getTopup(Number(req.query?.id ?? 0));
  if (!t || t.key_id !== row.id) return reply.code(404).send({ error: { message: 'заявка не найдена' } });
  return { id: t.id, status: t.status, amount: t.amount_milli / 1000 };
});

app.post('/v1/payments/yookassa/create', async (req, reply) => {
  const row = await authKey(req, reply);
  if (!row) return;
  if (!yookassaEnabled) {
    return reply.code(400).send({ error: { message: 'Оплата картой ещё не подключена, используйте ручное пополнение' } });
  }
  const credits = Number(req.body?.credits ?? 0);
  const pack = config.packs.find((p) => p.credits === credits);
  if (!pack) {
    return reply.code(400).send({ error: { message: `Доступные пакеты: ${config.packs.map((p) => p.credits).join(', ')}` } });
  }
  const amount = pack.price;
  const topupId = await db.createTopup({ keyId: row.id, amountMilli: credits * 1000, method: 'yookassa' });
  try {
    const res = await fetch('https://api.yookassa.ru/v3/payments', {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        'Idempotence-Key': crypto.randomUUID(),
        authorization: `Basic ${Buffer.from(`${config.yookassaShopId}:${config.yookassaSecretKey}`).toString('base64')}`,
      },
      body: JSON.stringify({
        amount: { value: amount.toFixed(2), currency: config.currency },
        capture: true,
        confirmation: { type: 'redirect', return_url: `${req.headers.origin ?? ''}/?paid=1` },
        description: `NAOMI: пополнение на ${credits} кредитов`,
        metadata: { topup_id: String(topupId), key_id: String(row.id) },
      }),
      signal: AbortSignal.timeout(15000),
    });
    const json = await res.json();
    if (!res.ok) {
      await db.rejectTopup(topupId);
      return reply.code(502).send({ error: { message: `ЮKassa: ${json.description ?? res.status}` } });
    }
    await db.setTopupProvider(topupId, json.id);
    return { id: topupId, payment_id: json.id, confirmation_url: json.confirmation?.confirmation_url };
  } catch (err) {
    await db.rejectTopup(topupId);
    return reply.code(502).send({ error: { message: `ЮKassa недоступна: ${err.message}` } });
  }
});

app.post('/payments/yookassa/webhook', async (req, reply) => {
  const evt = req.body ?? {};
  if (evt.event !== 'payment.succeeded') return reply.code(200).send();
  const topupId = Number(evt.object?.metadata?.topup_id ?? 0);
  if (!topupId) return reply.code(400).send({ error: { message: 'no metadata' } });
  const t = await db.getTopup(topupId);
  if (t && t.status === 'pending') await db.approveTopup(topupId);
  return reply.code(200).send();
});

app.get('/admin/topups', async (req, reply) => {
  if (!authAdmin(req, reply)) return;
  const status = req.query?.status && ['pending', 'paid', 'rejected'].includes(req.query.status)
    ? req.query.status : undefined;
  const topups = await db.listTopups(status);
  return { topups: topups.map((t) => ({ ...t, amount: t.amount_milli / 1000, amount_milli: undefined })) };
});

app.post('/admin/topups/:id/:action', async (req, reply) => {
  if (!authAdmin(req, reply)) return;
  const id = Number(req.params.id);
  const { action } = req.params;
  if (action === 'approve') {
    const t = await db.approveTopup(id);
    if (!t) return reply.code(404).send({ error: { message: 'заявка не найдена или уже обработана' } });
    return { ok: true, id, amount: t.amount_milli / 1000 };
  }
  if (action === 'reject') {
    const ok = await db.rejectTopup(id);
    if (!ok) return reply.code(404).send({ error: { message: 'заявка не найдена или уже обработана' } });
    return { ok: true, id };
  }
  return reply.code(400).send({ error: { message: 'Действия: approve, reject' } });
});

const regLimiter = new Map();

app.post('/v1/register', async (req, reply) => {
  if (!config.registration.enabled) {
    return reply.code(403).send({ error: { message: 'Самостоятельная регистрация пока закрыта, получите ключ у владельца' } });
  }
  const ip = req.ip ?? 'unknown';
  const dayAgo = Date.now() - 86400_000;
  const stamps = (regLimiter.get(ip) ?? []).filter((t) => t > dayAgo);
  if (stamps.length >= config.registration.perIpPerDay) {
    return reply.code(429).send({ error: { message: `С этого адреса уже создано максимум ключей за сутки (${config.registration.perIpPerDay})` } });
  }
  stamps.push(Date.now());
  regLimiter.set(ip, stamps);

  const name = String(req.body?.name ?? 'Гость').slice(0, 40);
  const key = `naomi_${crypto.randomBytes(20).toString('hex')}`;
  const row = await db.createKey({
    key,
    label: `регистрация: ${name}`,
    balanceMilli: config.registration.trialCredits * 1000,
  });
  console.log(`[NAOMI gateway] Регистрация: ${name} (ip ${ip}) → ${row.key.slice(0, 16)}… +${config.registration.trialCredits}₡`);
  return { key: row.key, balance: config.registration.trialCredits, label: row.label };
});

app.get('/health', async () => ({
  ok: true,
  models: config.models,
  payments: { yookassa: yookassaEnabled, manual: true },
  registration: config.registration,
  google: googleEnabled,
  oauth: oauthStatus(),
}));

app.listen({ port: config.port, host: '0.0.0.0' }, (err) => {
  if (err) {
    console.error(err);
    process.exit(1);
  }
  console.log(`[NAOMI gateway] Слушает http://0.0.0.0:${config.port}`);
  console.log(`[NAOMI gateway] GLM: ${config.glmBaseUrl} | модели: ${config.models.join(', ')}`);
  console.log(`[NAOMI gateway] Тариф: ${config.billing.perMillionInMilli / 1000}/1M вх + ${config.billing.perMillionOutMilli / 1000}/1M исх`);
});
