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

import { database } from "~/database/context";
import { participantExpectedValues, participants } from "~/database/schema";
import { eq, and, count, sql } from "drizzle-orm";
import type { ProbabilityDistribution, ScoringRules } from "~/services/ev-calculator";
import { calculateEV, normalizeProbabilities } from "~/services/ev-calculator";

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

/**
 * 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(participantExpectedValues)
    .where(
      and(
        eq(participantExpectedValues.participantId, participantId),
        eq(participantExpectedValues.sportsSeasonId, sportsSeasonId)
      )
    )
    .limit(1);

  const now = new Date();

  let result: ParticipantEV;

  if (existing.length > 0) {
    // Update existing
    const updated = await db
      .update(participantExpectedValues)
      .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(participantExpectedValues.id, existing[0].id))
      .returning();

    result = updated[0];
  } else {
    // Create new
    const created = await db
      .insert(participantExpectedValues)
      .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 participants table for draft room ranking
  await db
    .update(participants)
    .set({ expectedValue: expectedValue.toString(), updatedAt: now })
    .where(eq(participants.id, participantId));

  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(participantExpectedValues);
  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(participantExpectedValues)
    .where(
      and(
        eq(participantExpectedValues.participantId, participantId),
        eq(participantExpectedValues.sportsSeasonId, sportsSeasonId)
      )
    )
    .limit(1);

  return result[0] || null;
}

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

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

  // Reset participant EV to 0
  await db
    .update(participants)
    .set({ expectedValue: "0", updatedAt: new Date() })
    .where(eq(participants.id, participantId));
}

/**
 * Batch upsert multiple participant EVs within a single transaction.
 * All records succeed or all fail together.
 */
export async function batchUpsertParticipantEVs(
  inputs: CreateProbabilityInput[]
): Promise<ParticipantEV[]> {
  const db = 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: participantExpectedValues.id })
        .from(participantExpectedValues)
        .where(
          and(
            eq(participantExpectedValues.participantId, participantId),
            eq(participantExpectedValues.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(participantExpectedValues)
          // Only overwrite sourceOdds if explicitly provided; preserve existing value otherwise
          .set(sourceOdds !== undefined ? { ...baseValues, sourceOdds } : baseValues)
          .where(eq(participantExpectedValues.id, existing[0].id))
          .returning();
        result = updated;
      } else {
        const [created] = await tx
          .insert(participantExpectedValues)
          .values({ participantId, sportsSeasonId, ...baseValues, sourceOdds: sourceOdds ?? null })
          .returning();
        result = created;
      }

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

      results.push(result);
    }
  });

  return results;
}

/**
 * Save American odds for a batch of participants 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: participantExpectedValues.id })
        .from(participantExpectedValues)
        .where(
          and(
            eq(participantExpectedValues.participantId, participantId),
            eq(participantExpectedValues.sportsSeasonId, sportsSeasonId)
          )
        )
        .limit(1);

      if (existing.length > 0) {
        await tx
          .update(participantExpectedValues)
          .set({ sourceOdds, updatedAt: now })
          .where(eq(participantExpectedValues.id, existing[0].id));
      } else {
        // Insert a stub record — probabilities/EV will be filled in by the simulator
        await tx.insert(participantExpectedValues).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,
        });
      }
    }
  });
}

/**
 * Persist raw Elo ratings for a batch of participants (used by Elo-based simulators like snooker_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 }>
): Promise<void> {
  if (inputs.length === 0) return;
  const db = database();
  const now = new Date();

  await db
    .insert(participantExpectedValues)
    .values(
      inputs.map(({ participantId, sportsSeasonId, sourceElo }) => ({
        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,
        calculatedAt: now,
        updatedAt: now,
      }))
    )
    .onConflictDoUpdate({
      target: [participantExpectedValues.participantId, participantExpectedValues.sportsSeasonId],
      set: {
        sourceElo: sql`excluded.source_elo`,
        updatedAt: sql`excluded.updated_at`,
      },
    });
}

/**
 * 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
 */
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(participantExpectedValues)
    .set({
      expectedValue: newEV.toString(),
      calculatedAt: now,
      updatedAt: now,
    })
    .where(eq(participantExpectedValues.id, existing.id))
    .returning();

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

  return updated[0];
}

/**
 * Recalculate EVs for all participants 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)
    )
  );

  return allEVs.length;
}
