import got, { RequestError } from "got";
import os from "os";
import { execSync, spawn, ChildProcess } from "child_process";
import { existsSync } from "fs";

// Конфигурация
const API_URL = process.env.API_URL || "http://localhost:3000";
const SERVER_TYPE = (process.env.SERVER_TYPE || "stream") as "stream" | "local";
const SERVER_LOCATION =
  (process.env.SERVER_LOCATION as "ru1" | "uae1" | undefined) || undefined;
const SERVER_TIER =
  (process.env.SERVER_TIER as "demo" | "prod" | undefined) || undefined;
const BRANCH_ID = process.env.BRANCH_ID || undefined;
const LOCAL_IP = getLocalIp();
const HOSTNAME = os.hostname();
const REGISTER_INTERVAL_MS = parseInt(
  process.env.REGISTER_INTERVAL_MS || "30000",
  10
); // 30 секунд по умолчанию
const GPU_UPDATE_INTERVAL_MS = parseInt(
  process.env.GPU_UPDATE_INTERVAL_MS || "1000",
  10
); // 1 секунда по умолчанию
const SESSION_CHECK_INTERVAL_MS = parseInt(
  process.env.SESSION_CHECK_INTERVAL_MS || "1000",
  10
); // 1 секунда по умолчанию

// ID зарегистрированного сервера (заполняется после регистрации)
let SERVER_ID: string | null = null;

// Карта активных процессов: sessionId -> процесс приложения
const activeProcesses = new Map<string, ChildProcess>();

// Карта сессий в процессе запуска/остановки для предотвращения дублирования
const processingSessions = new Set<string>();

// Карта последнего времени логирования запланированных сессий (для уменьшения спама в логах)
const lastScheduledLogTime = new Map<string, number>();

interface ServerRegistrationData {
  localIp: string;
  hostname: string;
  type: "stream" | "local";
  gpuFreeMb: number;
  branchId?: string;
  location?: "ru1" | "uae1";
  tier?: "demo" | "prod";
}

interface ServerRegistrationResponse {
  server: {
    id: string;
    localIp: string;
    hostname: string;
    type: "stream" | "local";
    gpuFreeMb: number;
    branchId?: string;
    location?: "ru1" | "uae1";
    tier?: "demo" | "prod";
    createdAt: string;
    updatedAt: string;
  };
  registered: boolean;
}

interface SessionData {
  id: string;
  serverId: string | null; // Nullable - для stream сессий назначается динамически
  appId: string;
  userId: string;
  startAt: string;
  endAt: string | null;
  appPid: number | null;
  cirrusPid: number | null;
  mode: "stream" | "local";
  status: "starting" | "started" | "ending" | "ended";
  createdAt: string;
  updatedAt: string;
  app: {
    id: string;
    name: string;
    title: string;
    gpuLimitMb: number | null;
    psVersion: number | null;
  };
  user: {
    id: string;
    email: string;
    role: string;
  };
}

/**
 * Получить локальный IP адрес
 */
function getLocalIp(): string {
  const interfaces = os.networkInterfaces();

  for (const name of Object.keys(interfaces)) {
    const netInterface = interfaces[name];
    if (!netInterface) continue;

    for (const iface of netInterface) {
      // Пропустить внутренние и non-IPv4 адреса
      if (iface.family === "IPv4" && !iface.internal) {
        return iface.address;
      }
    }
  }

  return "127.0.0.1";
}

/**
 * Получить свободную память GPU через nvidia-smi
 * Возвращает количество свободной памяти в МБ
 * Выбрасывает ошибку, если не удалось получить данные
 */
