/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * Feeder Persistence Service - Senior Level Implementation
 * Database operations using Prisma
 */

import { Injectable, Logger } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { PrismaService } from "../../database/prisma.service";
import {
  Sport,
  MatchSummary,
  Competition,
  TransformedPlayer,
  MatchParticipation,
  TransformedMatchStats,
  MatchOfficial,
  ParsedMatchHeader,
  BasketballPlayerStats,
  DbEventPayload,
  DbMarketPayload,
  BasketballTeamStats,
} from "./feeder.types";
import { deriveStoredMatchStatus } from "../../common/utils/match-status.util";

@Injectable()
export class FeederPersistenceService {
  private readonly logger = new Logger(FeederPersistenceService.name);

  constructor(private readonly prisma: PrismaService) {}

  // ============================================
  // HELPER FUNCTIONS
  // ============================================
  private safeString(value: any): string | null {
    return value === null || value === undefined || value === ""
      ? null
      : String(value);
  }

  private safeInt(value: any): number | null {
    const num = parseInt(String(value), 10);
    return isNaN(num) ? null : num;
  }

  private safeFloat(value: any): number | null {
    const num = parseFloat(String(value));
    return isNaN(num) ? null : num;
  }

  private mapPositionToEnum(position: string | null): any {
    if (!position) return null;
    const pos = position.toLowerCase();
    if (pos.includes("kaleci") || pos.includes("goalkeeper"))
      return "goalkeeper";
    if (pos.includes("defans") || pos.includes("defender")) return "defender";
    if (pos.includes("orta saha") || pos.includes("midfielder"))
      return "midfielder";
    if (pos.includes("forvet") || pos.includes("striker")) return "striker";
    return null;
  }

  // ============================================
  // ODDS HELPER (TRANSACTION SAFE)
  // ============================================
  private async saveOddsInTransaction(
    tx: any,
    matchId: string,
    oddsArray: DbMarketPayload[],
  ): Promise<void> {
    if (oddsArray.length === 0) return;

    const existingCategories = await tx.oddCategory.findMany({
      where: { matchId },
      include: { selections: true },
    });

    for (const market of oddsArray) {
      if (!market || !market.name || !market.selectionCollection) continue;

      let category = existingCategories.find((c) => c.name === market.name);

      if (!category) {
        category = await tx.oddCategory.create({
          data: {
            matchId,
            categoryJsonId: this.safeInt(market.id),
            name: market.name,
          },
          include: { selections: true },
        });
        existingCategories.push(category);
      }

      for (const s of market.selectionCollection) {
        if (!s || s.odd === "-" || s.odd === "") continue;

        const sName = this.safeString(s.name);
        const sValue = this.safeString(s.odd);
        const sPos = this.safeString(s.position);

        if (!sName || !sValue) continue;

        const existingSel = category.selections.find(
          (sel) => sel.name === sName,
        );

        if (existingSel) {
          if (existingSel.oddValue !== sValue) {
            const oldVal = parseFloat(existingSel.oddValue || "0");
            const newVal = parseFloat(sValue);

            if (!isNaN(oldVal) && !isNaN(newVal)) {
              await tx.oddsHistory.create({
                data: {
                  selectionId: existingSel.dbId,
                  matchId: matchId,
                  previousValue: oldVal,
                  newValue: newVal,
                },
              });
            }

            await tx.oddSelection.update({
              where: { dbId: existingSel.dbId },
              data: { oddValue: sValue, position: sPos },
            });
          }
        } else {
          const newSel = await tx.oddSelection.create({
            data: {
              categoryId: category.dbId,
              name: sName,
              oddValue: sValue,
              openingValue: sValue,
              position: sPos,
            },
          });
          category.selections.push(newSel);
        }
      }
    }
  }

