import { Injectable, Logger } from "@nestjs/common";
import * as fs from "fs";
import * as path from "path";
import { PrismaService } from "../../database/prisma.service";
import {
  Sport,
  MatchQueryDto,
  LeagueWithMatchesDto,
  ActiveLeagueDto,
} from "./dto";
import { Prisma } from "@prisma/client";
import {
  FINISHED_STATE_VALUES_FOR_DB,
  FINISHED_STATUS_VALUES_FOR_DB,
  LIVE_STATE_VALUES_FOR_DB,
  LIVE_STATUS_VALUES_FOR_DB,
  getDisplayMatchStatus,
} from "../../common/utils/match-status.util";
import {
  countryFlagUrl,
  teamLogoUrl,
} from "../../common/utils/image-url.util";

@Injectable()
export class MatchesService {
  private readonly logger = new Logger(MatchesService.name);
  private qualifiedLeagueIds: string[] = [];
  private topLeagueIds: string[] = [];

  constructor(private readonly prisma: PrismaService) {
    this.loadQualifiedLeagues();
    this.loadTopLeagues();
  }

  /**
   * Per-match odds movement (opening→closing) from live_odds_history.
   * Returns { [market]: { [selection]: { open, close } } } with the same
   * Turkish market/selection labels used in match.odds, so the UI can line
   * them up directly. Returns {} if there is no data or the table is absent.
   */
  async getOddsMovement(
    matchId: string,
  ): Promise<Record<string, Record<string, { open: number; close: number }>>> {
    try {
      const rows = await this.prisma.$queryRawUnsafe<
        Array<{
          market: string;
          selection: string;
          open: number | null;
          close: number | null;
        }>
      >(
        `SELECT market, selection,
                (array_agg(new_value ORDER BY change_time ASC))[1]  AS open,
                (array_agg(new_value ORDER BY change_time DESC))[1] AS close
         FROM live_odds_history
         WHERE match_id = $1
         GROUP BY market, selection`,
        matchId,
      );
      const out: Record<
        string,
        Record<string, { open: number; close: number }>
      > = {};
      for (const r of rows) {
        if (r.open == null || r.close == null) continue;
        (out[r.market] ??= {})[r.selection] = {
          open: Number(r.open),
          close: Number(r.close),
        };
      }
      return out;
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      this.logger.warn(`getOddsMovement failed for ${matchId}: ${msg}`);
      return {};
    }
  }

  private loadTopLeagues() {
    try {
      const filePath = path.join(process.cwd(), "top_leagues.json");
      if (fs.existsSync(filePath)) {
        this.topLeagueIds = JSON.parse(fs.readFileSync(filePath, "utf8"));
      }
    } catch (e) {
      this.logger.warn(`Failed to load top_leagues.json: ${e.message}`);
    }
  }

  private loadQualifiedLeagues() {
    try {
      const filePath = path.join(process.cwd(), "qualified_leagues.json");
      if (fs.existsSync(filePath)) {
        this.qualifiedLeagueIds = JSON.parse(fs.readFileSync(filePath, "utf8"));
        this.logger.log(
          `Loaded ${this.qualifiedLeagueIds.length} qualified leagues for filtering.`,
        );
      }
    } catch (e) {
      this.logger.warn(`Failed to load qualified_leagues.json: ${e.message}`);
    }
  }

  private getCountryFlagUrl(countryId?: string | null): string | undefined {
    return countryFlagUrl(countryId);
  }

  private getTeamLogoUrl(teamId?: string | null): string | undefined {
    return teamLogoUrl(teamId);
  }

  private getLiveFilter(): Prisma.LiveMatchWhereInput {
    return {
      OR: [
        {
          status: {
            in: LIVE_STATUS_VALUES_FOR_DB,
          },
        },
        {
          state: {
            in: LIVE_STATE_VALUES_FOR_DB,
          },
        },
      ],
    };
  }

  private getFinishedFilter(): Prisma.LiveMatchWhereInput {
    return {
      OR: [
        {
          status: {
            in: FINISHED_STATUS_VALUES_FOR_DB,
          },
        },
        {
          state: {
            in: FINISHED_STATE_VALUES_FOR_DB,
          },
        },
        {
          AND: [
            { scoreHome: { not: null } },
            { scoreAway: { not: null } },
            {
              NOT: this.getLiveFilter(),
            },
          ],
        },
      ],
    };
  }

