import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { PlayerAction } from '../../entities/player-action.entity';
import { PlayerSession } from '../../entities/player-session.entity';
import { InstancesService } from '../instances/instances.service';
import { WebhooksService } from '../webhooks/webhooks.service';
import { PlayerActionDto } from './dto/player-action.dto';

export interface Player {
  steam_id: string;
  player_name: string;
  status: 'online' | 'offline' | 'banned';
  last_seen?: Date;
  ban_expires?: Date | null;
  total_sessions?: number;
  total_playtime_seconds?: number;
}

@Injectable()
export class PlayersService {
  constructor(
    @InjectRepository(PlayerAction)
    private readonly actionRepo: Repository<PlayerAction>,
    @InjectRepository(PlayerSession)
    private readonly sessionRepo: Repository<PlayerSession>,
    private readonly instancesService: InstancesService,
    private readonly webhooksService: WebhooksService,
  ) {}

  /**
   * Get players for a license.
   *
   * Primary source: player_sessions table (tracks session lifecycles).
   * Secondary source: player_actions table (determines ban status).
   * A player whose most recent action is a 'ban' (not subsequently 'unban') is shown as banned.
   * A player with an open session (session_end IS NULL) is shown as online.
   */
  async getPlayers(licenseId: string): Promise<{ players: Player[] }> {
    // Get distinct players from session history
    const sessions = await this.sessionRepo
      .createQueryBuilder('session')
      .where('session.license_id = :licenseId', { licenseId })
      .orderBy('session.session_start', 'DESC')
      .getMany();

    // Build per-player session aggregates
    const playerMap = new Map<string, Player>();

    for (const session of sessions) {
      if (!playerMap.has(session.steam_id)) {
        const isOnline = session.session_end === null;
        playerMap.set(session.steam_id, {
          steam_id: session.steam_id,
          player_name: session.player_name,
          status: isOnline ? 'online' : 'offline',
          last_seen: session.session_start,
          ban_expires: null,
          total_sessions: 0,
          total_playtime_seconds: 0,
        });
      }

      const entry = playerMap.get(session.steam_id)!;
      entry.total_sessions = (entry.total_sessions || 0) + 1;
      entry.total_playtime_seconds = (entry.total_playtime_seconds || 0) + (session.duration_seconds || 0);
    }

    // Overlay ban status from most recent action per player
    const recentActions = await this.actionRepo
      .createQueryBuilder('action')
      .where('action.license_id = :licenseId', { licenseId })
      .orderBy('action.created_at', 'DESC')
      .getMany();

    // Track the most recent action per steam_id to determine ban state
    const latestActionBySteamId = new Map<string, PlayerAction>();
    for (const action of recentActions) {
      if (!latestActionBySteamId.has(action.steam_id)) {
        latestActionBySteamId.set(action.steam_id, action);
      }
    }

    for (const [steamId, action] of latestActionBySteamId) {
      if (action.action_type === 'ban') {
        // Add player from actions even if they have no sessions
        if (!playerMap.has(steamId)) {
          playerMap.set(steamId, {
            steam_id: action.steam_id,
            player_name: action.player_name,
            status: 'banned',
            last_seen: action.created_at,
            ban_expires: action.duration_minutes
              ? new Date(action.created_at.getTime() + action.duration_minutes * 60000)
              : null,
            total_sessions: 0,
            total_playtime_seconds: 0,
          });
        } else {
          const entry = playerMap.get(steamId)!;
          entry.status = 'banned';
          entry.ban_expires = action.duration_minutes
            ? new Date(action.created_at.getTime() + action.duration_minutes * 60000)
            : null;
        }
      }
    }

    const players = Array.from(playerMap.values());

    return { players };
  }

  /**
   * Perform a moderation action on a player.
   * Supported actions: kick, ban, unban, warn, note.
   * kick/ban/unban are forwarded to the game server via NATS.
   */
  async performAction(
    licenseId: string,
    userId: string,
    dto: PlayerActionDto,
  ): Promise<{ success: boolean }> {
    // Insert action record
    const action = this.actionRepo.create({
      license_id: licenseId,
      steam_id: dto.steam_id,
      player_name: dto.player_name,
      action_type: dto.action_type,
      reason: dto.reason || null,
      duration_minutes: dto.duration_minutes || null,
      performed_by: userId,
    });

    await this.actionRepo.save(action);

    // Forward kick, ban, and unban to the game server via RCON
    if (dto.action_type === 'kick' || dto.action_type === 'ban' || dto.action_type === 'unban') {
      const rconCmd = this.buildRconCommand(dto);
      await this.instancesService.rconForLicense(licenseId, rconCmd);
    }

    // Fire webhook event for player bans. Fire-and-forget — a delivery failure
    // must never surface to the caller or roll back the ban action.
    if (dto.action_type === 'ban') {
      void this.webhooksService
        .dispatch(licenseId, 'player_banned', {
          steam_id: dto.steam_id,
          player_name: dto.player_name,
          reason: dto.reason ?? null,
          duration_minutes: dto.duration_minutes ?? null,
        })
        .catch(() => {
          // dispatch() already logs internally; swallow here to guarantee
          // the ban action result is unaffected.
        });
    }

    return { success: true };
  }

  private buildRconCommand(dto: PlayerActionDto): string {
    // Defense-in-depth against RCON command injection. The command is a single
    // line; an id or reason containing a newline/control char could break the
    // framing and inject a second console command. So:
    //  - the player id must be a safe token (no whitespace/control chars) — a
    //    permissive charset, not a Rust-only SteamID64 regex, so Conan (Funcom)
    //    and Dune ids still validate. Reject outright if not.
    //  - the free-text reason has control chars stripped and is length-capped.
    //  - duration is coerced to a non-negative integer.
    const id = dto.steam_id ?? '';
    if (!/^[A-Za-z0-9_.:-]{1,64}$/.test(id)) {
      throw new BadRequestException('Invalid player id');
    }
    const safeReason =
      (dto.reason ?? 'banned').replace(/[\u0000-\u001F]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 200) || 'banned';
    const secs = Number.isFinite(dto.duration_minutes)
      ? Math.max(0, Math.floor((dto.duration_minutes as number) * 60))
      : 0;

    switch (dto.action_type) {
      case 'kick':
        return `kick ${id}${dto.reason ? ' ' + safeReason : ''}`;
      case 'ban':
        // banid <steamId> <reason> <durationSeconds>  — 0 = permanent
        return `banid ${id} ${safeReason} ${secs}`;
      case 'unban':
        return `unban ${id}`;
      default:
        return '';
    }
  }
}