  // ============================================
  // MAIN SAVE FUNCTION
  // ============================================
  async saveMatch(
    sport: Sport,
    matchId: string,
    matchSummary: MatchSummary,
    league: Competition,
    homeTeamId: string,
    awayTeamId: string,
    headerData: ParsedMatchHeader | null,
    playersMap: Map<string, TransformedPlayer>,
    participationData: MatchParticipation[],
    eventData: DbEventPayload[],
    stats: TransformedMatchStats | null,
    basketballTeamStats: BasketballTeamStats | null,
    basketballPlayerStats: Partial<BasketballPlayerStats>[],
    oddsArray: DbMarketPayload[],
    officialsData: MatchOfficial[],
  ): Promise<boolean> {
    const teamsToUpsert = [
      {
        id: homeTeamId,
        name: matchSummary.homeTeam?.name || "Unknown",
        slug: matchSummary.homeTeam?.slug || homeTeamId,
        sport: sport,
      },
      {
        id: awayTeamId,
        name: matchSummary.awayTeam?.name || "Unknown",
        slug: matchSummary.awayTeam?.slug || awayTeamId,
        sport: sport,
      },
    ];

    // DATABASE TRANSACTION
    try {
      await this.prisma.$transaction(
        async (tx) => {
          // 1. Save Country
          const countryId = this.safeString(league.country?.id);
          if (countryId) {
            try {
              await tx.country.upsert({
                where: { id: countryId },
                update: {},
                create: {
                  id: countryId,
                  name: league.country.name || "Unknown",
                },
              });
            } catch (error: any) {
              if (error.code !== "P2002") throw error;
            }
          }

          // 2. Save League (Handle ID changes by checking unique constraint)
          let finalLeagueId = this.safeString(league.id);
          if (finalLeagueId && countryId) {
            const leagueName = league.name || "Unknown";

            // Check if league exists by unique constraint (name + country + sport)
            const existingLeague = await tx.league.findUnique({
              where: {
                name_countryId_sport: {
                  name: leagueName,
                  countryId: countryId,
                  sport: sport,
                },
              },
            });

            if (existingLeague) {
              // If exists with different ID, use existing ID to prevent constraint errors
              finalLeagueId = existingLeague.id;
              // Update sortOrder if changed
              if (league.sortOrder !== undefined) {
                await tx.$executeRaw`UPDATE leagues SET sort_order = ${league.sortOrder} WHERE id = ${finalLeagueId}`;
              }
            } else {
              // Create new league
              await tx.league.create({
                data: {
                  id: finalLeagueId,
                  name: leagueName,
                  countryId: countryId,
                  sport: sport,
                  competitionSlug: league.competitionSlug,
                } as any,
              });
              if (league.sortOrder !== undefined) {
                await tx.$executeRaw`UPDATE leagues SET sort_order = ${league.sortOrder} WHERE id = ${finalLeagueId}`;
              }
            }
          }

          // 3. Save Teams (BULK OPTIMIZED)
          const existingTeams = await tx.team.findMany({
            where: {
              id: { in: [homeTeamId, awayTeamId] },
            },
            select: { id: true },
          });

          const existingTeamIds = new Set(existingTeams.map((t) => t.id));
          const teamsToCreate = teamsToUpsert.filter(
            (t) => !existingTeamIds.has(t.id),
          );
          const teamsToUpdate = teamsToUpsert.filter((t) =>
            existingTeamIds.has(t.id),
          );

          if (teamsToCreate.length > 0) {
            await tx.team.createMany({
              data: teamsToCreate,
              skipDuplicates: true,
            });
          }

          for (const team of teamsToUpdate) {
            await tx.team.update({
              where: { id: team.id },
              data: {
                name: team.name,
              },
            });
          }

          // 4. Save Match
          const finalScoreHome =
            headerData?.scoreHome ?? this.safeInt(matchSummary.score?.home);
          const finalScoreAway =
            headerData?.scoreAway ?? this.safeInt(matchSummary.score?.away);
          const htScoreHome =
            headerData?.htScoreHome ??
            this.safeInt(matchSummary.score?.ht?.home);
          const htScoreAway =
            headerData?.htScoreAway ??
            this.safeInt(matchSummary.score?.ht?.away);

          const status = deriveStoredMatchStatus({
            state: headerData?.matchStatus ?? matchSummary.state,
            status: matchSummary.status,
            substate: matchSummary.substate,
            statusBoxContent: matchSummary.statusBoxContent,
            scoreHome: finalScoreHome,
            scoreAway: finalScoreAway,
            score: matchSummary.score,
          });

          await tx.match.upsert({
            where: { id: matchId },
            update: {
              scoreHome: finalScoreHome,
              scoreAway: finalScoreAway,
              htScoreHome: htScoreHome,
              htScoreAway: htScoreAway,
              status: status,
              state: headerData?.matchStatus || null,
            },
            create: {
              id: matchId,
              leagueId: finalLeagueId || undefined,
              homeTeamId: homeTeamId,
              awayTeamId: awayTeamId,
              sport: sport,
              matchName: matchSummary.matchName,
              matchSlug: matchSummary.matchSlug,
              mstUtc: BigInt(matchSummary.mstUtc || 0),
              status: status,
              state: headerData?.matchStatus || null,
              scoreHome: finalScoreHome,
              scoreAway: finalScoreAway,
              htScoreHome: htScoreHome,
              htScoreAway: htScoreAway,
              winner: matchSummary.winner || null,
              iddaaCode: this.safeString(matchSummary.iddaaCode),
            },
          });

          // 5. Save Players (BULK OPTIMIZED)
          const playersArray = Array.from(playersMap.values());
          if (playersArray.length > 0) {
            const existingPlayers = await tx.player.findMany({
              where: {
                id: { in: playersArray.map((p) => p.id) },
              },
              select: { id: true },
            });

            const existingPlayerIds = new Set(existingPlayers.map((p) => p.id));
            const playersToCreate = playersArray.filter(
              (p) => !existingPlayerIds.has(p.id),
            );
            const playersToUpdate = playersArray.filter((p) =>
              existingPlayerIds.has(p.id),
            );

            if (playersToCreate.length > 0) {
              await tx.player.createMany({
                data: playersToCreate.map((p) => ({
                  id: p.id,
                  name: p.name,
                  slug: p.slug,
                })),
                skipDuplicates: true,
              });
            }

            if (playersToUpdate.length > 0) {
              await Promise.all(
                playersToUpdate.map((p) =>
                  tx.player.update({
                    where: { id: p.id },
                    data: { name: p.name },
                  }),
                ),
              );
            }
          }

          // 6. Save Participation
          if (participationData.length > 0) {
            await tx.matchPlayerParticipation.deleteMany({
              where: { matchId: matchId },
            });

            await tx.matchPlayerParticipation.createMany({
              data: participationData.map((p) => ({
                matchId: p.matchId,
                playerId: p.playerId,
                teamId: p.teamId,
                position: this.mapPositionToEnum(p.position),
                shirtNumber: p.shirtNumber,
                isStarting: p.isStarting,
              })),
              skipDuplicates: true,
            });
          }

          // 7. Save Events
          if (eventData.length > 0) {
            await tx.matchPlayerEvents.deleteMany({
              where: { matchId: matchId },
            });

            await tx.matchPlayerEvents.createMany({
              data: eventData.map((e) => ({
                matchId: e.match_id,
                playerId: e.player_id,
                teamId: e.team_id,
                eventType: e.event_type,
                eventSubtype: e.event_subtype,
                timeMinute: e.time_minute,
                timeSeconds: e.time_seconds,
                periodId: e.period_id,
                assistPlayerId: e.assist_player_id,
                scoreAfter: e.score_after,
                playerOutId: e.player_out_id,
                position: e.position,
              })),
              skipDuplicates: true,
            });
          }

          // 8. Save Team Stats (Football)
          if (stats && sport === "football") {
            const statsRows = [
              {
                matchId,
                teamId: homeTeamId,
                possessionPercentage: stats.home.possesionPercentage,
                shotsOnTarget: stats.home.shotsOnTarget,
                shotsOffTarget: stats.home.shotsOffTarget,
                totalShots:
                  (stats.home.shotsOnTarget || 0) +
                    (stats.home.shotsOffTarget || 0) || null,
                totalPasses: stats.home.totalPasses,
                corners: stats.home.corners,
                fouls: stats.home.fouls,
                offsides: stats.home.offsides,
              },
              {
                matchId,
                teamId: awayTeamId,
                possessionPercentage: stats.away.possesionPercentage,
                shotsOnTarget: stats.away.shotsOnTarget,
                shotsOffTarget: stats.away.shotsOffTarget,
                totalShots:
                  (stats.away.shotsOnTarget || 0) +
                    (stats.away.shotsOffTarget || 0) || null,
                totalPasses: stats.away.totalPasses,
                corners: stats.away.corners,
                fouls: stats.away.fouls,
                offsides: stats.away.offsides,
              },
            ];

            for (const row of statsRows) {
              await tx.footballTeamStats.upsert({
                where: {
                  matchId_teamId: { matchId: row.matchId, teamId: row.teamId },
                },
                update: row,
                create: row,
              });
            }
          }

          // 8b. Save Team Stats (Basketball)
          if (basketballTeamStats && sport === "basketball") {
            const teams = [
              { id: homeTeamId, data: basketballTeamStats.home },
              { id: awayTeamId, data: basketballTeamStats.away },
            ];

            for (const t of teams) {
              if (!t.data) continue;
              await tx.basketballTeamStats.upsert({
                where: {
                  matchId_teamId: { matchId, teamId: t.id },
                },
                update: {
                  points: t.data.points,
                  rebounds: t.data.rebounds,
                  assists: t.data.assists,
                  fgMade: t.data.fgMade,
                  fgAttempted: t.data.fgAttempted,
                  threePtMade: t.data.threePtMade,
                  threePtAttempted: t.data.threePtAttempted,
                  ftMade: t.data.ftMade,
                  ftAttempted: t.data.ftAttempted,
                  steals: t.data.steals,
                  blocks: t.data.blocks,
                  turnovers: t.data.turnovers,
                  fouls: t.data.fouls,
                  q1Score: t.data.q1,
                  q2Score: t.data.q2,
                  q3Score: t.data.q3,
                  q4Score: t.data.q4,
                  otScore: t.data.ot,
                },
                create: {
                  matchId,
                  teamId: t.id,
                  points: t.data.points,
                  rebounds: t.data.rebounds,
                  assists: t.data.assists,
                  fgMade: t.data.fgMade,
                  fgAttempted: t.data.fgAttempted,
                  threePtMade: t.data.threePtMade,
                  threePtAttempted: t.data.threePtAttempted,
                  ftMade: t.data.ftMade,
                  ftAttempted: t.data.ftAttempted,
                  steals: t.data.steals,
                  blocks: t.data.blocks,
                  turnovers: t.data.turnovers,
                  fouls: t.data.fouls,
                  q1Score: t.data.q1,
                  q2Score: t.data.q2,
                  q3Score: t.data.q3,
                  q4Score: t.data.q4,
                  otScore: t.data.ot,
                },
              });
            }
          }

          // 8c. Save Player Stats (Basketball)
          if (basketballPlayerStats.length > 0 && sport === "basketball") {
            await tx.basketballPlayerStats.deleteMany({ where: { matchId } });

            for (const p of basketballPlayerStats) {
              if (!p.id || !p.teamId) continue;

              await tx.basketballPlayerStats.create({
                data: {
                  matchId,
                  playerId: p.id,
                  teamId: p.teamId,
                  minutes: p.minutes,
                  points: p.points,
                  rebounds: p.rebounds,
                  assists: p.assists,
                  fgMade: p.fgMade,
                  fgAttempted: p.fgAttempted,
                  threePtMade: p.threePtMade,
                  threePtAttempted: p.threePtAttempted,
                  ftMade: p.ftMade,
                  ftAttempted: p.ftAttempted,
                  steals: p.steals,
                  blocks: p.blocks,
                  turnovers: p.turnovers,
                  fouls: p.fouls,
                },
              });
            }
          }

          // 9. Save Odds (USING HELPER)
          await this.saveOddsInTransaction(tx, matchId, oddsArray);

          // 10. Save Officials
          if (sport === "football" && officialsData.length > 0) {
            await tx.matchOfficial.deleteMany({ where: { matchId } });
            const processedOfficials = new Set<string>();

            for (const o of officialsData) {
              const roleName = o.role || "Referee";
              const uniqueKey = `${o.name}_${roleName}`;

              if (processedOfficials.has(uniqueKey)) continue;
              processedOfficials.add(uniqueKey);

              const role = await tx.officialRole.upsert({
                where: { name: roleName },
                update: {},
                create: { name: roleName },
              });

              await tx.matchOfficial.create({
                data: {
                  matchId,
                  name: o.name,
                  roleId: role.id,
                },
              });
            }
          }
        },
        { maxWait: 40000, timeout: 40000 },
      );

      this.logger.log(`✅ SAVED: [${matchId}] ${matchSummary.matchName}`);
      return true;
    } catch (error: any) {
      this.logger.error(`❌ SAVE FAILED [${matchId}]: ${error.message}`);
      return false;
    }
  }