  private getUpcomingFilter(
    fromTimestampMs: number,
  ): Prisma.LiveMatchWhereInput {
    return {
      AND: [
        {
          mstUtc: {
            gte: BigInt(fromTimestampMs),
          },
        },
        {
          NOT: {
            OR: [this.getLiveFilter(), this.getFinishedFilter()],
          },
        },
      ],
    };
  }

  private getBrowseFilter(fromTimestampMs: number): Prisma.LiveMatchWhereInput {
    return {
      AND: [
        {
          mstUtc: {
            gte: BigInt(fromTimestampMs),
          },
        },
        {
          NOT: this.getFinishedFilter(),
        },
      ],
    };
  }

  /**
   * Find matches by query criteria
   */
  async findMatches(options: MatchQueryDto): Promise<string[]> {
    const {
      sport,
      limit = 50,
      leagueId,
      status,
      date,
      team,
      dateRange,
    } = options;

    // Build where conditions
    const where: Prisma.LiveMatchWhereInput = {
      sport: sport as any,
    };
    const andConditions: Prisma.LiveMatchWhereInput[] = [];

    if (leagueId) {
      where.leagueId = leagueId;
    } else if (this.qualifiedLeagueIds.length > 0) {
      // Only show matches from qualified leagues (leagues with historical data for AI analysis)
      where.leagueId = { in: this.qualifiedLeagueIds };
    }

    if (status === "LIVE") {
      andConditions.push(this.getLiveFilter());
    } else if (status === "UPCOMING" || status === "NOT_STARTED") {
      andConditions.push(this.getUpcomingFilter(Date.now()));
    } else if (status === "FINISHED") {
      andConditions.push(this.getFinishedFilter());
    } else if (status) {
      where.status = status;
    }

    // Date filter
    if (date) {
      const d = new Date(date);
      const startOfDay = new Date(d);
      startOfDay.setUTCHours(0, 0, 0, 0);
      const endOfDay = new Date(d);
      endOfDay.setUTCHours(23, 59, 59, 999);

      where.mstUtc = {
        gte: BigInt(startOfDay.getTime()),
        lte: BigInt(endOfDay.getTime()),
      };
    } else if (dateRange) {
      where.mstUtc = {
        gte: BigInt(new Date(dateRange.from).getTime()),
        lte: BigInt(new Date(dateRange.to).getTime()),
      };
    }

    // Team filter
    if (team) {
      if (team.role === "home") {
        where.homeTeamId = team.id;
      } else if (team.role === "away") {
        where.awayTeamId = team.id;
      } else {
        andConditions.push({
          OR: [{ homeTeamId: team.id }, { awayTeamId: team.id }],
        });
      }
    }

    // Default date filter: From today onwards if no specific filter
    if (!date && !dateRange && !status) {
      const today = new Date();
      today.setUTCHours(0, 0, 0, 0); // Start of today in UTC

      andConditions.push(this.getBrowseFilter(today.getTime()));
    }

    if (andConditions.length > 0) {
      where.AND = andConditions;
    }

    // Switch to live_matches table
    const matches = await this.prisma.liveMatch.findMany({
      where,
      select: { id: true },
      orderBy: { mstUtc: "asc" }, // Sort by nearest match first
      take: limit,
    });

    return matches.map((m) => m.id);
  }

  /**
   * Find upcoming matches from the live matches table
   * Used for Coupon Generator when no specific matches are selected
   */
  async findUpcomingMatches(
    sport: Sport,
    limit: number = 50,
  ): Promise<string[]> {
    console.log(`[MatchesService] Finding upcoming matches for ${sport}`);

    const matches = await this.prisma.liveMatch.findMany({
      where: {
        sport: sport as any,
        AND: [this.getUpcomingFilter(Date.now())],
      },
      select: { id: true },
      orderBy: { mstUtc: "asc" },
      take: limit,
    });
    console.log(
      `[MatchesService] Found ${matches.length} upcoming matches from live_matches`,
    );

    return matches.map((m) => m.id);
  }

  async filterUpcomingMatchIds(
    matchIds: string[],
    sport: Sport,
  ): Promise<string[]> {
    const uniqueIds = [...new Set(matchIds.filter((id) => !!id))];

    if (uniqueIds.length === 0) {
      return [];
    }

    const matches = await this.prisma.liveMatch.findMany({
      where: {
        id: { in: uniqueIds },
        sport: sport as any,
        AND: [this.getUpcomingFilter(Date.now())],
      },
      select: { id: true },
    });

    return matches.map((match) => match.id);
  }

