/**
 * Model for Participant Expected Values
 *
 * Manages probability distributions and calculated EVs for seasonParticipants
 * in sports seasons.
 */

import { database } from "~/database/context";
import { seasonParticipantExpectedValues, seasonParticipants, seasonParticipantSimulatorInputs } from "~/database/schema";
import { eq, and, count, inArray, sql } from "drizzle-orm";
import type { ProbabilityDistribution, ScoringRules } from "~/services/ev-calculator";
import { calculateEV, normalizeProbabilities, calculateReplacementLevel, calculateVORP } from "~/services/ev-calculator";
import { batchSaveParticipantSimulatorSourceOdds, batchUpsertParticipantSimulatorInputs } from "~/models/simulator";

export type ProbabilitySource = "manual" | "futures_odds" | "elo_simulation" | "performance_model";

export interface ParticipantEV {
  id: string;
  participantId: string;
  sportsSeasonId: string;
  probFirst: string;
  probSecond: string;
  probThird: string;
  probFourth: string;
  probFifth: string;
  probSixth: string;
  probSeventh: string;
  probEighth: string;
  expectedValue: string;
  source: ProbabilitySource | null;
  sourceOdds: number | null;
  sourceElo?: number | null;
  worldRanking?: number | null;
  calculatedAt: Date;
  updatedAt: Date;
}

export interface CreateProbabilityInput {
  participantId: string;
  sportsSeasonId: string;
  probabilities: ProbabilityDistribution;
  scoringRules: ScoringRules;
  source?: ProbabilitySource;
  sourceOdds?: number; // American odds if source is futures_odds
}

export interface UpdateProbabilityInput {
  probabilities: ProbabilityDistribution;
  scoringRules: ScoringRules;
  source?: ProbabilitySource;
}

/**
 * Recalculate and persist VORP for every participant in a sports season.
 *
 * VORP = participant EV − replacement level EV
 * Replacement level = average EV of seasonParticipants ranked 12th–14th in this season.
 *
 * Call this after any operation that changes EVs for seasonParticipants in the season.
 */
export async function syncVorpForSeason(
  sportsSeasonId: string,
  providedDb?: ReturnType<typeof database>
): Promise<void> {
  const db = providedDb || database();

  const allEvs = await getAllParticipantEVsForSeason(sportsSeasonId, db);
  if (allEvs.length === 0) return;

  const sorted = [...allEvs].toSorted(
    (a, b) => parseFloat(b.expectedValue) - parseFloat(a.expectedValue)
  );

  const sortedEvNumbers = sorted.map((ev) => parseFloat(ev.expectedValue));
  const replacementLevel = calculateReplacementLevel(sortedEvNumbers);

  const now = new Date();

  // Build a single CASE expression to update all seasonParticipants in one query
  // instead of N individual UPDATE statements.
  const vorpCaseExpr = sql`CASE ${sql.join(
    sorted.map((ev) => {
      const vorp = calculateVORP(parseFloat(ev.expectedValue), replacementLevel);
      return sql`WHEN ${seasonParticipants.id} = ${ev.participantId} THEN ${vorp.toFixed(4)}::numeric`;
    }),
    sql` `
  )} END`;

  const ids = sorted.map((ev) => ev.participantId);

  await db
    .update(seasonParticipants)
    .set({ vorpValue: vorpCaseExpr, updatedAt: now })
    .where(sql`${seasonParticipants.id} = ANY(${sql`ARRAY[${sql.join(ids.map((id) => sql`${id}`), sql`, `)}]::uuid[]`})`);
}

/**
 * Create or update participant probabilities and calculate EV
 *
 * @param input - Probabilities, scoring rules, and metadata
 * @returns Created/updated participant EV record
 * @throws Error if probabilities don't sum to 1.0 (within tolerance)
 */
