import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and, desc, asc, gte, lte, or, inArray, isNotNull } from "drizzle-orm";
import type { BracketRegion } from "~/lib/bracket-templates";
import { recalculateAffectedLeagues } from "./scoring-calculator";
import { recalculateParticipantQP } from "./qualifying-points";

export type EventType = "playoff_game" | "major_tournament" | "final_standings" | "schedule_event";

export function getEventTypeLabel(eventType: string): string {
  switch (eventType) {
    case "playoff_game": return "Bracket";
    case "major_tournament": return "Major Tournament";
    case "final_standings": return "Final Standings";
    case "schedule_event": return "Non-Scoring";
    default: return eventType;
  }
}

export interface CreateScoringEventData {
  sportsSeasonId: string;
  name: string;
  eventDate?: Date;
  eventStartsAt?: Date;
  eventType: EventType;
  isQualifyingEvent?: boolean;
  // Template system fields (Phase 2.6)
  bracketTemplateId?: string;
  scoringStartsAtRound?: string;
}

export interface UpdateScoringEventData {
  name?: string;
  eventDate?: Date | null;
  eventStartsAt?: Date | null;
  isComplete?: boolean;
  completedAt?: Date;
  bracketTemplateId?: string;
  scoringStartsAtRound?: string;
  /** Per-event region config for NCAA-style brackets (overrides template defaults) */
  bracketRegionConfig?: BracketRegion[] | null;
}

/**
 * Create a new scoring event
 */
export async function createScoringEvent(
  data: CreateScoringEventData,
  providedDb?: ReturnType<typeof database>
) {
  const db = providedDb || database();

  const [event] = await db
    .insert(schema.scoringEvents)
    .values({
      sportsSeasonId: data.sportsSeasonId,
      name: data.name,
      eventDate: data.eventDate ? data.eventDate.toISOString().split('T')[0] : undefined,
      eventStartsAt: data.eventStartsAt,
      eventType: data.eventType,
      isQualifyingEvent: data.isQualifyingEvent || false,
      bracketTemplateId: data.bracketTemplateId,
      scoringStartsAtRound: data.scoringStartsAtRound,
      isComplete: false,
    })
    .returning();

  return event;
}

/**
 * Get a scoring event by ID
 */
export async function getScoringEventById(
  eventId: string,
  providedDb?: ReturnType<typeof database>
) {
  const db = providedDb || database();

  return await db.query.scoringEvents.findFirst({
    where: eq(schema.scoringEvents.id, eventId),
    with: {
      sportsSeason: {
        with: {
          sport: true,
        },
      },
    },
  });
}

/**
 * Get all scoring events for a sports season
 */
export async function getScoringEventsForSportsSeason(
  sportsSeasonId: string,
  providedDb?: ReturnType<typeof database>
) {
  const db = providedDb || database();

  return await db.query.scoringEvents.findMany({
    where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
    orderBy: [desc(schema.scoringEvents.eventDate), desc(schema.scoringEvents.eventStartsAt), desc(schema.scoringEvents.createdAt)],
    with: {
      eventResults: {
        with: {
          participant: true,
        },
      },
    },
  });
}

/**
 * Get incomplete scoring events for a sports season
 */
export async function getIncompleteScoringEvents(
  sportsSeasonId: string,
  providedDb?: ReturnType<typeof database>
) {
  const db = providedDb || database();

  return await db.query.scoringEvents.findMany({
    where: and(
      eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
      eq(schema.scoringEvents.isComplete, false)
    ),
    orderBy: [desc(schema.scoringEvents.eventDate), desc(schema.scoringEvents.createdAt)],
  });
}

/**
 * Get qualifying events for a sports season
 */
export async function getQualifyingEvents(
  sportsSeasonId: string,
  providedDb?: ReturnType<typeof database>
) {
  const db = providedDb || database();

  return await db.query.scoringEvents.findMany({
    where: and(
      eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
      eq(schema.scoringEvents.isQualifyingEvent, true)
    ),
    orderBy: [desc(schema.scoringEvents.eventDate), desc(schema.scoringEvents.createdAt)],
  });
}

/**
 * Update a scoring event
 */