  /**
   * Get matches structured by league (from live_matches table)
   */
  async getMatchesAndStructureByIds(
    matchIds: string[],
    sport: Sport,
  ): Promise<LeagueWithMatchesDto[]> {
    if (!matchIds.length) return [];

    const matches = await this.prisma.liveMatch.findMany({
      where: { id: { in: matchIds } },
      include: {
        league: {
          include: {
            country: true,
          },
        },
        homeTeam: true,
        awayTeam: true,
      },
    });

    // Sort matches by time (ASC) before grouping to ensure correct order
    matches.sort((a, b) =>
      Number(BigInt(a.mstUtc || 0) - BigInt(b.mstUtc || 0)),
    );

    // Group by league
    const leaguesMap = new Map<string, LeagueWithMatchesDto>();

    for (const match of matches) {
      const leagueId = match.leagueId || "unknown";

      if (!leaguesMap.has(leagueId)) {
        const entry: any = {
          id: leagueId,
          name: match.league?.name || "Unknown League",
          code: match.league?.code || undefined,
          _league: match.league, // for sortOrder access
          country: {
            id: match.league?.country?.id || "",
            name: match.league?.country?.name || "",
            flagUrl:
              match.league?.country?.flagUrl ||
              this.getCountryFlagUrl(match.league?.country?.id),
          },
          sport: sport,
          matches: [],
        };
        leaguesMap.set(leagueId, entry);
      }

      const league = leaguesMap.get(leagueId)!;

      // Structure odds from JSON
      const structuredOdds: any[] = [];
      if (
        match.odds &&
        typeof match.odds === "object" &&
        !Array.isArray(match.odds)
      ) {
        const oddsObj = match.odds as Record<string, Record<string, number>>;
        for (const [marketName, selections] of Object.entries(oddsObj)) {
          const structuredSelections: Record<string, { odd: string }> = {};
          if (selections && typeof selections === "object") {
            for (const [selName, selOdd] of Object.entries(selections)) {
              structuredSelections[selName] = { odd: String(selOdd) };
            }
            structuredOdds.push({
              category_name: marketName,
              selections: structuredSelections,
            });
          }
        }
      }

      // Map status for frontend
      const displayStatus = getDisplayMatchStatus({
        state: match.state,
        status: match.status,
        substate: match.substate,
        scoreHome: match.scoreHome,
        scoreAway: match.scoreAway,
      });

      league.matches.push({
        id: match.id,
        matchName:
          match.matchName ||
          `${match.homeTeam?.name} vs ${match.awayTeam?.name}`,
        matchSlug: match.matchSlug || undefined,
        mstUtc: Number(match.mstUtc),
        status: displayStatus,
        state: match.state || undefined,
        scoreHome: match.scoreHome ?? undefined,
        scoreAway: match.scoreAway ?? undefined,
        htScoreHome: undefined, // LiveMatch table doesn't have HT scores separately usually
        htScoreAway: undefined,
        homeTeamName: match.homeTeam?.name || "Unknown",
        homeTeamLogo: match.homeTeamId
          ? this.getTeamLogoUrl(match.homeTeamId)
          : undefined,
        awayTeamName: match.awayTeam?.name || "Unknown",
        awayTeamLogo: match.awayTeamId
          ? this.getTeamLogoUrl(match.awayTeamId)
          : undefined,
        leagueName: match.league?.name,
        countryName: match.league?.country?.name,
        odds: structuredOdds,
      });
    }

    return Array.from(leaguesMap.values()).sort((a, b) => {
      // 1. top_leagues.json sırası (sabit öncelik listesi)
      const aTop = this.topLeagueIds.indexOf(a.id);
      const bTop = this.topLeagueIds.indexOf(b.id);
      const aTopPriority = aTop === -1 ? 999 : aTop;
      const bTopPriority = bTop === -1 ? 999 : bTop;
      if (aTopPriority !== bTopPriority) return aTopPriority - bTopPriority;

      // 2. Mackolik'ten gelen sortOrder (feeder her gün güncelliyor)
      const leagueA = (a as any)._league as any;
      const leagueB = (b as any)._league as any;
      const aSortOrder = leagueA?.sortOrder ?? 9999;
      const bSortOrder = leagueB?.sortOrder ?? 9999;
      if (aSortOrder !== bSortOrder) return aSortOrder - bSortOrder;

      // 3. Alfabetik fallback
      return (a.name || "").localeCompare(b.name || "");
    });
  }