export async function upsertParticipantEV(
  input: CreateProbabilityInput
): Promise<ParticipantEV> {
  const { participantId, sportsSeasonId, probabilities, scoringRules, source = "manual", sourceOdds } = input;

  // Always validate probabilities don't exceed 1.0
  const sum = Object.values(probabilities).reduce((a, b) => a + b, 0);
  if (sum > 1.01) {
    throw new Error(
      `Probabilities cannot sum to more than 1.0. Current sum: ${sum.toFixed(4)} (${(sum * 100).toFixed(1)}%)`
    );
  }

  // Calculate EV
  const expectedValue = calculateEV(probabilities, scoringRules);

  const db = database();

  // Check if record exists
  const existing = await db
    .select()
    .from(seasonParticipantExpectedValues)
    .where(
      and(
        eq(seasonParticipantExpectedValues.participantId, participantId),
        eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)
      )
    )
    .limit(1);

  const now = new Date();

  let result: ParticipantEV;

  if (existing.length > 0) {
    // Update existing
    const updated = await db
      .update(seasonParticipantExpectedValues)
      .set({
        probFirst: probabilities.probFirst.toString(),
        probSecond: probabilities.probSecond.toString(),
        probThird: probabilities.probThird.toString(),
        probFourth: probabilities.probFourth.toString(),
        probFifth: probabilities.probFifth.toString(),
        probSixth: probabilities.probSixth.toString(),
        probSeventh: probabilities.probSeventh.toString(),
        probEighth: probabilities.probEighth.toString(),
        expectedValue: expectedValue.toString(),
        source,
        ...(sourceOdds !== undefined ? { sourceOdds } : {}),
        calculatedAt: now,
        updatedAt: now,
      })
      .where(eq(seasonParticipantExpectedValues.id, existing[0].id))
      .returning();

    result = updated[0];
  } else {
    // Create new
    const created = await db
      .insert(seasonParticipantExpectedValues)
      .values({
        participantId,
        sportsSeasonId,
        probFirst: probabilities.probFirst.toString(),
        probSecond: probabilities.probSecond.toString(),
        probThird: probabilities.probThird.toString(),
        probFourth: probabilities.probFourth.toString(),
        probFifth: probabilities.probFifth.toString(),
        probSixth: probabilities.probSixth.toString(),
        probSeventh: probabilities.probSeventh.toString(),
        probEighth: probabilities.probEighth.toString(),
        expectedValue: expectedValue.toString(),
        source,
        sourceOdds: sourceOdds ?? null,
        calculatedAt: now,
        updatedAt: now,
      })
      .returning();

    result = created[0];
  }

  // Sync calculated EV to seasonParticipants table for draft room ranking
  await db
    .update(seasonParticipants)
    .set({ expectedValue: expectedValue.toString(), updatedAt: now })
    .where(eq(seasonParticipants.id, participantId));

  // Recalculate VORP for all seasonParticipants in this season (replacement level may have shifted)
  await syncVorpForSeason(sportsSeasonId);

  return result;
}

/**
 * Create or update with auto-normalization
 * Automatically normalizes probabilities if they don't sum to 100%
 */
export async function upsertParticipantEVWithNormalization(
  input: CreateProbabilityInput
): Promise<ParticipantEV> {
  const normalized = normalizeProbabilities(input.probabilities);

  return upsertParticipantEV({
    ...input,
    probabilities: normalized,
  });
}

export async function countAllParticipantEVs(): Promise<number> {
  const db = database();
  const result = await db.select({ value: count() }).from(seasonParticipantExpectedValues);
  return result[0].value;
}

/**
 * Get participant EV for a specific sports season
 */
export async function getParticipantEV(
  participantId: string,
  sportsSeasonId: string
): Promise<ParticipantEV | null> {
  const db = database();
  const result = await db
    .select()
    .from(seasonParticipantExpectedValues)
    .where(
      and(
        eq(seasonParticipantExpectedValues.participantId, participantId),
        eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)
      )
    )
    .limit(1);

  return result[0] || null;
}

/**
 * Get all participant EVs for a sports season
 */
export async function getAllParticipantEVsForSeason(
  sportsSeasonId: string,
  providedDb?: ReturnType<typeof database>
): Promise<ParticipantEV[]> {
  const db = providedDb || database();
  return db
    .select()
    .from(seasonParticipantExpectedValues)
    .where(eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
}

/**
 * Delete participant EV
 */
export async function deleteParticipantEV(
  participantId: string,
  sportsSeasonId: string
): Promise<void> {
  const db = database();
  await db
    .delete(seasonParticipantExpectedValues)
    .where(
      and(
        eq(seasonParticipantExpectedValues.participantId, participantId),
        eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)
      )
    );

  // Reset this participant's EV and VORP to 0 (no longer ranked)
  await db
    .update(seasonParticipants)
    .set({ expectedValue: "0", vorpValue: "0", updatedAt: new Date() })
    .where(eq(seasonParticipants.id, participantId));

  // Recalculate VORP for remaining seasonParticipants — replacement level may have shifted
  await syncVorpForSeason(sportsSeasonId);
}

/**
 * Batch upsert multiple participant EVs within a single transaction.
 * All records succeed or all fail together.
 */
