import {
  BadRequestException,
  HttpException,
  HttpStatus,
  Inject,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import type Redis from 'ioredis';

import type { EdgeResolveResponse, EdgeStartResponse } from '@hexahost/contracts';
import type { GameServer } from '@hexahost/database';
import { getPlayDomain } from '@hexahost/dns';

import { PrismaService } from '../prisma/prisma.service';
import { ServersService, REDIS_CLIENT } from '../servers/servers.service';

import { JoinService } from './join.service';

const STARTABLE_STATUSES = new Set<GameServer['status']>(['STOPPED', 'UNKNOWN']);
const WAIT_STATUSES = new Set<GameServer['status']>([
  'STARTING',
  'QUEUED',
  'PROVISIONING',
  'INSTALLING',
  'STOPPING',
]);
const REJECT_STATUSES = new Set<GameServer['status']>([
  'DRAFT',
  'ERROR',
  'DELETING',
  'DELETED',
  'BACKING_UP',
  'RESTORING',
]);

@Injectable()
export class EdgeService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly joinService: JoinService,
    private readonly serversService: ServersService,
    @Inject(REDIS_CLIENT) private readonly redis: Redis,
  ) {}

  async resolveSlug(slug: string, clientIp?: string): Promise<EdgeResolveResponse> {
    const server = await this.prisma.gameServer.findFirst({
      where: { joinSlug: slug },
      include: { node: true },
    });

    if (!server) {
      throw new NotFoundException(`Unknown join slug: ${slug}`);
    }

    return this.buildResolveResponse(server, clientIp);
  }

  async requestJoinStart(
    slug: string,
    clientIp: string,
  ): Promise<EdgeStartResponse> {
    if (!(await this.checkStartRateLimit(slug, clientIp))) {
      throw new HttpException(
        'Join-to-start rate limit exceeded',
        HttpStatus.TOO_MANY_REQUESTS,
      );
    }

    const server = await this.prisma.gameServer.findFirst({
      where: { joinSlug: slug },
      include: { node: true },
    });

    if (!server) {
      throw new NotFoundException(`Unknown join slug: ${slug}`);
    }

    if (!server.joinToStartEnabled) {
      throw new BadRequestException('Join-to-start is disabled for this server');
    }

    const initial = await this.buildResolveResponse(server, clientIp);

    if (initial.action === 'proxy') {
      return { resolve: initial, started: false };
    }

    if (initial.action === 'reject') {
      return { resolve: initial, started: false };
    }

    if (initial.action === 'wait') {
      return { resolve: initial, started: false };
    }

    const started = await this.serversService.startServerInternal(
      server.id,
      `join-to-start:${clientIp}`,
    );

    const refreshed = await this.prisma.gameServer.findUniqueOrThrow({
      where: { id: server.id },
      include: { node: true },
    });

    return {
      resolve: await this.buildResolveResponse(refreshed, clientIp),
      started: true,
      jobId: started.jobId,
      queuePosition: started.queuePosition,
    };
  }

  private async buildResolveResponse(
    server: GameServer & { node: { hostname: string } | null },
    clientIp?: string,
  ): Promise<EdgeResolveResponse> {
    const backend =
      server.node && server.hostPort !== null
        ? { host: server.node.hostname, port: server.hostPort }
        : null;

    const base = {
      serverId: server.id,
      slug: server.joinSlug ?? '',
      status: server.status,
      joinToStartEnabled: server.joinToStartEnabled,
      edition: server.edition,
      backend,
    };

    if (REJECT_STATUSES.has(server.status)) {
      return {
        ...base,
        action: 'reject',
        message: `Server is ${server.status.toLowerCase()}`,
      };
    }

    if (server.billingSuspendedAt) {
      return {
        ...base,
        action: 'reject',
        message: server.billingSuspendReason ?? 'Server is billing suspended',
      };
    }

    if (server.status === 'RUNNING' && backend) {
      return { ...base, action: 'proxy', backend };
    }

    if (!server.joinToStartEnabled) {
      return {
        ...base,
        action: 'reject',
        message: 'Join-to-start is disabled; start the server from the panel',
      };
    }

    if (WAIT_STATUSES.has(server.status)) {
      return {
        ...base,
        action: 'wait',
        message: `Server is ${server.status.toLowerCase()}`,
      };
    }

    if (STARTABLE_STATUSES.has(server.status)) {
      return {
        ...base,
        action: 'start',
        message: clientIp ? `Start requested from ${clientIp}` : undefined,
      };
    }

    return {
      ...base,
      action: 'reject',
      message: `Cannot join while server is ${server.status.toLowerCase()}`,
    };
  }

  private async checkStartRateLimit(slug: string, clientIp: string): Promise<boolean> {
    const slugKey = `edge:start:slug:${slug}`;
    const ipKey = `edge:start:ip:${clientIp}`;

    const slugCount = await this.redis.incr(slugKey);
    if (slugCount === 1) {
      await this.redis.expire(slugKey, 60);
    }
    if (slugCount > 3) {
      return false;
    }

    const ipCount = await this.redis.incr(ipKey);
    if (ipCount === 1) {
      await this.redis.expire(ipKey, 60);
    }

    return ipCount <= 10;
  }

  getPlayDomain(): string {
    return getPlayDomain();
  }
}