  /**
   * Get active leagues with match counts
   */
  async getActiveLeagues(sport: Sport): Promise<ActiveLeagueDto[]> {
    // Start of today in UTC — same reference point as findMatches browse filter
    const today = new Date();
    today.setUTCHours(0, 0, 0, 0);
    const todayMs = BigInt(today.getTime());

    // Build finished statuses/states for exclusion (mirroring getBrowseFilter logic)
    const finishedStatuses = FINISHED_STATUS_VALUES_FOR_DB;
    const finishedStates = FINISHED_STATE_VALUES_FOR_DB;
    const liveStatuses = LIVE_STATUS_VALUES_FOR_DB;
    const liveStates = LIVE_STATE_VALUES_FOR_DB;

    // Use raw query for complex aggregation
    // Filter: (mstUtc >= today AND NOT finished) OR is currently live
    const leagues = await this.prisma.$queryRaw<any[]>`
      SELECT 
        l.id, l.name, l.code, 
        c.id as country_id,
        c.name as country_name, 
        c.flag_url as country_flag,
        COUNT(lm.id)::int as match_count,
        COUNT(CASE WHEN lm.status IN (${Prisma.join(liveStatuses)})
                   OR lm.state IN (${Prisma.join(liveStates)}) THEN 1 END)::int as live_count
      FROM live_matches lm
      JOIN leagues l ON lm.league_id = l.id
      LEFT JOIN countries c ON l.country_id = c.id
      WHERE lm.sport = ${sport}
      ${this.qualifiedLeagueIds.length > 0 ? Prisma.sql`AND l.id IN (${Prisma.join(this.qualifiedLeagueIds)})` : Prisma.empty}
      AND (
        (lm.mst_utc >= ${todayMs} AND lm.status NOT IN (${Prisma.join(finishedStatuses)}) AND COALESCE(lm.state, '') NOT IN (${Prisma.join(finishedStates)}))
        OR lm.status IN (${Prisma.join(liveStatuses)})
        OR lm.state IN (${Prisma.join(liveStates)})
      )
      GROUP BY l.id, l.name, l.code, c.id, c.name, c.flag_url
      ORDER BY l.name ASC
    `;

    return leagues
      .filter((l) => l.match_count > 0)
      .sort((a, b) => {
        const aIdx = this.topLeagueIds.indexOf(a.id);
        const bIdx = this.topLeagueIds.indexOf(b.id);

        const aPriority = aIdx === -1 ? 999 : aIdx;
        const bPriority = bIdx === -1 ? 999 : bIdx;

        if (aPriority !== bPriority) return aPriority - bPriority;
        return (a.name || "").localeCompare(b.name || "");
      })
      .map((l) => ({
        id: l.id,
        name: l.name,
        code: l.code,
        countryName: l.country_name,
        countryFlag: l.country_flag || this.getCountryFlagUrl(l.country_id),
        matchCount: l.match_count,
        liveCount: l.live_count,
      }));
  }

  /**
   * List matches with pagination
   */
  async listMatches(sport: Sport, page: number = 1, limit: number = 20) {
    const skip = (page - 1) * limit;

    const [matches, total] = await Promise.all([
      this.prisma.match.findMany({
        where: { sport: sport as any },
        include: {
          homeTeam: true,
          awayTeam: true,
          league: {
            include: { country: true },
          },
        },
        orderBy: { mstUtc: "desc" },
        skip,
        take: limit,
      }),
      this.prisma.match.count({ where: { sport: sport as any } }),
    ]);

    return {
      matches: matches.map((m) => ({
        id: m.id,
        matchName: m.matchName,
        matchSlug: m.matchSlug,
        mstUtc: Number(m.mstUtc),
        scoreHome: m.scoreHome,
        scoreAway: m.scoreAway,
        status: m.status,
        homeTeamName: m.homeTeam?.name,
        homeTeamLogo: m.homeTeamId ? this.getTeamLogoUrl(m.homeTeamId) : null,
        awayTeamName: m.awayTeam?.name,
        awayTeamLogo: m.awayTeamId ? this.getTeamLogoUrl(m.awayTeamId) : null,
        leagueName: m.league?.name,
        countryName: m.league?.country?.name,
      })),
      total,
      page,
      totalPages: Math.ceil(total / limit),
    };
  }