export async function updateScoringEvent(
  eventId: string,
  data: UpdateScoringEventData,
  providedDb?: ReturnType<typeof database>
) {
  const db = providedDb || database();

  const updateData: Partial<typeof schema.scoringEvents.$inferInsert> = { updatedAt: new Date() };

  if (data.name !== undefined) updateData.name = data.name;
  if (data.eventDate !== undefined) {
    updateData.eventDate = data.eventDate ? data.eventDate.toISOString().split('T')[0] : null;
  }
  if (data.eventStartsAt !== undefined) updateData.eventStartsAt = data.eventStartsAt;
  if (data.isComplete !== undefined) updateData.isComplete = data.isComplete;
  if (data.completedAt !== undefined) updateData.completedAt = data.completedAt;
  if (data.bracketTemplateId !== undefined) updateData.bracketTemplateId = data.bracketTemplateId;
  if (data.scoringStartsAtRound !== undefined) updateData.scoringStartsAtRound = data.scoringStartsAtRound;
  if (data.bracketRegionConfig !== undefined) updateData.bracketRegionConfig = data.bracketRegionConfig;

  const [updated] = await db
    .update(schema.scoringEvents)
    .set(updateData)
    .where(eq(schema.scoringEvents.id, eventId))
    .returning();

  return updated;
}

/**
 * Mark a scoring event as complete
 */
export async function completeScoringEvent(
  eventId: string,
  providedDb?: ReturnType<typeof database>
) {
  const db = providedDb || database();

  return await updateScoringEvent(
    eventId,
    {
      isComplete: true,
      completedAt: new Date(),
    },
    db
  );
}

/**
 * Delete a scoring event.
 * For bracket events (playoff_game), also clears all participant results for the
 * sports season (placements are stored there with no FK back to the event) and
 * recalculates league standings.
 */
export async function deleteScoringEvent(
  eventId: string,
  providedDb?: ReturnType<typeof database>
) {
  const db = providedDb || database();

  const event = await db.query.scoringEvents.findFirst({
    where: eq(schema.scoringEvents.id, eventId),
  });

  if (!event) return;

  // For qualifying events: capture affected participant IDs before the cascade deletes
  // eventResults (which is how we know who had QP awarded from this event).
  let affectedParticipantIds: string[] = [];
  let wasQPProcessed = false;
  if (event.isQualifyingEvent) {
    const results = await db.query.eventResults.findMany({
      where: eq(schema.eventResults.scoringEventId, eventId),
    });
    affectedParticipantIds = results.map((r) => r.participantId);
    wasQPProcessed = results.some(
      (r) => r.qualifyingPointsAwarded && parseFloat(r.qualifyingPointsAwarded) > 0
    );
  }

  await db.transaction(async (tx) => {
    if (event.eventType === "playoff_game") {
      await tx
        .delete(schema.participantResults)
        .where(eq(schema.participantResults.sportsSeasonId, event.sportsSeasonId));
    }
    // Cascades: playoffMatches, eventResults, tournamentGroups/Members
    await tx.delete(schema.scoringEvents).where(eq(schema.scoringEvents.id, eventId));
  });

  if (event.eventType === "playoff_game") {
    await recalculateAffectedLeagues(event.sportsSeasonId, db);
  }

  if (event.isQualifyingEvent && affectedParticipantIds.length > 0) {
    // eventResults have been cascade-deleted; recalculate QP totals from remaining events
    for (const participantId of affectedParticipantIds) {
      await recalculateParticipantQP(participantId, event.sportsSeasonId, db);
    }

    // Decrement majorsCompleted if this event had already been processed
    if (wasQPProcessed) {
      const sportsSeason = await db.query.sportsSeasons.findFirst({
        where: eq(schema.sportsSeasons.id, event.sportsSeasonId),
      });
      if (sportsSeason && (sportsSeason.majorsCompleted ?? 0) > 0) {
        await db
          .update(schema.sportsSeasons)
          .set({ majorsCompleted: (sportsSeason.majorsCompleted ?? 1) - 1, updatedAt: new Date() })
          .where(eq(schema.sportsSeasons.id, event.sportsSeasonId));
      }
    }

    await recalculateAffectedLeagues(event.sportsSeasonId, db);
  }
}