function getGpuFreeMb(): number {
  try {
    // Выполняем nvidia-smi с форматом вывода только свободной памяти
    // --query-gpu=memory.free - запрашиваем свободную память
    // --format=csv,noheader,nounits - CSV формат без заголовков и единиц измерения
    const output = execSync(
      "nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits",
      {
        encoding: "utf-8",
        timeout: 5000, // 5 секунд таймаут
      }
    );

    // Парсим вывод (может быть несколько GPU, берём первый)
    const lines = output.trim().split("\n");
    if (lines.length > 0 && lines[0]) {
      const freeMb = parseInt(lines[0].trim(), 10);
      if (!isNaN(freeMb) && freeMb >= 0) {
        return freeMb;
      }
    }

    throw new Error("Не удалось распарсить вывод nvidia-smi");
  } catch (error) {
    console.error(
      `[${new Date().toISOString()}] ❌ Критическая ошибка при получении данных GPU:`,
      error instanceof Error ? error.message : error
    );
    throw new Error(
      `Невозможно получить данные о GPU через nvidia-smi: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
  }
}

/**
 * Регистрация сервера на главном сервере
 */
async function registerServer(isRecursive: boolean = false): Promise<void> {
  // Валидация для stream-серверов
  if (SERVER_TYPE === "stream" && !SERVER_LOCATION) {
    console.error(
      `[${new Date().toISOString()}] ❌ Ошибка: для stream-серверов обязательно должен быть указан SERVER_LOCATION`
    );
    process.exit(1);
  }

  // Валидация для local-серверов
  if (SERVER_TYPE === "local" && !BRANCH_ID) {
    console.error(
      `[${new Date().toISOString()}] ❌ Ошибка: для local-серверов обязательно должен быть указан BRANCH_ID`
    );
    process.exit(1);
  }

  // Установить tier по умолчанию для stream-серверов
  const tier = SERVER_TYPE === "stream" && !SERVER_TIER ? "demo" : SERVER_TIER;

  // Получаем актуальное значение свободной GPU памяти
  const gpuFreeMb = getGpuFreeMb();

  const registrationData: ServerRegistrationData = {
    localIp: LOCAL_IP,
    hostname: HOSTNAME,
    type: SERVER_TYPE,
    gpuFreeMb: gpuFreeMb,
    branchId: BRANCH_ID,
    location: SERVER_LOCATION,
    tier: tier,
  };

  console.log(`[${new Date().toISOString()}] Регистрация сервера...`);
  console.log("Данные:", JSON.stringify(registrationData, null, 2));

  try {
    const response = await got
      .post(`${API_URL}/servers/register`, {
        json: registrationData,
        timeout: {
          request: 10000, // 10 секунд таймаут
        },
        retry: {
          limit: 3,
          methods: ["POST"],
          statusCodes: [408, 413, 429, 500, 502, 503, 504],
        },
      })
      .json<ServerRegistrationResponse>();

    // Сохраняем ID сервера для дальнейших обновлений
    SERVER_ID = response.server.id;

    if (response.registered) {
      console.log(
        `[${new Date().toISOString()}] ✅ Сервер успешно зарегистрирован`
      );
      console.log("ID сервера:", response.server.id);
    } else {
      console.log(
        `[${new Date().toISOString()}] 🔄 Информация о сервере обновлена`
      );
    }

    // При первом запуске запускаем обновление GPU и проверку сессий после успешной регистрации
    if (!isRecursive && SERVER_ID) {
      updateGpuMemory();
      checkSessions();
    }
  } catch (error: unknown) {
    console.error(
      `[${new Date().toISOString()}] ❌ Ошибка регистрации сервера:`,
      error
    );

    if (error instanceof RequestError) {
      console.error("Детали ошибки:", {
        code: error.code,
        message: error.message,
      });
    } else if (error instanceof Error) {
      console.error("Ошибка:", error.message);
    }
  }

  // Планируем следующую регистрацию после завершения текущей
  setTimeout(() => registerServer(true), REGISTER_INTERVAL_MS);
}

/**
 * Обновить информацию о свободной памяти GPU на сервере
 */
async function updateGpuMemory(): Promise<void> {
  try {
    // Получаем актуальное значение свободной GPU памяти
    const gpuFreeMb = getGpuFreeMb();

    await got.patch(`${API_URL}/servers/${SERVER_ID}/gpu`, {
      json: { gpuFreeMb },
      timeout: {
        request: 5000, // 5 секунд таймаут
      },
      retry: {
        limit: 2,
        methods: ["PATCH"],
        statusCodes: [408, 429, 500, 502, 503, 504],
      },
    });

    console.log(
      `[${new Date().toISOString()}] 🎮 GPU память обновлена: ${gpuFreeMb} MB`
    );
  } catch (error: unknown) {
    console.error(
      `[${new Date().toISOString()}] ❌ Ошибка обновления GPU памяти:`,
      error instanceof RequestError ? error.message : error
    );
  }

  // Планируем следующее обновление после завершения текущего
  setTimeout(() => updateGpuMemory(), GPU_UPDATE_INTERVAL_MS);
}

/**
 * Получить сессии для этого сервера
 */
async function fetchSessions(): Promise<SessionData[]> {
  if (!SERVER_ID) {
    return [];
  }

  try {
    const response = await got
      .get(`${API_URL}/servers/${SERVER_ID}/sessions`, {
        timeout: {
          request: 5000,
        },
      })
      .json<{ sessions: SessionData[] }>();

    return response.sessions;
  } catch (error: unknown) {
    console.error(
      `[${new Date().toISOString()}] ❌ Ошибка получения сессий:`,
      error instanceof RequestError ? error.message : error
    );
    return [];
  }
}

/**
 * Обновить статус сессии на главном сервере
 */
async function updateSessionStatus(
  sessionId: string,
  status: "starting" | "started" | "ending" | "ended",
  appPid?: number,
  cirrusPid?: number
): Promise<void> {
  try {
    await got.patch(`${API_URL}/sessions/${sessionId}/status`, {
      json: {
        status,
        appPid,
        cirrusPid,
      },
      timeout: {
        request: 5000,
      },
    });

    console.log(
      `[${new Date().toISOString()}] ✅ Статус сессии ${sessionId} обновлен на "${status}"`
    );
  } catch (error: unknown) {
    console.error(
      `[${new Date().toISOString()}] ❌ Ошибка обновления статуса сессии:`,
      error instanceof RequestError ? error.message : error
    );
  }
}

/**
 * Запустить приложение для сессии
 */
async function startApplication(session: SessionData): Promise<void> {
  const { id: sessionId, app, serverId } = session;

  // Проверить, не обрабатывается ли уже эта сессия
  if (processingSessions.has(sessionId)) {
    return;
  }

  // Проверить, не запущено ли уже приложение для этой сессии
  if (activeProcesses.has(sessionId)) {
    console.log(
      `[${new Date().toISOString()}] ⚠️ Приложение для сессии ${sessionId} уже запущено`
    );
    return;
  }

  // Проверить, что сервер назначен
  // Main server автоматически назначает серверы для готовых к запуску сессий
  if (!serverId) {
    console.log(
      `[${new Date().toISOString()}] ⏳ Сессия ${sessionId} ожидает назначения сервера main сервером`
    );
    return;
  }

  // Проверить, что сессия назначена именно этому серверу
  if (serverId !== SERVER_ID) {
    // Это нормально - сессия назначена другому серверу
    return;
  }

  processingSessions.add(sessionId);

  try {
    console.log(
      `[${new Date().toISOString()}] 🚀 Запуск приложения "${app.name}" для сессии ${sessionId} (активных процессов: ${activeProcesses.size})`
    );

    // Формируем путь к exe файлу приложения
    // Путь: C:\apps\{appName}\{appName}.exe
    const appPath = `C:\\apps\\${app.name}\\${app.name}.exe`;
    
    console.log(
      `[${new Date().toISOString()}] 📂 Путь к приложению: ${appPath}`
    );

    // Проверяем существование exe файла
    if (!existsSync(appPath)) {
      throw new Error(
        `Файл приложения не найден: ${appPath}. Убедитесь, что приложение установлено.`
      );
    }

    // Запускаем exe приложение
    // Используем 'pipe' для stderr чтобы видеть ошибки, но 'ignore' для stdin/stdout
    const appProcess = spawn(appPath, [], {
      detached: false,
      stdio: ["ignore", "ignore", "pipe"], // stdin: ignore, stdout: ignore, stderr: pipe
      windowsHide: true, // Скрывать окно консоли на Windows
      cwd: `C:\\apps\\${app.name}`, // Устанавливаем рабочую директорию приложения
    });

    const appPid = appProcess.pid;

    if (!appPid) {
      throw new Error("Не удалось получить PID процесса");
    }

    // Сохранить процесс в карте активных процессов
    activeProcesses.set(sessionId, appProcess);

    // Логирование stderr для диагностики
    if (appProcess.stderr) {
      appProcess.stderr.on("data", (data) => {
        console.error(
          `[${new Date().toISOString()}] 🔴 STDERR [${sessionId}]: ${data.toString().trim()}`
        );
      });
    }

    // Обработка завершения процесса
    appProcess.on("exit", async (code, signal) => {
      console.log(
        `[${new Date().toISOString()}] 🛑 Приложение для сессии ${sessionId} завершилось с кодом ${code}${signal ? ` (сигнал: ${signal})` : ""}`
      );
      
      if (code !== 0 && code !== null) {
        console.error(
          `[${new Date().toISOString()}] ⚠️ Приложение завершилось с ошибкой! Код выхода: ${code}`
        );
      }
      
      activeProcesses.delete(sessionId);

      // Обновить статус на "ended"
      await updateSessionStatus(sessionId, "ended");
    });

    appProcess.on("error", async (error) => {
      console.error(
        `[${new Date().toISOString()}] ❌ Ошибка процесса для сессии ${sessionId}:`,
        error
      );
      activeProcesses.delete(sessionId);
      
      // Обновить статус на "ended" в случае ошибки
      await updateSessionStatus(sessionId, "ended");
    });

    // Обновить статус на "started" с PID
    await updateSessionStatus(sessionId, "started", appPid);

    console.log(
      `[${new Date().toISOString()}] ✅ Приложение запущено с PID ${appPid} (всего активных: ${activeProcesses.size})`
    );
  } catch (error) {
    console.error(
      `[${new Date().toISOString()}] ❌ Ошибка запуска приложения:`,
      error instanceof Error ? error.message : error
    );
    // Обновить статус на "ended" в случае ошибки
    await updateSessionStatus(sessionId, "ended");
  } finally {
    processingSessions.delete(sessionId);
  }
}

/**
 * Убить процесс и всё его дерево дочерних процессов (для Windows)
 */
function killProcessTree(pid: number): void {
  try {
    // На Windows используем taskkill с флагом /T для убийства дерева процессов
    // /F - принудительное завершение
    // /T - завершить указанный процесс и все дочерние процессы
    execSync(`taskkill /pid ${pid} /T /F`, {
      stdio: "ignore",
      timeout: 10000,
    });
    console.log(
      `[${new Date().toISOString()}] ✅ Дерево процессов для PID ${pid} успешно завершено`
    );
  } catch (error) {
    console.error(
      `[${new Date().toISOString()}] ⚠️ Ошибка при завершении дерева процессов PID ${pid}:`,
      error instanceof Error ? error.message : error
    );
  }
}

/**
 * Остановить приложение для сессии
 */
async function stopApplication(session: SessionData): Promise<void> {
  const { id: sessionId, appPid } = session;

  // Проверить, не обрабатывается ли уже эта сессия
  if (processingSessions.has(sessionId)) {
    return;
  }

  const appProcess = activeProcesses.get(sessionId);

  if (!appProcess && !appPid) {
    console.log(
      `[${new Date().toISOString()}] ⚠️ Приложение для сессии ${sessionId} не найдено в активных процессах`
    );
    // Всё равно обновляем статус на "ended"
    await updateSessionStatus(sessionId, "ended");
    return;
  }

  processingSessions.add(sessionId);

  try {
    console.log(
      `[${new Date().toISOString()}] 🛑 Остановка приложения для сессии ${sessionId} (PID: ${appPid || appProcess?.pid || "неизвестен"})`
    );

    // Используем PID из базы данных если он есть, иначе из процесса
    const pidToKill = appPid || appProcess?.pid;

    if (pidToKill) {
      // Убиваем весь процесс и все его дочерние процессы
      killProcessTree(pidToKill);
    } else if (appProcess) {
      // Если по какой-то причине нет PID, пробуем стандартный способ
      appProcess.kill("SIGTERM");
      await new Promise((resolve) => setTimeout(resolve, 2000));
      if (!appProcess.killed) {
        appProcess.kill("SIGKILL");
      }
    }

    activeProcesses.delete(sessionId);

    // Обновить статус на "ended"
    await updateSessionStatus(sessionId, "ended");

    console.log(
      `[${new Date().toISOString()}] ✅ Приложение и все дочерние процессы остановлены для сессии ${sessionId}`
    );
  } catch (error) {
    console.error(
      `[${new Date().toISOString()}] ❌ Ошибка остановки приложения:`,
      error instanceof Error ? error.message : error
    );
  } finally {
    processingSessions.delete(sessionId);
  }
}

/**
 * Проверить и обработать сессии
 */
async function checkSessions(): Promise<void> {
  if (!SERVER_ID) {
    return;
  }

  try {
    const sessions = await fetchSessions();
    // Получаем текущее время в UTC
    const now = new Date();

    // Обработать сессии со статусом "starting"
    // Запускать только если время startAt уже наступило
    // Примечание: PostgreSQL возвращает timestamp with timezone как ISO 8601 строку,
    // которая автоматически парсится в UTC при создании Date объекта
    const allStartingSessions = sessions.filter((s) => s.status === "starting");
    const startingSessions = allStartingSessions.filter((s) => {
      const startAt = new Date(s.startAt);
      return startAt <= now;
    });
    
    // Логировать запланированные сессии, которые ещё не пришло время запускать
    // Логируем не чаще раза в 10 секунд для каждой сессии, чтобы не спамить логами
    const scheduledSessions = allStartingSessions.filter((s) => {
      const startAt = new Date(s.startAt);
      return startAt > now;
    });
    
    if (scheduledSessions.length > 0) {
      for (const session of scheduledSessions) {
        const lastLogTime = lastScheduledLogTime.get(session.id) || 0;
        const timeSinceLastLog = now.getTime() - lastLogTime;
        
        // Логируем только если прошло больше 10 секунд с последнего лога
        if (timeSinceLastLog > 10000) {
          const startAt = new Date(session.startAt);
          const timeUntilStart = Math.round((startAt.getTime() - now.getTime()) / 1000);
          console.log(
            `[${new Date().toISOString()}] ⏰ Сессия ${session.id} (${session.app.name}) запланирована через ${timeUntilStart} сек`
          );
          lastScheduledLogTime.set(session.id, now.getTime());
        }
      }
    }
    
    // Очистить карту логирования для сессий, которые больше не запланированы
    const scheduledSessionIds = new Set(scheduledSessions.map((s) => s.id));
    for (const sessionId of lastScheduledLogTime.keys()) {
      if (!scheduledSessionIds.has(sessionId)) {
        lastScheduledLogTime.delete(sessionId);
      }
    }
    
    for (const session of startingSessions) {
      await startApplication(session);
    }

    // Обработать сессии со статусом "ending"
    const endingSessions = sessions.filter((s) => s.status === "ending");
    for (const session of endingSessions) {
      await stopApplication(session);
    }

    // Проверить, что все активные процессы соответствуют активным сессиям
    // Сессия считается активной, если:
    // 1. Статус "started"
    // 2. Статус "starting" И время startAt уже наступило
    const activeSessions = sessions.filter((s) => {
      if (s.status === "started") return true;
      if (s.status === "starting") {
        const startAt = new Date(s.startAt);
        return startAt <= now;
      }
      return false;
    });
    const activeSessionIds = new Set(activeSessions.map((s) => s.id));

    // Остановить процессы для сессий, которые больше не активны
    for (const [sessionId, process] of activeProcesses.entries()) {
      if (!activeSessionIds.has(sessionId)) {
        console.log(
          `[${new Date().toISOString()}] ⚠️ Найден процесс для неактивной сессии ${sessionId}, остановка`
        );
        process.kill("SIGTERM");
        activeProcesses.delete(sessionId);
      }
    }
  } catch (error: unknown) {
    console.error(
      `[${new Date().toISOString()}] ❌ Ошибка проверки сессий:`,
      error instanceof Error ? error.message : error
    );
  }

  // Планируем следующую проверку
  setTimeout(() => checkSessions(), SESSION_CHECK_INTERVAL_MS);
}

/**
 * Основная функция
 */
async function main() {
  console.log("=".repeat(60));
  console.log("🚀 Запуск сессионного сервера");
  console.log("=".repeat(60));
  console.log("Конфигурация:");
  console.log(`  API URL: ${API_URL}`);
  console.log(`  Hostname: ${HOSTNAME}`);
  console.log(`  Local IP: ${LOCAL_IP}`);
  console.log(`  Type: ${SERVER_TYPE}`);
  console.log(`  GPU Free MB: ${getGpuFreeMb()} (читается из nvidia-smi)`);
  if (SERVER_LOCATION) console.log(`  Location: ${SERVER_LOCATION}`);
  if (SERVER_TIER) console.log(`  Tier: ${SERVER_TIER}`);
  if (BRANCH_ID) console.log(`  Branch ID: ${BRANCH_ID}`);
  console.log(`  Register Interval: ${REGISTER_INTERVAL_MS}ms`);
  console.log(`  GPU Update Interval: ${GPU_UPDATE_INTERVAL_MS}ms`);
  console.log(`  Session Check Interval: ${SESSION_CHECK_INTERVAL_MS}ms`);
  console.log("=".repeat(60));

  // Запуск рекурсивной регистрации
  // Использует setTimeout вместо setInterval, чтобы избежать наложения запросов
  // После первой успешной регистрации запускается обновление GPU
  await registerServer();
}

// Запуск
main().catch((error: unknown) => {
  console.error(
    "Критическая ошибка:",
    error instanceof Error ? error.message : error
  );
  process.exit(1);
});
