import { randomUUID } from 'node:crypto';

import {
  Injectable,
  Logger,
  UnauthorizedException,
  Inject,
} from '@nestjs/common';
import type { WebSocket } from 'ws';
import type Redis from 'ioredis';

import { verifyTokenHash } from '@hexahost/auth';
import type { GameServer, GameNode, Prisma } from '@hexahost/database';

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

type GameServerStatus = GameServer['status'];

interface AgentEnvelope {
  protocolVersion?: number;
  messageId?: string;
  correlationId?: string;
  type?: string;
  timestamp?: string;
  payload?: Record<string, unknown>;
}

const AGENT_STATE_MAP: Record<string, GameServerStatus> = {
  draft: 'DRAFT',
  provisioning: 'PROVISIONING',
  installing: 'INSTALLING',
  stopped: 'STOPPED',
  starting: 'STARTING',
  running: 'RUNNING',
  stopping: 'STOPPING',
  error: 'ERROR',
  deleting: 'DELETING',
  deleted: 'DELETED',
};

@Injectable()
export class NodesService {
  private readonly logger = new Logger(NodesService.name);
  private readonly connections = new Map<string, WebSocket>();

  constructor(
    private readonly prisma: PrismaService,
    private readonly serversService: ServersService,
    @Inject(REDIS_CLIENT) private readonly redis: Redis,
  ) {}

  registerConnection(nodeId: string, socket: WebSocket): void {
    const existing = this.connections.get(nodeId);
    if (existing && existing !== socket) {
      existing.close(4000, 'replaced by new connection');
    }

    this.connections.set(nodeId, socket);
  }

  unregisterConnection(nodeId: string, socket: WebSocket): void {
    const current = this.connections.get(nodeId);
    if (current === socket) {
      this.connections.delete(nodeId);
    }
  }

  forwardCommand(nodeId: string, envelope: unknown): void {
    const socket = this.connections.get(nodeId);
    if (!socket || socket.readyState !== socket.OPEN) {
      this.logger.warn(`No active WebSocket for node ${nodeId}`);
      return;
    }

    socket.send(JSON.stringify(envelope));
  }

  createHelloAck(nodeId: string): AgentEnvelope {
    return {
      protocolVersion: 1,
      messageId: randomUUID(),
      type: 'agent.hello',
      timestamp: new Date().toISOString(),
      payload: {
        accepted: true,
        nodeId,
      },
    };
  }

  async validateEnrollment(nodeId: string, token: string): Promise<GameNode> {
    const node = await this.prisma.gameNode.findFirst({
      where: {
        OR: [{ id: nodeId }, { name: nodeId }],
      },
    });

    if (!node?.enrollmentTokenHash) {
      throw new UnauthorizedException('Node not enrolled');
    }

    if (!verifyTokenHash(token, node.enrollmentTokenHash)) {
      throw new UnauthorizedException('Invalid enrollment token');
    }

    return node;
  }

  async markNodeOnline(nodeId: string, agentVersion?: string): Promise<void> {
    await this.prisma.gameNode.update({
      where: { id: nodeId },
      data: {
        status: 'ONLINE',
        lastHeartbeatAt: new Date(),
        enrolledAt: new Date(),
        agentVersion,
        enrollmentTokenHash: null,
      },
    });
  }

  async handleAgentMessage(nodeId: string, raw: string): Promise<void> {
    let envelope: AgentEnvelope;

    try {
      envelope = JSON.parse(raw) as AgentEnvelope;
    } catch {
      this.logger.warn(`Invalid JSON from node ${nodeId}`);
      return;
    }

    switch (envelope.type) {
      case 'agent.hello':
        await this.markNodeOnline(
          nodeId,
          typeof envelope.payload?.['agentVersion'] === 'string'
            ? envelope.payload['agentVersion']
            : undefined,
        );
        break;
      case 'agent.heartbeat':
        await this.persistHeartbeat(nodeId, envelope.payload);
        break;
      case 'server.state':
        await this.handleServerState(envelope);
        break;
      case 'server.operation.completed':
        await this.handleOperationCompleted(envelope);
        break;
      case 'server.operation.failed':
        await this.handleOperationFailed(envelope);
        break;
      default:
        this.logger.debug(`Ignored agent message type ${envelope.type ?? 'unknown'}`);
    }
  }

