import {
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import type {
  Mission,
  MissionDurationMinutes,
  MissionStateResponse,
  Survivor,
  SurvivorStats,
} from '@fog-explorer/api-interfaces';
import { getLibraryVersion, KILLER_NAMES } from '@fog-explorer/encounter-library';
import seedrandom = require('seedrandom');
import { TwitchJwtPayload } from '../auth/twitch-jwt.guard';
import { PrismaService } from '../prisma/prisma.service';
import { MissionStoreService } from './mission-store.service';

const TICK_BASE_INTERVAL_MS = 60_000;
const TICK_JITTER_MS = 5_000;

@Injectable()
export class MissionsService {
  constructor(
    private readonly store: MissionStoreService,
    private readonly prisma: PrismaService
  ) {}

  async startMission(
    claims: TwitchJwtPayload,
    difficulty: number,
    durationMinutes: MissionDurationMinutes,
    characterName?: string,
  ): Promise<NonNullable<MissionStateResponse>> {
    const missionId = crypto.randomUUID();
    const survivorId = crypto.randomUUID();
    const now = new Date();
    const jitter = Math.floor(Math.random() * TICK_JITTER_MS);
    const nextTickAt = new Date(now.getTime() + TICK_BASE_INTERVAL_MS + jitter);
    const stats: SurvivorStats = { objectives: 5, survival: 5, altruism: 5 };
    const survivorName = characterName ?? defaultName(claims.opaque_user_id);
    const killerName = pickKiller(missionId);

    // Upsert user and create survivor + mission in one transaction.
    await this.prisma.$transaction(async (tx) => {
      const user = await tx.user.upsert({
        where: { opaqueUserId: claims.opaque_user_id },
        create: { id: crypto.randomUUID(), opaqueUserId: claims.opaque_user_id },
        update: {},
      });

      await tx.survivor.create({
        data: {
          id: survivorId,
          userId: user.id,
          channelId: claims.channel_id,
          name: survivorName,
          state: 'active',
          stats,
          perkSlots: [],
        },
      });

      await tx.mission.create({
        data: {
          id: missionId,
          channelId: claims.channel_id,
          difficulty,
          durationMinutes,
          status: 'active',
          killerName,
          encounterLibraryVersion: getLibraryVersion(),
          nextTickAt,
          participants: {
            create: {
              id: crypto.randomUUID(),
              survivorId,
              state: 'active',
              hookCount: 0,
            },
          },
        },
      });
    });

    const survivor: Survivor = {
      id: survivorId,
      opaqueUserId: claims.opaque_user_id,
      channelId: claims.channel_id,
      name: survivorName,
      state: 'active',
      stats,
      perkSlots: [],
      createdAt: now.toISOString(),
    };

    const mission: Mission = {
      id: missionId,
      groupId: null,
      participants: [{ survivorId, state: 'active', hookCount: 0 }],
      difficulty,
      durationMinutes,
      status: 'active',
      killerName,
      encounterLibraryVersion: getLibraryVersion(),
      nextTickAt: nextTickAt.toISOString(),
      tickIndex: 0,
      startedAt: now.toISOString(),
      endedAt: null,
    };

    const state: NonNullable<MissionStateResponse> = {
      mission,
      survivors: [survivor],
      recentLog: [],
    };

    await this.store.setActiveMission(state);
    await this.store.setChannelMissionId(claims.channel_id, missionId);
    await this.store.scheduleTick(missionId, nextTickAt.getTime());

    return state;
  }

  async abandonMission(claims: TwitchJwtPayload): Promise<void> {
    const state = await this.store.getStateForChannel(claims.channel_id);

    if (!state) {
      throw new NotFoundException('No active mission');
    }
    if (state.mission.status !== 'active') {
      throw new ForbiddenException('Mission is not active');
    }

    const isParticipant = state.survivors.some(
      (s) => s.opaqueUserId === claims.opaque_user_id,
    );
    if (!isParticipant) {
      throw new ForbiddenException('Not a participant in this mission');
    }

    const now = new Date().toISOString();
    const updated: NonNullable<MissionStateResponse> = {
      ...state,
      mission: { ...state.mission, status: 'abandoned', endedAt: now },
    };

    await this.prisma.mission.update({
      where: { id: state.mission.id },
      data: { status: 'abandoned', endedAt: new Date(now) },
    });

    await this.store.setActiveMission(updated);
    await this.store.removeMissionFromQueue(state.mission.id);
  }
}

function defaultName(opaqueUserId: string): string {
  return `Survivor ${opaqueUserId.slice(-4)}`;
}

function pickKiller(missionId: string): string {
  const rng = seedrandom(`killer:${missionId}`);
  return KILLER_NAMES[Math.floor(rng() * KILLER_NAMES.length)];
}
