import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { resolveTranslationFallback } from '@thebet365/shared';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Prisma } from '@prisma/client';
import { Decimal } from '@prisma/client/runtime/library';
import { PrismaService } from '../../shared/prisma/prisma.service';

export type MatchBetStatsSummary = {
  betCount: number;
  totalStake: string;
  pendingCount: number;
};
import type { ZhiboLeagueExport, ZhiboMatchExport, ZhiboMatchesBundleExport, ZhiboTeamExport } from './zhibo-match.types';
import {
  leagueCodeFromExport,
  resolveInternalStatus,
  resolveIsHot,
  resolveStartTime,
  teamCodeFromExport,
  toKickoffJson,
  toVenueJson,
  translationsFromZhiboNames,
} from './zhibo-match.mapper';
import { syncWc2026OutrightMarket } from './wc2026-outright.sync';

const OUTRIGHT_PLACEHOLDER_CODE = 'OUT';

@Injectable()
export class MatchesService {
  constructor(private prisma: PrismaService) {}

  async createLeague(code: string, translations: Record<string, string>) {
    const league = await this.prisma.league.create({ data: { code } });
    for (const [locale, value] of Object.entries(translations)) {
      await this.prisma.entityTranslation.create({
        data: {
          entityType: 'LEAGUE',
          entityId: league.id,
          locale,
          fieldName: 'name',
          value,
        },
      });
    }
    return league;
  }

  async createTeam(code: string, translations: Record<string, string>) {
    const team = await this.prisma.team.create({ data: { code } });
    for (const [locale, value] of Object.entries(translations)) {
      await this.prisma.entityTranslation.create({
        data: {
          entityType: 'TEAM',
          entityId: team.id,
          locale,
          fieldName: 'name',
          value,
        },
      });
    }
    return team;
  }

  async createMatch(data: {
    leagueId: bigint;
    homeTeamId: bigint;
    awayTeamId: bigint;
    startTime: Date;
    isHot?: boolean;
    displayOrder?: number;
    createdBy?: bigint;
    status?: string;
    publishTime?: Date;
    zhibo?: Partial<{
      officialMatchNo: number;
      stage: string;
      groupName: string;
      liveMatchId?: bigint;
      additionMatchId: bigint | null;
      channelId: string | null;
      matchName: string;
      venueJson: Prisma.InputJsonValue;
      kickoffJson: Prisma.InputJsonValue;
      externalStatus: string;
    }>;
  }) {
    const status = data.status ?? 'DRAFT';
    return this.prisma.match.create({
      data: {
        leagueId: data.leagueId,
        homeTeamId: data.homeTeamId,
        awayTeamId: data.awayTeamId,
        startTime: data.startTime,
        isHot: data.isHot ?? false,
        displayOrder: data.displayOrder ?? 0,
        createdBy: data.createdBy,
        status,
        publishTime: data.publishTime ?? (status === 'PUBLISHED' ? new Date() : undefined),
        officialMatchNo: data.zhibo?.officialMatchNo,
        stage: data.zhibo?.stage,
        groupName: data.zhibo?.groupName,
        liveMatchId: data.zhibo?.liveMatchId,
        additionMatchId: data.zhibo?.additionMatchId ?? undefined,
        channelId: data.zhibo?.channelId ?? undefined,
        matchName: data.zhibo?.matchName,
        venueJson: data.zhibo?.venueJson,
        kickoffJson: data.zhibo?.kickoffJson,
        externalStatus: data.zhibo?.externalStatus,
      },
    });
  }

  private async upsertEntityTranslations(
    entityType: 'LEAGUE' | 'TEAM',
    entityId: bigint,
    translations: Record<string, string>,
  ) {
    for (const [locale, value] of Object.entries(translations)) {
      await this.prisma.entityTranslation.upsert({
        where: {
          entityType_entityId_locale_fieldName: {
            entityType,
            entityId,
            locale,
            fieldName: 'name',
          },
        },
        create: { entityType, entityId, locale, fieldName: 'name', value },
        update: { value },
      });
    }
  }

  async upsertLeagueFromZhiboExport(league: ZhiboLeagueExport) {
    const code = leagueCodeFromExport(league);
    const record = await this.prisma.league.upsert({
      where: { code },
      create: { code, sportType: league.type || 'FOOTBALL' },
      update: { sportType: league.type || 'FOOTBALL' },
    });
    await this.upsertEntityTranslations('LEAGUE', record.id, {
      'zh-CN': league.zh,
      'en-US': league.en,
    });
    return record;
  }

  async upsertTeamFromZhiboExport(team: ZhiboTeamExport) {
    const translations = translationsFromZhiboNames(team.names, team.name);

    if (team.id != null) {
      const existing = await this.prisma.team.findFirst({
        where: { externalId: team.id },
      });
      if (existing) {
        const record = await this.prisma.team.update({
          where: { id: existing.id },
          data: {
            logoUrl: team.image || existing.logoUrl,
            externalId: team.id,
          },
        });
        await this.upsertEntityTranslations('TEAM', record.id, translations);
        return record;
      }
    }

    const code = teamCodeFromExport(team);
    const record = await this.prisma.team.upsert({
      where: { code },
      create: {
        code,
        externalId: team.id ?? undefined,
        logoUrl: team.image || undefined,
      },
      update: {
        logoUrl: team.image || undefined,
        externalId: team.id ?? undefined,
      },
    });

    await this.upsertEntityTranslations('TEAM', record.id, translations);
    return record;
  }

  private async findExistingZhiboMatch(
    leagueId: bigint,
    homeTeamId: bigint,
    awayTeamId: bigint,
    item: ZhiboMatchExport,
  ) {
    if (item.liveMatchId != null) {
      return this.prisma.match.findUnique({
        where: { liveMatchId: BigInt(item.liveMatchId) },
      });
    }
    if (item.officialMatchNo != null) {
      return this.prisma.match.findFirst({
        where: {
          leagueId,
          homeTeamId,
          awayTeamId,
          officialMatchNo: item.officialMatchNo,
        },
      });
    }
    return null;
  }

