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

import type {
  AddonListResponse,
  InstallAddonRequest,
  InstallAddonResponse,
  InstalledAddon,
} from '@hexahost/contracts';
import type { InstalledAddon as InstalledAddonRecord } from '@hexahost/database';
import {
  getModrinthProject,
  getModrinthVersion,
  isVersionCompatible,
  pickPrimaryFile,
  resolveInstallDirectory,
  resolveModrinthLoader,
  resolveProjectType,
} from '@hexahost/catalog';

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_ADDONS_QUEUE = Symbol('SERVER_ADDONS_QUEUE');

const STOPPED_ONLY = new Set(['STOPPED']);

@Injectable()
export class AddonsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly serverState: ServerStateService,
    private readonly auditService: AuditService,
    @Inject(SERVER_ADDONS_QUEUE) private readonly addonsQueue: Queue,
  ) {}

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

    const addons = await this.prisma.installedAddon.findMany({
      where: {
        serverId,
        status: { not: 'REMOVED' },
      },
      orderBy: { createdAt: 'desc' },
    });

    return { addons: addons.map((addon) => this.toResponse(addon)) };
  }

  async installAddon(
    userId: string,
    serverId: string,
    input: InstallAddonRequest,
  ): Promise<InstallAddonResponse> {
    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 (!STOPPED_ONLY.has(server.status)) {
      throw new BadRequestException(
        'Addons can only be installed while the server is STOPPED',
      );
    }

    const projectType = resolveProjectType(server.softwareFamily);
    const loader = resolveModrinthLoader(server.softwareFamily);

    if (!projectType || !loader) {
      throw new BadRequestException(
        'This server software does not support add-on installation',
      );
    }

    const version = await getModrinthVersion(input.versionId);
    if (version.project_id !== input.projectId) {
      throw new BadRequestException('Version does not belong to the selected project');
    }

    if (!isVersionCompatible(version, server.minecraftVersion, loader)) {
      throw new BadRequestException(
        'This version is not compatible with your server version and loader',
      );
    }

    const file = pickPrimaryFile(version);
    if (!file) {
      throw new BadRequestException('No installable file found for this version');
    }

    const project = await getModrinthProject(input.projectId);
    const projectName = project.title;
    const projectSlug = project.slug;

    const installDir = resolveInstallDirectory(projectType);
    const filePath = `${installDir}/${file.filename}`;

    const existing = await this.prisma.installedAddon.findFirst({
      where: {
        serverId,
        projectId: input.projectId,
        status: { in: ['PENDING', 'INSTALLING', 'INSTALLED'] },
      },
    });

    if (existing) {
      throw new ConflictException('This project is already installed on the server');
    }

    const addonType =
      projectType === 'plugin'
        ? 'PLUGIN'
        : projectType === 'mod'
          ? 'MOD'
          : projectType === 'modpack'
            ? 'MODPACK'
            : 'DATAPACK';

    const addon = await this.prisma.installedAddon.create({
      data: {
        serverId,
        projectId: input.projectId,
        projectSlug,
        projectName,
        versionId: input.versionId,
        versionNumber: version.version_number,
        addonType,
        fileName: file.filename,
        filePath,
        fileSha512: file.hashes['sha512'] ?? null,
        status: 'PENDING',
      },
    });

    const job = await this.addonsQueue.add('install-addon', {
      addonId: addon.id,
      serverId,
    });

    await this.auditService.record({
      action: 'server.addon.install',
      userId,
      entityType: 'game_server',
      entityId: serverId,
      metadata: {
        addonId: addon.id,
        projectId: input.projectId,
        versionId: input.versionId,
      },
    });

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

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

    if (!STOPPED_ONLY.has(server.status)) {
      throw new BadRequestException(
        'Addons can only be removed while the server is STOPPED',
      );
    }

    const addon = await this.prisma.installedAddon.findFirstOrThrow({
      where: { id: addonId, serverId },
    });

    await this.prisma.installedAddon.update({
      where: { id: addonId },
      data: { status: 'REMOVING' },
    });

    const job = await this.addonsQueue.add('remove-addon', {
      addonId,
      serverId,
    });

    await this.auditService.record({
      action: 'server.addon.remove',
      userId,
      entityType: 'game_server',
      entityId: serverId,
      metadata: { addonId },
    });

    return {
      addon: this.toResponse({ ...addon, status: 'REMOVING' }),
      jobId: job.id?.toString(),
    };
  }

  private toResponse(addon: InstalledAddonRecord): InstalledAddon {
    return {
      id: addon.id,
      serverId: addon.serverId,
      provider: addon.provider,
      projectId: addon.projectId,
      projectSlug: addon.projectSlug,
      projectName: addon.projectName,
      versionId: addon.versionId,
      versionNumber: addon.versionNumber,
      addonType: addon.addonType as InstalledAddon['addonType'],
      fileName: addon.fileName,
      filePath: addon.filePath,
      status: addon.status as InstalledAddon['status'],
      failureReason: addon.failureReason,
      installedAt: addon.installedAt?.toISOString() ?? null,
      createdAt: addon.createdAt.toISOString(),
    };
  }
}