  // ============================================
  // SELECTIVE UPDATE: LINEUPS ONLY
  // ============================================
  async saveLineups(
    matchId: string,
    playersMap: Map<string, TransformedPlayer>,
    participationData: MatchParticipation[],
    homeTeamId: string,
    awayTeamId: string,
  ): Promise<boolean> {
    try {
      await this.prisma.$transaction(
        async (tx) => {
          const matchInMainDb = await tx.match.findUnique({
            where: { id: matchId },
            select: { id: true },
          });

          if (matchInMainDb) {
            const playersArray = Array.from(playersMap.values());
            if (playersArray.length > 0) {
              const existingPlayers = await tx.player.findMany({
                where: {
                  id: { in: playersArray.map((p) => p.id) },
                },
                select: { id: true },
              });

              const existingPlayerIds = new Set(
                existingPlayers.map((p) => p.id),
              );
              const playersToCreate = playersArray.filter(
                (p) => !existingPlayerIds.has(p.id),
              );

              if (playersToCreate.length > 0) {
                await tx.player.createMany({
                  data: playersToCreate.map((p) => ({
                    id: p.id,
                    name: p.name,
                    slug: p.slug,
                  })),
                  skipDuplicates: true,
                });
              }
            }

            if (participationData.length > 0) {
              await tx.matchPlayerParticipation.deleteMany({
                where: { matchId: matchId },
              });

              await tx.matchPlayerParticipation.createMany({
                data: participationData.map((p) => ({
                  matchId: p.matchId,
                  playerId: p.playerId,
                  teamId: p.teamId,
                  position: this.mapPositionToEnum(p.position),
                  shirtNumber: p.shirtNumber,
                  isStarting: p.isStarting,
                })),
                skipDuplicates: true,
              });
            }
          }
        },
        { maxWait: 15000, timeout: 15000 },
      );

      this.logger.log(`✅ LINEUPS REFRESHED & SYNCED: [${matchId}]`);
      return true;
    } catch (error: any) {
      this.logger.error(`❌ LINEUP SAVE FAILED [${matchId}]: ${error.message}`);
      return false;
    }
  }

