import { eq, inArray, isNull, sql, lte, gte } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { getQPConfig, updateQPConfig } from "~/models/qualifying-points";
import { maybeResolveCompletedBracktForSportsSeason } from "~/services/brackt.server";

export async function countSportsSeasons(): Promise<number> {
  const db = database();
  const [{ count }] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(schema.sportsSeasons);
  return count;
}

export type SportsSeason = typeof schema.sportsSeasons.$inferSelect;
export type NewSportsSeason = typeof schema.sportsSeasons.$inferInsert;
export type SportsSeasonWithSport = SportsSeason & {
  sport: typeof schema.sports.$inferSelect;
};
export type SportsSeasonListItem = SportsSeasonWithSport & {
  participants: Array<{ id: string }>;
};
export type SportsSeasonStatus = "upcoming" | "active" | "completed";
export type ScoringType = "playoffs" | "regular_season" | "majors";

export async function createSportsSeason(data: NewSportsSeason): Promise<SportsSeason> {
  const db = database();
  const [sportsSeason] = await db
    .insert(schema.sportsSeasons)
    .values(data)
    .returning();
  return sportsSeason;
}

export async function findSportsSeasonById(id: string): Promise<SportsSeasonWithSport | undefined> {
  const db = database();
  return await db.query.sportsSeasons.findFirst({
    where: eq(schema.sportsSeasons.id, id),
    with: {
      sport: true,
    },
  }) as SportsSeasonWithSport | undefined;
}

export async function findSportsSeasonsByIds(ids: string[]): Promise<SportsSeasonWithSport[]> {
  if (ids.length === 0) return [];
  const db = database();
  return await db.query.sportsSeasons.findMany({
    where: inArray(schema.sportsSeasons.id, ids),
    with: { sport: true },
  }) as SportsSeasonWithSport[];
}

export async function findSportsSeasonsBySportId(sportId: string): Promise<SportsSeasonWithSport[]> {
  const db = database();
  return await db.query.sportsSeasons.findMany({
    where: eq(schema.sportsSeasons.sportId, sportId),
    orderBy: (sportsSeasons, { desc }) => [desc(sportsSeasons.year)],
    with: {
      sport: true,
    },
  }) as SportsSeasonWithSport[];
}

export async function findActiveSportsSeasonsBySportId(sportId: string): Promise<SportsSeason[]> {
  const db = database();
  return await db.query.sportsSeasons.findMany({
    where: (ss, { and }) =>
      and(
        eq(ss.sportId, sportId),
        eq(ss.status, "active")
      ),
    orderBy: (sportsSeasons, { desc }) => [desc(sportsSeasons.year)],
    with: {
      sport: true,
    },
  });
}

export async function findSportsSeasonsByYear(year: number): Promise<SportsSeason[]> {
  const db = database();
  return await db.query.sportsSeasons.findMany({
    where: eq(schema.sportsSeasons.year, year),
    orderBy: (sportsSeasons, { asc }) => [asc(sportsSeasons.name)],
    with: {
      sport: true,
    },
  });
}

export async function findSportsSeasonsByStatus(status: SportsSeasonStatus): Promise<SportsSeason[]> {
  const db = database();
  return await db.query.sportsSeasons.findMany({
    where: eq(schema.sportsSeasons.status, status),
    orderBy: (sportsSeasons, { desc }) => [desc(sportsSeasons.year)],
    with: {
      sport: true,
    },
  });
}

export async function findAllSportsSeasons(): Promise<SportsSeasonListItem[]> {
  const db = database();
  const results = await db.query.sportsSeasons.findMany({
    with: {
      sport: true,
      participants: {
        columns: {
          id: true,
        },
      },
    },
  });

  const statusOrder: Record<SportsSeasonStatus, number> = { active: 0, upcoming: 1, completed: 2 };

  return results.toSorted((a, b) => {
    const statusDiff = statusOrder[a.status] - statusOrder[b.status];
    if (statusDiff !== 0) return statusDiff;
    const sportNameDiff = a.sport.name.localeCompare(b.sport.name);
    if (sportNameDiff !== 0) return sportNameDiff;
    return a.year - b.year;
  });
}

export async function findAllAdminSportsSeasons(): Promise<SportsSeasonListItem[]> {
  const seasons = await findAllSportsSeasons();
  // Hide per-league private copies (fantasySeasonId IS NOT NULL); show the global template
  return seasons.filter((season) => season.fantasySeasonId === null);
}