/**
 * Check if all events for a sports season are complete
 */
export async function areAllEventsComplete(
  sportsSeasonId: string,
  providedDb?: ReturnType<typeof database>
): Promise<boolean> {
  const db = providedDb || database();

  const incompleteEvents = await getIncompleteScoringEvents(sportsSeasonId, db);
  return incompleteEvents.length === 0;
}

/**
 * Get upcoming (not yet complete) scoring events for a sports season,
 * ordered by eventDate ascending (soonest first).
 * Events without a date are included last.
 */
export async function getUpcomingScoringEvents(
  sportsSeasonId: string,
  limit = 5,
  providedDb?: ReturnType<typeof database>
) {
  const db = providedDb || database();

  return db.query.scoringEvents.findMany({
    where: and(
      eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
      eq(schema.scoringEvents.isComplete, false)
    ),
    orderBy: [asc(schema.scoringEvents.eventDate), asc(schema.scoringEvents.eventStartsAt), asc(schema.scoringEvents.createdAt)],
    limit,
  });
}

/**
 * Get the single next upcoming event for a sports season (for card display).
 */
export async function getNextScoringEvent(
  sportsSeasonId: string,
  providedDb?: ReturnType<typeof database>
) {
  const upcoming = await getUpcomingScoringEvents(sportsSeasonId, 1, providedDb);
  return upcoming[0] || null;
}

/**
 * Get recently completed scoring events for a sports season,
 * ordered by completedAt descending (most recent first).
 */
export async function getRecentCompletedEvents(
  sportsSeasonId: string,
  limit = 3,
  providedDb?: ReturnType<typeof database>
) {
  const db = providedDb || database();

  return db.query.scoringEvents.findMany({
    where: and(
      eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
      eq(schema.scoringEvents.isComplete, true)
    ),
    orderBy: [desc(schema.scoringEvents.completedAt), desc(schema.scoringEvents.eventDate)],
    limit,
  });
}

// Scoring patterns where each participant competes in every event.
// Valid scoringPatternEnum values: playoff_bracket | season_standings | qualifying_points
const ALL_COMPETE_PATTERNS = new Set(["season_standings", "qualifying_points"]);

export interface UpcomingParticipantEvent {
  id: string;
  name: string;
  eventDate: string | null;
  /**
   * Earliest scheduledAt timestamp (ISO string) across individual playoff match
   * games for this event — bracket sports only. Used when eventDate is null.
   */
  earliestGameTime: string | null;
  /**
   * e.g. "Semifinals Game #1" — derived from playoffMatches.round and
   * playoffMatchGames.gameNumber. null when unavailable or redundant with event name.
   */
  matchLabel: string | null;
  eventType: string;
  sportsSeasonId: string;
  /** Only the current user's drafted participants involved in this event */
  relevantParticipants: Array<{ id: string; name: string }>;
}

/**
 * Get upcoming scoring events (within the given date window) that involve
 * at least one of the given drafted participants.
 *
 * - Bracket sports (playoff_bracket, ucl_bracket): only events where a
 *   participant appears in a playoff match.
 * - All-compete sports (season_standings, qualifying_points, etc.): every
 *   upcoming event; all draftedParticipants are attached.
 *
 * Events without a date or that are already complete are excluded.
 */