  async createPlatformLeague(data: {
    leagueEn: string;
    leagueZh: string;
    leagueMs?: string;
    logoUrl?: string;
    displayOrder?: number;
  }) {
    const leagueEn = data.leagueEn.trim();
    const leagueZh = data.leagueZh.trim();
    if (!leagueEn && !leagueZh) {
      throw new BadRequestException('请填写赛事名称（中文或英文至少一项）');
    }
    const league = await this.upsertLeagueFromZhiboExport({
      type: 'FOOTBALL',
      en: leagueEn || leagueZh,
      zh: leagueZh || leagueEn,
    });
    if (data.leagueMs?.trim()) {
      await this.upsertEntityTranslations('LEAGUE', league.id, {
        'ms-MY': data.leagueMs.trim(),
      });
    }
    const updates: { logoUrl?: string; displayOrder?: number } = {};
    if (data.logoUrl?.trim()) updates.logoUrl = data.logoUrl.trim();
    if (data.displayOrder != null) updates.displayOrder = data.displayOrder;
    if (Object.keys(updates).length) {
      await this.prisma.league.update({ where: { id: league.id }, data: updates });
    }
    const [en, zh, ms] = await Promise.all([
      this.getTranslationExact('LEAGUE', league.id, 'en-US'),
      this.getTranslationExact('LEAGUE', league.id, 'zh-CN'),
      this.getTranslationExact('LEAGUE', league.id, 'ms-MY'),
    ]);
    const fresh = await this.prisma.league.findUniqueOrThrow({ where: { id: league.id } });
    return {
      id: fresh.id.toString(),
      code: fresh.code,
      logoUrl: fresh.logoUrl,
      displayOrder: fresh.displayOrder,
      leagueEn: en,
      leagueZh: zh,
      leagueMs: ms,
    };
  }

  async listAdminLeagues(opts: {
    page: number;
    pageSize: number;
    keyword?: string;
    status?: string;
  }) {
    const skip = (opts.page - 1) * opts.pageSize;
    const kw = opts.keyword?.trim();
    let idFilter: bigint[] | undefined;

    // 状态仅用于单场计数/展开列表筛选，不隐藏无该状态单场的联赛（含新建空联赛）
    if (kw) {
      const ids = new Set<bigint>();
      const trRows = await this.prisma.entityTranslation.findMany({
        where: {
          entityType: 'LEAGUE',
          fieldName: 'name',
          value: { contains: kw, mode: 'insensitive' },
        },
        select: { entityId: true },
      });
      for (const r of trRows) ids.add(r.entityId);
      const matchWhere: Prisma.MatchWhereInput = {
        deletedAt: null,
        isOutright: false,
        OR: [
          { matchName: { contains: kw, mode: 'insensitive' } },
          { homeTeam: { code: { contains: kw, mode: 'insensitive' } } },
          { awayTeam: { code: { contains: kw, mode: 'insensitive' } } },
        ],
      };
      if (opts.status) matchWhere.status = opts.status;
      const matchLeagues = await this.prisma.match.findMany({
        where: matchWhere,
        select: { leagueId: true },
        distinct: ['leagueId'],
      });
      for (const m of matchLeagues) ids.add(m.leagueId);
      idFilter = [...ids];
      if (!idFilter.length) {
        return { items: [], total: 0, page: opts.page, pageSize: opts.pageSize };
      }
    }

    const where: Prisma.LeagueWhereInput = { deletedAt: null };
    if (idFilter) where.id = { in: idFilter };

    const [leagues, total] = await Promise.all([
      this.prisma.league.findMany({
        where,
        orderBy: [{ displayOrder: 'asc' }, { id: 'desc' }],
        skip,
        take: opts.pageSize,
      }),
      this.prisma.league.count({ where }),
    ]);

    const items = await Promise.all(
      leagues.map(async (league) => {
        const [leagueEn, leagueZh, leagueMs, matchCount] = await Promise.all([
          this.getTranslationExact('LEAGUE', league.id, 'en-US'),
          this.getTranslationExact('LEAGUE', league.id, 'zh-CN'),
          this.getTranslationExact('LEAGUE', league.id, 'ms-MY'),
          this.prisma.match.count({
            where: {
              leagueId: league.id,
              deletedAt: null,
              isOutright: false,
              ...(opts.status ? { status: opts.status } : {}),
            },
          }),
        ]);
        return {
          id: league.id.toString(),
          code: league.code,
          logoUrl: league.logoUrl,
          displayOrder: league.displayOrder,
          leagueEn,
          leagueZh,
          leagueMs,
          matchCount,
          betStats: { betCount: 0, totalStake: '0', pendingCount: 0 },
        };
      }),
    );

    const leagueIds = leagues.map((l) => l.id);
    const leagueMatches = await this.prisma.match.findMany({
      where: {
        leagueId: { in: leagueIds },
        deletedAt: null,
        isOutright: false,
        ...(opts.status ? { status: opts.status } : {}),
      },
      select: { id: true, leagueId: true },
    });
    const fixtureTeamRows = await this.prisma.match.findMany({
      where: {
        leagueId: { in: leagueIds },
        deletedAt: null,
        isOutright: false,
      },
      select: {
        leagueId: true,
        homeTeam: { select: { id: true, code: true } },
        awayTeam: { select: { id: true, code: true } },
      },
    });
    const fixtureTeamSets = new Map<string, Set<string>>();
    for (const row of fixtureTeamRows) {
      const lid = row.leagueId.toString();
      if (!fixtureTeamSets.has(lid)) fixtureTeamSets.set(lid, new Set());
      const set = fixtureTeamSets.get(lid)!;
      for (const team of [row.homeTeam, row.awayTeam]) {
        if (team.code !== 'OUT') set.add(team.id.toString());
      }
    }
    const matchStats = await this.betStatsForMatches(
      leagueMatches.map((m) => m.id),
    );
    const leagueBetRollup = new Map<
      string,
      { betCount: number; totalStake: Decimal; pendingCount: number }
    >();
    for (const lm of leagueMatches) {
      const lid = lm.leagueId.toString();
      const cur = leagueBetRollup.get(lid) ?? {
        betCount: 0,
        totalStake: new Decimal(0),
        pendingCount: 0,
      };
      const ms = matchStats.get(lm.id.toString());
      if (ms) {
        cur.betCount += ms.betCount;
        cur.totalStake = cur.totalStake.add(new Decimal(ms.totalStake));
        cur.pendingCount += ms.pendingCount;
      }
      leagueBetRollup.set(lid, cur);
    }
    const outrightMatches = await this.prisma.match.findMany({
      where: {
        leagueId: { in: leagueIds },
        isOutright: true,
        deletedAt: null,
      },
      select: { id: true, leagueId: true },
    });
    const outrightTeamCounts = new Map<string, number>();
    if (outrightMatches.length > 0) {
      const matchIdToLeagueId = new Map(
        outrightMatches.map((m) => [m.id.toString(), m.leagueId.toString()]),
      );
      const outrightMatchIds = outrightMatches.map((m) => m.id);
      const markets = await this.prisma.market.findMany({
        where: {
          matchId: { in: outrightMatchIds },
          marketType: 'OUTRIGHT_WINNER',
        },
        select: { id: true, matchId: true },
      });
      if (markets.length > 0) {
        const marketIdToMatchId = new Map(
          markets.map((m) => [m.id.toString(), m.matchId.toString()]),
        );
        const selectionCounts = await this.prisma.marketSelection.groupBy({
          by: ['marketId'],
          where: {
            marketId: { in: markets.map((m) => m.id) },
            status: 'OPEN',
            selectionCode: { not: 'OUT' },
          },
          _count: { id: true },
        });
        for (const row of selectionCounts) {
          const matchId = marketIdToMatchId.get(row.marketId.toString());
          const leagueId = matchId ? matchIdToLeagueId.get(matchId) : undefined;
          if (leagueId) {
            outrightTeamCounts.set(leagueId, row._count.id);
          }
        }
      }
    }

    for (const item of items) {
      const roll = leagueBetRollup.get(item.id);
      if (roll) {
        item.betStats = {
          betCount: roll.betCount,
          totalStake: roll.totalStake.toString(),
          pendingCount: roll.pendingCount,
        };
      }
      (item as { fixtureTeamCount?: number }).fixtureTeamCount =
        fixtureTeamSets.get(item.id)?.size ?? 0;
      (item as { outrightTeamCount?: number }).outrightTeamCount =
        outrightTeamCounts.get(item.id) ?? 0;
    }

    return { items, total, page: opts.page, pageSize: opts.pageSize };
  }

