import {
  BadRequestException,
  Inject,
  Injectable,
} from '@nestjs/common';
import { Queue } from 'bullmq';

import type { ServerBackup } from '@hexahost/database';
import type {
  BackupActionResponse,
  BackupDownloadUrlResponse,
  BackupListResponse,
  BackupResponse,
  BackupRestoreResponse,
  BackupSchedule,
  CreateBackupRequest,
  UpdateBackupSchedule,
} from '@hexahost/contracts';
import {
  createPresignedGetUrl,
  createS3Client,
  deleteStoredObject,
  loadStorageConfig,
} from '@hexahost/storage';

import { AuditService } from '../../auth/audit.service';
import { PrismaService } from '../../prisma/prisma.service';
import { findOwnedServer } from '../shared/server-ownership.util';
import { ServerStateService } from '../server-state.service';

export const SERVER_BACKUPS_QUEUE = Symbol('SERVER_BACKUPS_QUEUE');

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

  async listBackups(userId: string, serverId: string): Promise<BackupListResponse> {
    await findOwnedServer(this.prisma, userId, serverId);

    const backups = await this.prisma.serverBackup.findMany({
      where: {
        serverId,
        status: { not: 'DELETED' },
      },
      orderBy: { createdAt: 'desc' },
    });

    return { backups: backups.map((backup) => this.toResponse(backup)) };
  }

  async createBackup(
    userId: string,
    serverId: string,
    input: CreateBackupRequest,
  ): Promise<BackupActionResponse> {
    const server = await findOwnedServer(this.prisma, userId, serverId);
    this.serverState.assertNotInProgress(server.status);

    if (!server.nodeId) {
      throw new BadRequestException('Server is not assigned to a node');
    }

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

    const previousStatus = server.status;
    const backup = await this.prisma.serverBackup.create({
      data: {
        serverId,
        type: 'MANUAL',
        status: 'PENDING',
        label: input.label ?? 'Manual backup',
      },
    });

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

    await this.prisma.gameServerStateTransition.create({
      data: {
        gameServerId: serverId,
        fromStatus: previousStatus,
        toStatus: 'BACKING_UP',
        reason: 'Manual backup requested',
        actorId: userId,
      },
    });

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

    await this.auditService.record({
      action: 'server.backup.create',
      userId,
      entityType: 'game_server',
      entityId: serverId,
      metadata: { backupId: backup.id },
    });

    return {
      backup: this.toResponse(backup),
      jobId: job.id?.toString(),
    };
  }

  async getDownloadUrl(
    userId: string,
    serverId: string,
    backupId: string,
  ): Promise<BackupDownloadUrlResponse> {
    await findOwnedServer(this.prisma, userId, serverId);

    const backup = await this.prisma.serverBackup.findFirstOrThrow({
      where: { id: backupId, serverId, status: 'AVAILABLE' },
    });

    if (!backup.s3Key) {
      throw new BadRequestException('Backup is not stored yet');
    }

    const config = loadStorageConfig();
    const client = createS3Client(config);
    const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
    const downloadUrl = await createPresignedGetUrl(
      client,
      config.S3_BUCKET,
      backup.s3Key,
      15 * 60,
    );

    return {
      downloadUrl,
      expiresAt: expiresAt.toISOString(),
    };
  }

  async deleteBackup(
    userId: string,
    serverId: string,
    backupId: string,
  ): Promise<BackupResponse> {
    await findOwnedServer(this.prisma, userId, serverId);

    const backup = await this.prisma.serverBackup.findFirstOrThrow({
      where: { id: backupId, serverId },
    });

    if (backup.s3Key) {
      const config = loadStorageConfig();
      const client = createS3Client(config);
      await deleteStoredObject(client, config.S3_BUCKET, backup.s3Key);
    }

    const updated = await this.prisma.serverBackup.update({
      where: { id: backupId },
      data: { status: 'DELETED' },
    });

    await this.auditService.record({
      action: 'server.backup.delete',
      userId,
      entityType: 'game_server',
      entityId: serverId,
      metadata: { backupId },
    });

    return this.toResponse(updated);
  }

  async restoreBackup(
    userId: string,
    serverId: string,
    backupId: string,
  ): Promise<BackupRestoreResponse> {
    const server = await findOwnedServer(this.prisma, userId, serverId);
    this.serverState.assertNotInProgress(server.status);

    if (!server.nodeId) {
      throw new BadRequestException('Server is not assigned to a node');
    }

    if (server.status === 'RUNNING') {
      throw new BadRequestException('Server must be stopped before restore');
    }

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

    const backup = await this.prisma.serverBackup.findFirstOrThrow({
      where: { id: backupId, serverId, status: 'AVAILABLE' },
    });

    const restore = await this.prisma.backupRestore.create({
      data: {
        serverId,
        backupId: backup.id,
        status: 'PENDING',
      },
    });

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

    await this.prisma.gameServerStateTransition.create({
      data: {
        gameServerId: serverId,
        fromStatus: 'STOPPED',
        toStatus: 'RESTORING',
        reason: 'Restore requested',
        actorId: userId,
      },
    });

    const job = await this.backupsQueue.add('restore-backup', {
      restoreId: restore.id,
      serverId,
      backupId: backup.id,
    });

    await this.auditService.record({
      action: 'server.backup.restore',
      userId,
      entityType: 'game_server',
      entityId: serverId,
      metadata: { backupId, restoreId: restore.id },
    });

    return {
      restore: {
        id: restore.id,
        serverId,
        backupId: backup.id,
        safetyBackupId: restore.safetyBackupId,
        status: restore.status,
        failureReason: restore.failureReason,
        createdAt: restore.createdAt.toISOString(),
        completedAt: restore.completedAt?.toISOString() ?? null,
      },
      jobId: job.id?.toString(),
    };
  }

  async getSchedule(userId: string, serverId: string): Promise<BackupSchedule> {
    await findOwnedServer(this.prisma, userId, serverId);

    const schedule = await this.prisma.backupSchedule.upsert({
      where: { serverId },
      create: {
        serverId,
        enabled: false,
        intervalHours: 24,
        retentionCount: 5,
        nextRunAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
      },
      update: {},
    });

    return this.toScheduleResponse(schedule);
  }

  async updateSchedule(
    userId: string,
    serverId: string,
    input: UpdateBackupSchedule,
  ): Promise<BackupSchedule> {
    await findOwnedServer(this.prisma, userId, serverId);

    const schedule = await this.prisma.backupSchedule.upsert({
      where: { serverId },
      create: {
        serverId,
        enabled: input.enabled ?? false,
        intervalHours: input.intervalHours ?? 24,
        retentionCount: input.retentionCount ?? 5,
        nextRunAt: new Date(Date.now() + (input.intervalHours ?? 24) * 60 * 60 * 1000),
      },
      update: {
        enabled: input.enabled,
        intervalHours: input.intervalHours,
        retentionCount: input.retentionCount,
        nextRunAt:
          input.enabled === true
            ? new Date(Date.now() + (input.intervalHours ?? 24) * 60 * 60 * 1000)
            : undefined,
      },
    });

    return this.toScheduleResponse(schedule);
  }

  private toResponse(backup: ServerBackup): BackupResponse {
    return {
      id: backup.id,
      serverId: backup.serverId,
      type: backup.type,
      status: backup.status,
      s3Key: backup.s3Key,
      sizeBytes: backup.sizeBytes?.toString() ?? null,
      sha256: backup.sha256,
      label: backup.label,
      failureReason: backup.failureReason,
      createdAt: backup.createdAt.toISOString(),
      completedAt: backup.completedAt?.toISOString() ?? null,
      expiresAt: backup.expiresAt?.toISOString() ?? null,
    };
  }

  private toScheduleResponse(schedule: {
    enabled: boolean;
    intervalHours: number;
    retentionCount: number;
    nextRunAt: Date | null;
    lastRunAt: Date | null;
  }): BackupSchedule {
    return {
      enabled: schedule.enabled,
      intervalHours: schedule.intervalHours,
      retentionCount: schedule.retentionCount,
      nextRunAt: schedule.nextRunAt?.toISOString() ?? null,
      lastRunAt: schedule.lastRunAt?.toISOString() ?? null,
    };
  }
}
