import { Injectable } from '@nestjs/common';
import { Decimal } from '@prisma/client/runtime/library';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { appBadRequest, appConflict, appNotFound } from '../../shared/common/app-error';
import { MatchBetStatsService } from './match-bet-stats.service';

const TERMINAL_MATCH_STATUSES = new Set(['SETTLED', 'CANCELLED', 'VOID']);

export type MatchArchiveWarning = 'PENDING_BETS' | 'UNSETTLED_MATCH' | 'PREVIEW_BATCH';

export type MatchArchivePreview = {
  matchId: string;
  matchStatus: string;
  isOutright: boolean;
  title: string;
  pendingBetCount: number;
  pendingStake: string;
  hasPreviewSettlementBatch: boolean;
  requiresForce: boolean;
  warnings: MatchArchiveWarning[];
};

export type LeagueBlockingMatch = {
  id: string;
  status: string;
  isOutright: boolean;
  title: string;
  pendingCount: number;
};

export type LeagueArchivePreview = {
  leagueId: string;
  canArchive: boolean;
  blockingMatches: LeagueBlockingMatch[];
  totalPendingBets: number;
};

@Injectable()
export class CatalogArchiveService {
  constructor(
    private prisma: PrismaService,
    private matchBetStats: MatchBetStatsService,
  ) {}

  async getMatchArchivePreview(matchId: bigint): Promise<MatchArchivePreview> {
    const match = await this.requireActiveMatch(matchId);
    const [pending, hasPreviewBatch] = await Promise.all([
      this.pendingBetSummary(matchId),
      this.hasPreviewBatch(matchId),
    ]);
    const warnings = this.buildMatchWarnings(match.status, pending.pendingBetCount, hasPreviewBatch);
    const requiresForce = warnings.length > 0;
    const title = await this.matchTitle(match);

    return {
      matchId: match.id.toString(),
      matchStatus: match.status,
      isOutright: match.isOutright,
      title,
      pendingBetCount: pending.pendingBetCount,
      pendingStake: pending.pendingStake,
      hasPreviewSettlementBatch: hasPreviewBatch,
      requiresForce,
      warnings,
    };
  }

  async archiveMatch(matchId: bigint, opts: { force: boolean }) {
    const match = await this.requireActiveMatch(matchId);
    if (match.status === 'DRAFT') {
      throw appBadRequest('MATCH_DELETE_DRAFT_ONLY');
    }
    const preview = await this.getMatchArchivePreview(matchId);
    if (preview.requiresForce && !opts.force) {
      throw appConflict('ARCHIVE_BLOCKED', preview);
    }

    const now = new Date();
    await this.prisma.$transaction(async (tx) => {
      await tx.settlementBatch.deleteMany({
        where: { matchId, status: 'PREVIEW' },
      });
      await tx.marketSelection.updateMany({
        where: { market: { matchId } },
        data: { status: 'CLOSED' },
      });
      await tx.market.updateMany({
        where: { matchId },
        data: { status: 'CLOSED' },
      });
      await tx.match.update({
        where: { id: matchId },
        data: {
          deletedAt: now,
          status: TERMINAL_MATCH_STATUSES.has(match.status) ? match.status : 'CANCELLED',
        },
      });
    });

    return { matchId: matchId.toString(), archivedAt: now.toISOString() };
  }

  async getLeagueArchivePreview(leagueId: bigint): Promise<LeagueArchivePreview> {
    const league = await this.prisma.league.findFirst({
      where: { id: leagueId, deletedAt: null },
    });
    if (!league) throw appNotFound('LEAGUE_NOT_FOUND');

    const matches = await this.prisma.match.findMany({
      where: { leagueId, deletedAt: null },
      include: { homeTeam: true, awayTeam: true },
      orderBy: [{ isOutright: 'desc' }, { id: 'asc' }],
    });

    const matchIds = matches.map((m) => m.id);
    const stats = await this.matchBetStats.betStatsForMatches(matchIds);
    const previewBatches = matchIds.length
      ? await this.prisma.settlementBatch.findMany({
          where: { matchId: { in: matchIds }, status: 'PREVIEW' },
          select: { matchId: true },
        })
      : [];
    const previewBatchMatchIds = new Set(previewBatches.map((b) => b.matchId?.toString()));

    const blockingMatches: LeagueBlockingMatch[] = [];
    let totalPendingBets = 0;

    for (const match of matches) {
      const mid = match.id.toString();
      const stat = stats.get(mid) ?? { betCount: 0, totalStake: '0', pendingCount: 0 };
      totalPendingBets += stat.pendingCount;

      const hasPreview = previewBatchMatchIds.has(mid);
      const blocks = this.isLeagueMatchBlocking(match.status, stat.betCount, stat.pendingCount, hasPreview);
      if (blocks) {
        blockingMatches.push({
          id: mid,
          status: match.status,
          isOutright: match.isOutright,
          title: await this.matchTitle(match),
          pendingCount: stat.pendingCount,
        });
      }
    }

    if (totalPendingBets > 0 && !blockingMatches.length) {
      // Pending bets exist but each match might be terminal — still block league archive
      for (const match of matches) {
        const mid = match.id.toString();
        const stat = stats.get(mid)!;
        if (stat.pendingCount > 0) {
          blockingMatches.push({
            id: mid,
            status: match.status,
            isOutright: match.isOutright,
            title: await this.matchTitle(match),
            pendingCount: stat.pendingCount,
          });
        }
      }
    }

    const canArchive = blockingMatches.length === 0 && totalPendingBets === 0;

    return {
      leagueId: leagueId.toString(),
      canArchive,
      blockingMatches,
      totalPendingBets,
    };
  }