  async listAdminLeagueMatches(
    leagueId: bigint,
    opts: { status?: string; keyword?: string; locale?: string },
  ) {
    const where: Prisma.MatchWhereInput = {
      leagueId,
      deletedAt: null,
      isOutright: false,
    };
    if (opts.status) where.status = opts.status;
    const kw = opts.keyword?.trim();
    if (kw) {
      where.OR = [
        { matchName: { contains: kw, mode: 'insensitive' } },
        { homeTeam: { code: { contains: kw, mode: 'insensitive' } } },
        { awayTeam: { code: { contains: kw, mode: 'insensitive' } } },
      ];
    }
    const items = await this.prisma.match.findMany({
      where,
      include: { homeTeam: true, awayTeam: true },
      orderBy: [{ displayOrder: 'asc' }, { startTime: 'desc' }],
    });
    const locale = opts.locale ?? 'zh-CN';
    const betStatsMap = await this.betStatsForMatches(items.map((m) => m.id));
    return Promise.all(
      items.map(async (m) => {
        const [homeTeamName, awayTeamName] = await Promise.all([
          this.getTranslation('TEAM', m.homeTeamId, locale),
          this.getTranslation('TEAM', m.awayTeamId, locale),
        ]);
        const raw = betStatsMap.get(m.id.toString());
        const betCount = raw?.betCount ?? 0;
        const totalStake = raw?.totalStake ?? '0';
        const pendingBets = raw?.pendingCount ?? 0;
        return {
          id: m.id.toString(),
          status: m.status,
          isOutright: m.isOutright,
          isHot: m.isHot,
          displayOrder: m.displayOrder,
          startTime: m.startTime,
          matchName: m.matchName,
          homeTeamName,
          awayTeamName,
          homeTeam: { code: m.homeTeam.code },
          awayTeam: { code: m.awayTeam.code },
          betCount,
          totalStake,
          pendingBets,
        };
      }),
    );
  }

  /** 批量汇总多场关联注单（按 bet 去重计注单数） */
  async betStatsForMatches(
    matchIds: bigint[],
  ): Promise<Map<string, MatchBetStatsSummary>> {
    const result = new Map<string, MatchBetStatsSummary>();
    if (!matchIds.length) return result;

    const legs = await this.prisma.betSelection.findMany({
      where: { matchId: { in: matchIds } },
      select: {
        matchId: true,
        betId: true,
        bet: { select: { stake: true, status: true } },
      },
    });

    const byMatch = new Map<
      string,
      Map<string, { stake: Decimal; status: string }>
    >();
    for (const leg of legs) {
      if (leg.matchId == null) continue;
      const mid = leg.matchId.toString();
      if (!byMatch.has(mid)) byMatch.set(mid, new Map());
      const bets = byMatch.get(mid)!;
      if (!bets.has(leg.betId.toString())) {
        bets.set(leg.betId.toString(), {
          stake: leg.bet.stake,
          status: leg.bet.status,
        });
      }
    }

    for (const id of matchIds) {
      const mid = id.toString();
      const bets = byMatch.get(mid);
      if (!bets) {
        result.set(mid, {
          betCount: 0,
          totalStake: '0',
          pendingCount: 0,
        });
        continue;
      }
      let totalStake = new Decimal(0);
      let pendingCount = 0;
      for (const b of bets.values()) {
        totalStake = totalStake.add(b.stake);
        if (b.status === 'PENDING') pendingCount += 1;
      }
      result.set(mid, {
        betCount: bets.size,
        totalStake: totalStake.toString(),
        pendingCount,
      });
    }
    return result;
  }

