import { Injectable, NotFoundException } from '@nestjs/common';
import { createHash } from 'crypto';
import { nanoid } from 'nanoid';
import { PromptBuilderService } from '../ai/prompt-builder.service';
import { VllmClientService } from '../ai/vllm-client.service';
import { PrismaService } from '../database/prisma.service';
import { RiotAccountService } from '../riot/riot-account.service';
import { MatchQueueFilter, RiotMatchService } from '../riot/riot-match.service';
import { CreateAnalysisDto } from './dto/create-analysis.dto';
import { GgtiScoreService } from './ggti-score.service';
import { LookupAnalysisDto } from '../riot/dto/search-riot-user.dto';

const DEFAULT_VISIBLE_MATCH_COUNT = 20;

@Injectable()
export class AnalysisService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly riotAccountService: RiotAccountService,
    private readonly riotMatchService: RiotMatchService,
    private readonly ggtiScoreService: GgtiScoreService,
    private readonly promptBuilder: PromptBuilderService,
    private readonly vllm: VllmClientService,
  ) {}

  async createAnalysisRequest(dto: CreateAnalysisDto) {
    return this.runAnalysisNow(dto);
  }

  async getRequest(requestId: number) {
    const request = await this.prisma.analysisRequest.findUnique({
      where: { id: requestId },
      include: { riotAccount: true },
    });
    if (!request) throw new NotFoundException('Analysis request not found');
    const latestSnapshot = await this.prisma.playerAnalysisSnapshot.findFirst({
      where: { riotAccountId: request.riotAccountId },
      orderBy: { analyzedAt: 'desc' },
      select: { id: true, analyzedAt: true },
    });
    return {
      ...request,
      latestSnapshotId: latestSnapshot?.id ?? null,
      lastUpdatedAt: latestSnapshot?.analyzedAt ?? null,
    };
  }

  async getSnapshot(snapshotId: number) {
    const snapshot = await this.prisma.playerAnalysisSnapshot.findUnique({
      where: { id: snapshotId },
      include: this.snapshotInclude(),
    });
    if (!snapshot) throw new NotFoundException('Analysis snapshot not found');
    const totalMatchCount = await this.prisma.participantMatch.count({
      where: { riotAccountId: snapshot.riotAccountId },
    });
    return {
      ...this.formatSnapshot(snapshot),
      totalMatchCount,
    };
  }

  async getSnapshotMatches(
    snapshotId: number,
    offset = 0,
    limit = 20,
    queueFilter?: string,
  ) {
    const snapshot = await this.prisma.playerAnalysisSnapshot.findUnique({
      where: { id: snapshotId },
      select: {
        riotAccountId: true,
        riotAccount: {
          select: {
            puuid: true,
            summoners: {
              orderBy: { updatedAt: 'desc' },
              take: 1,
              select: {
                region: true,
              },
            },
          },
        },
      },
    });

    if (!snapshot) {
      throw new NotFoundException('Analysis snapshot not found');
    }

    const safeOffset = Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
    const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.min(20, Math.floor(limit))) : 20;
    const normalizedQueueFilter = normalizeMatchQueueFilter(queueFilter);
    const minimumCount = safeOffset + safeLimit + 1;
    const where = this.buildMatchQueueFilterWhere(snapshot.riotAccountId, normalizedQueueFilter);
    let total = await this.prisma.participantMatch.count({
      where,
    });

    if (safeOffset > 0 && total < minimumCount) {
      const region = snapshot.riotAccount.summoners[0]?.region ?? 'KR';

      await this.riotMatchService.ensureMatchesForFilter(
        {
          id: snapshot.riotAccountId,
          puuid: snapshot.riotAccount.puuid,
        },
        region,
        normalizedQueueFilter,
        minimumCount,
      );

      total = await this.prisma.participantMatch.count({
        where,
      });
    }

    const participantMatches = await this.prisma.participantMatch.findMany({
      where,
      orderBy: [
        { match: { gameStartAt: 'desc' } },
        { id: 'desc' },
      ],
      skip: safeOffset,
      take: safeLimit,
      include: {
        match: true,
        timelineFeature: true,
        matchInsight: true,
      },
    });

    const items = participantMatches.map((participantMatch) =>
      this.formatRecentMatchItem(participantMatch, participantMatch.matchInsight),
    );

    return {
      items,
      total,
      nextOffset: safeOffset + items.length,
      hasMore: safeOffset + items.length < total,
    };
  }

  async getMatchDetail(participantMatchId: number) {
    const participantMatch = await this.prisma.participantMatch.findUnique({
      where: { id: BigInt(participantMatchId) },
      include: {
        riotAccount: true,
        matchInsight: true,
        match: true,
      },
    });

    if (!participantMatch) {
      throw new NotFoundException('Participant match not found');
    }

    const rawMatch = participantMatch.match.rawJson as {
      info?: {
        participants?: Array<Record<string, unknown>>;
        teams?: Array<Record<string, unknown>>;
      };
    };
    const rawParticipants = Array.isArray(rawMatch.info?.participants) ? rawMatch.info?.participants ?? [] : [];
    const rawTeams = Array.isArray(rawMatch.info?.teams) ? rawMatch.info?.teams ?? [] : [];
    const playerRawParticipant =
      rawParticipants.find(
        (candidate) =>
          Number(candidate.participantId ?? 0) === participantMatch.participantId,
      ) ?? null;
    const playerTeamId = participantMatch.teamId;
    const blueParticipants = rawParticipants
      .filter((candidate) => Number(candidate.teamId ?? 0) === 100)
      .map((candidate) => this.formatMatchParticipant(candidate, participantMatch.riotAccount.puuid));
    const redParticipants = rawParticipants
      .filter((candidate) => Number(candidate.teamId ?? 0) === 200)
      .map((candidate) => this.formatMatchParticipant(candidate, participantMatch.riotAccount.puuid));

    return {
      participantMatchId: participantMatch.id,
      matchId: participantMatch.match.matchId,
      queueId: participantMatch.match.queueId,
      gameStartAt: participantMatch.match.gameStartAt,
      gameDurationSec: participantMatch.match.gameDurationSec,
      teamId: participantMatch.teamId,
      win: participantMatch.win,
      aiSummary: participantMatch.matchInsight?.aiSummary ?? this.buildFallbackMatchSummary(participantMatch),
      aiDetail: participantMatch.matchInsight?.aiDetail ?? this.buildFallbackMatchDetail(participantMatch),
      tags: Array.isArray(participantMatch.matchInsight?.tagsJson) ? participantMatch.matchInsight.tagsJson : [],
      player: {
        participantId: participantMatch.participantId,
        teamId: participantMatch.teamId,
        gameName:
          this.extractRiotIdGameName(playerRawParticipant) ??
          participantMatch.riotAccount.gameName,
        tagLine:
          this.extractRiotIdTagLine(playerRawParticipant) ??
          participantMatch.riotAccount.tagLine,
      },
      teams: [
        this.formatTeamDetail(
          100,
          blueParticipants,
          rawTeams.find((team) => Number(team.teamId ?? 0) === 100) ?? null,
        ),
        this.formatTeamDetail(
          200,
          redParticipants,
          rawTeams.find((team) => Number(team.teamId ?? 0) === 200) ?? null,
        ),
      ],
      playerTeamSide: playerTeamId === 100 ? 'BLUE' : 'RED',
    };
  }

  async lookupLatest(dto: LookupAnalysisDto) {
    let riotAccount = await this.prisma.riotAccount.findFirst({
      where: {
        gameName: dto.gameName.trim(),
        tagLine: dto.tagLine.trim(),
      },
    });

    if (!riotAccount && dto.syncIfMissing !== false) {
      const account = await this.riotAccountService.findOrSyncAccount(dto);
      riotAccount = await this.prisma.riotAccount.findUnique({
        where: { puuid: account.puuid },
      });
    }

    if (!riotAccount) {
      return {
        found: false,
        latestSnapshotId: null,
        snapshot: null,
        pendingRequestId: null,
        pendingStatus: null,
      };
    }

    const latestSnapshot = await this.prisma.playerAnalysisSnapshot.findFirst({
      where: { riotAccountId: riotAccount.id },
      orderBy: { analyzedAt: 'desc' },
      include: this.snapshotInclude(),
    });

    const pendingRequest = await this.prisma.analysisRequest.findFirst({
      where: {
        riotAccountId: riotAccount.id,
        status: { in: ['PENDING', 'COLLECTING_MATCHES', 'CALCULATING', 'GENERATING_AI'] },
      },
      orderBy: { createdAt: 'desc' },
    });

    return {
      found: true,
      latestSnapshotId: latestSnapshot?.id ?? null,
      snapshot: latestSnapshot ? this.formatSnapshot(latestSnapshot) : null,
      pendingRequestId: pendingRequest?.id ?? null,
      pendingStatus: pendingRequest?.status ?? null,
      lastUpdatedAt: latestSnapshot?.analyzedAt ?? null,
    };
  }

  async enqueueRefresh(dto: CreateAnalysisDto) {
    const riotAccount = await this.resolveRiotAccountForRequest(dto);
    const latestSnapshot = await this.prisma.playerAnalysisSnapshot.findFirst({
      where: { riotAccountId: riotAccount.id },
      orderBy: { analyzedAt: 'desc' },
    });

    const existingRequest = await this.prisma.analysisRequest.findFirst({
      where: {
        riotAccountId: riotAccount.id,
        status: { in: ['PENDING', 'COLLECTING_MATCHES', 'CALCULATING', 'GENERATING_AI'] },
      },
      orderBy: { createdAt: 'desc' },
    });

    if (existingRequest) {
      return {
        requestId: existingRequest.id,
        status: existingRequest.status,
        latestSnapshotId: latestSnapshot?.id ?? null,
      };
    }

    const request = await this.prisma.analysisRequest.create({
      data: {
        riotAccountId: riotAccount.id,
        status: 'PENDING',
        requestedMatchCount: dto.matchCount ?? 0,
      },
    });

    setImmediate(() => {
      void this.processRequest(request.id, dto).catch(() => undefined);
    });

    return {
      requestId: request.id,
      status: 'PENDING',
      latestSnapshotId: latestSnapshot?.id ?? null,
    };
  }

  private async runAnalysisNow(dto: CreateAnalysisDto) {
    const riotAccount = await this.resolveRiotAccountForRequest(dto);
    const request = await this.prisma.analysisRequest.create({
      data: {
        riotAccountId: riotAccount.id,
        status: 'COLLECTING_MATCHES',
        requestedMatchCount: dto.matchCount ?? 0,
        startedAt: new Date(),
      },
    });

    const snapshot = await this.processRequest(request.id, dto, riotAccount.id, riotAccount.gameName);

    return {
      requestId: request.id,
      status: 'DONE',
      latestSnapshotId: snapshot.id,
    };
  }

  private async processRequest(
    requestId: bigint | number,
    dto: CreateAnalysisDto,
    riotAccountId?: bigint,
    gameName?: string,
  ) {
    const request = await this.prisma.analysisRequest.findUniqueOrThrow({
      where: { id: BigInt(requestId) },
      include: { riotAccount: true },
    });

    await this.prisma.analysisRequest.update({
      where: { id: request.id },
      data: { status: 'COLLECTING_MATCHES', startedAt: request.startedAt ?? new Date() },
    });

    try {
      await this.riotAccountService.syncSummonerProfile(
        riotAccountId ?? request.riotAccountId,
        request.riotAccount.puuid,
        dto.region,
      );

      await this.riotMatchService.collectRecentMatchesForAccount(
        {
          id: riotAccountId ?? request.riotAccountId,
          puuid: request.riotAccount.puuid,
        },
        dto.region,
        dto.matchCount,
      );

      await this.prisma.analysisRequest.update({
        where: { id: request.id },
        data: { status: 'GENERATING_AI' },
      });

      const snapshot = await this.createSnapshot(
        riotAccountId ?? request.riotAccountId,
        gameName ?? request.riotAccount.gameName,
      );

      await this.prisma.analysisRequest.update({
        where: { id: request.id },
        data: { status: 'DONE', finishedAt: new Date() },
      });

      return snapshot;
    } catch (error) {
      await this.prisma.analysisRequest.update({
        where: { id: request.id },
        data: {
          status: 'FAILED',
          errorCode: 'ANALYSIS_FAILED',
          errorMessage: error instanceof Error ? error.message.slice(0, 500) : 'unknown error',
          finishedAt: new Date(),
        },
      });
      throw error;
    }
  }

  private async resolveRiotAccountForRequest(dto: CreateAnalysisDto) {
    if (dto.snapshotId) {
      const snapshot = await this.prisma.playerAnalysisSnapshot.findUnique({
        where: { id: BigInt(dto.snapshotId) },
        include: { riotAccount: true },
      });

      if (snapshot?.riotAccount) {
        return snapshot.riotAccount;
      }
    }

    const cachedAccount = await this.prisma.riotAccount.findFirst({
      where: {
        gameName: dto.gameName.trim(),
        tagLine: dto.tagLine.trim(),
      },
    });

    if (cachedAccount) {
      return cachedAccount;
    }

    const syncedAccount = await this.riotAccountService.findOrSyncAccount(dto);
    return this.prisma.riotAccount.findUniqueOrThrow({
      where: { puuid: syncedAccount.puuid },
    });
  }

  private snapshotInclude() {
    return {
      shareCards: true,
      riotAccount: {
        include: {
          summoners: {
            orderBy: { updatedAt: 'desc' as const },
            take: 1,
          },
        },
      },
      matchInsights: {
        include: {
          participantMatch: {
            include: {
              match: true,
              timelineFeature: true,
            },
          },
        },
      },
    };
  }

  private formatSnapshot(snapshot: any) {
    const latestSummoner = snapshot.riotAccount.summoners[0] ?? null;

    return {
      snapshotId: snapshot.id,
      gameName: snapshot.riotAccount.gameName,
      tagLine: snapshot.riotAccount.tagLine,
      profileIconId: latestSummoner?.profileIconId ?? null,
      summonerLevel: latestSummoner?.summonerLevel ?? null,
      currentRanks: {
        solo: this.formatRankQueue(
          latestSummoner?.soloTier,
          latestSummoner?.soloRank,
          latestSummoner?.soloLeaguePoints,
          latestSummoner?.soloWins,
          latestSummoner?.soloLosses,
        ),
        flex: this.formatRankQueue(
          latestSummoner?.flexTier,
          latestSummoner?.flexRank,
          latestSummoner?.flexLeaguePoints,
          latestSummoner?.flexWins,
          latestSummoner?.flexLosses,
        ),
      },
      ggtiType: snapshot.ggtiType,
      sampleMatchCount: snapshot.sampleMatchCount,
      primaryChampionName: snapshot.primaryChampionName,
      traitScores: snapshot.traitScores,
      featureSummary: snapshot.featureSummary,
      aiSummary: snapshot.aiSummary,
      aiDetail: snapshot.aiDetail,
      shareKey: snapshot.shareCards[0]?.shareKey ?? null,
      analyzedAt: snapshot.analyzedAt,
      recentMatches: snapshot.matchInsights
        .sort(
          (a: any, b: any) =>
            b.participantMatch.match.gameStartAt.getTime() - a.participantMatch.match.gameStartAt.getTime(),
        )
        .map((insight: any) => this.formatRecentMatchItem(insight.participantMatch, insight)),
    };
  }

  private formatRankQueue(
    tier?: string | null,
    rank?: string | null,
    leaguePoints?: number | null,
    wins?: number | null,
    losses?: number | null,
  ) {
    if (!tier || !rank || leaguePoints == null) {
      return null;
    }

    return {
      tier,
      rank,
      leaguePoints,
      wins: wins ?? 0,
      losses: losses ?? 0,
    };
  }

  private formatRecentMatchItem(participantMatch: any, insight?: any) {
    const rawParticipant = this.extractRawParticipant(
      participantMatch.match.rawJson,
      participantMatch.participantId,
    );

    return {
      participantMatchId: participantMatch.id,
      matchId: participantMatch.match.matchId,
      queueId: participantMatch.match.queueId,
      gameStartAt: participantMatch.match.gameStartAt,
      gameDurationSec: participantMatch.match.gameDurationSec,
      championName: participantMatch.championName,
      lane: participantMatch.lane,
      role: participantMatch.role,
      win: participantMatch.win,
      kills: participantMatch.kills,
      deaths: participantMatch.deaths,
      assists: participantMatch.assists,
      killParticipation: participantMatch.killParticipation,
      visionScore: participantMatch.visionScore,
      totalMinionsKilled: participantMatch.totalMinionsKilled + participantMatch.neutralMinionsKilled,
      goldEarned: participantMatch.goldEarned,
      totalDamageToChampions: participantMatch.totalDamageToChampions,
      items: this.extractItems(rawParticipant),
      summonerSpellIds: this.extractSummonerSpellIds(rawParticipant),
      kdaRatio: this.kdaRatio(
        participantMatch.kills,
        participantMatch.deaths,
        participantMatch.assists,
      ),
      featureScores: participantMatch.timelineFeature
        ? {
            aggression: Number(participantMatch.timelineFeature.aggressionScore),
            stability: Number(participantMatch.timelineFeature.stabilityScore),
            roaming: Number(participantMatch.timelineFeature.roamingScore),
            vision: Number(participantMatch.timelineFeature.visionScore),
            objective: Number(participantMatch.timelineFeature.objectiveScore),
            growth: Number(participantMatch.timelineFeature.growthScore),
            fightJoin: Number(participantMatch.timelineFeature.fightJoinScore),
          }
        : null,
      aiSummary: insight?.aiSummary ?? this.buildFallbackMatchSummary(participantMatch),
      aiDetail: insight?.aiDetail ?? this.buildFallbackMatchDetail(participantMatch),
      tags: Array.isArray(insight?.tagsJson) ? insight.tagsJson : this.buildFallbackMatchTags(participantMatch),
    };
  }

  private buildMatchQueueFilterWhere(
    riotAccountId: bigint,
    queueFilter: MatchQueueFilter,
  ) {
    switch (queueFilter) {
      case 'RANKED_SOLO':
        return { riotAccountId, match: { queueId: 420 } };
      case 'RANKED_FLEX':
        return { riotAccountId, match: { queueId: 440 } };
      case 'ARAM':
        return { riotAccountId, match: { queueId: 450 } };
      case 'GAME_TYPES':
        return {
          riotAccountId,
          match: {
            queueId: {
              notIn: [420, 440, 450],
            },
          },
        };
      case 'ALL':
      default:
        return { riotAccountId };
    }
  }

  private async createSnapshot(riotAccountId: bigint, gameName: string) {
    const participants = await this.prisma.participantMatch.findMany({
      where: { riotAccountId },
      orderBy: [
        { match: { gameStartAt: 'desc' } },
        { id: 'desc' },
      ],
      include: {
        match: true,
        timelineFeature: true,
      },
    });

    const scores = this.ggtiScoreService.calculate(participants);
    const ggtiType = this.ggtiScoreService.resolveType(scores);
    const primaryChampionName = this.resolvePrimaryChampion(participants);
    type Participant = (typeof participants)[number];
    const featureSummary = {
      matchCount: participants.length,
      averageKills: this.avg(participants.map((p: Participant) => p.kills)),
      averageDeaths: this.avg(participants.map((p: Participant) => p.deaths)),
      averageAssists: this.avg(participants.map((p: Participant) => p.assists)),
    };

    let aiSummary: string | null = null;
    let aiDetail: string | null = null;
    let aiResponse: Record<string, unknown> | null = null;
    let aiErrorMessage: string | null = null;
    let latencyMs: number | null = null;
    const prompt = this.promptBuilder.buildPlayerAnalysisPrompt({
      gameName,
      ggtiType,
      sampleMatchCount: participants.length,
      primaryChampions: primaryChampionName ? [primaryChampionName] : [],
      traitScores: scores,
      behaviorSummary: featureSummary,
    });
    const promptHash = createHash('sha256').update(JSON.stringify(prompt)).digest('hex');
    const model = process.env.VLLM_MODEL ?? 'qwen2.5-32b-gptq-int4';

    try {
      const startedAt = Date.now();
      aiResponse = (await this.vllm.chat(prompt)) as Record<string, unknown>;
      latencyMs = Date.now() - startedAt;
      const content = (aiResponse as { choices?: Array<{ message?: { content?: string } }> })?.choices?.[0]?.message?.content;
      const parsed = typeof content === 'string' ? JSON.parse(content) : null;
      aiSummary = parsed?.summary ?? null;
      aiDetail = parsed?.detail ?? null;
    } catch (error) {
      aiErrorMessage = error instanceof Error ? error.message.slice(0, 500) : 'unknown error';
      aiSummary = `${ggtiType} 성향의 플레이어입니다.`;
      aiDetail = 'AI 분석 생성에 실패해 기본 성향 설명을 제공합니다.';
    }

    const snapshot = await this.prisma.playerAnalysisSnapshot.create({
      data: {
        riotAccountId,
        ggtiType,
        sampleMatchCount: participants.length,
        primaryChampionName,
        traitScores: scores,
        featureSummary,
        aiSummary,
        aiDetail,
        analyzedAt: new Date(),
      },
    });

    await this.prisma.aiRequestLog.create({
      data: {
        snapshotId: snapshot.id,
        requestSource: 'BACKEND_SYNC',
        purpose: 'PLAYER_ANALYSIS',
        model,
        promptHash,
        requestJson: prompt as object,
        responseJson: (aiResponse as object | null) ?? undefined,
        status: aiErrorMessage ? 'FAILED' : 'SUCCESS',
        errorMessage: aiErrorMessage,
        latencyMs,
      },
    });

    await this.prisma.aiAnalysisCache.upsert({
      where: {
        promptHash_model_purpose: {
          promptHash,
          model,
          purpose: 'PLAYER_ANALYSIS',
        },
      },
      update: {
        snapshotId: snapshot.id,
        responseJson: (aiResponse as object | null) ?? {},
        content: aiSummary ?? `${ggtiType} 성향의 플레이어입니다.`,
        latencyMs,
      },
      create: {
        snapshotId: snapshot.id,
        promptHash,
        model,
        purpose: 'PLAYER_ANALYSIS',
        promptJson: prompt as object,
        responseJson: (aiResponse as object | null) ?? {},
        content: aiSummary ?? `${ggtiType} 성향의 플레이어입니다.`,
        latencyMs,
      },
    });

    await this.prisma.shareCard.create({
      data: {
        snapshotId: snapshot.id,
        shareKey: nanoid(10),
        title: `${ggtiType} 플레이어`,
        description: aiSummary ?? 'GGTI 플레이 성향 분석 결과',
      },
    });

    await this.createMatchInsights(
      snapshot.id,
      riotAccountId,
      gameName,
      participants.slice(0, DEFAULT_VISIBLE_MATCH_COUNT),
    );

    return snapshot;
  }

  private async createMatchInsights(
    snapshotId: bigint,
    riotAccountId: bigint,
    gameName: string,
    participants: Array<{
      id: bigint;
      matchId: bigint;
      championName: string;
      lane: string | null;
      role: string | null;
      win: boolean;
      kills: number;
      deaths: number;
      assists: number;
      visionScore: number;
      totalMinionsKilled: number;
      neutralMinionsKilled: number;
      goldEarned: number;
      totalDamageToChampions: number;
      killParticipation: unknown;
      match: {
        queueId: number;
      };
      timelineFeature: {
        earlyDeathCount: number;
        roamEventCount: number;
        objectiveJoinCount: number;
        teamfightJoinCount: number;
        first10mCs: number;
        first10mGold: number;
        aggressionScore: unknown;
        stabilityScore: unknown;
        roamingScore: unknown;
        visionScore: unknown;
        objectiveScore: unknown;
        growthScore: unknown;
        fightJoinScore: unknown;
      } | null;
    }>,
  ) {
    const model = process.env.VLLM_MODEL ?? 'qwen2.5-32b-gptq-int4';

    for (const participant of participants) {
      const prompt = this.promptBuilder.buildMatchAnalysisPrompt({
        gameName,
        championName: participant.championName,
        queueId: participant.match.queueId,
        win: participant.win,
        lane: participant.lane,
        role: participant.role,
        stats: {
          kills: participant.kills,
          deaths: participant.deaths,
          assists: participant.assists,
          visionScore: participant.visionScore,
          totalMinionsKilled: participant.totalMinionsKilled,
          neutralMinionsKilled: participant.neutralMinionsKilled,
          goldEarned: participant.goldEarned,
          totalDamageToChampions: participant.totalDamageToChampions,
          killParticipation:
            typeof participant.killParticipation === 'number'
              ? participant.killParticipation
              : participant.killParticipation
                ? Number(participant.killParticipation)
                : null,
        },
        timelineSummary: participant.timelineFeature
          ? {
              earlyDeathCount: participant.timelineFeature.earlyDeathCount,
              roamEventCount: participant.timelineFeature.roamEventCount,
              objectiveJoinCount: participant.timelineFeature.objectiveJoinCount,
              teamfightJoinCount: participant.timelineFeature.teamfightJoinCount,
              first10mCs: participant.timelineFeature.first10mCs,
              first10mGold: participant.timelineFeature.first10mGold,
              aggressionScore: Number(participant.timelineFeature.aggressionScore),
              stabilityScore: Number(participant.timelineFeature.stabilityScore),
              roamingScore: Number(participant.timelineFeature.roamingScore),
              visionScore: Number(participant.timelineFeature.visionScore),
              objectiveScore: Number(participant.timelineFeature.objectiveScore),
              growthScore: Number(participant.timelineFeature.growthScore),
              fightJoinScore: Number(participant.timelineFeature.fightJoinScore),
            }
          : {},
      });
      const promptHash = createHash('sha256').update(JSON.stringify(prompt)).digest('hex');
      const existing = await this.prisma.participantMatchInsight.findUnique({
        where: { participantMatchId: participant.id },
      });
      if (existing?.promptHash === promptHash && existing.model === model) {
        await this.prisma.participantMatchInsight.update({
          where: { participantMatchId: participant.id },
          data: { snapshotId },
        });
        continue;
      }

      let aiSummary = this.buildFallbackMatchSummary(participant);
      let aiDetail = this.buildFallbackMatchDetail(participant);
      let tags = this.buildFallbackMatchTags(participant);
      let responseJson: Record<string, unknown> | null = null;
      let errorMessage: string | null = null;
      let latencyMs: number | null = null;

      try {
        const startedAt = Date.now();
        responseJson = (await this.vllm.chat(prompt)) as Record<string, unknown>;
        latencyMs = Date.now() - startedAt;
        const content = (responseJson as { choices?: Array<{ message?: { content?: string } }> })?.choices?.[0]?.message?.content;
        const parsed = typeof content === 'string' ? JSON.parse(content) : null;
        aiSummary = parsed?.summary ?? aiSummary;
        aiDetail = parsed?.detail ?? aiDetail;
        tags = Array.isArray(parsed?.tags) ? parsed.tags.slice(0, 3) : tags;
      } catch (error) {
        errorMessage = error instanceof Error ? error.message.slice(0, 500) : 'unknown error';
      }

      await this.prisma.aiRequestLog.create({
        data: {
          snapshotId,
          requestSource: 'BACKEND_SYNC',
          purpose: 'MATCH_ANALYSIS',
          model,
          promptHash,
          requestJson: prompt as object,
          responseJson: (responseJson as object | null) ?? undefined,
          status: errorMessage ? 'FAILED' : 'SUCCESS',
          errorMessage,
          latencyMs,
        },
      });

      await this.prisma.aiAnalysisCache.upsert({
        where: {
          promptHash_model_purpose: {
            promptHash,
            model,
            purpose: 'MATCH_ANALYSIS',
          },
        },
        update: {
          snapshotId,
          responseJson: (responseJson as object | null) ?? {},
          content: aiSummary,
          latencyMs,
        },
        create: {
          snapshotId,
          promptHash,
          model,
          purpose: 'MATCH_ANALYSIS',
          promptJson: prompt as object,
          responseJson: (responseJson as object | null) ?? {},
          content: aiSummary,
          latencyMs,
        },
      });

      await this.prisma.participantMatchInsight.upsert({
        where: { participantMatchId: participant.id },
        update: {
          snapshotId,
          matchId: participant.matchId,
          riotAccountId,
          model,
          promptHash,
          aiSummary,
          aiDetail,
          tagsJson: tags,
        },
        create: {
          snapshotId,
          participantMatchId: participant.id,
          matchId: participant.matchId,
          riotAccountId,
          model,
          promptHash,
          aiSummary,
          aiDetail,
          tagsJson: tags,
        },
      });
    }
  }

  private resolvePrimaryChampion(participants: Array<{ championName: string }>) {
    const counts = new Map<string, number>();
    for (const participant of participants) {
      counts.set(participant.championName, (counts.get(participant.championName) ?? 0) + 1);
    }
    return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
  }

  private avg(values: number[]) {
    if (values.length === 0) return 0;
    return Math.round((values.reduce((sum, value) => sum + value, 0) / values.length) * 10) / 10;
  }

  private buildFallbackMatchSummary(participant: {
    win: boolean;
    championName: string;
    kills: number;
    deaths: number;
    assists: number;
  }) {
    return `${participant.championName} ${participant.win ? '승리' : '패배'} 경기, ${participant.kills}/${participant.deaths}/${participant.assists} 흐름`;
  }

  private buildFallbackMatchDetail(participant: {
    win: boolean;
    kills: number;
    deaths: number;
    assists: number;
    visionScore: number;
    totalMinionsKilled: number;
    neutralMinionsKilled: number;
  }) {
    const cs = participant.totalMinionsKilled + participant.neutralMinionsKilled;
    return `${participant.win ? '이긴 경기였고' : '패배한 경기였고'} KDA는 ${participant.kills}/${participant.deaths}/${participant.assists}입니다. 시야 점수 ${participant.visionScore}, CS ${cs} 기준으로 경기 기여도를 요약했습니다.`;
  }

  private buildFallbackMatchTags(participant: {
    kills: number;
    deaths: number;
    assists: number;
    visionScore: number;
    killParticipation: unknown;
  }) {
    const tags: string[] = [];
    if (participant.kills >= 8) tags.push('킬 관여 강함');
    if (participant.deaths >= 7) tags.push('데스 관리 필요');
    if (participant.assists >= 10) tags.push('교전 합류 빈도 높음');
    if (participant.visionScore >= 40) tags.push('시야 기여 높음');
    const kp =
      typeof participant.killParticipation === 'number'
        ? participant.killParticipation
        : participant.killParticipation
          ? Number(participant.killParticipation)
          : null;
    if (kp !== null && kp >= 0.65) tags.push('팀 기여도 높음');
    return tags.slice(0, 3);
  }

  private extractRawParticipant(rawJson: unknown, participantId: number) {
    const participants = (rawJson as { info?: { participants?: unknown[] } })?.info?.participants;
    if (!Array.isArray(participants)) return null;
    const found = participants.find(
      (participant) =>
        (participant as { participantId?: unknown })?.participantId === participantId,
    );
    return (found as Record<string, unknown> | undefined) ?? null;
  }

  private extractItems(rawParticipant: Record<string, unknown> | null) {
    if (!rawParticipant) return [];
    const itemKeys = ['item0', 'item1', 'item2', 'item3', 'item4', 'item5', 'item6'];
    return itemKeys
      .map((key) => rawParticipant[key])
      .filter((value): value is number => typeof value === 'number' && value > 0);
  }

  private extractSummonerSpellIds(rawParticipant: Record<string, unknown> | null) {
    if (!rawParticipant) return [];
    return ['summoner1Id', 'summoner2Id']
      .map((key) => rawParticipant[key])
      .filter((value): value is number => typeof value === 'number' && value > 0);
  }

  private kdaRatio(kills: number, deaths: number, assists: number) {
    const ratio = deaths === 0 ? kills + assists : (kills + assists) / deaths;
    return Math.round(ratio * 100) / 100;
  }

  private formatMatchParticipant(rawParticipant: Record<string, unknown>, playerPuuid: string) {
    const kills = this.numericField(rawParticipant.kills);
    const deaths = this.numericField(rawParticipant.deaths);
    const assists = this.numericField(rawParticipant.assists);
    const totalMinionsKilled = this.numericField(rawParticipant.totalMinionsKilled);
    const neutralMinionsKilled = this.numericField(rawParticipant.neutralMinionsKilled);
    const gameDurationSec = Math.max(1, this.numericField(rawParticipant.timePlayed) || this.numericField(rawParticipant.gameDuration));
    const damage = this.numericField(rawParticipant.totalDamageDealtToChampions);
    const damageTaken = this.numericField(rawParticipant.totalDamageTaken);

    return {
      participantId: this.numericField(rawParticipant.participantId),
      teamId: this.numericField(rawParticipant.teamId),
      championId: this.numericField(rawParticipant.championId),
      championName: this.stringField(rawParticipant.championName),
      summonerName:
        this.extractRiotIdGameName(rawParticipant) ??
        this.stringField(rawParticipant.summonerName) ??
        'Unknown',
      tagLine: this.extractRiotIdTagLine(rawParticipant),
      puuid: this.stringField(rawParticipant.puuid),
      level: this.numericField(rawParticipant.champLevel),
      win: Boolean(rawParticipant.win),
      kills,
      deaths,
      assists,
      kdaRatio: this.kdaRatio(kills, deaths, assists),
      killParticipation: this.numericField((rawParticipant.challenges as Record<string, unknown> | undefined)?.killParticipation) ?? null,
      visionScore: this.numericField(rawParticipant.visionScore),
      wardsPlaced: this.numericField(rawParticipant.wardsPlaced),
      wardsKilled: this.numericField(rawParticipant.wardsKilled),
      controlWardsBought: this.numericField(rawParticipant.detectorWardsPlaced),
      totalDamageToChampions: damage,
      totalDamageTaken: damageTaken,
      cs: totalMinionsKilled + neutralMinionsKilled,
      csPerMinute: Math.round((((totalMinionsKilled + neutralMinionsKilled) / Math.max(1, gameDurationSec / 60)) * 10)) / 10,
      spells: this.extractSummonerSpellIds(rawParticipant),
      items: this.extractItems(rawParticipant),
      perks: this.extractPerks(rawParticipant),
      isPlayer: this.stringField(rawParticipant.puuid) === playerPuuid,
      goldEarned: this.numericField(rawParticipant.goldEarned),
    };
  }

  private formatTeamDetail(
    teamId: number,
    participants: Array<ReturnType<AnalysisService['formatMatchParticipant']>>,
    rawTeam: Record<string, unknown> | null,
  ) {
    const objectives = (rawTeam?.objectives as Record<string, Record<string, unknown>> | undefined) ?? {};
    return {
      teamId,
      side: teamId === 100 ? 'BLUE' : 'RED',
      result: participants.some((participant) => participant.win) ? 'Victory' : 'Defeat',
      totalKills: participants.reduce((sum, participant) => sum + participant.kills, 0),
      totalGold: participants.reduce((sum, participant) => sum + participant.goldEarned, 0),
      objectives: {
        baron: this.numericField(objectives.baron?.kills),
        dragon: this.numericField(objectives.dragon?.kills),
        riftHerald: this.numericField(objectives.riftHerald?.kills),
        horde: this.numericField(objectives.horde?.kills),
        tower: this.numericField(objectives.tower?.kills),
        inhibitor: this.numericField(objectives.inhibitor?.kills),
      },
      participants,
    };
  }

  private extractPerks(rawParticipant: Record<string, unknown>) {
    const styles =
      ((rawParticipant.perks as Record<string, unknown> | undefined)?.styles as Array<Record<string, unknown>> | undefined) ?? [];
    const primaryStyle = styles[0];
    const subStyle = styles[1];
    const primarySelection =
      ((primaryStyle?.selections as Array<Record<string, unknown>> | undefined) ?? [])[0];

    return [
      this.numericField(primarySelection?.perk),
      this.numericField(subStyle?.style),
    ].filter((value): value is number => value > 0);
  }

  private extractRiotIdGameName(rawParticipant: Record<string, unknown> | null) {
    if (!rawParticipant) return null;
    return this.stringField(rawParticipant.riotIdGameName) ?? this.stringField(rawParticipant.gameName);
  }

  private extractRiotIdTagLine(rawParticipant: Record<string, unknown> | null) {
    if (!rawParticipant) return null;
    return this.stringField(rawParticipant.riotIdTagline) ?? this.stringField(rawParticipant.tagLine);
  }

  private numericField(value: unknown) {
    return typeof value === 'number' ? value : typeof value === 'string' && value.trim() ? Number(value) || 0 : 0;
  }

  private stringField(value: unknown) {
    return typeof value === 'string' && value.trim() ? value.trim() : null;
  }
}

function normalizeMatchQueueFilter(value?: string): MatchQueueFilter {
  switch (value) {
    case 'RANKED_SOLO':
    case 'RANKED_FLEX':
    case 'ARAM':
    case 'GAME_TYPES':
      return value;
    case 'ALL':
    default:
      return 'ALL';
  }
}
