import { eq, inArray, count, and, sql, asc, desc } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";

export type Participant = typeof schema.seasonParticipants.$inferSelect;
export type NewParticipant = typeof schema.seasonParticipants.$inferInsert;

export async function createParticipant(data: NewParticipant): Promise<Participant> {
  const db = database();

  // Auto-link to canonical participant (by sportId + name) if caller didn't
  // supply one. Needed so syncTournamentResults can match this roster entry
  // to canonical tournament_results; without the link the participant is
  // invisible to the canonical layer.
  let canonicalId = data.participantId;
  if (!canonicalId) {
    const season = await db.query.sportsSeasons.findFirst({
      where: eq(schema.sportsSeasons.id, data.sportsSeasonId),
      columns: { sportId: true },
    });
    if (season) {
      const existing = await db.query.participants.findFirst({
        where: and(
          eq(schema.participants.sportId, season.sportId),
          eq(schema.participants.name, data.name),
        ),
      });
      if (existing) {
        canonicalId = existing.id;
      } else {
        const [created] = await db
          .insert(schema.participants)
          .values({ sportId: season.sportId, name: data.name })
          .returning();
        canonicalId = created.id;
      }
    }
  }

  const [participant] = await db
    .insert(schema.seasonParticipants)
    .values({ ...data, participantId: canonicalId })
    .returning();
  return participant;
}

export async function createManyParticipants(data: NewParticipant[]): Promise<Participant[]> {
  const db = database();
  if (data.length === 0) return [];

  // Build canonical links per row, just like createParticipant does.
  const seasonIds = Array.from(new Set(data.map((d) => d.sportsSeasonId)));
  const seasons = await db.query.sportsSeasons.findMany({
    where: inArray(schema.sportsSeasons.id, seasonIds),
    columns: { id: true, sportId: true },
  });
  const sportBySeason = new Map(seasons.map((s) => [s.id, s.sportId]));

  const enriched: NewParticipant[] = [];
  for (const row of data) {
    if (row.participantId) {
      enriched.push(row);
      continue;
    }
    const sportId = sportBySeason.get(row.sportsSeasonId);
    if (!sportId) {
      enriched.push(row);
      continue;
    }
    const existing = await db.query.participants.findFirst({
      where: and(
        eq(schema.participants.sportId, sportId),
        eq(schema.participants.name, row.name),
      ),
    });
    let canonicalId = existing?.id;
    if (!canonicalId) {
      const [created] = await db
        .insert(schema.participants)
        .values({ sportId, name: row.name })
        .returning();
      canonicalId = created.id;
    }
    enriched.push({ ...row, participantId: canonicalId });
  }

  return await db
    .insert(schema.seasonParticipants)
    .values(enriched)
    .returning();
}

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

export async function findParticipantByName(
  sportsSeasonId: string,
  name: string
): Promise<Participant | undefined> {
  const db = database();
  const results = await db
    .select()
    .from(schema.seasonParticipants)
    .where(
      and(
        eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
        sql`lower(${schema.seasonParticipants.name}) = lower(${name})`
      )
    )
    .limit(1);
  return results[0];
}

export async function findParticipantsBySportsSeasonId(sportsSeasonId: string): Promise<Participant[]> {
  const db = database();
  return await db.query.seasonParticipants.findMany({
    where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
    orderBy: (participants, { asc: orderAsc }) => [orderAsc(participants.name)],
  });
}

export async function findParticipantsByExternalId(
  externalId: string
): Promise<Participant[]> {
  const db = database();
  return await db.query.seasonParticipants.findMany({
    where: eq(schema.seasonParticipants.externalId, externalId),
    with: {
      sportsSeason: {
        with: {
          sport: true,
        },
      },
    },
  });
}

export async function updateParticipant(
  id: string,
  data: Partial<NewParticipant>
): Promise<Participant> {
  const db = database();
  const [participant] = await db
    .update(schema.seasonParticipants)
    .set({ ...data, updatedAt: new Date() })
    .where(eq(schema.seasonParticipants.id, id))
    .returning();
  return participant;
}

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

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

export interface CopyParticipantsResult {
  copied: number;
  skipped: number;
  evStubsCopied: number;
}