  private async upsertTeamByCode(data: {
    code: string;
    teamZh: string;
    teamEn: string;
    teamMs?: string;
    logoUrl?: string;
  }) {
    const code = data.code.trim().toUpperCase();
    if (!code) throw new BadRequestException('请填写球队代码');
    const logoUrl = data.logoUrl?.trim() || undefined;
    const team = await this.prisma.team.upsert({
      where: { code },
      create: { code, logoUrl },
      update: logoUrl ? { logoUrl } : {},
    });
    const translations: Record<string, string> = {
      'zh-CN': data.teamZh.trim() || data.teamEn.trim(),
      'en-US': data.teamEn.trim() || data.teamZh.trim(),
    };
    if (data.teamMs?.trim()) translations['ms-MY'] = data.teamMs.trim();
    await this.upsertEntityTranslations('TEAM', team.id, translations);
    return team;
  }

  async createPlatformMatch(data: {
    leagueId?: bigint;
    leagueEn?: string;
    leagueZh?: string;
    leagueMs?: string;
    homeTeamCode?: string;
    awayTeamCode?: string;
    homeTeamZh: string;
    homeTeamEn: string;
    homeTeamMs?: string;
    awayTeamZh: string;
    awayTeamEn: string;
    awayTeamMs?: string;
    startTime: Date;
    isHot?: boolean;
    displayOrder?: number;
    matchName?: string;
    stage?: string;
    groupName?: string;
    leagueLogoUrl?: string;
    homeTeamLogoUrl?: string;
    awayTeamLogoUrl?: string;
    createdBy?: bigint;
  }) {
    const homeEn = data.homeTeamEn.trim();
    const homeZh = data.homeTeamZh.trim();
    const homeMs = data.homeTeamMs?.trim() ?? '';
    const awayEn = data.awayTeamEn.trim();
    const awayZh = data.awayTeamZh.trim();
    const awayMs = data.awayTeamMs?.trim() ?? '';
    if ((!homeEn && !homeZh && !homeMs) || (!awayEn && !awayZh && !awayMs)) {
      throw new BadRequestException('请填写主客队名称（中文、英文或马来文至少一项）');
    }

    let league;
    if (data.leagueId) {
      league = await this.prisma.league.findFirst({
        where: { id: data.leagueId, deletedAt: null },
      });
      if (!league) throw new NotFoundException('赛事不存在');
    } else {
      const leagueEn = data.leagueEn?.trim() ?? '';
      const leagueZh = data.leagueZh?.trim() ?? '';
      const leagueMs = data.leagueMs?.trim() ?? '';
      if (!leagueEn && !leagueZh && !leagueMs) {
        throw new BadRequestException('请填写赛事名称（中文、英文或马来文至少一项）');
      }
      league = await this.upsertLeagueFromZhiboExport({
        type: 'FOOTBALL',
        en: leagueEn || leagueZh || leagueMs,
        zh: leagueZh || leagueEn || leagueMs,
      });
      if (leagueMs) {
        await this.upsertEntityTranslations('LEAGUE', league.id, {
          'ms-MY': leagueMs,
        });
      }
      if (data.leagueLogoUrl?.trim()) {
        await this.prisma.league.update({
          where: { id: league.id },
          data: { logoUrl: data.leagueLogoUrl.trim() },
        });
      }
    }
    const homeCode = data.homeTeamCode?.trim().toUpperCase();
    const awayCode = data.awayTeamCode?.trim().toUpperCase();
    let homeTeam;
    let awayTeam;
    if (homeCode && awayCode) {
      if (homeCode === awayCode) {
        throw new BadRequestException('主客队不能为同一支球队，请选择不同的队伍');
      }
      homeTeam = await this.upsertTeamByCode({
        code: homeCode,
        teamZh: homeZh,
        teamEn: homeEn,
        teamMs: homeMs,
        logoUrl: data.homeTeamLogoUrl,
      });
      awayTeam = await this.upsertTeamByCode({
        code: awayCode,
        teamZh: awayZh,
        teamEn: awayEn,
        teamMs: awayMs,
        logoUrl: data.awayTeamLogoUrl,
      });
    } else {
      homeTeam = await this.upsertTeamFromZhiboExport({
        id: null,
        name: homeEn || homeZh || homeMs,
        names: {
          zh: homeZh || null,
          en: homeEn || null,
          zhTw: '',
          vi: null,
          km: null,
          ms: homeMs || null,
        },
        image: data.homeTeamLogoUrl?.trim() || '',
      });
      awayTeam = await this.upsertTeamFromZhiboExport({
        id: null,
        name: awayEn || awayZh || awayMs,
        names: {
          zh: awayZh || null,
          en: awayEn || null,
          zhTw: '',
          vi: null,
          km: null,
          ms: awayMs || null,
        },
        image: data.awayTeamLogoUrl?.trim() || '',
      });
    }

    if (homeTeam.id === awayTeam.id) {
      throw new BadRequestException('主客队不能为同一支球队，请填写不同的队名');
    }

    const matchName =
      data.matchName?.trim() ||
      `${homeEn || homeZh || homeMs} - ${awayEn || awayZh || awayMs}`;

    return this.createMatch({
      leagueId: league.id,
      homeTeamId: homeTeam.id,
      awayTeamId: awayTeam.id,
      startTime: data.startTime,
      isHot: data.isHot ?? false,
      displayOrder: data.displayOrder ?? 0,
      createdBy: data.createdBy,
      status: 'DRAFT',
      zhibo: {
        matchName,
        stage: data.stage?.trim() || undefined,
        groupName: data.groupName?.trim() || undefined,
      },
    });
  }

  private async requireAdminMatch(matchId: bigint) {
    const match = await this.prisma.match.findFirst({
      where: { id: matchId, deletedAt: null },
      include: { homeTeam: true, awayTeam: true, league: true },
    });
    if (!match) throw new NotFoundException('赛事不存在');
    return match;
  }