export async function getUpcomingEventsForDraftedParticipants(
  sportsSeasonId: string,
  scoringPattern: string,
  draftedParticipants: Array<{ id: string; name: string }>,
  dateFrom: Date,
  dateTo: Date,
  providedDb?: ReturnType<typeof database>
): Promise<UpcomingParticipantEvent[]> {
  if (draftedParticipants.length === 0) return [];

  const db = providedDb || database();
  const dateFromStr = dateFrom.toISOString().split("T")[0];
  const dateToStr = dateTo.toISOString().split("T")[0];
  const draftedIds = draftedParticipants.map((p) => p.id);
  const draftedMap = new Map(draftedParticipants.map((p) => [p.id, p]));

  if (ALL_COMPETE_PATTERNS.has(scoringPattern)) {
    const events = await db.query.scoringEvents.findMany({
      where: and(
        eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
        eq(schema.scoringEvents.isComplete, false),
        isNotNull(schema.scoringEvents.eventDate),
        gte(schema.scoringEvents.eventDate, dateFromStr),
        lte(schema.scoringEvents.eventDate, dateToStr)
      ),
      orderBy: [asc(schema.scoringEvents.eventDate)],
    });

    return events.map((e) => ({
      id: e.id,
      name: e.name,
      eventDate: e.eventDate,
      earliestGameTime: e.eventStartsAt ? e.eventStartsAt.toISOString() : null,
      matchLabel: null,
      eventType: e.eventType,
      sportsSeasonId: e.sportsSeasonId,
      relevantParticipants: draftedParticipants,
    }));
  }

  // Bracket sports: join through playoff_matches → playoff_match_games to find relevant events.
  // scoringEvent.eventDate may be null — admins often set dates on individual games
  // (playoffMatchGames.scheduledAt) rather than on the event record itself.
  // We include an event if EITHER its eventDate OR any of its game scheduledAt timestamps
  // falls within the window.
  //
  // Use start-of-day for the timestamp lower bound so games that already happened
  // earlier today are still included.
  const dateFromTimestamp = new Date(dateFromStr + "T00:00:00.000Z");
  const dateToTimestamp = new Date(dateToStr + "T23:59:59.999Z");

  const rows = await db
    .selectDistinct({
      id: schema.scoringEvents.id,
      name: schema.scoringEvents.name,
      eventDate: schema.scoringEvents.eventDate,
      eventType: schema.scoringEvents.eventType,
      sportsSeasonId: schema.scoringEvents.sportsSeasonId,
      participant1Id: schema.playoffMatches.participant1Id,
      participant2Id: schema.playoffMatches.participant2Id,
      round: schema.playoffMatches.round,
      gameNumber: schema.playoffMatchGames.gameNumber,
      scheduledAt: schema.playoffMatchGames.scheduledAt,
    })
    .from(schema.scoringEvents)
    .innerJoin(
      schema.playoffMatches,
      eq(schema.playoffMatches.scoringEventId, schema.scoringEvents.id)
    )
    .leftJoin(
      schema.playoffMatchGames,
      eq(schema.playoffMatchGames.playoffMatchId, schema.playoffMatches.id)
    )
    .where(
      and(
        eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
        eq(schema.scoringEvents.isComplete, false),
        or(
          inArray(schema.playoffMatches.participant1Id, draftedIds),
          // participant2Id is nullable UUID — same underlying type as participant1Id;
          // the notNull difference only exists at the TypeScript layer
          // oxlint-disable-next-line typescript/no-explicit-any -- participant2Id is nullable UUID; notNull difference only exists at the TypeScript layer
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          inArray(schema.playoffMatches.participant2Id as any, draftedIds)
        ),
        or(
          // scoringEvent has an explicit date within the window
          and(
            isNotNull(schema.scoringEvents.eventDate),
            gte(schema.scoringEvents.eventDate, dateFromStr),
            lte(schema.scoringEvents.eventDate, dateToStr)
          ),
          // Or one of the individual games is scheduled within the window
          and(
            isNotNull(schema.playoffMatchGames.scheduledAt),
            gte(schema.playoffMatchGames.scheduledAt, dateFromTimestamp),
            lte(schema.playoffMatchGames.scheduledAt, dateToTimestamp)
          )
        )
      )
    )
    .orderBy(asc(schema.scoringEvents.eventDate));

  // Determine max game number per event across ALL games (not date-filtered)
  // so we can distinguish single-game matchups from multi-game series.
  const eventIdsFromRows = [...new Set(rows.map((r) => r.id))];
  const maxGameNumberByEvent = new Map<string, number>();
  if (eventIdsFromRows.length > 0) {
    const allGameRows = await db
      .selectDistinct({
        eventId: schema.scoringEvents.id,
        gameNumber: schema.playoffMatchGames.gameNumber,
      })
      .from(schema.scoringEvents)
      .innerJoin(
        schema.playoffMatches,
        eq(schema.playoffMatches.scoringEventId, schema.scoringEvents.id)
      )
      .innerJoin(
        schema.playoffMatchGames,
        eq(schema.playoffMatchGames.playoffMatchId, schema.playoffMatches.id)
      )
      .where(inArray(schema.scoringEvents.id, eventIdsFromRows));

    for (const row of allGameRows) {
      if (row.gameNumber !== null) {
        const current = maxGameNumberByEvent.get(row.eventId) ?? 0;
        maxGameNumberByEvent.set(row.eventId, Math.max(current, row.gameNumber));
      }
    }
  }

  // Group rows into calendar entries.
  // Series events (max game number > 1) get one entry per game so each game
  // appears as its own row on the calendar. Single-game events get one entry.
  const entryMap = new Map<string, UpcomingParticipantEvent>();
  const participantsByEntry = new Map<string, Map<string, { id: string; name: string }>>();
  const gameKeysByEntry = new Map<string, Set<string>>();
  const earliestTimeByEntry = new Map<string, Date>();
  const isSeriesByEntry = new Map<string, boolean>();

  for (const row of rows) {
    const isSeries = (maxGameNumberByEvent.get(row.id) ?? 1) > 1;
    const entryKey =
      isSeries && row.gameNumber !== null ? `${row.id}|${row.gameNumber}` : row.id;

    if (!entryMap.has(entryKey)) {
      entryMap.set(entryKey, {
        id: entryKey,
        name: row.name,
        eventDate: row.eventDate,
        earliestGameTime: null,
        matchLabel: null,
        eventType: row.eventType,
        sportsSeasonId: row.sportsSeasonId,
        relevantParticipants: [],
      });
      participantsByEntry.set(entryKey, new Map());
      gameKeysByEntry.set(entryKey, new Set());
      isSeriesByEntry.set(entryKey, isSeries);
    }

    const pMap = participantsByEntry.get(entryKey);
    if (pMap) {
      if (row.participant1Id) {
        const p1 = draftedMap.get(row.participant1Id);
        if (p1) pMap.set(row.participant1Id, p1);
      }
      if (row.participant2Id) {
        const p2 = draftedMap.get(row.participant2Id);
        if (p2) pMap.set(row.participant2Id, p2);
      }
    }

    if (row.round && row.gameNumber !== null) {
      gameKeysByEntry.get(entryKey)?.add(`${row.round}|${row.gameNumber}`);
    }

    if (row.scheduledAt) {
      const prev = earliestTimeByEntry.get(entryKey);
      if (!prev || row.scheduledAt < prev) {
        earliestTimeByEntry.set(entryKey, row.scheduledAt);
      }
    }
  }

  for (const [entryKey, event] of entryMap) {
    event.relevantParticipants = Array.from(participantsByEntry.get(entryKey)?.values() ?? []);

    const earliest = earliestTimeByEntry.get(entryKey);
    event.earliestGameTime = earliest ? earliest.toISOString() : null;

    const isSeries = isSeriesByEntry.get(entryKey) ?? false;
    const gameKeys = gameKeysByEntry.get(entryKey) ?? new Set<string>();

    if (isSeries) {
      if (gameKeys.size >= 1) {
        const [round, gameNumberStr] = [...gameKeys][0].split("|");
        event.matchLabel =
          round === event.name ? `Game #${gameNumberStr}` : `${round} Game #${gameNumberStr}`;
      }
    } else {
      // Single-game: omit the game number, just show the round name.
      if (gameKeys.size === 1) {
        const [round] = [...gameKeys][0].split("|");
        event.matchLabel = round === event.name ? null : round;
      } else if (gameKeys.size > 1) {
        const rounds = new Set([...gameKeys].map((k) => k.split("|")[0]));
        if (rounds.size === 1) {
          const round = [...rounds][0];
          event.matchLabel = round === event.name ? null : round;
        }
      }
    }
  }

  return Array.from(entryMap.values());
}