  private normalizeTeamStat(stat: any, sport?: string) {
    if (!stat) return null;

    const base = {
      id: stat.id,
      matchId: stat.matchId,
      teamId: stat.teamId,
      createdAt: stat.createdAt,
    };

    if ((sport || "").toLowerCase() === "basketball") {
      return {
        ...base,
        points: stat.points,
        rebounds: stat.rebounds,
        assists: stat.assists,
        fgMade: stat.fgMade,
        fgAttempted: stat.fgAttempted,
        threePtMade: stat.threePtMade,
        threePtAttempted: stat.threePtAttempted,
        ftMade: stat.ftMade,
        ftAttempted: stat.ftAttempted,
        steals: stat.steals,
        blocks: stat.blocks,
        turnovers: stat.turnovers,
        q1Score: stat.q1Score,
        q2Score: stat.q2Score,
        q3Score: stat.q3Score,
        q4Score: stat.q4Score,
        otScore: stat.otScore,
      };
    }

    return {
      ...base,
      possessionPercentage: stat.possessionPercentage,
      shotsOnTarget: stat.shotsOnTarget,
      shotsOffTarget: stat.shotsOffTarget,
      totalShots: stat.totalShots,
      totalPasses: stat.totalPasses,
      corners: stat.corners,
      fouls: stat.fouls,
      offsides: stat.offsides,
    };
  }

