import { HttpService } from '@nestjs/axios';
import {
  BadGatewayException,
  Injectable,
  ServiceUnavailableException,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AxiosError } from 'axios';
import { firstValueFrom } from 'rxjs';
import { PrismaService } from '../database/prisma.service';
import { CollectMatchesDto } from './dto/search-riot-user.dto';
import { RiotAccountService } from './riot-account.service';
import { Prisma, RiotAccount } from '@prisma/client';

type RiotMatch = {
  metadata: {
    matchId: string;
    participants: string[];
  };
  info: {
    queueId: number;
    gameVersion: string;
    gameStartTimestamp: number;
    gameDuration: number;
    participants: Array<{
      puuid: string;
      participantId: number;
      teamId: number;
      championId: number;
      championName: string;
      lane: string;
      role: string;
      win: boolean;
      kills: number;
      deaths: number;
      assists: number;
      goldEarned: number;
      totalDamageDealtToChampions: number;
      totalDamageTaken: number;
      visionScore: number;
      wardsPlaced: number;
      wardsKilled: number;
      detectorWardsPlaced: number;
      neutralMinionsKilled: number;
      totalMinionsKilled: number;
      challenges?: {
        killParticipation?: number;
      };
    }>;
  };
};

const MATCH_ID_PAGE_SIZE = 100;
const RECENT_MATCH_COMPARE_COUNT = 20;

export type MatchQueueFilter = 'ALL' | 'RANKED_SOLO' | 'RANKED_FLEX' | 'ARAM' | 'GAME_TYPES';

@Injectable()
export class RiotMatchService {
  constructor(
    private readonly http: HttpService,
    private readonly config: ConfigService,
    private readonly prisma: PrismaService,
    private readonly accountService: RiotAccountService,
  ) {}

  async collectRecentMatches(dto: CollectMatchesDto) {
    const account = await this.accountService.findOrSyncAccount(dto);
    const riotAccount = await this.prisma.riotAccount.findUniqueOrThrow({
      where: { puuid: account.puuid },
    });
    return this.collectRecentMatchesForAccount(riotAccount, dto.region, dto.matchCount);
  }

  async collectRecentMatchesForAccount(
    riotAccount: Pick<RiotAccount, 'id' | 'puuid'>,
    region: string,
    matchCount?: number,
  ) {
    const matchIds = await this.fetchMatchIds(riotAccount.puuid, matchCount);
    const { inserted, skipped } = await this.collectMatchIdsForAccount(
      riotAccount,
      region,
      matchIds,
    );

    return {
      puuid: riotAccount.puuid,
      requested: matchIds.length,
      inserted,
      skipped,
    };
  }

  async ensureMatchesForFilter(
    riotAccount: Pick<RiotAccount, 'id' | 'puuid'>,
    region: string,
    queueFilter: MatchQueueFilter,
    minimumCount: number,
  ) {
    const requiredCount = Math.max(0, minimumCount);
    if (requiredCount === 0) {
      return { recentUnchanged: true, remoteExhausted: false };
    }

    const firstBatchIds = await this.fetchMatchIdsPage(
      riotAccount.puuid,
      0,
      MATCH_ID_PAGE_SIZE,
    );
    if (firstBatchIds.length === 0) {
      return { recentUnchanged: true, remoteExhausted: true };
    }

    const compareCount = Math.min(RECENT_MATCH_COMPARE_COUNT, firstBatchIds.length);
    const latestApiIds = firstBatchIds.slice(0, compareCount);
    const latestStoredIds = await this.getStoredMatchIds(riotAccount.id, compareCount);
    const recentUnchanged =
      latestApiIds.length === latestStoredIds.length &&
      latestApiIds.every((matchId, index) => latestStoredIds[index] === matchId);

    let remoteExhausted = false;
    let nextStart = 0;

    if (!recentUnchanged) {
      await this.collectMatchIdsForAccount(riotAccount, region, firstBatchIds);
      nextStart = firstBatchIds.length;
      remoteExhausted = firstBatchIds.length < MATCH_ID_PAGE_SIZE;
    } else {
      const matchedCount = await this.countStoredMatchesByFilter(riotAccount.id, queueFilter);
      if (matchedCount >= requiredCount) {
        return { recentUnchanged: true, remoteExhausted: false };
      }

      nextStart = await this.countStoredMatches(riotAccount.id);
    }

    let matchedCount = await this.countStoredMatchesByFilter(riotAccount.id, queueFilter);

    while (matchedCount < requiredCount && !remoteExhausted) {
      const pageIds = await this.fetchMatchIdsPage(
        riotAccount.puuid,
        nextStart,
        MATCH_ID_PAGE_SIZE,
      );
      if (pageIds.length === 0) {
        remoteExhausted = true;
        break;
      }

      await this.collectMatchIdsForAccount(riotAccount, region, pageIds);
      nextStart += pageIds.length;
      matchedCount = await this.countStoredMatchesByFilter(riotAccount.id, queueFilter);
      if (pageIds.length < MATCH_ID_PAGE_SIZE) {
        remoteExhausted = true;
      }
    }

    return {
      recentUnchanged,
      remoteExhausted,
    };
  }