  private async persistHeartbeat(
    nodeId: string,
    payload?: Record<string, unknown>,
  ): Promise<void> {
    const now = new Date();

    await this.prisma.$transaction([
      this.prisma.gameNode.update({
        where: { id: nodeId },
        data: {
          lastHeartbeatAt: now,
          status: 'ONLINE',
          agentVersion:
            typeof payload?.['agentVersion'] === 'string'
              ? payload['agentVersion']
              : undefined,
        },
      }),
      this.prisma.gameNodeHeartbeat.create({
        data: {
          nodeId,
          payload: (payload ?? undefined) as Prisma.InputJsonValue | undefined,
        },
      }),
    ]);
  }

  private async handleServerState(envelope: AgentEnvelope): Promise<void> {
    const serverId = envelope.payload?.['serverId'];
    const state = envelope.payload?.['state'];

    if (typeof serverId !== 'string' || typeof state !== 'string') {
      return;
    }

    const mapped = AGENT_STATE_MAP[state.toLowerCase()];
    if (!mapped) {
      return;
    }

    await this.serversService.applyAgentStatus(serverId, mapped, {
      correlationId: envelope.correlationId,
      reason: 'agent server.state',
      errorCode:
        typeof envelope.payload?.['errorCode'] === 'string'
          ? envelope.payload['errorCode']
          : undefined,
      errorMessage:
        typeof envelope.payload?.['errorMessage'] === 'string'
          ? envelope.payload['errorMessage']
          : undefined,
    });
  }

  private async handleOperationCompleted(
    envelope: AgentEnvelope,
  ): Promise<void> {
    const serverId = envelope.payload?.['serverId'];
    const operation = envelope.payload?.['operation'];
    const messageId = envelope.messageId;

    if (typeof serverId !== 'string') {
      return;
    }

    const target = this.resolveCompletedStatus(operation);
    if (target) {
      await this.serversService.applyAgentStatus(serverId, target, {
        correlationId: envelope.correlationId,
        reason: `agent operation completed: ${String(operation ?? 'unknown')}`,
      });
    }

    if (typeof messageId === 'string') {
      await this.publishNodeResponse(messageId, {
        success: true,
        resultCode: typeof operation === 'string' ? operation : 'completed',
      });
    }
  }

  private async handleOperationFailed(envelope: AgentEnvelope): Promise<void> {
    const serverId = envelope.payload?.['serverId'];
    const messageId = envelope.messageId;

    if (typeof serverId === 'string') {
      await this.serversService.applyAgentStatus(serverId, 'ERROR', {
        correlationId: envelope.correlationId,
        reason: 'agent operation failed',
        errorCode:
          typeof envelope.payload?.['errorCode'] === 'string'
            ? envelope.payload['errorCode']
            : undefined,
        errorMessage:
          typeof envelope.payload?.['errorMessage'] === 'string'
            ? envelope.payload['errorMessage']
            : undefined,
      });
    }

    if (typeof messageId === 'string') {
      await this.publishNodeResponse(messageId, {
        success: false,
        errorCode:
          typeof envelope.payload?.['errorCode'] === 'string'
            ? envelope.payload['errorCode']
            : 'OPERATION_FAILED',
        errorMessage:
          typeof envelope.payload?.['errorMessage'] === 'string'
            ? envelope.payload['errorMessage']
            : 'Agent operation failed',
      });
    }
  }

  private async publishNodeResponse(
    messageId: string,
    response: {
      success: boolean;
      resultCode?: string;
      errorCode?: string;
      errorMessage?: string;
    },
  ): Promise<void> {
    await this.redis.publish(
      `hgc:node:responses:${messageId}`,
      JSON.stringify(response),
    );
  }

  private resolveCompletedStatus(operation: unknown): GameServerStatus | null {
    switch (operation) {
      case 'provision':
        return 'STOPPED';
      case 'install':
        return 'STOPPED';
      case 'start':
        return 'RUNNING';
      case 'stop':
        return 'STOPPED';
      case 'delete':
        return 'DELETED';
      default:
        return null;
    }
  }
}
