import { randomUUID } from 'node:crypto';

import {
  ConflictException,
  ForbiddenException,
  Inject,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Queue } from 'bullmq';
import type Redis from 'ioredis';

import type {
  CreateServerRequest,
  ServerActionResponse,
  ServerListResponse,
  ServerResponse,
} from '@hexahost/contracts';
import type { GameServer } from '@hexahost/database';

type GameServerStatus = GameServer['status'];

import { PrismaService } from '../prisma/prisma.service';

import { ServerStateService } from './server-state.service';

export const REDIS_CLIENT = Symbol('SERVERS_REDIS_CLIENT');
export const SERVER_LIFECYCLE_QUEUE = Symbol('SERVER_LIFECYCLE_QUEUE');
export const SERVER_PROVISIONING_QUEUE = Symbol('SERVER_PROVISIONING_QUEUE');

export const NODE_COMMANDS_CHANNEL = 'hgc:node:commands' as const;

@Injectable()
export class ServersService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly serverState: ServerStateService,
    @Inject(REDIS_CLIENT) private readonly redis: Redis,
    @Inject(SERVER_LIFECYCLE_QUEUE)
    private readonly lifecycleQueue: Queue,
    @Inject(SERVER_PROVISIONING_QUEUE)
    private readonly provisioningQueue: Queue,
  ) {}

  async createServer(
    userId: string,
    input: CreateServerRequest,
  ): Promise<ServerResponse> {
    const server = await this.prisma.gameServer.create({
      data: {
        userId,
        name: input.name,
        edition: input.edition,
        softwareFamily: input.softwareFamily,
        minecraftVersion: input.minecraftVersion,
        eulaAccepted: input.eulaAccepted,
        ramMb: input.ramMb ?? 1024,
        planId: input.planId,
        status: 'DRAFT',
      },
    });

    await this.recordTransition(server.id, null, 'DRAFT', 'created', userId);

    return this.toResponse(server);
  }

  async listServers(userId: string): Promise<ServerListResponse> {
    const servers = await this.prisma.gameServer.findMany({
      where: { userId },
      orderBy: { createdAt: 'desc' },
    });

    return {
      servers: servers.map((server) => this.toResponse(server)),
    };
  }

  async getServer(userId: string, serverId: string): Promise<ServerResponse> {
    const server = await this.findOwnedServer(userId, serverId);
    return this.toResponse(server);
  }

  async startServer(
    userId: string,
    serverId: string,
  ): Promise<ServerActionResponse> {
    const server = await this.findOwnedServer(userId, serverId);
    this.serverState.assertNotInProgress(server.status);

    const targetStatus = this.serverState.resolveStartTarget(server.status);
    this.serverState.assertTransition(server.status, targetStatus);

    const correlationId = randomUUID();
    const updated = await this.transitionServer(
      server,
      targetStatus,
      'start requested',
      userId,
      correlationId,
    );

    let jobId: string | undefined;

    if (targetStatus === 'PROVISIONING') {
      const job = await this.provisioningQueue.add(
        'provision-server',
        { serverId, correlationId },
        { jobId: correlationId },
      );
      jobId = job.id;
    } else {
      const job = await this.lifecycleQueue.add(
        'start-server',
        { serverId, action: 'start', correlationId },
        { jobId: correlationId },
      );
      jobId = job.id;

      if (updated.nodeId) {
        await this.publishNodeCommand(updated.nodeId, {
          protocolVersion: 1,
          messageId: randomUUID(),
          correlationId,
          type: 'server.start',
          timestamp: new Date().toISOString(),
          payload: {
            serverId,
            generation: updated.version,
            desiredGeneration: updated.version,
          },
        });
      }
    }

    return {
      server: this.toResponse(updated),
      jobId,
    };
  }

  async stopServer(
    userId: string,
    serverId: string,
  ): Promise<ServerActionResponse> {
    const server = await this.findOwnedServer(userId, serverId);
    this.serverState.assertNotInProgress(server.status);

    const targetStatus = this.serverState.resolveStopTarget(server.status);
    this.serverState.assertTransition(server.status, targetStatus);

    const correlationId = randomUUID();
    const updated = await this.transitionServer(
      server,
      targetStatus,
      'stop requested',
      userId,
      correlationId,
    );

    const job = await this.lifecycleQueue.add(
      'stop-server',
      { serverId, action: 'stop', correlationId },
      { jobId: correlationId },
    );

    if (updated.nodeId) {
      await this.publishNodeCommand(updated.nodeId, {
        protocolVersion: 1,
        messageId: randomUUID(),
        correlationId,
        type: 'server.stop',
        timestamp: new Date().toISOString(),
        payload: {
          serverId,
          generation: updated.version,
        },
      });
    }

    return {
      server: this.toResponse(updated),
      jobId: job.id,
    };
  }

  async publishNodeCommand(
    nodeId: string,
    envelope: Record<string, unknown>,
  ): Promise<void> {
    await this.redis.publish(
      NODE_COMMANDS_CHANNEL,
      JSON.stringify({ nodeId, envelope }),
    );
  }

  async applyAgentStatus(
    serverId: string,
    nextStatus: GameServerStatus,
    metadata?: {
      correlationId?: string;
      reason?: string;
      errorCode?: string;
      errorMessage?: string;
    },
  ): Promise<void> {
    const server = await this.prisma.gameServer.findUnique({
      where: { id: serverId },
    });

    if (!server || server.status === nextStatus) {
      return;
    }

    this.serverState.assertTransition(server.status, nextStatus);

    await this.transitionServer(
      server,
      nextStatus,
      metadata?.reason ?? 'agent reported state',
      null,
      metadata?.correlationId,
      metadata?.errorCode,
      metadata?.errorMessage,
    );
  }

  private async findOwnedServer(
    userId: string,
    serverId: string,
  ): Promise<GameServer> {
    const server = await this.prisma.gameServer.findUnique({
      where: { id: serverId },
    });

    if (!server) {
      throw new NotFoundException('Server not found');
    }

    if (server.userId !== userId) {
      throw new ForbiddenException('You do not own this server');
    }

    return server;
  }

  private async transitionServer(
    server: GameServer,
    toStatus: GameServerStatus,
    reason: string,
    actorId: string | null,
    correlationId?: string,
    errorCode?: string,
    errorMessage?: string,
  ): Promise<GameServer> {
    if (server.status === toStatus) {
      throw new ConflictException('Server is already in the requested state');
    }

    const updated = await this.prisma.gameServer.update({
      where: { id: server.id },
      data: {
        status: toStatus,
        version: { increment: 1 },
      },
    });

    await this.recordTransition(
      server.id,
      server.status,
      toStatus,
      reason,
      actorId,
      correlationId,
      errorCode,
      errorMessage,
    );

    return updated;
  }

  private async recordTransition(
    gameServerId: string,
    fromStatus: GameServerStatus | null,
    toStatus: GameServerStatus,
    reason: string,
    actorId: string | null,
    correlationId?: string,
    errorCode?: string,
    errorMessage?: string,
  ): Promise<void> {
    await this.prisma.gameServerStateTransition.create({
      data: {
        gameServerId,
        fromStatus,
        toStatus,
        reason,
        actorId,
        correlationId,
        errorCode,
        errorMessage,
      },
    });
  }

  private toResponse(server: GameServer): ServerResponse {
    return {
      id: server.id,
      userId: server.userId,
      planId: server.planId,
      nodeId: server.nodeId,
      name: server.name,
      status: server.status,
      version: server.version,
      edition: server.edition,
      softwareFamily: server.softwareFamily,
      minecraftVersion: server.minecraftVersion,
      eulaAccepted: server.eulaAccepted,
      hostPort: server.hostPort,
      containerId: server.containerId,
      dataPath: server.dataPath,
      ramMb: server.ramMb,
      createdAt: server.createdAt.toISOString(),
      updatedAt: server.updatedAt.toISOString(),
    };
  }
}