  /**
   * Get full match details by ID
   */
  async getMatchDetailsById(matchId: string) {
    let match: any = await this.prisma.match.findUnique({
      where: { id: matchId },
      include: {
        league: { include: { country: true } },
        homeTeam: true,
        awayTeam: true,
        footballTeamStats: true,
        basketballTeamStats: true,
        playerParticipations: {
          include: { player: true },
          orderBy: [{ isStarting: "desc" }, { position: "asc" }],
        },
        playerEvents: {
          include: {
            player: true,
            assistPlayer: true,
            substitutedOut: true,
          },
          orderBy: [{ periodId: "asc" }, { timeMinute: "asc" }],
        },
        oddCategories: {
          include: { selections: true },
        },
        officials: true,
      },
    });

    if (!match) {
      // Try to find in LiveMatch table
      const liveMatch = await this.prisma.liveMatch.findUnique({
        where: { id: matchId },
        include: {
          league: { include: { country: true } },
          homeTeam: true,
          awayTeam: true,
        },
      });

      if (liveMatch) {
        // Map liveMatch status
        const displayStatus = getDisplayMatchStatus({
          state: liveMatch.state,
          status: liveMatch.status,
          substate: liveMatch.substate,
          scoreHome: liveMatch.scoreHome,
          scoreAway: liveMatch.scoreAway,
        });

        match = {
          ...liveMatch,
          matchName:
            liveMatch.matchName ||
            `${liveMatch.homeTeam?.name} vs ${liveMatch.awayTeam?.name}`,
          status: displayStatus,
          mstUtc: liveMatch.mstUtc,
          score: {
            home: liveMatch.scoreHome,
            away: liveMatch.scoreAway,
            htHome: (liveMatch as any).htScoreHome ?? null,
            htAway: (liveMatch as any).htScoreAway ?? null,
          },
          date: new Date(Number(liveMatch.mstUtc)),
          // Fill missing relations with empty arrays
          teamStats: [],
          playerParticipations: (() => {
            const parsed: Array<{
              teamId: string;
              isStarting: boolean;
              shirtNumber: string | number | null;
              position: string | null;
              player: { id: string; name: string };
            }> = [];
            const canTrustFeedLineups =
              displayStatus === "LIVE" || displayStatus === "Finished";
            if (!canTrustFeedLineups) {
              return parsed;
            }
            if (liveMatch.lineups && typeof liveMatch.lineups === "object") {
              const lu = liveMatch.lineups as Record<string, any>;
              const addPlayers = (teamLu: any, teamId: string | null) => {
                if (!teamLu || !teamId) return;
                if (teamLu.xi && Array.isArray(teamLu.xi)) {
                  teamLu.xi.forEach((p: any) => {
                    parsed.push({
                      teamId,
                      isStarting: true,
                      shirtNumber: p.shirtNumber || p.number,
                      position: p.position || p.pos,
                      player: {
                        id: p.personId || p.id || p.playerId || "unknown",
                        name:
                          p.matchName || p.name || p.playerName || "Bilinmiyor",
                      },
                    });
                  });
                }
                if (teamLu.subs && Array.isArray(teamLu.subs)) {
                  teamLu.subs.forEach((p: any) => {
                    parsed.push({
                      teamId,
                      isStarting: false,
                      shirtNumber: p.shirtNumber || p.number,
                      position: p.position || p.pos,
                      player: {
                        id: p.personId || p.id || p.playerId || "unknown",
                        name:
                          p.matchName || p.name || p.playerName || "Bilinmiyor",
                      },
                    });
                  });
                }
              };
              addPlayers(lu.home, liveMatch.homeTeamId);
              addPlayers(lu.away, liveMatch.awayTeamId);
            }
            return parsed;
          })(),
          playerEvents: [],
          oddCategories: [], // Will handle odds parsing below
          officials: [],
          isLiveSource: true, // Flag to indicate source
        };
      }
    }

    if (!match) return null;

    const detailDisplayStatus = getDisplayMatchStatus({
      state: match.state,
      status: match.status,
      substate: match.substate,
      scoreHome: match.scoreHome,
      scoreAway: match.scoreAway,
    });
    const canTrustStoredLineups =
      this.canTrustStoredLineups(detailDisplayStatus);

    if (Array.isArray(match.playerParticipations)) {
      if (!canTrustStoredLineups) {
        match.playerParticipations = [];
      }

      const hasHomeLineup = match.playerParticipations.some(
        (p: any) => p.teamId === match.homeTeamId && p.isStarting,
      );
      const hasAwayLineup = match.playerParticipations.some(
        (p: any) => p.teamId === match.awayTeamId && p.isStarting,
      );

      if (!hasHomeLineup || !hasAwayLineup) {
        const sidelined =
          match.sidelined && typeof match.sidelined === "object"
            ? (match.sidelined as Record<string, any>)
            : {};
        const matchDateMs = Number(match.mstUtc || Date.now());
        const probableLineups: any[] = [];

        if (!hasHomeLineup && match.homeTeamId) {
          probableLineups.push(
            ...(await this.buildProbableLineupForTeam({
              teamId: match.homeTeamId,
              beforeDateMs: matchDateMs,
              sidelinedTeamData: sidelined.homeTeam,
            })),
          );
        }

        if (!hasAwayLineup && match.awayTeamId) {
          probableLineups.push(
            ...(await this.buildProbableLineupForTeam({
              teamId: match.awayTeamId,
              beforeDateMs: matchDateMs,
              sidelinedTeamData: sidelined.awayTeam,
            })),
          );
        }

        if (probableLineups.length > 0) {
          match.playerParticipations = canTrustStoredLineups
            ? [...match.playerParticipations, ...probableLineups]
            : probableLineups;
          match.lineupSource = "probable_xi";
        }
      }
    }

    // Structure odds
    const odds: Record<
      string,
      Record<string, { odd: string; sov?: number }>
    > = {};

    if (
      match.isLiveSource &&
      match.odds &&
      typeof match.odds === "object" &&
      !Array.isArray(match.odds)
    ) {
      // Parse JSON odds from LiveMatch
      const oddsObj = match.odds as Record<string, Record<string, number>>;
      for (const [marketName, selections] of Object.entries(oddsObj)) {
        odds[marketName] = {};
        if (selections && typeof selections === "object") {
          for (const [selName, selOdd] of Object.entries(selections)) {
            odds[marketName][selName] = { odd: String(selOdd) };
          }
        }
      }
    } else if (match.oddCategories) {
      // Parse relation odds from Match
      for (const cat of match.oddCategories) {
        if (!cat.name) continue;
        odds[cat.name] = {};
        for (const sel of cat.selections) {
          if (sel.name) {
            odds[cat.name][sel.name] = {
              odd: sel.oddValue || "",
              sov: sel.sov ?? undefined,
            };
          }
        }
      }
    }

    const sportStats =
      match.sport === "basketball"
        ? match.basketballTeamStats || []
        : match.footballTeamStats || [];
    const normalizedTeamStats = sportStats.map((s: any) =>
      this.normalizeTeamStat(s, match.sport),
    );
    const homeStat = sportStats.find((s: any) => s.teamId === match.homeTeamId);
    const awayStat = sportStats.find((s: any) => s.teamId === match.awayTeamId);

    return {
      ...match,
      teamStats: normalizedTeamStats,
      mstUtc: Number(match.mstUtc),
      date: match.date || new Date(Number(match.mstUtc)),
      score: match.score || {
        home: match.scoreHome,
        away: match.scoreAway,
        htHome: match.htScoreHome ?? null,
        htAway: match.htScoreAway ?? null,
      },
      homeTeam: {
        ...match.homeTeam,
        logo: match.homeTeamId
          ? this.getTeamLogoUrl(match.homeTeamId)
          : match.homeTeam?.logoUrl || null,
      },
      awayTeam: {
        ...match.awayTeam,
        logo: match.awayTeamId
          ? this.getTeamLogoUrl(match.awayTeamId)
          : match.awayTeam?.logoUrl || null,
      },
      stats: {
        home: this.normalizeTeamStat(homeStat, match.sport),
        away: this.normalizeTeamStat(awayStat, match.sport),
      },
      lineups: {
        home: match.playerParticipations.filter(
          (p: any) => p.teamId === match.homeTeamId,
        ),
        away: match.playerParticipations.filter(
          (p: any) => p.teamId === match.awayTeamId,
        ),
      },
      events: match.playerEvents,
      odds,
    };
  }