export async function batchUpsertParticipantEVs(
  inputs: CreateProbabilityInput[],
  providedDb?: ReturnType<typeof database>
): Promise<ParticipantEV[]> {
  const db = providedDb || database();
  const results: ParticipantEV[] = [];

  await db.transaction(async (tx) => {
    const now = new Date();

    for (const input of inputs) {
      const { participantId, sportsSeasonId, probabilities, scoringRules, source = "manual", sourceOdds } = input;

      const sum = Object.values(probabilities).reduce((a, b) => a + b, 0);
      if (sum > 1.01) {
        throw new Error(
          `Probabilities for participant ${participantId} cannot sum to more than 1.0. Current sum: ${sum.toFixed(4)}`
        );
      }

      const expectedValue = calculateEV(probabilities, scoringRules);

      const existing = await tx
        .select({ id: seasonParticipantExpectedValues.id })
        .from(seasonParticipantExpectedValues)
        .where(
          and(
            eq(seasonParticipantExpectedValues.participantId, participantId),
            eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)
          )
        )
        .limit(1);

      const baseValues = {
        probFirst: probabilities.probFirst.toString(),
        probSecond: probabilities.probSecond.toString(),
        probThird: probabilities.probThird.toString(),
        probFourth: probabilities.probFourth.toString(),
        probFifth: probabilities.probFifth.toString(),
        probSixth: probabilities.probSixth.toString(),
        probSeventh: probabilities.probSeventh.toString(),
        probEighth: probabilities.probEighth.toString(),
        expectedValue: expectedValue.toString(),
        source,
        calculatedAt: now,
        updatedAt: now,
      };

      let result: ParticipantEV;

      if (existing.length > 0) {
        const [updated] = await tx
          .update(seasonParticipantExpectedValues)
          // Only overwrite sourceOdds if explicitly provided; preserve existing value otherwise
          .set(sourceOdds !== undefined ? { ...baseValues, sourceOdds } : baseValues)
          .where(eq(seasonParticipantExpectedValues.id, existing[0].id))
          .returning();
        result = updated;
      } else {
        const [created] = await tx
          .insert(seasonParticipantExpectedValues)
          .values({ participantId, sportsSeasonId, ...baseValues, sourceOdds: sourceOdds ?? null })
          .returning();
        result = created;
      }

      await tx
        .update(seasonParticipants)
        .set({ expectedValue: expectedValue.toString(), updatedAt: now })
        .where(eq(seasonParticipants.id, participantId));

      results.push(result);
    }
  });

  // Sync VORP for all affected seasons
  const uniqueSeasonIds = [...new Set(inputs.map((i) => i.sportsSeasonId))];
  await Promise.all(uniqueSeasonIds.map((id) => syncVorpForSeason(id, db)));

  return results;
}

/**
 * Save American odds for a batch of seasonParticipants without touching probabilities or EV.
 * Used by the futures-odds admin page to persist odds before running the full simulation.
 */
export async function batchSaveSourceOdds(
  inputs: Array<{ participantId: string; sportsSeasonId: string; sourceOdds: number }>
): Promise<void> {
  const db = database();

  await db.transaction(async (tx) => {
    const now = new Date();

    for (const { participantId, sportsSeasonId, sourceOdds } of inputs) {
      const existing = await tx
        .select({ id: seasonParticipantExpectedValues.id })
        .from(seasonParticipantExpectedValues)
        .where(
          and(
            eq(seasonParticipantExpectedValues.participantId, participantId),
            eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)
          )
        )
        .limit(1);

      if (existing.length > 0) {
        await tx
          .update(seasonParticipantExpectedValues)
          .set({ sourceOdds, updatedAt: now })
          .where(eq(seasonParticipantExpectedValues.id, existing[0].id));
      } else {
        // Insert a stub record — probabilities/EV will be filled in by the simulator
        await tx.insert(seasonParticipantExpectedValues).values({
          participantId,
          sportsSeasonId,
          probFirst: "0",
          probSecond: "0",
          probThird: "0",
          probFourth: "0",
          probFifth: "0",
          probSixth: "0",
          probSeventh: "0",
          probEighth: "0",
          expectedValue: "0",
          source: "futures_odds",
          sourceOdds,
          calculatedAt: now,
          updatedAt: now,
        });
      }
    }
  });

  await batchSaveParticipantSimulatorSourceOdds(inputs);
}

export async function clearSourceOddsForParticipants(
  sportsSeasonId: string,
  participantIds: string[]
): Promise<void> {
  if (participantIds.length === 0) return;
  const db = database();
  const now = new Date();
  await db.transaction(async (tx) => {
    await tx
      .update(seasonParticipantExpectedValues)
      .set({ sourceOdds: null, updatedAt: now })
      .where(
        and(
          eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId),
          inArray(seasonParticipantExpectedValues.participantId, participantIds)
        )
      );
    await tx
      .update(seasonParticipantSimulatorInputs)
      .set({
        sourceOdds: null,
        rating: null,
        metadata: sql`coalesce(${seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb) - 'ratingMethod'`,
        updatedAt: now,
      })
      .where(
        and(
          eq(seasonParticipantSimulatorInputs.sportsSeasonId, sportsSeasonId),
          inArray(seasonParticipantSimulatorInputs.participantId, participantIds)
        )
      );
  });
}