export async function findDraftableSportsSeasons() {
  const db = database();
  const today = sql`CURRENT_DATE`;
  const seasons = await db.query.sportsSeasons.findMany({
    where: (ss, { and }) =>
      and(
        lte(ss.draftOn, today),
        gte(ss.draftOff, today),
        isNull(ss.fantasySeasonId)
      ),
    orderBy: (sportsSeasons, { desc, asc }) => [desc(sportsSeasons.year), asc(sportsSeasons.name)],
    with: {
      sport: true,
      participants: { columns: { id: true } },
    },
  });
  return seasons.map(({ participants, ...s }) => ({
    ...s,
    participantCount: participants.length,
  }));
}

export async function updateSportsSeason(
  id: string,
  data: Partial<NewSportsSeason>
): Promise<SportsSeason> {
  const db = database();
  const previous = await db.query.sportsSeasons.findFirst({
    where: eq(schema.sportsSeasons.id, id),
    columns: { status: true },
  });
  const [sportsSeason] = await db
    .update(schema.sportsSeasons)
    .set({ ...data, updatedAt: new Date() })
    .where(eq(schema.sportsSeasons.id, id))
    .returning();

  if (previous && previous.status !== "completed" && sportsSeason.status === "completed") {
    await maybeResolveCompletedBracktForSportsSeason(sportsSeason.id, db);
  }

  return sportsSeason;
}

export async function updateSportsSeasonStatus(
  id: string,
  status: SportsSeasonStatus
): Promise<SportsSeason> {
  return await updateSportsSeason(id, { status });
}

export async function deleteSportsSeason(id: string): Promise<void> {
  const db = database();
  await db.delete(schema.sportsSeasons).where(eq(schema.sportsSeasons.id, id));
}

export function shiftDateByYears(dateStr: string, years: number): string {
  const d = new Date(dateStr + "T12:00:00Z");
  d.setUTCFullYear(d.getUTCFullYear() + years);
  return d.toISOString().split("T")[0];
}

function shiftTimestampByYears(date: Date, years: number): Date {
  const d = new Date(date);
  d.setUTCFullYear(d.getUTCFullYear() + years);
  return d;
}

/**
 * Clone a sports season: creates a new season from newData, then copies
 * participants (name/shortName/externalId only), simulator structure/config,
 * scoring events (dates shifted by yearDelta), and QP config. Volatile simulator
 * inputs such as futures odds and Elo ratings are copied only when explicitly
 * requested.
 */