  private async fetchMatchIds(puuid: string, count?: number) {
    const targetCount = typeof count === 'number' && count > 0 ? count : null;
    const matchIds: string[] = [];
    let start = 0;

    while (true) {
      const batchSize =
        targetCount === null
          ? MATCH_ID_PAGE_SIZE
          : Math.min(MATCH_ID_PAGE_SIZE, targetCount - matchIds.length);

      if (batchSize <= 0) {
        break;
      }

      const response = await this.fetchMatchIdsPage(puuid, start, batchSize);
      if (response.length === 0) {
        break;
      }

      matchIds.push(...response);
      start += response.length;

      if (response.length < batchSize) {
        break;
      }
    }

    return targetCount === null ? matchIds : matchIds.slice(0, targetCount);
  }

  private async fetchMatchIdsPage(puuid: string, start: number, count: number) {
    const url = `https://asia.api.riotgames.com/lol/match/v5/matches/by-puuid/${encodeURIComponent(puuid)}/ids`;
    return this.get<string[]>(url, { start, count });
  }

  private async collectMatchIdsForAccount(
    riotAccount: Pick<RiotAccount, 'id' | 'puuid'>,
    region: string,
    matchIds: string[],
  ) {
    const game = await this.prisma.game.findUniqueOrThrow({ where: { code: 'LOL' } });
    let inserted = 0;
    let skipped = 0;

    for (const riotMatchId of matchIds) {
      const existingMatch = await this.prisma.match.findUnique({
        where: { matchId: riotMatchId },
        select: { id: true },
      });

      const match = await this.fetchMatch(riotMatchId);
      const savedMatch = await this.prisma.match.upsert({
        where: { matchId: match.metadata.matchId },
        update: {
          region,
          queueId: match.info.queueId,
          gameVersion: match.info.gameVersion,
          gameStartAt: new Date(match.info.gameStartTimestamp),
          gameDurationSec: match.info.gameDuration,
          rawJson: match as object,
        },
        create: {
          gameId: game.id,
          matchId: match.metadata.matchId,
          region,
          queueId: match.info.queueId,
          gameVersion: match.info.gameVersion,
          gameStartAt: new Date(match.info.gameStartTimestamp),
          gameDurationSec: match.info.gameDuration,
          rawJson: match as object,
        },
      });

      for (const participant of match.info.participants) {
        const participantAccount =
          participant.puuid === riotAccount.puuid
            ? riotAccount
            : await this.prisma.riotAccount.upsert({
                where: { puuid: participant.puuid },
                update: {},
                create: {
                  gameId: game.id,
                  puuid: participant.puuid,
                  gameName: 'unknown',
                  tagLine: 'unknown',
                },
              });

        await this.prisma.participantMatch.upsert({
          where: {
            matchId_participantId: {
              matchId: savedMatch.id,
              participantId: participant.participantId,
            },
          },
          update: {
            riotAccountId: participantAccount.id,
            teamId: participant.teamId,
            championId: participant.championId,
            championName: participant.championName,
            lane: participant.lane,
            role: participant.role,
            win: participant.win,
            kills: participant.kills,
            deaths: participant.deaths,
            assists: participant.assists,
            goldEarned: participant.goldEarned,
            totalDamageToChampions: participant.totalDamageDealtToChampions,
            damageTaken: participant.totalDamageTaken,
            visionScore: participant.visionScore,
            wardsPlaced: participant.wardsPlaced,
            wardsKilled: participant.wardsKilled,
            controlWardsBought: participant.detectorWardsPlaced,
            neutralMinionsKilled: participant.neutralMinionsKilled,
            totalMinionsKilled: participant.totalMinionsKilled,
            killParticipation: participant.challenges?.killParticipation,
          },
          create: {
            matchId: savedMatch.id,
            riotAccountId: participantAccount.id,
            participantId: participant.participantId,
            teamId: participant.teamId,
            championId: participant.championId,
            championName: participant.championName,
            lane: participant.lane,
            role: participant.role,
            win: participant.win,
            kills: participant.kills,
            deaths: participant.deaths,
            assists: participant.assists,
            goldEarned: participant.goldEarned,
            totalDamageToChampions: participant.totalDamageDealtToChampions,
            damageTaken: participant.totalDamageTaken,
            visionScore: participant.visionScore,
            wardsPlaced: participant.wardsPlaced,
            wardsKilled: participant.wardsKilled,
            controlWardsBought: participant.detectorWardsPlaced,
            neutralMinionsKilled: participant.neutralMinionsKilled,
            totalMinionsKilled: participant.totalMinionsKilled,
            killParticipation: participant.challenges?.killParticipation,
          },
        });
      }

      if (existingMatch) {
        skipped += 1;
      } else {
        inserted += 1;
      }
    }

    return { inserted, skipped };
  }