  async getAdminMatchDetail(matchId: bigint) {
    const match = await this.requireAdminMatch(matchId);
    const scoreRow = await this.prisma.matchScore.findUnique({
      where: { matchId },
    });
    const markets = await this.prisma.market.findMany({
      where: { matchId },
      include: { selections: { orderBy: { sortOrder: 'asc' } } },
      orderBy: { sortOrder: 'asc' },
    });
    const [leagueEn, leagueZh, leagueMs, homeEn, homeZh, homeMs, awayEn, awayZh, awayMs] =
      await Promise.all([
      this.getTranslationExact('LEAGUE', match.leagueId, 'en-US'),
      this.getTranslationExact('LEAGUE', match.leagueId, 'zh-CN'),
      this.getTranslationExact('LEAGUE', match.leagueId, 'ms-MY'),
      this.getTranslationExact('TEAM', match.homeTeamId, 'en-US'),
      this.getTranslationExact('TEAM', match.homeTeamId, 'zh-CN'),
      this.getTranslationExact('TEAM', match.homeTeamId, 'ms-MY'),
      this.getTranslationExact('TEAM', match.awayTeamId, 'en-US'),
      this.getTranslationExact('TEAM', match.awayTeamId, 'zh-CN'),
      this.getTranslationExact('TEAM', match.awayTeamId, 'ms-MY'),
    ]);
    return {
      id: match.id.toString(),
      status: match.status,
      isOutright: match.isOutright,
      isHot: match.isHot,
      displayOrder: match.displayOrder,
      startTime: match.startTime.toISOString(),
      leagueId: match.leagueId.toString(),
      leagueCode: match.league.code,
      leagueEn,
      leagueZh,
      leagueMs,
      leagueLogoUrl: match.league.logoUrl ?? '',
      homeTeamEn: homeEn,
      homeTeamZh: homeZh,
      homeTeamMs: homeMs,
      homeTeamCode: match.homeTeam.code,
      homeTeamLogoUrl: match.homeTeam.logoUrl ?? '',
      awayTeamEn: awayEn,
      awayTeamZh: awayZh,
      awayTeamMs: awayMs,
      awayTeamCode: match.awayTeam.code,
      awayTeamLogoUrl: match.awayTeam.logoUrl ?? '',
      matchName: match.matchName ?? '',
      stage: match.stage ?? '',
      groupName: match.groupName ?? '',
      score: scoreRow
        ? {
            htHome: scoreRow.htHomeScore ?? 0,
            htAway: scoreRow.htAwayScore ?? 0,
            ftHome: scoreRow.ftHomeScore ?? 0,
            ftAway: scoreRow.ftAwayScore ?? 0,
          }
        : null,
      markets: markets.map((m) => ({
        id: m.id.toString(),
        marketType: m.marketType,
        period: m.period,
        lineValue: m.lineValue != null ? Number(m.lineValue) : null,
        status: m.status,
        promoLabel: m.promoLabel ?? '',
        sortOrder: m.sortOrder,
        selections: m.selections.map((s) => ({
          id: s.id.toString(),
          selectionCode: s.selectionCode,
          selectionName: s.selectionName,
          odds: Number(s.odds),
          status: s.status,
          sortOrder: s.sortOrder,
        })),
      })),
    };
  }

  async updatePlatformMatch(
    matchId: bigint,
    data: {
      leagueEn: string;
      leagueZh: string;
      leagueMs?: string;
      homeTeamZh: string;
      homeTeamEn: string;
      homeTeamMs?: string;
      awayTeamZh: string;
      awayTeamEn: string;
      awayTeamMs?: string;
      startTime: Date;
      isHot?: boolean;
      displayOrder?: number;
      matchName?: string;
      stage?: string;
      groupName?: string;
      leagueLogoUrl?: string;
      homeTeamLogoUrl?: string;
      awayTeamLogoUrl?: string;
      updatedBy?: bigint;
    },
  ) {
    const match = await this.requireAdminMatch(matchId);
    if (match.isOutright) {
      throw new BadRequestException('冠军盘请通过盘口管理维护');
    }
    if (!['DRAFT', 'PUBLISHED'].includes(match.status)) {
      throw new BadRequestException('当前状态不可编辑');
    }

    const matchName =
      data.matchName?.trim() ||
      `${data.homeTeamEn.trim() || data.homeTeamZh.trim() || data.homeTeamMs?.trim() || ''} - ${data.awayTeamEn.trim() || data.awayTeamZh.trim() || data.awayTeamMs?.trim() || ''}`;

    await Promise.all([
      this.upsertEntityTranslations('LEAGUE', match.leagueId, {
        'zh-CN': data.leagueZh.trim(),
        'en-US': data.leagueEn.trim(),
        'ms-MY': (data.leagueMs ?? '').trim(),
      }),
      this.upsertEntityTranslations('TEAM', match.homeTeamId, {
        'zh-CN': data.homeTeamZh.trim(),
        'en-US': data.homeTeamEn.trim(),
        'ms-MY': (data.homeTeamMs ?? '').trim(),
      }),
      this.upsertEntityTranslations('TEAM', match.awayTeamId, {
        'zh-CN': data.awayTeamZh.trim(),
        'en-US': data.awayTeamEn.trim(),
        'ms-MY': (data.awayTeamMs ?? '').trim(),
      }),
    ]);

    const logoUpdates: Promise<unknown>[] = [];
    if (data.leagueLogoUrl !== undefined) {
      logoUpdates.push(
        this.prisma.league.update({
          where: { id: match.leagueId },
          data: { logoUrl: data.leagueLogoUrl.trim() || null },
        }),
      );
    }
    if (data.homeTeamLogoUrl !== undefined) {
      logoUpdates.push(
        this.prisma.team.update({
          where: { id: match.homeTeamId },
          data: { logoUrl: data.homeTeamLogoUrl.trim() || null },
        }),
      );
    }
    if (data.awayTeamLogoUrl !== undefined) {
      logoUpdates.push(
        this.prisma.team.update({
          where: { id: match.awayTeamId },
          data: { logoUrl: data.awayTeamLogoUrl.trim() || null },
        }),
      );
    }
    if (logoUpdates.length) await Promise.all(logoUpdates);

    return this.prisma.match.update({
      where: { id: matchId },
      data: {
        startTime: data.startTime,
        isHot: data.isHot ?? match.isHot,
        displayOrder: data.displayOrder ?? match.displayOrder,
        matchName,
        stage: data.stage !== undefined ? data.stage.trim() || null : match.stage,
        groupName: data.groupName !== undefined ? data.groupName.trim() || null : match.groupName,
        updatedBy: data.updatedBy,
      },
    });
  }

