import { HttpService } from '@nestjs/axios';
import {
  BadGatewayException,
  Injectable,
  NotFoundException,
  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 {
  ChampionSuggestionQueryDto,
  RiotAccountSuggestionQueryDto,
  SearchRiotUserDto,
} from './dto/search-riot-user.dto';

type RiotAccountResponse = {
  puuid: string;
  gameName: string;
  tagLine: string;
};

type RiotSummonerResponse = {
  id?: string;
  accountId?: string;
  puuid: string;
  profileIconId: number;
  revisionDate: number;
  summonerLevel: number;
};

type RiotLeagueEntryResponse = {
  queueType: string;
  tier: string;
  rank: string;
  leaguePoints: number;
  wins: number;
  losses: number;
};

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

  async getSuggestions(query: RiotAccountSuggestionQueryDto) {
    const rawQuery = query.query.trim();
    const [gameNamePart, tagLinePart] = rawQuery.split('#');
    const gameName = gameNamePart?.trim();
    const explicitTagLine = tagLinePart?.trim();
    const hintTagLine = query.tagLine?.trim();

    if (!gameName) return [];

    const accounts = await this.prisma.riotAccount.findMany({
      where: {
        AND: [
          {
            gameName: {
              contains: gameName,
            },
          },
          explicitTagLine
            ? {
                tagLine: {
                  startsWith: explicitTagLine,
                },
              }
            : {},
        ],
      },
      orderBy: [{ updatedAt: 'desc' }],
      take: Math.max((query.limit ?? 6) * 4, 18),
      select: {
        puuid: true,
        gameName: true,
        tagLine: true,
        lastSyncedAt: true,
        updatedAt: true,
      },
    });

    type Account = (typeof accounts)[number];

    return accounts
      .map((account: Account) => ({
        account,
        score: this.scoreAccountSuggestion(account, {
          gameName,
          rawQuery,
          explicitTagLine,
          hintTagLine,
        }),
      }))
      .filter((item) => item.score > 0)
      .sort((left, right) => {
        if (right.score !== left.score) return right.score - left.score;

        const leftSynced = left.account.lastSyncedAt?.getTime() ?? 0;
        const rightSynced = right.account.lastSyncedAt?.getTime() ?? 0;
        if (rightSynced !== leftSynced) return rightSynced - leftSynced;

        return right.account.updatedAt.getTime() - left.account.updatedAt.getTime();
      })
      .slice(0, query.limit ?? 6)
      .map(({ account }) => ({
        puuid: account.puuid,
        gameName: account.gameName,
        tagLine: account.tagLine,
        displayName: `${account.gameName}#${account.tagLine}`,
        source: 'LOCAL_CACHE',
        lastSyncedAt: account.lastSyncedAt,
      }));
  }

  async getChampionSuggestions(query: ChampionSuggestionQueryDto) {
    const asset = await this.prisma.gameStaticAsset.findUnique({
      where: { assetKey: 'LOL_CHAMPIONS' },
      select: { payload: true, version: true },
    });

    const payload =
      (asset?.payload as
        | {
            data?: Record<string, { id?: string; name?: string; title?: string; key?: string }>;
          }
        | Record<string, { id?: string; name?: string; title?: string; key?: string }>) ?? {};
    const champions = 'data' in payload ? payload.data ?? {} : payload;
    const normalized = query.query.trim().toLowerCase();
    if (!normalized) return [];

    return Object.values(champions)
      .map((champion) => ({
        champion,
        score: this.scoreChampionSuggestion(champion, normalized),
      }))
      .filter((item) => item.score > 0)
      .sort((left, right) => {
        if (right.score !== left.score) return right.score - left.score;
        return (left.champion.name ?? '').localeCompare(right.champion.name ?? '', 'ko');
      })
      .slice(0, query.limit ?? 6)
      .map(({ champion }) => ({
        key: champion.id ?? champion.name ?? '',
        name: champion.name ?? champion.id ?? '',
        title: champion.title ?? '',
        imageUrl: champion.id
          ? `https://ddragon.leagueoflegends.com/cdn/${asset?.version ?? '16.9.1'}/img/champion/${champion.id}.png`
          : null,
      }));
  }

  async findCachedOrSyncAccount(dto: SearchRiotUserDto) {
    const normalizedGameName = dto.gameName.trim().normalize('NFC');
    const normalizedTagLine = dto.tagLine.trim().toUpperCase();

    const cachedAccounts = await this.prisma.riotAccount.findMany({
      where: {
        tagLine: {
          equals: normalizedTagLine,
        },
      },
      include: {
        summoners: {
          orderBy: { updatedAt: 'desc' },
          take: 1,
        },
      },
      orderBy: [{ lastSyncedAt: 'desc' }, { updatedAt: 'desc' }],
      take: 10,
    });

    const cachedAccount =
      cachedAccounts.find((account) => account.gameName.trim().normalize('NFC') === normalizedGameName) ??
      cachedAccounts.find((account) => {
        const cachedName = account.gameName.trim().normalize('NFC');
        return cachedName.includes(normalizedGameName) || normalizedGameName.includes(cachedName);
      });

    if (cachedAccount) {
      const latestSummoner = cachedAccount.summoners[0];

      return {
        puuid: cachedAccount.puuid,
        gameName: cachedAccount.gameName,
        tagLine: cachedAccount.tagLine,
        summoner: {
          summonerId: latestSummoner?.summonerId ?? null,
          profileIconId: latestSummoner?.profileIconId ?? null,
          summonerLevel: latestSummoner?.summonerLevel?.toString() ?? null,
        },
      };
    }

    return this.findOrSyncAccount(dto);
  }

  async findOrSyncAccount(dto: SearchRiotUserDto) {
    const account = await this.fetchAccount(dto.gameName, dto.tagLine);
    const game = await this.prisma.game.upsert({
      where: { code: 'LOL' },
      update: {},
      create: { code: 'LOL', name: 'League of Legends' },
    });

    const savedAccount = await this.prisma.riotAccount.upsert({
      where: { puuid: account.puuid },
      update: {
        gameName: account.gameName,
        tagLine: account.tagLine,
        lastSyncedAt: new Date(),
      },
      create: {
        gameId: game.id,
        puuid: account.puuid,
        gameName: account.gameName,
        tagLine: account.tagLine,
        lastSyncedAt: new Date(),
      },
    });

    const savedSummoner = await this.syncSummonerProfile(savedAccount.id, account.puuid, dto.region);

    return {
      puuid: savedAccount.puuid,
      gameName: savedAccount.gameName,
      tagLine: savedAccount.tagLine,
      summoner: {
        summonerId: savedSummoner.summonerId,
        profileIconId: savedSummoner.profileIconId,
        summonerLevel: savedSummoner.summonerLevel?.toString() ?? null,
      },
    };
  }

  async syncSummonerProfile(riotAccountId: bigint, puuid: string, region: string) {
    const summoner = await this.fetchSummoner(region, puuid);
    const encryptedSummonerId = summoner.id?.trim() || null;
    const leagueEntries = await this.fetchLeagueEntries(region, puuid);
    const queueRanks = this.extractQueueRanks(leagueEntries);
    const now = new Date();

    await this.prisma.riotAccount.update({
      where: { id: riotAccountId },
      data: {
        lastSyncedAt: now,
      },
    });

    const existingSummoner = await this.prisma.riotSummoner.findFirst({
      where: {
        riotAccountId,
        region,
      },
      select: {
        id: true,
      },
    });

    const data = {
      summonerId: encryptedSummonerId ?? summoner.puuid,
      accountId: summoner.accountId ?? summoner.puuid,
      region,
      profileIconId: summoner.profileIconId,
      summonerLevel: BigInt(summoner.summonerLevel),
      revisionDate: BigInt(summoner.revisionDate),
      soloTier: queueRanks.solo?.tier ?? null,
      soloRank: queueRanks.solo?.rank ?? null,
      soloLeaguePoints: queueRanks.solo?.leaguePoints ?? null,
      soloWins: queueRanks.solo?.wins ?? null,
      soloLosses: queueRanks.solo?.losses ?? null,
      flexTier: queueRanks.flex?.tier ?? null,
      flexRank: queueRanks.flex?.rank ?? null,
      flexLeaguePoints: queueRanks.flex?.leaguePoints ?? null,
      flexWins: queueRanks.flex?.wins ?? null,
      flexLosses: queueRanks.flex?.losses ?? null,
    };

    if (existingSummoner) {
      return this.prisma.riotSummoner.update({
        where: { id: existingSummoner.id },
        data,
      });
    }

    return this.prisma.riotSummoner.create({
      data: {
        riotAccountId,
        ...data,
      },
    });
  }

  private async fetchAccount(gameName: string, tagLine: string) {
    const url = `https://asia.api.riotgames.com/riot/account/v1/accounts/by-riot-id/${encodeURIComponent(gameName)}/${encodeURIComponent(tagLine)}`;
    const data = await this.get<RiotAccountResponse>(url);
    if (!data?.puuid) throw new NotFoundException('Riot account not found');
    return data;
  }

  private async fetchSummoner(region: string, puuid: string) {
    const url = `https://${region.toLowerCase()}.api.riotgames.com/lol/summoner/v4/summoners/by-puuid/${encodeURIComponent(puuid)}`;
    const data = await this.get<RiotSummonerResponse>(url);
    if (!data?.puuid) throw new NotFoundException('Riot summoner not found');
    return data;
  }

  private async fetchLeagueEntries(region: string, puuid: string) {
    const url = `https://${region.toLowerCase()}.api.riotgames.com/lol/league/v4/entries/by-puuid/${encodeURIComponent(puuid)}`;
    return this.get<RiotLeagueEntryResponse[]>(url);
  }

  private async get<T>(url: string): Promise<T> {
    const apiKey = this.config.getOrThrow<string>('RIOT_API_KEY');
    try {
      const response = await firstValueFrom(
        this.http.get<T>(url, {
          headers: { 'X-Riot-Token': apiKey },
          timeout: 10000,
        }),
      );
      return response.data;
    } catch (error) {
      if (error instanceof AxiosError) {
        const status = error.response?.status;
        if (status === 400) {
          throw new BadGatewayException('Riot API rejected the request payload or identifier');
        }
        if (status === 401) {
          throw new UnauthorizedException('Riot API authentication failed');
        }
        if (status === 403) {
          throw new BadGatewayException('Riot API request was forbidden');
        }
        if (status === 404) {
          throw new NotFoundException('Riot account not found');
        }
        if (status === 429) {
          throw new ServiceUnavailableException('Riot API rate limit exceeded');
        }
        throw new BadGatewayException('Riot API request failed');
      }
      throw error;
    }
  }

  private scoreAccountSuggestion(
    account: {
      gameName: string;
      tagLine: string;
    },
    input: {
      gameName: string;
      rawQuery: string;
      explicitTagLine?: string;
      hintTagLine?: string;
    },
  ) {
    const normalizedName = normalizeAutocompleteValue(account.gameName);
    const normalizedTag = normalizeAutocompleteValue(account.tagLine);
    const normalizedQueryName = normalizeAutocompleteValue(input.gameName);
    const normalizedRawQuery = normalizeAutocompleteValue(input.rawQuery);
    const normalizedExplicitTag = normalizeAutocompleteValue(input.explicitTagLine ?? '');
    const normalizedHintTag = normalizeAutocompleteValue(input.hintTagLine ?? '');
    const normalizedDisplay = `${normalizedName}#${normalizedTag}`;
    let score = 0;

    if (normalizedDisplay === normalizedRawQuery) score += 260;
    if (normalizedName === normalizedQueryName) score += 220;
    else if (normalizedName.startsWith(normalizedQueryName)) score += 170;
    else if (normalizedName.includes(normalizedQueryName)) score += 120;
    else if (normalizedQueryName.includes(normalizedName)) score += 60;

    if (normalizedExplicitTag) {
      if (normalizedTag === normalizedExplicitTag) score += 90;
      else if (normalizedTag.startsWith(normalizedExplicitTag)) score += 40;
      else return 0;
    } else if (normalizedHintTag) {
      if (normalizedTag === normalizedHintTag) score += 28;
      else if (normalizedTag.startsWith(normalizedHintTag)) score += 12;
    }

    score -= Math.max(0, normalizedName.length - normalizedQueryName.length);
    return score;
  }

  private extractQueueRanks(entries: RiotLeagueEntryResponse[]) {
    const solo = entries.find((entry) => entry.queueType === 'RANKED_SOLO_5x5') ?? null;
    const flex = entries.find((entry) => entry.queueType === 'RANKED_FLEX_SR') ?? null;
    return { solo, flex };
  }

  private scoreChampionSuggestion(
    champion: { id?: string; name?: string; title?: string; key?: string },
    normalizedQuery: string,
  ) {
    const normalizedName = normalizeAutocompleteValue(champion.name ?? '');
    const normalizedId = normalizeAutocompleteValue(champion.id ?? '');
    const normalizedTitle = normalizeAutocompleteValue(champion.title ?? '');
    let score = 0;

    if (normalizedName === normalizedQuery) score += 230;
    else if (normalizedName.startsWith(normalizedQuery)) score += 180;
    else if (normalizedName.includes(normalizedQuery)) score += 130;

    if (normalizedId === normalizedQuery) score += 210;
    else if (normalizedId.startsWith(normalizedQuery)) score += 165;
    else if (normalizedId.includes(normalizedQuery)) score += 120;

    if (normalizedTitle === normalizedQuery) score += 70;
    else if (normalizedTitle.startsWith(normalizedQuery)) score += 55;
    else if (normalizedTitle.includes(normalizedQuery)) score += 30;

    score -= Math.max(0, normalizedName.length - normalizedQuery.length);
    return score;
  }
}

function normalizeAutocompleteValue(value: string) {
  return value.trim().normalize('NFC').toLowerCase();
}
