import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AgentHost } from '../../entities/agent-host.entity';
import { GameInstance } from '../../entities/game-instance.entity';
import { ServerConnection } from '../../entities/server-connection.entity';

export interface FleetInstanceDto {
  id: string;
  agent_instance_id: string;
  game: string;
  label: string | null;
  state: string;
  uptime_seconds: number;
  last_seen_at: string | null;
}

export interface FleetHostDto {
  id: string;
  hostname: string;
  status: string;
  agent_version: string | null;
  os: string | null;
  arch: string | null;
  cpu_percent: number | null;
  cpu_cores: number | null;
  mem_total_mb: number | null;
  mem_used_mb: number | null;
  uptime_seconds: number | null;
  disks: AgentHost['disks'];
  last_heartbeat_at: string | null;
  instances: FleetInstanceDto[];
}

export interface FleetSummaryDto {
  host_count: number;
  instance_count: number;
  online_host_count: number;
}

export interface FleetResponseDto {
  hosts: FleetHostDto[];
  summary: FleetSummaryDto;
}

@Injectable()
export class FleetService {
  constructor(
    @InjectRepository(AgentHost)
    private readonly hostRepo: Repository<AgentHost>,
    @InjectRepository(GameInstance)
    private readonly instanceRepo: Repository<GameInstance>,
    @InjectRepository(ServerConnection)
    private readonly connectionRepo: Repository<ServerConnection>,
  ) {}

  /**
   * Remove a host and its game instances from the fleet.
   *
   * Refuses while the host is `connected` — a live agent re-registers on its
   * next heartbeat, so the operator must stop the agent first. Deletes the
   * host's instances explicitly (the FK is SET NULL, which would otherwise
   * orphan them); instance_stats cascade. If this was the license's last host,
   * the legacy single-server connection row is cleared too so the old
   * Dashboard doesn't show a stale server.
   */
  async deleteHost(
    licenseId: string,
    hostId: string,
  ): Promise<{ deleted: true; instances_removed: number }> {
    const host = await this.hostRepo.findOne({ where: { id: hostId, license_id: licenseId } });
    if (!host) throw new NotFoundException('Host not found');
    if (host.status === 'connected') {
      throw new ConflictException(
        'Host is online — stop the agent first, or it will re-register on its next heartbeat',
      );
    }

    const del = await this.instanceRepo.delete({ license_id: licenseId, host_id: hostId });
    await this.hostRepo.delete({ id: hostId, license_id: licenseId });

    const remaining = await this.hostRepo.count({ where: { license_id: licenseId } });
    if (remaining === 0) {
      await this.connectionRepo.delete({ license_id: licenseId });
    }

    return { deleted: true, instances_removed: del.affected ?? 0 };
  }

  async getFleet(licenseId: string): Promise<FleetResponseDto> {
    const [hosts, instances] = await Promise.all([
      this.hostRepo.find({
        where: { license_id: licenseId },
        order: { hostname: 'ASC' },
      }),
      this.instanceRepo.find({
        where: { license_id: licenseId },
        order: { game: 'ASC', label: 'ASC' },
      }),
    ]);

    // Group instances by host_id. Bigint columns come back as strings from pg — coerce.
    const instancesByHost = new Map<string | null, FleetInstanceDto[]>();
    for (const inst of instances) {
      const key = inst.host_id ?? null;
      if (!instancesByHost.has(key)) {
        instancesByHost.set(key, []);
      }
      instancesByHost.get(key)!.push({
        id: inst.id,
        agent_instance_id: inst.agent_instance_id,
        game: inst.game,
        label: inst.label,
        state: inst.state,
        uptime_seconds: Number(inst.uptime_seconds),
        last_seen_at: inst.last_seen_at ? inst.last_seen_at.toISOString() : null,
      });
    }

    const hostDtos: FleetHostDto[] = hosts.map((h) => ({
      id: h.id,
      hostname: h.hostname,
      status: h.status,
      agent_version: h.agent_version,
      os: h.os,
      arch: h.arch,
      cpu_percent: h.cpu_percent !== null && h.cpu_percent !== undefined ? Number(h.cpu_percent) : null,
      cpu_cores: h.cpu_cores !== null && h.cpu_cores !== undefined ? Number(h.cpu_cores) : null,
      mem_total_mb: h.mem_total_mb !== null && h.mem_total_mb !== undefined ? Number(h.mem_total_mb) : null,
      mem_used_mb: h.mem_used_mb !== null && h.mem_used_mb !== undefined ? Number(h.mem_used_mb) : null,
      uptime_seconds: h.uptime_seconds !== null && h.uptime_seconds !== undefined ? Number(h.uptime_seconds) : null,
      disks: h.disks,
      last_heartbeat_at: h.last_heartbeat_at ? h.last_heartbeat_at.toISOString() : null,
      instances: instancesByHost.get(h.id) ?? [],
    }));

    // Append synthetic "unassigned" bucket only if orphaned instances exist
    const unassigned = instancesByHost.get(null) ?? [];
    if (unassigned.length > 0) {
      hostDtos.push({
        id: '__unassigned__',
        hostname: 'Unassigned',
        status: 'offline',
        agent_version: null,
        os: null,
        arch: null,
        cpu_percent: null,
        cpu_cores: null,
        mem_total_mb: null,
        mem_used_mb: null,
        uptime_seconds: null,
        disks: null,
        last_heartbeat_at: null,
        instances: unassigned,
      });
    }

    const online_host_count = hosts.filter((h) => h.status === 'connected').length;
    const instance_count = instances.length;

    return {
      hosts: hostDtos,
      summary: {
        host_count: hosts.length,
        instance_count,
        online_host_count,
      },
    };
  }
}