export interface DashboardScoringEvent {
  id: string;
  name: string;
  eventDate: string | null;
  eventStartsAt: Date | null;
  eventType: string;
  isComplete: boolean;
  completedAt: Date | null;
  sportsSeasonId: string;
  sportsSeasonName: string;
  sportName: string;
  sportSlug: string | null;
}

/**
 * Get all scoring events (excluding schedule_event type) for the given dates,
 * joined with their sports season and sport info. Used for the admin dashboard
 * "Games to Score" widget.
 *
 * An event is included if EITHER its own eventDate falls on one of the given
 * dates, OR any of its playoffMatchGames has a scheduledAt timestamp on one of
 * those dates. This ensures bracket events where the admin set dates on
 * individual games (rather than the event itself) are still surfaced.
 */
export async function getEventsForDates(
  dates: string[],
  providedDb?: ReturnType<typeof database>
): Promise<DashboardScoringEvent[]> {
  if (dates.length === 0) return [];

  const db = providedDb || database();

  // Build timestamp bounds covering the full span of the requested dates so we
  // can range-check the playoffMatchGames.scheduledAt timestamp column.
  const sortedDates = [...dates].toSorted();
  const dateFromTimestamp = new Date(sortedDates[0] + "T00:00:00.000Z");
  const dateToTimestamp = new Date(sortedDates[sortedDates.length - 1] + "T23:59:59.999Z");

  // Step 1: collect distinct event IDs where the event date OR any game's
  // scheduledAt falls within the requested dates.
  const rows = await db
    .selectDistinct({ id: schema.scoringEvents.id })
    .from(schema.scoringEvents)
    .leftJoin(
      schema.playoffMatches,
      eq(schema.playoffMatches.scoringEventId, schema.scoringEvents.id)
    )
    .leftJoin(
      schema.playoffMatchGames,
      eq(schema.playoffMatchGames.playoffMatchId, schema.playoffMatches.id)
    )
    .where(
      and(
        inArray(schema.scoringEvents.eventType, [
          "playoff_game",
          "major_tournament",
          "final_standings",
        ]),
        or(
          inArray(schema.scoringEvents.eventDate, dates),
          and(
            isNotNull(schema.playoffMatchGames.scheduledAt),
            gte(schema.playoffMatchGames.scheduledAt, dateFromTimestamp),
            lte(schema.playoffMatchGames.scheduledAt, dateToTimestamp)
          )
        )
      )
    );

  if (rows.length === 0) return [];

  const eventIds = rows.map((r) => r.id);

  // Step 2: fetch the matched events with sport season + sport info.
  const events = await db.query.scoringEvents.findMany({
    where: inArray(schema.scoringEvents.id, eventIds),
    orderBy: [
      asc(schema.scoringEvents.eventDate),
      asc(schema.scoringEvents.eventStartsAt),
      asc(schema.scoringEvents.createdAt),
    ],
    with: {
      sportsSeason: {
        with: {
          sport: true,
        },
      },
    },
  });

  return events.map((e) => ({
    id: e.id,
    name: e.name,
    eventDate: e.eventDate,
    eventStartsAt: e.eventStartsAt,
    eventType: e.eventType,
    isComplete: e.isComplete,
    completedAt: e.completedAt,
    sportsSeasonId: e.sportsSeasonId,
    sportsSeasonName: e.sportsSeason.name,
    sportName: e.sportsSeason.sport.name,
    sportSlug: e.sportsSeason.sport.slug,
  }));
}

/**
 * Bulk create scoring events for a sports season.
 * Returns created events in order.
 */
export async function bulkCreateScoringEvents(
  sportsSeasonId: string,
  events: Array<{ name: string; eventDate?: Date; eventStartsAt?: Date; eventType: EventType; isQualifyingEvent?: boolean }>,
  providedDb?: ReturnType<typeof database>
) {
  const db = providedDb || database();

  if (events.length === 0) return [];

  const rows = events.map((e) => ({
    sportsSeasonId,
    name: e.name,
    eventDate: e.eventDate ? e.eventDate.toISOString().split("T")[0] : undefined,
    eventStartsAt: e.eventStartsAt,
    eventType: e.eventType,
    isQualifyingEvent: e.isQualifyingEvent ?? false,
    isComplete: false,
  }));

  return db.insert(schema.scoringEvents).values(rows).returning();
}
