import { eq, and, or } from "drizzle-orm";
import db from "../../db";
import { serverSessions } from "../../db/schema/serverSessions";
import { servers } from "../../db/schema/servers";
import { apps } from "../../db/schema/apps";

export type SessionMode = "stream" | "local";
export type SessionStatus = "starting" | "started" | "ending" | "ended";

export interface CreateSessionParams {
  appId: string;
  userId: string;
  mode: SessionMode;
  serverId?: string;
}

export interface UpdateSessionParams {
  status?: SessionStatus;
  appPid?: number;
  cirrusPid?: number;
  endAt?: Date;
}

/**
 * Сервис для работы с игровыми/streaming сессиями
 */
export const serverSessionService = {
  /**
   * Найти сессию по ID
   */
  async findById(sessionId: string) {
    const session = await db.query.serverSessions.findFirst({
      where: eq(serverSessions.id, sessionId),
      with: {
        app: true,
        user: {
          columns: {
            id: true,
            email: true,
            role: true,
          },
        },
        server: true,
      },
    });

    return session || null;
  },

  /**
   * Найти сессию по ID для конкретного пользователя
   */
  async findByIdForUser(sessionId: string, userId: string) {
    const session = await db.query.serverSessions.findFirst({
      where: and(
        eq(serverSessions.id, sessionId),
        eq(serverSessions.userId, userId)
      ),
      with: {
        app: true,
        user: {
          columns: {
            id: true,
            email: true,
            role: true,
          },
        },
        server: true,
      },
    });

    return session || null;
  },

  /**
   * Получить все сессии пользователя
   */
  async findByUserId(
    userId: string,
    filters?: {
      status?: SessionStatus;
      mode?: SessionMode;
    }
  ) {
    const conditions = [eq(serverSessions.userId, userId)];

    if (filters?.status) {
      conditions.push(eq(serverSessions.status, filters.status));
    }

    if (filters?.mode) {
      conditions.push(eq(serverSessions.mode, filters.mode));
    }

    const sessions = await db
      .select({
        id: serverSessions.id,
        serverId: serverSessions.serverId,
        appId: serverSessions.appId,
        userId: serverSessions.userId,
        startAt: serverSessions.startAt,
        endAt: serverSessions.endAt,
        appPid: serverSessions.appPid,
        cirrusPid: serverSessions.cirrusPid,
        mode: serverSessions.mode,
        status: serverSessions.status,
        createdAt: serverSessions.createdAt,
        updatedAt: serverSessions.updatedAt,
        app: {
          id: apps.id,
          name: apps.name,
          title: apps.title,
        },
      })
      .from(serverSessions)
      .leftJoin(apps, eq(serverSessions.appId, apps.id))
      .where(and(...conditions))
      .orderBy(serverSessions.createdAt);

    return sessions;
  },

  /**
   * Получить все сессии для конкретного сервера
   */
  async findByServerId(
    serverId: string,
    filters?: {
      status?: SessionStatus;
      mode?: SessionMode;
    }
  ) {
    const conditions = [eq(serverSessions.serverId, serverId)];

    if (filters?.status) {
      conditions.push(eq(serverSessions.status, filters.status));
    }

    if (filters?.mode) {
      conditions.push(eq(serverSessions.mode, filters.mode));
    }

    const sessions = await db.query.serverSessions.findMany({
      where: and(...conditions),
      with: {
        app: true,
        user: {
          columns: {
            id: true,
            email: true,
            role: true,
          },
        },
      },
      orderBy: (serverSessions, { asc }) => [asc(serverSessions.createdAt)],
    });

    return sessions;
  },

  /**
   * Получить сессии без назначенного сервера
   */
  async findUnassignedSessions(filters?: {
    status?: SessionStatus;
    mode?: SessionMode;
  }) {
    const { isNull } = await import("drizzle-orm");
    const conditions = [isNull(serverSessions.serverId)];

    if (filters?.status) {
      conditions.push(eq(serverSessions.status, filters.status));
    }

    if (filters?.mode) {
      conditions.push(eq(serverSessions.mode, filters.mode));
    }

    const sessions = await db.query.serverSessions.findMany({
      where: and(...conditions),
      with: {
        app: true,
        user: {
          columns: {
            id: true,
            email: true,
            role: true,
          },
        },
      },
      orderBy: (serverSessions, { asc }) => [asc(serverSessions.createdAt)],
    });

    return sessions;
  },

  /**
   * Проверить, есть ли у пользователя активная сессия для данного приложения
   */
  async hasActiveSession(userId: string, appId: string) {
    const session = await db.query.serverSessions.findFirst({
      where: and(
        eq(serverSessions.userId, userId),
        eq(serverSessions.appId, appId),
        or(
          eq(serverSessions.status, "starting"),
          eq(serverSessions.status, "started")
        )
      ),
    });

    return !!session;
  },

  /**
   * Выбрать доступный сервер
   */
  async selectAvailableServer(mode: SessionMode): Promise<string | undefined> {
    if (mode === "stream") {
      const server = await db.query.servers.findFirst({
        where: and(eq(servers.type, "stream"), eq(servers.tier, "prod")),
      });
      return server?.id;
    } else {
      const server = await db.query.servers.findFirst({
        where: eq(servers.type, "local"),
      });
      return server?.id;
    }
  },

  /**
   * Создать новую сессию
   */
  async create(params: CreateSessionParams) {
    const { appId, userId, mode, serverId } = params;

    // Для local-сессий выбираем сервер сразу
    // Для stream-сессий сервер будет назначен динамически при запуске
    let selectedServerId = serverId;
    if (mode === "local" && !selectedServerId) {
      selectedServerId = await this.selectAvailableServer(mode);
      if (!selectedServerId) {
        throw new Error(`No available ${mode} servers`);
      }
    }

    // Вычислить время окончания (по умолчанию +30 минут)
    const endAt = new Date();
    endAt.setMinutes(endAt.getMinutes() + 30);

    // Создать сессию
    const [newSession] = await db
      .insert(serverSessions)
      .values({
        serverId: selectedServerId, // Может быть null для stream-сессий
        appId,
        userId,
        mode,
        status: "starting",
        endAt,
      })
      .returning();

    return newSession;
  },

  /**
   * Обновить сессию
   */
  async update(sessionId: string, params: UpdateSessionParams) {
    const updateData: any = {
      updatedAt: new Date(),
    };

    if (params.status) {
      updateData.status = params.status;
    }

    if (params.appPid !== undefined) {
      updateData.appPid = params.appPid;
    }

    if (params.cirrusPid !== undefined) {
      updateData.cirrusPid = params.cirrusPid;
    }

    if (params.endAt) {
      updateData.endAt = params.endAt;
    }

    const [updatedSession] = await db
      .update(serverSessions)
      .set(updateData)
      .where(eq(serverSessions.id, sessionId))
      .returning();

    return updatedSession;
  },

  /**
   * Завершить сессию (изменить статус на "ending")
   */
  async end(sessionId: string) {
    const [updatedSession] = await db
      .update(serverSessions)
      .set({
        status: "ending",
        updatedAt: new Date(),
      })
      .where(eq(serverSessions.id, sessionId))
      .returning();

    return updatedSession;
  },

  /**
   * Продлить сессию
   */
  async extend(sessionId: string, minutes: number) {
    const session = await this.findById(sessionId);

    if (!session) {
      throw new Error("Session not found");
    }

    const newEndAt = session.endAt ? new Date(session.endAt) : new Date();
    newEndAt.setMinutes(newEndAt.getMinutes() + minutes);

    const [updatedSession] = await db
      .update(serverSessions)
      .set({
        endAt: newEndAt,
        updatedAt: new Date(),
      })
      .where(eq(serverSessions.id, sessionId))
      .returning();

    return updatedSession;
  },

  /**
   * Назначить сервер для сессии
   * Выбирает сервер с максимальной свободной GPU памятью
   */
  async assignServer(sessionId: string, requiredGpuMb?: number) {
    const session = await this.findById(sessionId);

    if (!session) {
      throw new Error("Session not found");
    }

    if (session.serverId) {
      // Сервер уже назначен
      return session;
    }

    // Импортируем serverService динамически чтобы избежать циклических зависимостей
    const { serverService } = await import("../server");

    let selectedServer;

    if (session.mode === "stream") {
      // Для stream-сессий выбираем сервер с максимальной свободной памятью
      // Ищем среди всех tier (prod и demo)
      const availableServers = await serverService.findAvailableStreamServers();

      if (availableServers.length === 0) {
        throw new Error(
          "No available stream servers (check that stream servers are registered)"
        );
      }

      console.log(
        `[${new Date().toISOString()}] 📊 Найдено ${
          availableServers.length
        } stream-серверов:`,
        availableServers
          .map((s) => `${s.hostname} (${s.tier}, ${s.gpuFreeMb}MB)`)
          .join(", ")
      );

      // Фильтруем серверы по доступной GPU памяти
      // Требуемая память берется из gpuLimitMb приложения или используются все доступные серверы
      const memoryOkServers = requiredGpuMb
        ? availableServers.filter((s) => {
            const hasEnough = s.gpuFreeMb >= requiredGpuMb;
            if (!hasEnough) {
              console.log(
                `[${new Date().toISOString()}] ⚠️ Сервер ${s.id} (${
                  s.hostname
                }) пропущен по памяти: ${s.gpuFreeMb}MB < ${requiredGpuMb}MB`
              );
            }
            return hasEnough;
          })
        : availableServers;

      if (memoryOkServers.length === 0) {
        const maxAvailable = Math.max(
          ...availableServers.map((s) => s.gpuFreeMb)
        );
        throw new Error(
          `No servers with enough GPU memory (required: ${requiredGpuMb}MB, max available: ${maxAvailable}MB)`
        );
      }

      // Проверяем количество активных сессий на каждом сервере
      // Максимум одновременных сессий на один stream-сервер (по умолчанию 3)
      const MAX_SESSIONS_PER_SERVER = parseInt(
        process.env.MAX_SESSIONS_PER_STREAM_SERVER || "3",
        10
      );
      const suitableServers = [];

      for (const server of memoryOkServers) {
        // Подсчитываем активные сессии (starting или started)
        const activeSessions = await this.findByServerId(server.id, {});
        const activeCount = activeSessions.filter(
          (s) => s.status === "starting" || s.status === "started"
        ).length;

        if (activeCount >= MAX_SESSIONS_PER_SERVER) {
          console.log(
            `[${new Date().toISOString()}] ⚠️ Сервер ${server.id} (${
              server.hostname
            }) пропущен по загрузке: ${activeCount}/${MAX_SESSIONS_PER_SERVER} активных сессий`
          );
        } else {
          console.log(
            `[${new Date().toISOString()}] ✅ Сервер ${server.id} (${
              server.hostname
            }) доступен: ${activeCount}/${MAX_SESSIONS_PER_SERVER} активных сессий`
          );
          suitableServers.push(server);
        }
      }

      if (suitableServers.length === 0) {
        throw new Error(
          `No available servers (all servers have ${MAX_SESSIONS_PER_SERVER} or more active sessions)`
        );
      }

      // Берем первый сервер (уже отсортирован по убыванию gpuFreeMb)
      selectedServer = suitableServers[0];

      console.log(
        `[${new Date().toISOString()}] ✅ Выбран сервер ${selectedServer.id} (${
          selectedServer.hostname
        }) с ${
          selectedServer.gpuFreeMb
        }MB свободной памяти для сессии ${sessionId} (требуется: ${
          requiredGpuMb || "не указано"
        }MB)`
      );
    } else {
      // Для local-сессий используем существующую логику
      const serverId = await this.selectAvailableServer(session.mode);
      if (!serverId) {
        throw new Error("No available local servers");
      }
      selectedServer = await serverService.findById(serverId);
    }

    if (!selectedServer) {
      throw new Error("Failed to select server");
    }

    // Назначаем сервер сессии
    const [updatedSession] = await db
      .update(serverSessions)
      .set({
        serverId: selectedServer.id,
        updatedAt: new Date(),
      })
      .where(eq(serverSessions.id, sessionId))
      .returning();

    return updatedSession;
  },

  /**
   * Автоматически назначить серверы для всех unassigned сессий, готовых к запуску
   * Вызывается периодически main сервером
   */
  async autoAssignServers() {
    const now = new Date();

    // Находим все unassigned сессии со статусом "starting" и startAt <= now
    const { isNull } = await import("drizzle-orm");
    const unassignedSessions = await db.query.serverSessions.findMany({
      where: and(
        isNull(serverSessions.serverId),
        eq(serverSessions.status, "starting")
      ),
      with: {
        app: true,
      },
    });

    // Фильтруем сессии, у которых уже наступило время запуска
    const readySessions = unassignedSessions.filter((session) => {
      const startAt = new Date(session.startAt);
      return startAt <= now;
    });

    const results = {
      total: readySessions.length,
      assigned: 0,
      failed: 0,
      errors: [] as string[],
    };

    // Назначаем сервер для каждой готовой сессии
    for (const session of readySessions) {
      try {
        const requiredGpuMb = session.app.gpuLimitMb || undefined;
        console.log(
          `[${new Date().toISOString()}] 🔍 Назначение сервера для сессии ${
            session.id
          } (приложение: ${session.app.name}, требуется GPU: ${
            requiredGpuMb || "не указано"
          }MB)`
        );

        await this.assignServer(session.id, requiredGpuMb);
        results.assigned++;
      } catch (error) {
        results.failed++;
        const errorMsg = error instanceof Error ? error.message : String(error);
        results.errors.push(
          `Session ${session.id} (${session.app.name}): ${errorMsg}`
        );

        console.error(
          `[${new Date().toISOString()}] ❌ Не удалось назначить сервер для сессии ${
            session.id
          }:`,
          errorMsg
        );
      }
    }

    return results;
  },
};