  // ============================================
  // SELECTIVE UPDATE: ODDS ONLY (HISTORY-AWARE)
  // ============================================
  async saveOdds(
    matchId: string,
    oddsArray: DbMarketPayload[],
  ): Promise<boolean> {
    try {
      await this.prisma.$transaction(
        async (tx) => {
          // 1. MAIN DB LOGIC
          const matchInMainDb = await tx.match.findUnique({
            where: { id: matchId },
            select: { id: true },
          });

          if (matchInMainDb && oddsArray.length > 0) {
            await this.saveOddsInTransaction(tx, matchId, oddsArray);
          }

          // 2. LIVE MATCH DB LOGIC
          const liveMatch = await tx.liveMatch.findUnique({
            where: { id: matchId },
            select: { id: true },
          });

          if (liveMatch && oddsArray.length > 0) {
            const oddsJson: Record<string, Record<string, number>> = {};
            for (const m of oddsArray) {
              oddsJson[m.name] = {};
              for (const s of m.selectionCollection) {
                const val = parseFloat(s.odd);
                if (!isNaN(val)) oddsJson[m.name][s.name] = val;
              }
            }

            await tx.liveMatch.update({
              where: { id: matchId },
              data: {
                odds: oddsJson as any,
                oddsUpdatedAt: new Date(),
              },
            });
          }
        },
        { maxWait: 15000, timeout: 15000 },
      );

      this.logger.log(`✅ ODDS REFRESHED: [${matchId}]`);
      return true;
    } catch (error: any) {
      this.logger.error(`❌ ODDS SAVE FAILED [${matchId}]: ${error.message}`);
      return false;
    }
  }