  private async getStoredMatchIds(riotAccountId: bigint, take: number) {
    if (take <= 0) return [];

    const rows = await this.prisma.participantMatch.findMany({
      where: { riotAccountId },
      orderBy: [{ match: { gameStartAt: 'desc' } }, { id: 'desc' }],
      take,
      select: {
        match: {
          select: {
            matchId: true,
          },
        },
      },
    });

    return rows.map((row) => row.match.matchId);
  }

  private async countStoredMatches(riotAccountId: bigint) {
    return this.prisma.participantMatch.count({
      where: { riotAccountId },
    });
  }

  private async countStoredMatchesByFilter(
    riotAccountId: bigint,
    queueFilter: MatchQueueFilter,
  ) {
    return this.prisma.participantMatch.count({
      where: this.buildQueueFilterWhere(riotAccountId, queueFilter),
    });
  }

  private buildQueueFilterWhere(
    riotAccountId: bigint,
    queueFilter: MatchQueueFilter,
  ): Prisma.ParticipantMatchWhereInput {
    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 fetchMatch(matchId: string) {
    const url = `https://asia.api.riotgames.com/lol/match/v5/matches/${encodeURIComponent(matchId)}`;
    return this.get<RiotMatch>(url);
  }

  private async get<T>(url: string, params?: Record<string, unknown>): Promise<T> {
    const apiKey = this.config.getOrThrow<string>('RIOT_API_KEY');
    try {
      const response = await firstValueFrom(
        this.http.get<T>(url, {
          params,
          headers: { 'X-Riot-Token': apiKey },
          timeout: 15000,
        }),
      );
      return response.data;
    } catch (error) {
      if (error instanceof AxiosError) {
        const status = error.response?.status;
        if (status === 401) {
          throw new UnauthorizedException('Riot API authentication failed');
        }
        if (status === 403) {
          throw new BadGatewayException('Riot match API request was forbidden');
        }
        if (status === 429) {
          throw new ServiceUnavailableException('Riot API rate limit exceeded');
        }
        throw new BadGatewayException('Riot match API request failed');
      }
      throw error;
    }
  }
}