/**
 * Copy participants from one sports season to another, skipping duplicates by
 * name. Also copies EV stubs (sourceOdds, sourceElo, worldRanking) so that
 * running a simulation immediately after copying produces differentiated
 * results.
 *
 * Canonical skill data (golf SG:Total, tennis surface Elos) is automatically
 * available through the canonical participant link — no copying needed.
 */
export async function copyParticipantsFromSeason(
  sourceSportsSeasonId: string,
  targetSportsSeasonId: string,
): Promise<CopyParticipantsResult> {
  const db = database();

  const targetParticipants = await findParticipantsBySportsSeasonId(targetSportsSeasonId);
  const targetNames = new Set(targetParticipants.map((p) => p.name.toLowerCase()));

  const sourceParticipants = await findParticipantsBySportsSeasonId(sourceSportsSeasonId);
  const toCopy = sourceParticipants.filter((p) => !targetNames.has(p.name.toLowerCase()));

  if (toCopy.length === 0) {
    return { copied: 0, skipped: sourceParticipants.length, evStubsCopied: 0 };
  }

  const sourceEvRows = await db.query.seasonParticipantExpectedValues.findMany({
    where: eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sourceSportsSeasonId),
  });
  const sourceEvByKey = new Map(sourceEvRows.filter((r) => r.sourceOdds !== null || r.sourceElo !== null).map((r) => {
    const sp = sourceParticipants.find((p) => p.id === r.participantId);
    return sp ? [sp.externalId ?? sp.name, r] as const : null;
  }).filter((x): x is NonNullable<typeof x> => x !== null));

  return await db.transaction(async (tx) => {
    const newParticipants = await tx
      .insert(schema.seasonParticipants)
      .values(
        toCopy.map((p) => ({
          sportsSeasonId: targetSportsSeasonId,
          name: p.name,
          shortName: p.shortName,
          externalId: p.externalId,
          participantId: p.participantId,
        })),
      )
      .returning();

    let evStubsCopied = 0;
    if (sourceEvByKey.size > 0 && newParticipants.length > 0) {
      const now = new Date();
      const evRows = newParticipants
        .map((newP) => {
          const key = newP.externalId ?? newP.name;
          const sourceEv = sourceEvByKey.get(key);
          if (!sourceEv) return null;
          return {
            participantId: newP.id,
            sportsSeasonId: targetSportsSeasonId,
            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);
        evStubsCopied = evRows.length;
      }
    }

    return {
      copied: newParticipants.length,
      skipped: sourceParticipants.length - toCopy.length,
      evStubsCopied,
    };
  });
}

/**
 * Get all participants for a season with sport information
 * Used for draft eligibility calculations
 */
export async function getParticipantsForSeasonWithSports(seasonId: string, providedDb?: ReturnType<typeof database>) {
  const db = providedDb || database();

  // First get all sports seasons for this season
  const seasonSports = await db.query.seasonSports.findMany({
    where: eq(schema.seasonSports.seasonId, seasonId),
  });

  if (seasonSports.length === 0) {
    return [];
  }

  const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId);

  // Get all participants for these sports seasons with sport info
  const participants = await db
    .select({
      id: schema.seasonParticipants.id,
      name: schema.seasonParticipants.name,
      sport: {
        id: schema.sports.id,
        name: schema.sports.name,
      },
    })
    .from(schema.seasonParticipants)
    .innerJoin(
      schema.sportsSeasons,
      eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id)
    )
    .innerJoin(
      schema.sports,
      eq(schema.sportsSeasons.sportId, schema.sports.id)
    )
    .where(
      sportsSeasonIds.length === 1
        ? eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds[0])
        : inArray(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds)
    );

  return participants;
}

export async function getDraftParticipants(seasonId: string) {
  const db = database();

  const seasonSports = await db.query.seasonSports.findMany({
    where: eq(schema.seasonSports.seasonId, seasonId),
  });

  const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId);
  if (sportsSeasonIds.length === 0) return [];

  return await db
    .select({
      id: schema.seasonParticipants.id,
      name: schema.seasonParticipants.name,
      vorpValue: schema.seasonParticipants.vorpValue,
      sport: schema.sports,
    })
    .from(schema.seasonParticipants)
    .innerJoin(schema.sportsSeasons, eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id))
    .innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id))
    .where(
      sportsSeasonIds.length === 1
        ? eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds[0])
        : inArray(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds)
    )
    .orderBy(desc(schema.seasonParticipants.vorpValue), asc(schema.seasonParticipants.name));
}