export async function cloneSportsSeason(
  sourceId: string,
  newData: NewSportsSeason,
  options: { copySimulatorInputs?: boolean } = {}
): Promise<SportsSeason> {
  const db = database();

  const sourceSeason = await db.query.sportsSeasons.findFirst({
    where: eq(schema.sportsSeasons.id, sourceId),
  });

  if (!sourceSeason) {
    throw new Error(`Source season ${sourceId} not found`);
  }

  const yearDelta = newData.year - sourceSeason.year;

  // Merge ELO calibration fields from source (form doesn't expose them)
  const mergedData: NewSportsSeason = {
    eloCalibrationExponent: sourceSeason.eloCalibrationExponent,
    eloMinRating: sourceSeason.eloMinRating,
    eloMaxRating: sourceSeason.eloMaxRating,
    ...newData,
  };

  // Read all source data before writing anything
  const sourceParticipants = await db.query.seasonParticipants.findMany({
    where: eq(schema.seasonParticipants.sportsSeasonId, sourceId),
  });

  const sourceEvents = await db.query.scoringEvents.findMany({
    where: eq(schema.scoringEvents.sportsSeasonId, sourceId),
  });

  const sourceQPConfig =
    newData.scoringPattern === "qualifying_points"
      ? await getQPConfig(sourceId, db)
      : null;

  const sourceSimulatorConfig = await db.query.sportsSeasonSimulatorConfigs.findFirst({
    where: eq(schema.sportsSeasonSimulatorConfigs.sportsSeasonId, sourceId),
  });

  const sourceEvRows = options.copySimulatorInputs
    ? await db.query.seasonParticipantExpectedValues.findMany({
        where: eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sourceId),
      })
    : [];

  const sourceSimulatorInputs = options.copySimulatorInputs
    ? await db.query.seasonParticipantSimulatorInputs.findMany({
        where: eq(schema.seasonParticipantSimulatorInputs.sportsSeasonId, sourceId),
      })
    : [];

  // Build lookup: (externalId ?? name) → source EV, only for rows that have
  // actual input data (odds or Elo) worth carrying forward
  type SourceEv = typeof sourceEvRows[number];
  const sourceEvById = new Map(sourceEvRows.map((r) => [r.participantId, r]));
  const sourceEvByKey = new Map<string, SourceEv>();
  for (const p of sourceParticipants) {
    const ev = sourceEvById.get(p.id);
    if (ev && (ev.sourceOdds !== null || ev.sourceElo !== null)) {
      sourceEvByKey.set(p.externalId ?? p.name, ev);
    }
  }

  // All writes in a single transaction — partial clone is never committed
  return await db.transaction(async (tx: typeof db) => {
    const [newSeason] = await tx
      .insert(schema.sportsSeasons)
      .values(mergedData)
      .returning();

    const newParticipants =
      sourceParticipants.length > 0
        ? await tx
            .insert(schema.seasonParticipants)
            .values(
              sourceParticipants.map((p: typeof sourceParticipants[number]) => ({
                sportsSeasonId: newSeason.id,
                name: p.name,
                shortName: p.shortName,
                externalId: p.externalId,
                participantId: p.participantId,
              }))
            )
            .returning()
        : [];

    // Copy futures odds / Elo stubs matched by externalId then name
    if (sourceSimulatorConfig) {
      await tx.insert(schema.sportsSeasonSimulatorConfigs).values({
        sportsSeasonId: newSeason.id,
        simulatorType: sourceSimulatorConfig.simulatorType,
        config: sourceSimulatorConfig.config,
      });
    }

    if (options.copySimulatorInputs && sourceEvByKey.size > 0 && newParticipants.length > 0) {
      const now = new Date();
      const evRows = newParticipants
        .map((newP: typeof newParticipants[number]) => {
          const key = newP.externalId ?? newP.name;
          const sourceEv = sourceEvByKey.get(key);
          if (!sourceEv) return null;
          return {
            participantId: newP.id,
            sportsSeasonId: newSeason.id,
            probFirst: "0", probSecond: "0", probThird: "0", probFourth: "0",
            probFifth: "0", probSixth: "0", probSeventh: "0", probEighth: "0",
            expectedValue: "0",
            source: sourceEv.source,
            sourceOdds: sourceEv.sourceOdds,
            sourceElo: sourceEv.sourceElo,
            worldRanking: sourceEv.worldRanking,
            calculatedAt: now,
            updatedAt: now,
          };
        })
        .filter((r): r is NonNullable<typeof r> => r !== null);

      if (evRows.length > 0) {
        await tx.insert(schema.seasonParticipantExpectedValues).values(evRows);
      }
    }

    if (options.copySimulatorInputs && sourceSimulatorInputs.length > 0 && newParticipants.length > 0) {
      const sourceParticipantById = new Map(sourceParticipants.map((p) => [p.id, p]));
      const sourceInputByKey = new Map(
        sourceSimulatorInputs.flatMap((input) => {
          const sourceParticipant = sourceParticipantById.get(input.participantId);
          return sourceParticipant ? [[sourceParticipant.externalId ?? sourceParticipant.name, input] as const] : [];
        })
      );
      const simulatorInputRows = newParticipants
        .map((newP: typeof newParticipants[number]) => {
          const sourceInput = sourceInputByKey.get(newP.externalId ?? newP.name);
          if (!sourceInput) return null;
          return {
            participantId: newP.id,
            sportsSeasonId: newSeason.id,
            sourceOdds: sourceInput.sourceOdds,
            sourceElo: sourceInput.sourceElo,
            worldRanking: sourceInput.worldRanking,
            rating: sourceInput.rating,
            projectedWins: sourceInput.projectedWins,
            projectedTablePoints: sourceInput.projectedTablePoints,
            seed: sourceInput.seed,
            region: sourceInput.region,
            metadata: sourceInput.metadata,
          };
        })
        .filter((row): row is NonNullable<typeof row> => row !== null);

      if (simulatorInputRows.length > 0) {
        await tx.insert(schema.seasonParticipantSimulatorInputs).values(simulatorInputRows);
      }
    }

    if (sourceEvents.length > 0) {
      await tx.insert(schema.scoringEvents).values(
        sourceEvents.map((e: typeof sourceEvents[number]) => ({
          sportsSeasonId: newSeason.id,
          name: e.name,
          eventType: e.eventType,
          isQualifyingEvent: e.isQualifyingEvent,
          // Carries over the source season's tournament IDs. For qualifying_points seasons
          // cloned into a new year, qualifying events will still point to the old year's
          // canonical tournaments — admin should delete and recreate them once the new
          // year's tournaments exist.
          tournamentId: e.tournamentId ?? null,
          bracketTemplateId: e.bracketTemplateId,
          scoringStartsAtRound: e.scoringStartsAtRound,
          isComplete: false,
          eventDate: e.eventDate ? shiftDateByYears(e.eventDate, yearDelta) : null,
          eventStartsAt: e.eventStartsAt ? shiftTimestampByYears(e.eventStartsAt, yearDelta) : null,
        }))
      );
    }

    if (sourceQPConfig) {
      await updateQPConfig(
        newSeason.id,
        sourceQPConfig.map((qp: typeof sourceQPConfig[number]) => ({
          placement: qp.placement,
          points: parseFloat(qp.points),
        })),
        tx as ReturnType<typeof database>
      );
    }

    return newSeason;
  });
}
