import {
  Injectable,
  Logger,
  OnModuleInit,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import type { FastifyRequest } from 'fastify';
import type { WebSocket } from 'ws';

import { consoleCommandSchema } from '@hexahost/contracts';

import { AuditService } from '../../auth/audit.service';
import { NodeBridgeService } from '../../nodes/node-bridge.service';
import { PrismaService } from '../../prisma/prisma.service';
import { findOwnedServer } from '../shared/server-ownership.util';

import { ConsoleSessionService } from './console-session.service';
import { ConsoleService } from './console.service';

@Injectable()
export class ConsoleGateway implements OnModuleInit {
  private readonly logger = new Logger(ConsoleGateway.name);

  constructor(
    private readonly adapterHost: HttpAdapterHost,
    private readonly consoleService: ConsoleService,
    private readonly consoleSession: ConsoleSessionService,
    private readonly nodeBridge: NodeBridgeService,
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  onModuleInit(): void {
    const fastify = this.adapterHost.httpAdapter.getInstance();

    fastify.get(
      '/api/v1/servers/:serverId/console/ws',
      { websocket: true },
      (socket: WebSocket, request: FastifyRequest) => {
        void this.handleConnection(socket, request);
      },
    );
  }

  private async handleConnection(
    socket: WebSocket,
    request: FastifyRequest,
  ): Promise<void> {
    const params = request.params as { serverId?: string };
    const query = request.query as Record<string, string | string[] | undefined>;
    const serverId = params.serverId;
    const token = this.readQueryParam(query['token']);

    if (!serverId || !token) {
      socket.close(4400, 'serverId and token are required');
      return;
    }

    let userId: string;

    try {
      ({ userId } = this.consoleService.validateToken(token, serverId));
      await findOwnedServer(this.prisma, userId, serverId);
    } catch (error) {
      const reason =
        error instanceof Error ? error.message : 'Console authorization failed';
      socket.close(4401, reason);
      return;
    }

    this.consoleSession.subscribe(serverId, socket);

    socket.on('close', () => {
      this.consoleSession.unsubscribe(serverId, socket);
    });

    socket.on('message', (data) => {
      void this.handleCommand(serverId, userId, data.toString());
    });

    socket.on('error', (error) => {
      this.logger.error(
        `Console WebSocket error for server ${serverId}: ${error.message}`,
      );
    });

    socket.send(JSON.stringify({ type: 'connected', serverId }));
  }

  private async handleCommand(
    serverId: string,
    userId: string,
    raw: string,
  ): Promise<void> {
    let parsed: unknown;

    try {
      parsed = JSON.parse(raw);
    } catch {
      return;
    }

    const result = consoleCommandSchema.safeParse(parsed);
    if (!result.success) {
      return;
    }

    const server = await this.prisma.gameServer.findUnique({
      where: { id: serverId },
    });

    if (!server?.nodeId || server.status !== 'RUNNING') {
      return;
    }

    try {
      await this.nodeBridge.sendAgentRequest(
        server.nodeId,
        'server.command',
        {
          serverId,
          generation: server.version,
          command: result.data.command,
        },
        15_000,
      );

      await this.auditService.record({
        action: 'server.console.command',
        userId,
        entityType: 'game_server',
        entityId: serverId,
        metadata: { command: result.data.command },
      });
    } catch (error) {
      const detail = error instanceof Error ? error.message : String(error);
      this.logger.warn(`Console command failed for ${serverId}: ${detail}`);
    }
  }

  private readQueryParam(
    value: string | string[] | undefined,
  ): string | undefined {
    if (Array.isArray(value)) {
      return value[0];
    }

    return value;
  }
}
