import { Injectable, Logger } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { Prisma } from "@prisma/client";
import { PrismaService } from "../database/prisma.service";
import { TaskLockService } from "./task-lock.service";
import {
  resolveOutcomeForPick,
  computeUnitProfit,
  type MatchResult,
  type PickRef,
} from "./prediction-settlement.market-resolver";

interface UnresolvedRow {
  id: bigint;
  match_id: string;
  payload_summary: Prisma.JsonValue;
  odds_snapshot: Prisma.JsonValue;
  score_home: number | null;
  score_away: number | null;
  ht_score_home: number | null;
  ht_score_away: number | null;
  status: string | null;
}

const BATCH_SIZE = 500;

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly taskLock: TaskLockService,
  ) {}

  /**
   * Runs after the previous-day match sync. Reconciles prediction_runs against actual results.
   */
  @Cron("30 8 * * *", { timeZone: "Europe/Istanbul" })
  async settleCompletedPredictions() {
    if (process.env.FEEDER_MODE === "historical") {
      this.logger.debug("Skipping settlement in historical feeder mode");
      return;
    }
    await this.taskLock.runWithLease(
      "settleCompletedPredictions",
      2 * 60 * 60 * 1000,
      () => this.runSettlement(),
      this.logger,
    );
  }

  async runSettlement(): Promise<{ scanned: number; updated: number }> {
    let scanned = 0;
    let updated = 0;
    let cursor = 0n;

    for (;;) {
      const rows = await this.prisma.$queryRaw<UnresolvedRow[]>(Prisma.sql`
        SELECT pr.id,
               pr.match_id,
               pr.payload_summary,
               pr.odds_snapshot,
               m.score_home,
               m.score_away,
               m.ht_score_home,
               m.ht_score_away,
               m.status
        FROM prediction_runs pr
        JOIN matches m ON m.id = pr.match_id
        WHERE pr.eventual_outcome IS NULL
          AND pr.id > ${cursor}
          AND m.status = 'FT'
          AND m.score_home IS NOT NULL
          AND m.score_away IS NOT NULL
        ORDER BY pr.id ASC
        LIMIT ${BATCH_SIZE}
      `);

      if (rows.length === 0) break;

      for (const row of rows) {
        scanned += 1;
        const settled = this.settleRow(row);
        if (settled === null) continue;

        await this.prisma.$executeRaw(Prisma.sql`
          UPDATE prediction_runs
          SET eventual_outcome = ${settled.outcome},
              unit_profit = ${settled.unitProfit}
          WHERE id = ${row.id}
        `);
        updated += 1;
      }

      cursor = rows[rows.length - 1].id;
    }

    this.logger.log(
      `Settlement finished: scanned=${scanned} updated=${updated}`,
    );

    const settled = await this.computeOddsMovementAndCleanup();
    this.logger.log(
      `Odds movement: computed=${settled.computed} cleaned=${settled.cleaned}`,
    );

    return { scanned, updated };
  }

  private async computeOddsMovementAndCleanup(): Promise<{
    computed: number;
    cleaned: number;
  }> {
    let computed = 0;
    let cleaned = 0;

    const finishedMatchIds = await this.prisma.$queryRaw<{ id: string }[]>(
      Prisma.sql`
        SELECT DISTINCT oh.match_id AS id
        FROM odds_history oh
        JOIN matches m ON m.id = oh.match_id
        WHERE m.status = 'FT'
        LIMIT 500
      `,
    );

    if (finishedMatchIds.length === 0) return { computed, cleaned };

    const matchIds = finishedMatchIds.map((r) => r.id);

    for (const matchId of matchIds) {
      try {
        await this.computeMovementForMatch(matchId);
        computed++;
      } catch (e) {
        this.logger.warn(`Movement calc failed for ${matchId}: ${e}`);
      }

      await this.prisma.oddsHistory.deleteMany({ where: { matchId } });
      cleaned++;
    }

    return { computed, cleaned };
  }

  private async computeMovementForMatch(matchId: string): Promise<void> {
    const selections = await this.prisma.$queryRaw<
      {
        name: string;
        category_name: string;
        opening_value: string | null;
        odd_value: string | null;
      }[]
    >(Prisma.sql`
      SELECT os.name, oc.name AS category_name,
             os.opening_value, os.odd_value
      FROM odd_selections os
      JOIN odd_categories oc ON oc.db_id = os.odd_category_db_id
      WHERE oc.match_id = ${matchId}
        AND os.opening_value IS NOT NULL
        AND os.odd_value IS NOT NULL
    `);

    const movements: Record<string, number> = {};

    for (const sel of selections) {
      const opening = parseFloat(sel.opening_value!);
      const closing = parseFloat(sel.odd_value!);
      if (!Number.isFinite(opening) || !Number.isFinite(closing) || opening <= 0)
        continue;

      const movement = ((closing - opening) / opening) * 100;
      const cat = (sel.category_name ?? "").toLowerCase();
      const name = (sel.name ?? "").toLowerCase();

      if (cat.includes("maç sonucu") || cat.includes("mac sonucu") || cat === "ms") {
        if (name === "1") movements.home = movement;
        else if (name === "x" || name === "0") movements.draw = movement;
        else if (name === "2") movements.away = movement;
      } else if (
        (cat.includes("2,5") || cat.includes("2.5")) &&
        (cat.includes("alt/üst") || cat.includes("alt/ust"))
      ) {
        if (name.includes("üst") || name.includes("ust") || name.includes("over"))
          movements.o25 = movement;
      } else if (
        cat.includes("karşılıklı gol") ||
        cat.includes("karsilikli gol") ||
        cat === "kg"
      ) {
        if (name === "var" || name.includes("yes")) movements.btts = movement;
      }
    }

    if (Object.keys(movements).length === 0) return;

    const vals = Object.values(movements);
    const sharpness =
      vals.length > 0
        ? vals.reduce((sum, v) => sum + Math.abs(v), 0) / vals.length
        : 0;

    await this.prisma.footballAiFeature.updateMany({
      where: { matchId },
      data: {
        oddsMovementHome: movements.home ?? null,
        oddsMovementDraw: movements.draw ?? null,
        oddsMovementAway: movements.away ?? null,
        oddsMovementO25: movements.o25 ?? null,
        oddsMovementBtts: movements.btts ?? null,
        oddsSharpness: sharpness || null,
      },
    });
  }

  private settleRow(
    row: UnresolvedRow,
  ): { outcome: string; unitProfit: number } | null {
    const summary = (row.payload_summary ?? {}) as Record<string, any>;
    const pick = this.pickToEvaluate(summary);
    if (!pick) {
      return { outcome: "NO_BET", unitProfit: 0 };
    }

    const result: MatchResult = {
      scoreHome: row.score_home!,
      scoreAway: row.score_away!,
      htScoreHome: row.ht_score_home,
      htScoreAway: row.ht_score_away,
    };

    const won = resolveOutcomeForPick(pick, result);
    if (won === null) {
      this.logger.debug(
        `Cannot resolve market=${pick.market} pick=${pick.pick} for run=${row.id}`,
      );
      return null;
    }

    const odds = this.extractOdds(row.odds_snapshot, pick);
    const stake = pick.stake_units ?? 1;
    const profit = computeUnitProfit(won, stake, odds);

    return {
      outcome: `${won ? "WON" : "LOST"}:${pick.market}:${pick.pick}`,
      unitProfit: profit,
    };
  }

  private pickToEvaluate(summary: Record<string, any>): PickRef | null {
    const advice = summary.bet_advice ?? {};
    if (advice.playable !== true) return null;

    const main = summary.main_pick;
    if (main && main.playable && main.market && main.pick) {
      return {
        market: String(main.market),
        pick: String(main.pick),
        stake_units: Number(
          main.stake_units ?? advice.suggested_stake_units ?? 1,
        ),
        odds: Number(main.odds ?? 0) || null,
      };
    }

    const fallback = summary.value_pick;
    if (fallback && fallback.playable && fallback.market && fallback.pick) {
      return {
        market: String(fallback.market),
        pick: String(fallback.pick),
        stake_units: Number(advice.suggested_stake_units ?? 1),
        odds: Number(fallback.odds ?? 0) || null,
      };
    }
    return null;
  }

  private extractOdds(
    oddsSnapshot: Prisma.JsonValue,
    pick: PickRef,
  ): number | null {
    if (pick.odds && pick.odds > 1) return pick.odds;
    if (!oddsSnapshot || typeof oddsSnapshot !== "object") return null;
    const snap = oddsSnapshot as Record<string, any>;
    const odds = snap.odds;
    if (!odds || typeof odds !== "object") return null;
    const category = odds[pick.market];
    if (!category || typeof category !== "object") return null;
    const value = Number(category[pick.pick]);
    return Number.isFinite(value) && value > 1 ? value : null;
  }
}