/**
 * Persist raw Elo ratings (and optional world rankings) for a batch of seasonParticipants.
 * Used by Elo-based simulators like snooker_bracket and darts_bracket.
 * Creates stub records if none exist; does not touch probabilities or EV.
 * Uses INSERT ... ON CONFLICT DO UPDATE to avoid N+1 queries.
 */
export async function batchSaveSourceElos(
  inputs: Array<{
    participantId: string;
    sportsSeasonId: string;
    sourceElo: number | null;
    worldRanking?: number | null;
    projectedWins?: number | null;
    projectedTablePoints?: number | null;
    metadata?: Record<string, unknown> | null;
  }>
): Promise<void> {
  if (inputs.length === 0) return;
  const db = database();
  const now = new Date();

  await db
    .insert(seasonParticipantExpectedValues)
    .values(
      inputs.map(({ participantId, sportsSeasonId, sourceElo, worldRanking }) => ({
        participantId,
        sportsSeasonId,
        probFirst: "0",
        probSecond: "0",
        probThird: "0",
        probFourth: "0",
        probFifth: "0",
        probSixth: "0",
        probSeventh: "0",
        probEighth: "0",
        expectedValue: "0",
        source: "elo_simulation" as const,
        sourceElo,
        worldRanking: worldRanking ?? null,
        calculatedAt: now,
        updatedAt: now,
      }))
    )
    .onConflictDoUpdate({
      target: [seasonParticipantExpectedValues.participantId, seasonParticipantExpectedValues.sportsSeasonId],
      set: {
        sourceElo: sql`excluded.source_elo`,
        worldRanking: sql`excluded.world_ranking`,
        updatedAt: sql`excluded.updated_at`,
      },
    });

  await batchUpsertParticipantSimulatorInputs(
    inputs.map((input) => ({
      participantId: input.participantId,
      sportsSeasonId: input.sportsSeasonId,
      sourceElo: input.sourceElo,
      worldRanking: input.worldRanking ?? null,
      projectedWins: input.projectedWins ?? null,
      projectedTablePoints: input.projectedTablePoints ?? null,
      metadata: input.metadata ?? null,
    }))
  );
}

/**
 * Convert database record to ProbabilityDistribution
 */
export function toProbabilityDistribution(ev: ParticipantEV): ProbabilityDistribution {
  return {
    probFirst: parseFloat(ev.probFirst),
    probSecond: parseFloat(ev.probSecond),
    probThird: parseFloat(ev.probThird),
    probFourth: parseFloat(ev.probFourth),
    probFifth: parseFloat(ev.probFifth),
    probSixth: parseFloat(ev.probSixth),
    probSeventh: parseFloat(ev.probSeventh),
    probEighth: parseFloat(ev.probEighth),
  };
}

/**
 * Recalculate EV for a participant with new scoring rules.
 * Keeps probabilities the same, only updates EV based on new scoring.
 *
 * NOTE: This function does NOT sync VORP. If calling this directly (outside
 * of recalculateAllEVsForSeason), call syncVorpForSeason(sportsSeasonId)
 * afterwards to keep draft order correct.
 */
export async function recalculateEV(
  participantId: string,
  sportsSeasonId: string,
  newScoringRules: ScoringRules
): Promise<ParticipantEV | null> {
  const existing = await getParticipantEV(participantId, sportsSeasonId);

  if (!existing) {
    return null;
  }

  const probabilities = toProbabilityDistribution(existing);
  const newEV = calculateEV(probabilities, newScoringRules);

  const db = database();
  const now = new Date();
  const updated = await db
    .update(seasonParticipantExpectedValues)
    .set({
      expectedValue: newEV.toString(),
      calculatedAt: now,
      updatedAt: now,
    })
    .where(eq(seasonParticipantExpectedValues.id, existing.id))
    .returning();

  // Sync recalculated EV to seasonParticipants table
  await db
    .update(seasonParticipants)
    .set({ expectedValue: newEV.toString(), updatedAt: now })
    .where(eq(seasonParticipants.id, participantId));

  return updated[0];
}

/**
 * Recalculate EVs for all seasonParticipants in a sports season
 * Used when scoring rules change
 */
export async function recalculateAllEVsForSeason(
  sportsSeasonId: string,
  newScoringRules: ScoringRules
): Promise<number> {
  const allEVs = await getAllParticipantEVsForSeason(sportsSeasonId);

  await Promise.all(
    allEVs.map((ev) =>
      recalculateEV(ev.participantId, sportsSeasonId, newScoringRules)
    )
  );

  // Sync VORP once after all EVs are updated
  await syncVorpForSeason(sportsSeasonId);

  return allEVs.length;
}