  // ============================================
  // FULL DATA FETCH FOR AI
  // ============================================
  async getMatchFullDetails(matchId: string) {
    const match = await this.prisma.match.findUnique({
      where: { id: matchId },
      include: {
        homeTeam: true,
        awayTeam: true,
        league: true,
        oddCategories: {
          include: { selections: true },
        },
        playerParticipations: {
          select: { playerId: true, teamId: true, isStarting: true },
        },
      },
    });

    if (!match) return null;

    const homeLineup = match.playerParticipations
      .filter((p) => p.teamId === match.homeTeamId)
      .map((p) => p.playerId);
    const awayLineup = match.playerParticipations
      .filter((p) => p.teamId === match.awayTeamId)
      .map((p) => p.playerId);

    const getForm = async (teamId: string) => {
      const history = await this.prisma.match.findMany({
        where: {
          OR: [{ homeTeamId: teamId }, { awayTeamId: teamId }],
          status: "FT",
          mstUtc: { lt: match.mstUtc },
        },
        orderBy: { mstUtc: "desc" },
        take: 5,
      });

      if (history.length === 0) return { avg_gf: 1.2, avg_ga: 1.2 };

      let totalGF = 0;
      let totalGA = 0;
      for (const m of history) {
        if (m.homeTeamId === teamId) {
          totalGF += m.scoreHome ?? 0;
          totalGA += m.scoreAway ?? 0;
        } else {
          totalGF += m.scoreAway ?? 0;
          totalGA += m.scoreHome ?? 0;
        }
      }
      return {
        avg_gf: totalGF / history.length,
        avg_ga: totalGA / history.length,
      };
    };

    const homeForm = await getForm(match.homeTeamId!);
    const awayForm = await getForm(match.awayTeamId!);

    const odds: any[] = [];
    for (const cat of match.oddCategories) {
      for (const sel of cat.selections) {
        odds.push({
          category: cat.name,
          selection: sel.name,
          odd_value: this.safeFloat(sel.oddValue),
        });
      }
    }

    return {
      match_id: match.id,
      home_team: match.homeTeam?.name || "Unknown",
      away_team: match.awayTeam?.name || "Unknown",
      home_team_id: match.homeTeamId,
      away_team_id: match.awayTeamId,
      league_id: match.leagueId,
      league_name: match.league?.name,
      date: match.mstUtc.toString(),
      score_home: match.scoreHome,
      score_away: match.scoreAway,
      status: match.status,
      odds: odds,
      home_form: homeForm,
      away_form: awayForm,
      home_lineup: homeLineup,
      away_lineup: awayLineup,
    };
  }