  /**
   * Get team ID by name (for legacy compatibility)
   */
  async getTeamIdByName(
    teamName: string,
    sport: Sport,
  ): Promise<string | null> {
    const trimmedName = teamName.trim();

    // Exact match first
    let team = await this.prisma.team.findFirst({
      where: { name: trimmedName, sport: sport as any },
      select: { id: true },
    });

    if (team) return team.id;

    // Fuzzy search
    team = await this.prisma.team.findFirst({
      where: {
        name: { contains: trimmedName, mode: "insensitive" },
        sport: sport as any,
      },
      select: { id: true },
    });

    return team?.id || null;
  }

  private async buildProbableLineupForTeam(params: {
    teamId: string;
    beforeDateMs: number;
    sidelinedTeamData?: any;
    matchLimit?: number;
    lookbackDays?: number;
    maxStalenessDays?: number;
  }) {
    const matchLimit = params.matchLimit ?? 5;
    const lookbackDays = params.lookbackDays ?? 370;
    const maxStalenessDays = params.maxStalenessDays ?? 120;
    const beforeDateMs = params.beforeDateMs || Date.now();
    const minDateMs = Math.max(
      0,
      beforeDateMs - lookbackDays * 24 * 60 * 60 * 1000,
    );
    const excluded = this.extractSidelinedPlayerIds(params.sidelinedTeamData);

    const rows = await this.prisma.$queryRaw<any[]>`
      SELECT
        mpp.player_id AS "playerId",
        p.name AS "playerName",
        mpp.position AS "position",
        mpp.shirt_number AS "shirtNumber",
        m.id AS "matchId",
        m.mst_utc AS "mstUtc"
      FROM match_player_participation mpp
      JOIN matches m ON m.id = mpp.match_id
      JOIN players p ON p.id = mpp.player_id
      WHERE mpp.team_id = ${params.teamId}
        AND mpp.is_starting = true
        AND NOT EXISTS (
          SELECT 1
          FROM match_player_participation later_mpp
          JOIN matches later_m ON later_m.id = later_mpp.match_id
          WHERE later_mpp.player_id = mpp.player_id
            AND later_mpp.team_id <> ${params.teamId}
            AND later_m.mst_utc > m.mst_utc
            AND later_m.mst_utc < ${BigInt(beforeDateMs)}
            AND (
              later_m.status = 'FT'
              OR later_m.state = 'postGame'
              OR (later_m.score_home IS NOT NULL AND later_m.score_away IS NOT NULL)
            )
        )
        AND m.id IN (
          SELECT m2.id
          FROM matches m2
          JOIN match_player_participation recent_mpp
            ON recent_mpp.match_id = m2.id
           AND recent_mpp.team_id = ${params.teamId}
           AND recent_mpp.is_starting = true
          WHERE (m2.home_team_id = ${params.teamId} OR m2.away_team_id = ${params.teamId})
            AND (
              m2.status = 'FT'
              OR m2.state = 'postGame'
              OR (m2.score_home IS NOT NULL AND m2.score_away IS NOT NULL)
            )
            AND m2.mst_utc < ${BigInt(beforeDateMs)}
            AND m2.mst_utc >= ${BigInt(minDateMs)}
          GROUP BY m2.id
          HAVING COUNT(recent_mpp.*) >= 9
          ORDER BY MAX(m2.mst_utc) DESC
          LIMIT ${matchLimit}
        )
      ORDER BY m.mst_utc DESC
    `;

    if (!rows.length) return [];

    const latestMst = Math.max(...rows.map((row) => Number(row.mstUtc || 0)));
    const ageDays =
      latestMst > 0
        ? (beforeDateMs - latestMst) / (24 * 60 * 60 * 1000)
        : Number.POSITIVE_INFINITY;
    const staleProjection = ageDays > maxStalenessDays;

    const matchOrder = new Map<string, number>();
    for (const row of rows) {
      const matchId = String(row.matchId);
      if (!matchOrder.has(matchId)) {
        matchOrder.set(matchId, matchOrder.size);
      }
    }

    const playerMap = new Map<
      string,
      {
        playerId: string;
        playerName: string;
        position: string | null;
        shirtNumber: number | null;
        score: number;
        starts: number;
        lastSeenRank: number;
      }
    >();

    for (const row of rows) {
      const playerId = String(row.playerId);
      if (excluded.has(playerId)) continue;

      const rank = matchOrder.get(String(row.matchId)) ?? matchLimit;
      const recencyWeight = Math.max(1, matchLimit - rank);
      const score = recencyWeight + (rank === 0 ? 3 : rank === 1 ? 1.5 : 0);
      const existing = playerMap.get(playerId);

      if (!existing) {
        playerMap.set(playerId, {
          playerId,
          playerName: row.playerName || "Bilinmiyor",
          position: row.position ?? null,
          shirtNumber:
            row.shirtNumber === null || row.shirtNumber === undefined
              ? null
              : Number(row.shirtNumber),
          score,
          starts: 1,
          lastSeenRank: rank,
        });
      } else {
        existing.score += score;
        existing.starts += 1;
        existing.lastSeenRank = Math.min(existing.lastSeenRank, rank);
        existing.position = existing.position || row.position || null;
        existing.shirtNumber =
          existing.shirtNumber ??
          (row.shirtNumber === null || row.shirtNumber === undefined
            ? null
            : Number(row.shirtNumber));
      }
    }

    const ranked = [...playerMap.values()]
      .sort((a, b) => {
        if (b.score !== a.score) return b.score - a.score;
        if (b.starts !== a.starts) return b.starts - a.starts;
        return a.lastSeenRank - b.lastSeenRank;
      })
      .slice(0, 11);

    const coverage = Math.min(1, ranked.length / 11);
    const historyScore = Math.min(1, matchOrder.size / matchLimit);
    const stableCore = ranked.filter((p) => p.starts >= 2).length / 11;
    const stalenessFactor = Math.max(
      0.35,
      Math.min(1, maxStalenessDays / Math.max(ageDays, 1)),
    );
    const confidence = Math.max(
      0,
      Math.min(
        staleProjection ? 0.58 : 0.88,
        (coverage * 0.45 + historyScore * 0.25 + stableCore * 0.3) *
          stalenessFactor,
      ),
    );

    return ranked.map((p) => ({
      teamId: params.teamId,
      isStarting: true,
      shirtNumber: p.shirtNumber,
      position: p.position,
      isProbable: true,
      lineupSource: "probable_xi",
      projectionConfidence: Number(confidence.toFixed(3)),
      projectionAgeDays: Number(ageDays.toFixed(1)),
      projectionStale: staleProjection,
      projectionMatchLimit: matchLimit,
      projectionLookbackDays: lookbackDays,
      projectionMaxStalenessDays: maxStalenessDays,
      player: {
        id: p.playerId,
        name: p.playerName,
      },
    }));
  }

  private extractSidelinedPlayerIds(teamData: any): Set<string> {
    if (!teamData || typeof teamData !== "object") return new Set();
    const players = Array.isArray(teamData.players) ? teamData.players : [];
    return new Set(
      players
        .map((player: any) =>
          String(
            player?.playerId ??
              player?.player_id ??
              player?.id ??
              player?.personId ??
              "",
          ),
        )
        .filter(Boolean),
    );
  }

  private canTrustStoredLineups(displayStatus?: string): boolean {
    const normalized = String(displayStatus || "").toLowerCase();
    return (
      normalized === "live" || normalized === "finished" || normalized === "ft"
    );
  }
}
