import {
  ConflictException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';

import {
  generateSecureToken,
  hashToken,
  UserRole,
} from '@hexahost/auth';
import type { NodeListResponse, NodeSummary } from '@hexahost/contracts';
import { computeAvailableRamMb } from '@hexahost/scheduler';

import { PrismaService } from '../prisma/prisma.service';
import { loadNodeSnapshots } from '../scheduling/node-snapshots';

export interface CreateNodeInput {
  name: string;
  hostname: string;
  maxServers?: number;
  maxRamMb?: number;
  platformRamMb?: number;
}

export interface CreateNodeResponse {
  node: {
    id: string;
    name: string;
    hostname: string;
    status: string;
    maxServers: number;
    maxRamMb: number;
    createdAt: string;
  };
  enrollmentToken: string;
}

@Injectable()
export class AdminNodesService {
  constructor(private readonly prisma: PrismaService) {}

  assertSuperAdmin(roles: string[]): void {
    if (!roles.includes(UserRole.SUPER_ADMIN)) {
      throw new ForbiddenException('Super administrator access required');
    }
  }

  async listNodes(): Promise<NodeListResponse> {
    const snapshots = await loadNodeSnapshots();
    const nodes = await this.prisma.gameNode.findMany({
      orderBy: { createdAt: 'asc' },
    });

    const snapshotById = new Map(snapshots.map((snapshot) => [snapshot.id, snapshot]));

    return {
      nodes: nodes.map((node) => this.toSummary(node, snapshotById.get(node.id))),
    };
  }

  async createNode(input: CreateNodeInput): Promise<CreateNodeResponse> {
    const enrollmentToken = generateSecureToken();

    const node = await this.prisma.gameNode.create({
      data: {
        name: input.name,
        hostname: input.hostname,
        maxServers: input.maxServers ?? 10,
        maxRamMb: input.maxRamMb ?? 16384,
        platformRamMb: input.platformRamMb ?? 2048,
        enrollmentTokenHash: hashToken(enrollmentToken),
      },
    });

    return {
      node: {
        id: node.id,
        name: node.name,
        hostname: node.hostname,
        status: node.status,
        maxServers: node.maxServers,
        maxRamMb: node.maxRamMb,
        createdAt: node.createdAt.toISOString(),
      },
      enrollmentToken,
    };
  }

  async requestDrain(nodeId: string): Promise<NodeSummary> {
    const node = await this.requireNode(nodeId);

    if (node.status === 'DRAINING' || node.status === 'MAINTENANCE') {
      throw new ConflictException('Node is already draining or in maintenance');
    }

    const updated = await this.prisma.gameNode.update({
      where: { id: nodeId },
      data: {
        status: 'DRAINING',
        drainRequestedAt: new Date(),
      },
    });

    const snapshot = (await loadNodeSnapshots([nodeId]))[0];
    return this.toSummary(updated, snapshot);
  }

  async setMaintenance(nodeId: string, enabled: boolean): Promise<NodeSummary> {
    const node = await this.requireNode(nodeId);

    if (enabled && node.status === 'MAINTENANCE') {
      const snapshot = (await loadNodeSnapshots([nodeId]))[0];
      return this.toSummary(node, snapshot);
    }

    const updated = await this.prisma.gameNode.update({
      where: { id: nodeId },
      data: {
        status: enabled ? 'MAINTENANCE' : 'OFFLINE',
        drainRequestedAt: null,
      },
    });

    const snapshot = (await loadNodeSnapshots([nodeId]))[0];
    return this.toSummary(updated, snapshot);
  }

  private async requireNode(nodeId: string) {
    const node = await this.prisma.gameNode.findUnique({ where: { id: nodeId } });
    if (!node) {
      throw new NotFoundException('Node not found');
    }
    return node;
  }

  private toSummary(
    node: {
      id: string;
      name: string;
      hostname: string;
      status: NodeSummary['status'];
      maxServers: number;
      maxRamMb: number;
      platformRamMb: number;
      activeServers: number;
      lastHeartbeatAt: Date | null;
      agentVersion: string | null;
      drainRequestedAt: Date | null;
      createdAt: Date;
    },
    snapshot?: {
      reservedRamMb: number;
    },
  ): NodeSummary {
    const reservedRamMb = snapshot?.reservedRamMb ?? 0;
    const availableRamMb = snapshot
      ? computeAvailableRamMb({
          id: node.id,
          status: node.status,
          maxServers: node.maxServers,
          maxRamMb: node.maxRamMb,
          platformRamMb: node.platformRamMb,
          activeServers: node.activeServers,
          lastHeartbeatAt: node.lastHeartbeatAt,
          reservedRamMb,
          activeAllocationCount: 0,
        })
      : Math.max(0, node.maxRamMb - node.platformRamMb - reservedRamMb);

    return {
      id: node.id,
      name: node.name,
      hostname: node.hostname,
      status: node.status,
      maxServers: node.maxServers,
      maxRamMb: node.maxRamMb,
      platformRamMb: node.platformRamMb,
      activeServers: node.activeServers,
      reservedRamMb,
      availableRamMb,
      lastHeartbeatAt: node.lastHeartbeatAt?.toISOString() ?? null,
      agentVersion: node.agentVersion,
      drainRequestedAt: node.drainRequestedAt?.toISOString() ?? null,
      createdAt: node.createdAt.toISOString(),
    };
  }
}
