import {
  BadRequestException,
  Inject,
  Injectable,
  ServiceUnavailableException,
} from '@nestjs/common';
import { Queue } from 'bullmq';
import { randomUUID } from 'node:crypto';

import type {
  InitiateWorldUpload,
  WorldDownloadUrlResponse,
  WorldInfoResponse,
  WorldUploadInitResponse,
  WorldUploadResponse,
} from '@hexahost/contracts';
import type { GameServer, WorldUpload } from '@hexahost/database';
import {
  createPresignedPutUrl,
  createS3Client,
  headStoredObject,
  loadStorageConfig,
  worldUploadObjectKey,
} from '@hexahost/storage';

import { AuditService } from '../../auth/audit.service';
import { NodeBridgeService } from '../../nodes/node-bridge.service';
import { PrismaService } from '../../prisma/prisma.service';
import { findOwnedServer } from '../shared/server-ownership.util';
import { ServerStateService } from '../server-state.service';
import { SERVER_BACKUPS_QUEUE } from '../backups/backups.service';

const WORLD_MANAGE_STATUSES = new Set<GameServer['status']>(['STOPPED']);

@Injectable()
export class WorldsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly nodeBridge: NodeBridgeService,
    private readonly serverState: ServerStateService,
    private readonly auditService: AuditService,
    @Inject(SERVER_BACKUPS_QUEUE) private readonly backupsQueue: Queue,
  ) {}

  async getWorldInfo(userId: string, serverId: string): Promise<WorldInfoResponse> {
    const server = await this.requireNode(serverId, userId);

    const response = await this.nodeBridge.sendAgentRequest(
      server.nodeId!,
      'server.world.validate',
      {
        serverId,
        generation: server.version,
        worldName: server.activeWorldName,
      },
    );

    if (!response.success) {
      throw new ServiceUnavailableException(
        response.errorMessage ?? 'Failed to load world info',
      );
    }

    const result = response.result as {
      worlds?: Array<{
        name: string;
        path: string;
        sizeBytes: number;
        hasLevelDat: boolean;
      }>;
    };

    return {
      activeWorldName: server.activeWorldName,
      worlds: (result.worlds ?? []).map((world) => ({
        name: world.name,
        path: world.path,
        sizeBytes: String(world.sizeBytes),
        hasLevelDat: world.hasLevelDat,
      })),
    };
  }

  async initiateUpload(
    userId: string,
    serverId: string,
    input: InitiateWorldUpload,
  ): Promise<WorldUploadInitResponse> {
    const server = await this.requireStoppedServer(userId, serverId);
    const uploadId = randomUUID();
    const config = loadStorageConfig();
    const client = createS3Client(config);
    const s3Key = worldUploadObjectKey(serverId, uploadId);
    const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
    const uploadUrl = await createPresignedPutUrl(
      client,
      config.S3_BUCKET,
      s3Key,
      60 * 60,
      'application/gzip',
    );

    await this.prisma.worldUpload.create({
      data: {
        id: uploadId,
        serverId,
        status: 'UPLOADING',
        s3Key,
        worldName: input.worldName,
      },
    });

    return {
      uploadId,
      uploadUrl,
      expiresAt: expiresAt.toISOString(),
      s3Key,
    };
  }

  async completeUpload(
    userId: string,
    serverId: string,
    uploadId: string,
  ): Promise<WorldUploadResponse> {
    const server = await this.requireStoppedServer(userId, serverId);
    const upload = await this.prisma.worldUpload.findFirstOrThrow({
      where: { id: uploadId, serverId },
    });

    const config = loadStorageConfig();
    const client = createS3Client(config);
    const head = await headStoredObject(client, config.S3_BUCKET, upload.s3Key);

    if (head.sizeBytes <= 0) {
      throw new BadRequestException('Uploaded world archive is empty');
    }

    const updated = await this.prisma.worldUpload.update({
      where: { id: uploadId },
      data: {
        status: 'READY',
        sizeBytes: BigInt(head.sizeBytes),
      },
    });

    await this.backupsQueue.add('apply-world-upload', {
      uploadId,
      serverId,
    });

    await this.auditService.record({
      action: 'server.world.upload',
      userId,
      entityType: 'game_server',
      entityId: serverId,
      metadata: { uploadId, worldName: upload.worldName },
    });

    return this.toUploadResponse(updated);
  }

  async requestWorldDownload(
    userId: string,
    serverId: string,
  ): Promise<WorldDownloadUrlResponse> {
    const server = await this.requireNode(serverId, userId);

    if (server.status !== 'RUNNING' && server.status !== 'STOPPED') {
      throw new BadRequestException(
        `Cannot download world while server is ${server.status}`,
      );
    }

    const backup = await this.prisma.serverBackup.create({
      data: {
        serverId,
        type: 'MANUAL',
        status: 'PENDING',
        label: 'World download',
      },
    });

    const previousStatus = server.status as 'RUNNING' | 'STOPPED';

    await this.prisma.gameServer.update({
      where: { id: serverId },
      data: { status: 'BACKING_UP' },
    });

    await this.prisma.gameServerStateTransition.create({
      data: {
        gameServerId: serverId,
        fromStatus: server.status,
        toStatus: 'BACKING_UP',
        reason: 'World download backup started',
        actorId: userId,
      },
    });

    await this.backupsQueue.add('create-backup', {
      backupId: backup.id,
      serverId,
      previousStatus,
    });

    return {
      backupId: backup.id,
      downloadUrl: '',
      expiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
    };
  }

  private async requireNode(serverId: string, userId: string): Promise<GameServer> {
    const server = await findOwnedServer(this.prisma, userId, serverId);
    if (!server.nodeId) {
      throw new BadRequestException('Server is not assigned to a node');
    }
    return server;
  }

  private async requireStoppedServer(
    userId: string,
    serverId: string,
  ): Promise<GameServer> {
    const server = await this.requireNode(serverId, userId);
    this.serverState.assertNotInProgress(server.status);

    if (!WORLD_MANAGE_STATUSES.has(server.status)) {
      throw new BadRequestException(
        `World changes require server to be STOPPED (current: ${server.status})`,
      );
    }

    return server;
  }

  private toUploadResponse(upload: WorldUpload): WorldUploadResponse {
    return {
      id: upload.id,
      serverId: upload.serverId,
      status: upload.status,
      worldName: upload.worldName,
      sizeBytes: upload.sizeBytes?.toString() ?? null,
      failureReason: upload.failureReason,
      createdAt: upload.createdAt.toISOString(),
      completedAt: upload.completedAt?.toISOString() ?? null,
    };
  }
}
