import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PinoLogger } from 'nestjs-pino';
import { PrismaService } from '../prisma/prisma.service';
import { EncounterService } from '../missions/encounter.service';
import { MissionStoreService } from '../missions/mission-store.service';
import { TwitchPubSubService } from './twitch-pubsub.service';

@Injectable()
export class TickService {
  constructor(
    private readonly logger: PinoLogger,
    private readonly store: MissionStoreService,
    private readonly encounters: EncounterService,
    private readonly prisma: PrismaService,
    private readonly pubsub: TwitchPubSubService,
  ) {}

  @Cron(CronExpression.EVERY_10_SECONDS)
  async heartbeat(): Promise<void> {
    const now = Date.now();
    const dueMissionIds = await this.store.getDueMissionIds(now);

    await Promise.allSettled(
      dueMissionIds.map((id) => this.processMission(id)),
    );
  }

  private async processMission(missionId: string): Promise<void> {
    const token = await this.store.acquireLock(missionId);
    if (!token) return;

    // Fetch channelId up-front so every log line in this mission carries it.
    const channelId = await this.getChannelId(missionId);
    const log = this.logger.logger.child({ missionId, channelId });

    try {
      const state = await this.store.getActiveMission(missionId);
      if (!state || state.mission.status !== 'active') {
        await this.store.removeMissionFromQueue(missionId);
        return;
      }

      const updated = this.encounters.processTick(state, channelId);
      const newLogs = updated.recentLog.slice(
        0,
        updated.recentLog.length - state.recentLog.length,
      );

      // Write to Postgres in a transaction.
      await this.prisma.$transaction(async (tx) => {
        await tx.mission.update({
          where: { id: missionId },
          data: {
            tickIndex: updated.mission.tickIndex,
            nextTickAt: new Date(updated.mission.nextTickAt),
            status: updated.mission.status,
            endedAt: updated.mission.endedAt
              ? new Date(updated.mission.endedAt)
              : null,
          },
        });

        for (const survivor of updated.survivors) {
          const participant = updated.mission.participants.find(
            (p) => p.survivorId === survivor.id,
          );
          if (!participant) continue;
          await tx.missionParticipant.updateMany({
            where: { missionId, survivorId: survivor.id },
            data: {
              state: participant.state,
              hookCount: participant.hookCount,
            },
          });
          await tx.survivor.update({
            where: { id: survivor.id },
            data: { state: survivor.state },
          });
        }

        if (newLogs.length > 0) {
          await tx.missionLog.createMany({
            data: newLogs.map((entry) => ({
              id: crypto.randomUUID(),
              missionId,
              tickIndex: entry.tickIndex,
              encounterKey: entry.encounterKey,
              renderedText: entry.logText,
              seed: entry.seed,
              modifiersApplied: entry.modifiersApplied,
            })),
          });
        }
      });

      // Update Redis cache.
      await this.store.setActiveMission(updated);

      if (updated.mission.status === 'active') {
        const nextMs = new Date(updated.mission.nextTickAt).getTime();
        await this.store.scheduleTick(missionId, nextMs);
      } else {
        await this.store.removeMissionFromQueue(missionId);
        log.info({
          msg: 'mission ended',
          status: updated.mission.status,
          tickIndex: updated.mission.tickIndex,
        });
      }

      await this.pubsub.broadcast(channelId, updated);
    } catch (err) {
      log.error({ msg: 'tick failed', err });
    } finally {
      await this.store.releaseLock(missionId, token);
    }
  }

  private async getChannelId(missionId: string): Promise<string | null> {
    const mission = await this.prisma.mission.findUnique({
      where: { id: missionId },
      select: { channelId: true },
    });
    return mission?.channelId ?? null;
  }
}