  async deleteMatch(matchId: bigint) {
    const match = await this.requireAdminMatch(matchId);
    if (match.isOutright) {
      throw new BadRequestException('冠军盘不可删除');
    }
    if (match.status !== 'DRAFT') {
      throw new BadRequestException('仅草稿状态可删除');
    }
    const betCount = await this.prisma.betSelection.count({ where: { matchId } });
    if (betCount > 0) {
      throw new BadRequestException('该赛事已有注单关联，无法删除');
    }
    return this.prisma.match.update({
      where: { id: matchId },
      data: { deletedAt: new Date() },
    });
  }

  async createMatchFromZhiboExport(
    item: ZhiboMatchExport,
    createdBy?: bigint,
    opts?: { asDraft?: boolean },
  ) {
    const league = await this.upsertLeagueFromZhiboExport(item.league);
    const [homeTeam, awayTeam] = await Promise.all([
      this.upsertTeamFromZhiboExport(item.homeTeam),
      this.upsertTeamFromZhiboExport(item.awayTeam),
    ]);

    const status = opts?.asDraft ? 'DRAFT' : resolveInternalStatus(item);
    const startTime = resolveStartTime(item.kickoff);
    const liveMatchId =
      item.liveMatchId != null ? BigInt(item.liveMatchId) : undefined;
    const payload = {
      leagueId: league.id,
      homeTeamId: homeTeam.id,
      awayTeamId: awayTeam.id,
      startTime,
      isHot: resolveIsHot(item),
      displayOrder: item.sortOrder,
      createdBy,
      status,
      publishTime: status === 'PUBLISHED' ? new Date() : undefined,
      zhibo: {
        officialMatchNo: item.officialMatchNo,
        stage: item.stage,
        groupName: item.groupName,
        liveMatchId,
        additionMatchId: item.additionMatchId != null ? BigInt(item.additionMatchId) : null,
        channelId: item.channelId,
        matchName: item.matchName,
        venueJson: toVenueJson(item.venue),
        kickoffJson: toKickoffJson(item.kickoff),
        externalStatus: item.status.state,
      },
    };

    const existing = await this.findExistingZhiboMatch(
      league.id,
      homeTeam.id,
      awayTeam.id,
      item,
    );
    if (existing) {
      return this.prisma.match.update({
        where: { id: existing.id },
        data: {
          leagueId: payload.leagueId,
          homeTeamId: payload.homeTeamId,
          awayTeamId: payload.awayTeamId,
          startTime: payload.startTime,
          isHot: payload.isHot,
          displayOrder: payload.displayOrder,
          status: payload.status,
          publishTime: existing.publishTime ?? payload.publishTime,
          officialMatchNo: payload.zhibo.officialMatchNo,
          stage: payload.zhibo.stage,
          groupName: payload.zhibo.groupName,
          liveMatchId: payload.zhibo.liveMatchId ?? undefined,
          additionMatchId: payload.zhibo.additionMatchId ?? undefined,
          channelId: payload.zhibo.channelId ?? undefined,
          matchName: payload.zhibo.matchName,
          venueJson: payload.zhibo.venueJson,
          kickoffJson: payload.zhibo.kickoffJson,
          externalStatus: payload.zhibo.externalStatus,
          updatedBy: createdBy,
        },
      });
    }

    return this.createMatch(payload);
  }

  async importZhiboMatchesBundle(bundle: ZhiboMatchesBundleExport, createdBy?: bigint) {
    if (!bundle.matches?.length) {
      throw new BadRequestException('matches array is required');
    }

    const results: Array<{ liveMatchId: string; id: string; status: string; skipped?: boolean; reason?: string }> = [];

    for (const item of bundle.matches) {
      try {
        const match = await this.createMatchFromZhiboExport(item, createdBy, { asDraft: true });
        results.push({
          liveMatchId: item.liveMatchId != null ? String(item.liveMatchId) : '',
          id: match.id.toString(),
          status: match.status,
        });
      } catch (err) {
        const message = err instanceof Error ? err.message : 'import failed';
        results.push({
          liveMatchId: item.liveMatchId != null ? String(item.liveMatchId) : '',
          id: '',
          status: 'error',
          reason: message,
        });
      }
    }

    return {
      total: bundle.matches.length,
      imported: results.filter((r) => !r.skipped && r.status !== 'error').length,
      skipped: results.filter((r) => r.skipped).length,
      failed: results.filter((r) => r.status === 'error').length,
      results,
    };
  }

  async publishMatch(matchId: bigint) {
    return this.prisma.match.update({
      where: { id: matchId },
      data: { status: 'PUBLISHED', publishTime: new Date() },
    });
  }

  async closeMatch(matchId: bigint) {
    return this.prisma.match.update({
      where: { id: matchId },
      data: { status: 'CLOSED', closeTime: new Date() },
    });
  }