  // ============================================
  // CHECKERS
  // ============================================
  async matchExists(matchId: string): Promise<boolean> {
    const match = await this.prisma.match.findUnique({
      where: { id: matchId },
      select: { id: true },
    });
    return !!match;
  }

  async getExistingMatchIds(matchIds: string[]): Promise<string[]> {
    if (matchIds.length === 0) return [];

    // Use raw SQL for performance — Prisma's { some: {} } relation filters
    // generate heavy correlated subqueries that hang on Raspberry Pi with
    // large tables (15M+ odd_selections, 3M+ participations).
    const result = await this.prisma.$queryRaw<Array<{ id: string }>>(
      Prisma.sql`
      SELECT m.id
      FROM matches m
      WHERE m.id = ANY(${matchIds}::text[])
        AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
        AND (
          (m.sport = 'football'
           AND EXISTS (SELECT 1 FROM football_team_stats fts WHERE fts.match_id = m.id)
           AND (SELECT count(*) FROM match_player_participation mpp
                WHERE mpp.match_id = m.id AND mpp.is_starting = true) >= 18)
          OR
          (m.sport = 'basketball'
           AND EXISTS (SELECT 1 FROM basketball_team_stats bts WHERE bts.match_id = m.id)
           AND EXISTS (SELECT 1 FROM basketball_player_stats bps WHERE bps.match_id = m.id))
        )
      `,
    );

    return result.map((r) => r.id);
  }