  async archiveLeague(leagueId: bigint) {
    const league = await this.prisma.league.findFirst({
      where: { id: leagueId, deletedAt: null },
    });
    if (!league) throw appNotFound('LEAGUE_NOT_FOUND');

    const preview = await this.getLeagueArchivePreview(leagueId);
    if (!preview.canArchive) {
      throw appConflict('LEAGUE_ARCHIVE_NOT_READY', preview);
    }

    const now = new Date();
    await this.prisma.$transaction(async (tx) => {
      const matches = await tx.match.findMany({
        where: { leagueId, deletedAt: null },
        select: { id: true, status: true },
      });
      const matchIds = matches.map((m) => m.id);
      if (matchIds.length) {
        await tx.marketSelection.updateMany({
          where: { market: { matchId: { in: matchIds } } },
          data: { status: 'CLOSED' },
        });
        await tx.market.updateMany({
          where: { matchId: { in: matchIds } },
          data: { status: 'CLOSED' },
        });
        await tx.match.updateMany({
          where: { id: { in: matchIds } },
          data: { deletedAt: now },
        });
      }
      await tx.league.update({
        where: { id: leagueId },
        data: { deletedAt: now, isActive: false },
      });
    });

    return { leagueId: leagueId.toString(), archivedAt: now.toISOString() };
  }

  private async requireActiveMatch(matchId: bigint) {
    const match = await this.prisma.match.findFirst({
      where: { id: matchId, deletedAt: null },
      include: { homeTeam: true, awayTeam: true, league: true },
    });
    if (!match) throw appNotFound('MATCH_NOT_FOUND');
    return match;
  }

  private async pendingBetSummary(matchId: bigint) {
    const bets = await this.prisma.bet.findMany({
      where: { status: 'PENDING', selections: { some: { matchId } } },
      select: { stake: true },
    });
    let pendingStake = new Decimal(0);
    for (const bet of bets) {
      pendingStake = pendingStake.add(bet.stake);
    }
    return {
      pendingBetCount: bets.length,
      pendingStake: pendingStake.toString(),
    };
  }

  private async hasPreviewBatch(matchId: bigint) {
    const batch = await this.prisma.settlementBatch.findFirst({
      where: { matchId, status: 'PREVIEW' },
      select: { id: true },
    });
    return batch != null;
  }

  private buildMatchWarnings(
    status: string,
    pendingBetCount: number,
    hasPreviewBatch: boolean,
  ): MatchArchiveWarning[] {
    const warnings: MatchArchiveWarning[] = [];
    if (pendingBetCount > 0) warnings.push('PENDING_BETS');
    if (!TERMINAL_MATCH_STATUSES.has(status) && status !== 'DRAFT') {
      warnings.push('UNSETTLED_MATCH');
    }
    if (hasPreviewBatch) warnings.push('PREVIEW_BATCH');
    return warnings;
  }

  private isLeagueMatchBlocking(
    status: string,
    betCount: number,
    pendingCount: number,
    hasPreviewBatch: boolean,
  ): boolean {
    if (pendingCount > 0) return true;
    if (hasPreviewBatch) return true;
    if (TERMINAL_MATCH_STATUSES.has(status)) return false;
    if (status === 'DRAFT' && betCount === 0) return false;
    return true;
  }

  private async matchTitle(match: {
    id: bigint;
    isOutright: boolean;
    matchName: string | null;
    homeTeamId: bigint;
    awayTeamId: bigint;
    homeTeam?: { code: string } | null;
    awayTeam?: { code: string } | null;
  }) {
    if (match.isOutright) {
      const name = match.matchName?.trim();
      if (name) return name;
      return `Outright #${match.id}`;
    }
    const [home, away] = await Promise.all([
      this.getTranslationExact('TEAM', match.homeTeamId, 'zh-CN'),
      this.getTranslationExact('TEAM', match.awayTeamId, 'zh-CN'),
    ]);
    if (home && away) return `${home} vs ${away}`;
    return `${match.homeTeam?.code ?? '?'} vs ${match.awayTeam?.code ?? '?'}`;
  }

  private async getTranslationExact(entityType: string, entityId: bigint, locale: string) {
    const row = await this.prisma.entityTranslation.findFirst({
      where: { entityType, entityId, locale, fieldName: 'name' },
    });
    return row?.value ?? '';
  }
}