  async cancelMatch(matchId: bigint) {
    return this.prisma.match.update({
      where: { id: matchId },
      data: { status: 'CANCELLED' },
    });
  }

  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 ?? '';
  }

  async getTranslation(entityType: string, entityId: bigint, locale: string) {
    const translations = await this.prisma.entityTranslation.findMany({
      where: { entityType, entityId },
    });
    const map = Object.fromEntries(
      translations.filter((t) => t.fieldName === 'name').map((t) => [t.locale, t.value]),
    );
    return resolveTranslationFallback(map, locale);
  }

  async enrichMatch(match: Record<string, unknown>, locale: string) {
    const m = match as {
      id: bigint;
      leagueId: bigint;
      homeTeamId: bigint;
      awayTeamId: bigint;
      startTime: Date;
      status?: string;
      isHot?: boolean;
      displayOrder?: number;
      matchName?: string | null;
      stage?: string | null;
      groupName?: string | null;
      homeTeam?: { code: string; logoUrl?: string | null };
      awayTeam?: { code: string; logoUrl?: string | null };
      league?: { logoUrl?: string | null };
      markets?: Array<Record<string, unknown>>;
    };
    const [leagueName, homeName, awayName] = await Promise.all([
      this.getTranslation('LEAGUE', m.leagueId, locale),
      this.getTranslation('TEAM', m.homeTeamId, locale),
      this.getTranslation('TEAM', m.awayTeamId, locale),
    ]);
    const base = {
      id: m.id.toString(),
      leagueId: m.leagueId.toString(),
      leagueName,
      leagueLogoUrl: m.league?.logoUrl ?? null,
      homeTeamName: homeName,
      awayTeamName: awayName,
      homeTeamCode: m.homeTeam?.code ?? '',
      awayTeamCode: m.awayTeam?.code ?? '',
      homeTeamLogoUrl: m.homeTeam?.logoUrl ?? null,
      awayTeamLogoUrl: m.awayTeam?.logoUrl ?? null,
      startTime: m.startTime.toISOString(),
      isHot: m.isHot ?? false,
      displayOrder: m.displayOrder ?? 0,
      matchName: m.matchName ?? null,
      stage: m.stage ?? null,
      groupName: m.groupName ?? null,
      status: m.status ?? 'PUBLISHED',
    };
    if (m.markets) {
      return {
        ...base,
        markets: m.markets.map((market) => ({
          id: (market.id as bigint).toString(),
          marketType: market.marketType as string,
          period: market.period as string,
          lineValue: market.lineValue != null ? Number(market.lineValue) : null,
          allowParlay: (market.allowParlay as boolean | undefined) ?? true,
          promoLabel: (market.promoLabel as string | null | undefined) ?? null,
          selections: ((market.selections as Array<Record<string, unknown>>) ?? []).map((s) => ({
            id: (s.id as bigint).toString(),
            selectionCode: s.selectionCode as string,
            selectionName: s.selectionName as string,
            odds: Number(s.odds),
            oddsVersion: (s.oddsVersion as bigint).toString(),
          })),
        })),
      };
    }
    return base;
  }

  async listPublished(locale = 'en-US', leagueId?: bigint) {
    const now = new Date();
    const matches = await this.prisma.match.findMany({
      where: {
        status: 'PUBLISHED',
        isOutright: false,
        sportType: 'FOOTBALL',
        deletedAt: null,
        startTime: { gt: now },
        ...(leagueId ? { leagueId } : {}),
      },
      include: {
        league: true,
        homeTeam: true,
        awayTeam: true,
        markets: {
          where: { status: 'OPEN' },
          include: { selections: { where: { status: 'OPEN' }, orderBy: { sortOrder: 'asc' } } },
          orderBy: { sortOrder: 'asc' },
        },
      },
      orderBy: [{ isHot: 'desc' }, { displayOrder: 'asc' }, { startTime: 'asc' }],
    });

    return Promise.all(matches.map((m) => this.enrichMatch(m, locale)));
  }

  async getMatchDetail(matchId: bigint, locale = 'en-US') {
    const match = await this.prisma.match.findFirst({
      where: {
        id: matchId,
        deletedAt: null,
        sportType: 'FOOTBALL',
        isOutright: false,
        status: { in: ['PUBLISHED', 'CLOSED'] },
      },
      include: {
        league: true,
        homeTeam: true,
        awayTeam: true,
        markets: {
          where: { status: 'OPEN' },
          include: { selections: { where: { status: 'OPEN' }, orderBy: { sortOrder: 'asc' } } },
          orderBy: { sortOrder: 'asc' },
        },
        score: true,
      },
    });
    if (!match) throw new NotFoundException('Match not found');
    return this.enrichMatch(match, locale);
  }

  async listOutrights(locale = 'en-US') {
    try {
      await syncWc2026OutrightMarket(this.prisma, { forceCanonical: false });
    } catch {
      /* 联赛未 seed 时忽略，仍返回已有数据 */
    }

    const matches = await this.prisma.match.findMany({
      where: {
        status: 'PUBLISHED',
        isOutright: true,
        sportType: 'FOOTBALL',
        deletedAt: null,
      },
      include: {
        markets: {
          where: { marketType: 'OUTRIGHT_WINNER', status: 'OPEN' },
          include: {
            selections: {
              where: { status: 'OPEN' },
              orderBy: { sortOrder: 'asc' },
            },
          },
        },
      },
      orderBy: [{ displayOrder: 'asc' }, { startTime: 'asc' }],
    });

    const results = [];
    for (const match of matches) {
      const leagueName = await this.getTranslation('LEAGUE', match.leagueId, locale);
      const market = match.markets[0];
      if (!market) continue;

      const selections = await Promise.all(
        market.selections
          .filter((sel) => sel.selectionCode !== OUTRIGHT_PLACEHOLDER_CODE)
          .map(async (sel) => {
            const team = await this.prisma.team.findUnique({
              where: { code: sel.selectionCode },
            });
            const teamName = team
              ? await this.getTranslation('TEAM', team.id, locale)
              : sel.selectionName;
            return {
              id: sel.id.toString(),
              teamCode: sel.selectionCode,
              teamName,
              rank: sel.sortOrder + 1,
              odds: sel.odds.toString(),
              oddsVersion: sel.oddsVersion.toString(),
            };
          }),
      );

      if (selections.length === 0) continue;

      results.push({
        id: match.id.toString(),
        leagueId: match.leagueId.toString(),
        leagueName,
        title: `*${leagueName} 冠军`,
        marketId: market.id.toString(),
        selections,
      });
    }
    return results;
  }

  private marketLabelKey(marketType: string, locale = 'zh-CN'): string {
    type LangMap = Record<string, string>;
    const labels: Record<string, LangMap> = {
      FT_1X2:           { 'zh-CN': '全场独赢',     'en-US': 'FT 1X2',          'ms-MY': '1X2 Penuh' },
      FT_HANDICAP:      { 'zh-CN': '全场让球',     'en-US': 'FT Handicap',      'ms-MY': 'Handicap Penuh' },
      FT_OVER_UNDER:    { 'zh-CN': '全场大小',     'en-US': 'FT O/U',           'ms-MY': 'Atas/Bawah Penuh' },
      FT_ODD_EVEN:      { 'zh-CN': '全场单双',     'en-US': 'FT Odd/Even',      'ms-MY': 'Ganjil/Genap Penuh' },
      HT_1X2:           { 'zh-CN': '半场独赢',     'en-US': 'HT 1X2',           'ms-MY': '1X2 Separuh' },
      HT_HANDICAP:      { 'zh-CN': '半场让球',     'en-US': 'HT Handicap',      'ms-MY': 'Handicap Separuh' },
      HT_OVER_UNDER:    { 'zh-CN': '半场大小',     'en-US': 'HT O/U',           'ms-MY': 'Atas/Bawah Separuh' },
      OUTRIGHT_WINNER:  { 'zh-CN': '冠军',         'en-US': 'Outright',         'ms-MY': 'Juara' },
      FT_CORRECT_SCORE: { 'zh-CN': '波胆',         'en-US': 'Correct Score',    'ms-MY': 'Skor Tepat' },
      HT_CORRECT_SCORE: { 'zh-CN': '上半场波胆',   'en-US': '1H Correct Score', 'ms-MY': 'Skor Tepat PB1' },
      SH_CORRECT_SCORE: { 'zh-CN': '下半场波胆',   'en-US': '2H Correct Score', 'ms-MY': 'Skor Tepat PB2' },
    };
    const entry = labels[marketType];
    if (!entry) return marketType;
    return entry[locale] ?? entry['en-US'] ?? marketType;
  }

  async enrichBetsForHistory(
    bets: Array<{
      betNo: string;
      betType: string;
      stake: unknown;
      totalOdds: unknown;
      potentialReturn: unknown;
      actualReturn: unknown;
      status: string;
      placedAt: Date;
      selections: Array<{
        matchId: bigint | null;
        marketType: string;
        selectionNameSnapshot: string;
        odds: unknown;
        resultStatus?: string | null;
      }>;
    }>,
    locale: string,
  ) {
    const matchIds = [
      ...new Set(
        bets.flatMap((b) =>
          b.selections.map((s) => s.matchId).filter((id): id is bigint => id != null),
        ),
      ),
    ];

    const matches =
      matchIds.length > 0
        ? await this.prisma.match.findMany({
            where: { id: { in: matchIds } },
            include: { homeTeam: true, awayTeam: true, score: true },
          })
        : [];

    const matchMeta = new Map<
      string,
      {
        leagueName: string;
        matchTitle: string;
        isOutright: boolean;
        score: { ht: string | null; ft: string | null } | null;
      }
    >();

    for (const m of matches) {
      const [leagueName, homeName, awayName] = await Promise.all([
        this.getTranslation('LEAGUE', m.leagueId, locale),
        this.getTranslation('TEAM', m.homeTeamId, locale),
        this.getTranslation('TEAM', m.awayTeamId, locale),
      ]);
      const s = m.score;
      const ftScore =
        s?.ftHomeScore != null && s?.ftAwayScore != null
          ? `${s.ftHomeScore}-${s.ftAwayScore}`
          : null;
      const htScore =
        s?.htHomeScore != null && s?.htAwayScore != null
          ? `${s.htHomeScore}-${s.htAwayScore}`
          : null;
      matchMeta.set(m.id.toString(), {
        leagueName,
        matchTitle: m.isOutright ? leagueName : `${homeName} vs ${awayName}`,
        isOutright: m.isOutright,
        score: ftScore || htScore ? { ht: htScore, ft: ftScore } : null,
      });
    }

    return bets.map((bet) => {
      const firstMatchId = bet.selections.find((s) => s.matchId)?.matchId?.toString();
      const meta = firstMatchId ? matchMeta.get(firstMatchId) : undefined;
      const isParlay = bet.betType === 'PARLAY' || bet.selections.length > 1;

      const legs = bet.selections.map((sel) => {
        const mid = sel.matchId?.toString();
        const m = mid ? matchMeta.get(mid) : undefined;
        return {
          marketType: sel.marketType,
          marketLabel: this.marketLabelKey(sel.marketType, locale),
          selectionName: sel.selectionNameSnapshot,
          odds: sel.odds,
          resultStatus: sel.resultStatus,
          matchTitle: m?.matchTitle ?? sel.selectionNameSnapshot,
          leagueName: m?.leagueName ?? '',
          score: m?.score ?? null,
        };
      });

      const firstScore = meta?.score ?? legs[0]?.score ?? null;

      return {
        betNo: bet.betNo,
        betType: bet.betType,
        stake: bet.stake,
        totalOdds: bet.totalOdds,
        potentialReturn: bet.potentialReturn,
        actualReturn: bet.actualReturn,
        status: bet.status,
        placedAt: bet.placedAt,
        leagueName: isParlay
          ? 'Parlay'
          : meta?.leagueName ?? legs[0]?.leagueName ?? '',
        legCount: bet.selections.length,
        matchTitle: isParlay
          ? ''
          : meta?.matchTitle ?? legs[0]?.matchTitle ?? bet.betNo,
        pickLabel: isParlay
          ? ''
          : `${legs[0]?.marketLabel ?? ''}: ${legs[0]?.selectionName ?? ''}`,
        legs,
        isParlay,
        matchScore: isParlay ? null : firstScore,
      };
    });
  }

  @Cron(CronExpression.EVERY_MINUTE)
  async autoCloseMatches() {
    const now = new Date();
    await this.prisma.match.updateMany({
      where: {
        status: 'PUBLISHED',
        isOutright: false,
        startTime: { lte: now },
      },
      data: { status: 'CLOSED', closeTime: now },
    });
  }
}