  /**
   * For a list of match IDs that ALREADY exist in DB,
   * returns which data scopes are missing per match.
   * Only checks completed (Ended) football/basketball matches.
   */
  async getMissingScopes(matchIds: string[]): Promise<Map<string, string[]>> {
    const result = new Map<string, string[]>();
    if (matchIds.length === 0) return result;

    // Use raw SQL for performance on Raspberry Pi.
    // Note: state is 'postGame' in DB, not 'Ended'.
    const rows = await this.prisma.$queryRawUnsafe<
      Array<{
        id: string;
        sport: string;
        fts_count: bigint;
        pp_count: bigint;
        bts_count: bigint;
        bps_count: bigint;
        oc_count: bigint;
      }>
    >(
      `
      SELECT m.id, m.sport::text,
        (SELECT count(*) FROM football_team_stats fts WHERE fts.match_id = m.id) as fts_count,
        (SELECT count(*) FROM match_player_participation mpp WHERE mpp.match_id = m.id) as pp_count,
        (SELECT count(*) FROM basketball_team_stats bts WHERE bts.match_id = m.id) as bts_count,
        (SELECT count(*) FROM basketball_player_stats bps WHERE bps.match_id = m.id) as bps_count,
        (SELECT count(*) FROM odd_categories oc WHERE oc.match_id = m.id) as oc_count
      FROM matches m
      WHERE m.id = ANY($1::text[])
        AND m.state = 'postGame'
      `,
      matchIds,
    );

    for (const m of rows) {
      const missing: string[] = [];

      if (m.sport === "football") {
        if (Number(m.fts_count) === 0) missing.push("stats");
        if (Number(m.pp_count) < 18) missing.push("lineups");
      } else if (m.sport === "basketball") {
        if (Number(m.bts_count) === 0) missing.push("stats");
        if (Number(m.bps_count) === 0) missing.push("lineups");
      }

      if (Number(m.oc_count) === 0) missing.push("odds");

      if (missing.length > 0) {
        result.set(m.id, missing);
      }
    }

    return result;
  }

  async hasOdds(matchId: string): Promise<boolean> {
    const category = await this.prisma.oddCategory.findFirst({
      where: { matchId },
    });
    if (category) return true;

    const live = await this.prisma.liveMatch.findUnique({
      where: { id: matchId },
      select: { odds: true },
    });
    return !!(live?.odds && Object.keys(live.odds as any).length > 0);
  }

  async getMatch(matchId: string): Promise<any | null> {
    const match = await this.prisma.match.findUnique({
      where: { id: matchId },
      include: {
        homeTeam: true,
        awayTeam: true,
      },
    });

    if (match) return match;

    const liveMatch = await this.prisma.liveMatch.findUnique({
      where: { id: matchId },
      include: {
        homeTeam: true,
        awayTeam: true,
        league: true,
      },
    });

    if (liveMatch) {
      return {
        ...liveMatch,
        leagueId: liveMatch.leagueId,
        homeTeamId: liveMatch.homeTeamId,
        awayTeamId: liveMatch.awayTeamId,
        scoreHome: liveMatch.scoreHome,
        scoreAway: liveMatch.scoreAway,
        mstUtc: liveMatch.mstUtc,
        sport: liveMatch.sport || "football",
      };
    }

    return null;
  }

  async getPlayerCount(matchId: string): Promise<number> {
    const relationalCount = await this.prisma.matchPlayerParticipation.count({
      where: { matchId },
    });

    if (relationalCount > 0) return relationalCount;

    const liveMatch = await this.prisma.liveMatch.findUnique({
      where: { id: matchId },
      select: { lineups: true },
    });

    if (liveMatch?.lineups) {
      try {
        const lineups = liveMatch.lineups as any;
        const homeXi = lineups.home?.xi?.length || 0;
        const awayXi = lineups.away?.xi?.length || 0;
        return homeXi + awayXi;
      } catch (e) {
        return 0;
      }
    }

    return 0;
  }

  // ============================================
  // STATE MANAGEMENT
  // ============================================
  async getState(key: string): Promise<string | null> {
    const setting = await this.prisma.appSetting.findUnique({
      where: { key },
    });
    return setting?.value || null;
  }

  async setState(key: string, value: string): Promise<void> {
    await this.prisma.appSetting.upsert({
      where: { key },
      update: { value, updatedAt: new Date() },
      create: { key, value },
    });
  }
}
