import type { ProviderEvent, ProviderSport, SportsDataProvider } from "@/server/providers/odds/types";

type TheOddsApiConfig = {
  baseUrl: string;
  apiKey: string;
  bookmakers: string[];
  regions: string[];
  markets: string[];
  sportKeys: string[];
  preMatchWindowDays: number;
  fetchAdvancedMarketsOnList: boolean;
  pricing: OddsPricingConfig;
};

type OddsPricingConfig = {
  sourceBookmaker: string;
  upliftPercent: number;
  minMarginPercent: number;
  maxOdds: number;
};

type TheOddsSport = {
  key: string;
  group: string;
  title: string;
  description: string;
  active: boolean;
  has_outrights: boolean;
};

type TheOddsOutcome = {
  name: string;
  price: number;
  point?: number;
  description?: string;
};

type TheOddsMarket = {
  key: "h2h" | "spreads" | "totals" | string;
  last_update?: string;
  outcomes: TheOddsOutcome[];
};

type TheOddsBookmaker = {
  key: string;
  title: string;
  last_update: string;
  markets: TheOddsMarket[];
};

type TheOddsEvent = {
  id: string;
  sport_key: string;
  sport_title: string;
  commence_time: string;
  home_team: string;
  away_team: string;
  bookmakers: TheOddsBookmaker[];
};

const SPORT_NAMES: Record<string, string> = {
  football: "Football",
  tennis: "Tennis",
  basketball: "Basketball",
  hockey: "Hockey",
  rugby: "Rugby",
  mma: "MMA",
  baseball: "Baseball",
  "american-football": "Football US",
  cricket: "Cricket",
  volleyball: "Volleyball",
  handball: "Handball",
  esports: "Esports"
};

const BULK_MARKETS = new Set(["h2h", "spreads", "totals"]);
const DEFAULT_BULK_MARKETS = ["h2h", "spreads", "totals"];
const EVENT_MARKET_CHUNK_SIZE = 6;
const EVENT_DETAIL_CONCURRENCY = 3;
const SPORT_FETCH_CONCURRENCY = 4;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;

const MARKET_LABELS: Record<string, { name: string; type: string }> = {
  h2h_3_way: { name: "1X2", type: "THREE_WAY" },
  double_chance: { name: "Double chance", type: "DOUBLE_CHANCE" },
  btts: { name: "Les deux équipes marquent", type: "BOOLEAN" },
  btts_h1: { name: "Les deux équipes marquent - 1re mi-temps", type: "BOOLEAN" },
  btts_h2: { name: "Les deux équipes marquent - 2e mi-temps", type: "BOOLEAN" },
  draw_no_bet: { name: "Remboursé si nul", type: "DRAW_NO_BET" },
  correct_score: { name: "Score exact", type: "CORRECT_SCORE" },
  team_totals: { name: "Total équipe", type: "TEAM_TOTAL" },
  alternate_team_totals: { name: "Total équipe alternatif", type: "TEAM_TOTAL" },
  alternate_spreads: { name: "Handicap alternatif", type: "HANDICAP" },
  alternate_totals: { name: "Total alternatif", type: "TOTAL" },
  h2h_h1: { name: "Résultat 1re mi-temps", type: "THREE_WAY" },
  spreads_h1: { name: "Handicap 1re mi-temps", type: "HANDICAP" },
  totals_h1: { name: "Total 1re mi-temps", type: "TOTAL" },
  h2h_h2: { name: "Résultat 2e mi-temps", type: "THREE_WAY" },
  spreads_h2: { name: "Handicap 2e mi-temps", type: "HANDICAP" },
  totals_h2: { name: "Total 2e mi-temps", type: "TOTAL" },
  corners: { name: "Corners", type: "CORNERS" },
  alternate_corners: { name: "Corners alternatifs", type: "CORNERS" },
  cards: { name: "Cartons", type: "CARDS" },
  alternate_cards: { name: "Cartons alternatifs", type: "CARDS" },
  player_points: { name: "Points joueur", type: "PLAYER_PROPS" },
  player_rebounds: { name: "Rebonds joueur", type: "PLAYER_PROPS" },
  player_assists: { name: "Passes décisives joueur", type: "PLAYER_PROPS" },
  player_threes: { name: "3 points joueur", type: "PLAYER_PROPS" },
  player_blocks: { name: "Contres joueur", type: "PLAYER_PROPS" },
  player_steals: { name: "Interceptions joueur", type: "PLAYER_PROPS" },
  player_turnovers: { name: "Balles perdues joueur", type: "PLAYER_PROPS" },
  player_points_rebounds_assists: { name: "Points + rebonds + passes", type: "PLAYER_PROPS" },
  player_goals: { name: "But joueur", type: "PLAYER_PROPS" },
  player_shots: { name: "Tirs joueur", type: "PLAYER_PROPS" },
  player_shots_on_goal: { name: "Tirs cadrés joueur", type: "PLAYER_PROPS" },
  player_shots_on_target: { name: "Tirs cadrés joueur", type: "PLAYER_PROPS" },
  player_tackles: { name: "Tacles joueur", type: "PLAYER_PROPS" },
  player_cards: { name: "Carton joueur", type: "PLAYER_PROPS" },
  player_saves: { name: "Arrêts gardien", type: "PLAYER_PROPS" },
  player_pass_tds: { name: "Touchdowns passe", type: "PLAYER_PROPS" },
  player_pass_yds: { name: "Yards à la passe", type: "PLAYER_PROPS" },
  player_rush_yds: { name: "Yards à la course", type: "PLAYER_PROPS" },
  player_receptions: { name: "Réceptions joueur", type: "PLAYER_PROPS" },
  player_reception_yds: { name: "Yards réception", type: "PLAYER_PROPS" }
};

export class TheOddsApiProvider implements SportsDataProvider {
  private sportsPromise?: Promise<TheOddsSport[]>;

  constructor(private readonly config: TheOddsApiConfig) {}

  async listSports(): Promise<ProviderSport[]> {
    const sports = await this.fetchSports();
    const bySlug = new Map<string, ProviderSport>();

    for (const sport of sports.filter((item) => item.active && !item.has_outrights)) {
      const slug = mapSportSlug(sport);
      bySlug.set(slug, {
        externalId: `the-odds-api:${sport.group}:${slug}`,
        slug,
        name: SPORT_NAMES[slug] ?? titleCase(slug)
      });
    }

    return [...bySlug.values()].sort((a, b) => a.name.localeCompare(b.name));
  }

  async listEvents(input?: { liveOnly?: boolean; sportSlug?: string }): Promise<ProviderEvent[]> {
    if (input?.liveOnly) {
      return [];
    }

    const sports = await this.fetchSports();
    const sportsByKey = new Map(sports.map((sport) => [sport.key, sport]));
    const sportKeys = resolveSportKeys(this.config.sportKeys, sports, input?.sportSlug);
    const advancedMarkets = this.config.fetchAdvancedMarketsOnList ? this.config.markets.filter((market) => !BULK_MARKETS.has(market)) : [];
    const events: ProviderEvent[] = [];

    const perSportEvents = await mapWithConcurrency(sportKeys, SPORT_FETCH_CONCURRENCY, async (sportKey) => {
      let oddsEvents: TheOddsEvent[];
      try {
        oddsEvents = await this.fetchOdds(sportKey);
      } catch {
        return [];
      }

      const upcomingEvents = oddsEvents.filter((oddsEvent) => new Date(oddsEvent.commence_time).getTime() > Date.now());
      const normalizedEvents = await mapWithConcurrency(upcomingEvents, EVENT_DETAIL_CONCURRENCY, async (oddsEvent) => {
        const eventWithMarkets =
          advancedMarkets.length > 0
            ? mergeEventBookmakers(oddsEvent, await this.fetchEventMarketChunks(sportKey, oddsEvent.id, advancedMarkets))
            : oddsEvent;
        const sport = sportsByKey.get(oddsEvent.sport_key);
        const normalized = normalizeEvent(eventWithMarkets, this.config, sport);

        if (!normalized) return null;
        if (input?.sportSlug && normalized.sportSlug !== input.sportSlug) return null;

        return normalized;
      });

      return normalizedEvents.filter((event): event is ProviderEvent => Boolean(event));
    });

    events.push(...perSportEvents.flat());

    return dedupeEvents(events);
  }

  async getEventByExternalId(externalId: string, input?: { sportSlug?: string }): Promise<ProviderEvent | null> {
    const eventId = parseExternalEventId(externalId);
    if (!eventId) return null;

    const sports = await this.fetchSports();
    const sportsByKey = new Map(sports.map((sport) => [sport.key, sport]));
    const sportKeys = resolveSportKeys(this.config.sportKeys, sports, input?.sportSlug);
    const bulkMarkets = this.config.markets.filter((market) => BULK_MARKETS.has(market));
    const advancedMarkets = this.config.markets.filter((market) => !BULK_MARKETS.has(market));

    for (const sportKey of sportKeys) {
      try {
        const baseEvent = await this.fetchEventMarkets(sportKey, eventId, bulkMarkets.length > 0 ? bulkMarkets : DEFAULT_BULK_MARKETS);
        if (new Date(baseEvent.commence_time).getTime() <= Date.now()) return null;

        const eventWithMarkets =
          advancedMarkets.length > 0
            ? mergeEventBookmakers(baseEvent, await this.fetchEventMarketChunks(sportKey, eventId, advancedMarkets))
            : baseEvent;

        return normalizeEvent(eventWithMarkets, this.config, sportsByKey.get(eventWithMarkets.sport_key));
      } catch {
        // The event endpoint requires the exact sport key; try the next matching key for this sport slug.
      }
    }

    return null;
  }

  async healthCheck() {
    const started = Date.now();
    const sports = await this.fetchSports();
    return {
      ok: sports.length > 0,
      latencyMs: Date.now() - started,
      message: `the-odds-api sports=${sports.length} bookmakers=${this.config.bookmakers.join(",")}`
    };
  }

  private async fetchSports() {
    if (!this.sportsPromise) {
      this.sportsPromise = this.request<TheOddsSport[]>("/v4/sports", { all: "false" });
    }
    return this.sportsPromise;
  }

  private async fetchOdds(sportKey: string) {
    const bulkMarkets = this.config.markets.filter((market) => BULK_MARKETS.has(market));
    const now = new Date();
    const windowEnd = new Date(now.getTime() + this.config.preMatchWindowDays * ONE_DAY_MS);
    const params: Record<string, string> = {
      markets: (bulkMarkets.length > 0 ? bulkMarkets : DEFAULT_BULK_MARKETS).join(","),
      oddsFormat: "decimal",
      dateFormat: "iso",
      commenceTimeFrom: toTheOddsApiIso(now),
      commenceTimeTo: toTheOddsApiIso(windowEnd)
    };

    const bookmakers = bookmakerPriority(this.config);
    if (bookmakers.length > 0) {
      params.bookmakers = bookmakers.join(",");
    } else {
      params.regions = this.config.regions.join(",");
    }

    return this.request<TheOddsEvent[]>(`/v4/sports/${sportKey}/odds`, params);
  }

  private async fetchEventMarketChunks(sportKey: string, eventId: string, markets: string[]) {
    const events: TheOddsEvent[] = [];

    for (const chunk of chunkMarkets(markets, EVENT_MARKET_CHUNK_SIZE)) {
      try {
        events.push(await this.fetchEventMarkets(sportKey, eventId, chunk));
      } catch {
        for (const market of chunk) {
          try {
            events.push(await this.fetchEventMarkets(sportKey, eventId, [market]));
          } catch {
            // Some sports or API plans do not expose every requested market. Keep the main pre-match sync alive.
          }
        }
      }
    }

    return events;
  }

  private async fetchEventMarkets(sportKey: string, eventId: string, markets: string[]) {
    const params: Record<string, string> = {
      markets: markets.join(","),
      oddsFormat: "decimal",
      dateFormat: "iso"
    };
    const bookmakers = bookmakerPriority(this.config);
    if (bookmakers.length > 0) {
      params.bookmakers = bookmakers.join(",");
    } else {
      params.regions = this.config.regions.join(",");
    }

    return this.request<TheOddsEvent>(`/v4/sports/${sportKey}/events/${eventId}/odds`, params);
  }

  private async request<T>(path: string, params: Record<string, string>) {
    const url = new URL(path, this.config.baseUrl);
    url.searchParams.set("apiKey", this.config.apiKey);
    for (const [name, value] of Object.entries(params)) {
      if (value) url.searchParams.set(name, value);
    }

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 8_000);

    try {
      const response = await fetch(url, {
        headers: { accept: "application/json" },
        cache: "no-store",
        signal: controller.signal
      });

      if (!response.ok) {
        const body = await response.text();
        throw new Error(`The Odds API request failed: ${response.status} ${body.slice(0, 180)}`);
      }

      return response.json() as Promise<T>;
    } finally {
      clearTimeout(timeout);
    }
  }
}

function resolveSportKeys(configuredKeys: string[], sports: TheOddsSport[], sportSlug?: string) {
  const activeSports = sports.filter((sport) => sport.active && !sport.has_outrights);

  if (sportSlug) {
    const matchingKeys = activeSports.filter((sport) => mapSportSlug(sport) === sportSlug).map((sport) => sport.key);
    return matchingKeys.length > 0 ? matchingKeys : [sportSlug];
  }

  if (configuredKeys.includes("auto")) {
    return activeSports.map((sport) => sport.key);
  }

  if (configuredKeys.length > 0) {
    return unique(
      configuredKeys.flatMap((key) => {
        if (activeSports.some((sport) => sport.key === key)) return [key];
        const matchingKeys = activeSports.filter((sport) => mapSportSlug(sport) === key).map((sport) => sport.key);
        return matchingKeys.length > 0 ? matchingKeys : [key];
      })
    );
  }

  return ["upcoming"];
}

function normalizeEvent(event: TheOddsEvent, config: TheOddsApiConfig, sport?: TheOddsSport): ProviderEvent | null {
  const priorities = bookmakerPriority(config);
  const markets = orderedMarketKeys(event.bookmakers, config.markets).flatMap((marketKey) => {
    const picked = pickBookmakerMarket(event.bookmakers, marketKey, priorities);
    if (!picked) return [];
    return normalizeMarket(priceMarket(picked.market, config.pricing), event);
  });
  const enrichedMarkets = appendDerivedMarkets(dedupeProviderMarkets(markets), event, config);
  if (enrichedMarkets.length === 0) return null;

  return {
    externalId: `the-odds-api:${event.id}`,
    sportSlug: mapSportSlug(sport ?? event),
    competition: event.sport_title,
    country: inferCountry(event.sport_title, sport?.group),
    homeName: event.home_team,
    awayName: event.away_team,
    startsAt: event.commence_time,
    isLive: false,
    markets: enrichedMarkets
  };
}

function bookmakerPriority(config: Pick<TheOddsApiConfig, "bookmakers" | "pricing">) {
  return unique([config.pricing.sourceBookmaker, ...config.bookmakers].filter(Boolean));
}

function normalizeMarket(market: TheOddsMarket, event: TheOddsEvent): ProviderEvent["markets"] {
  if (market.key === "h2h" || market.key === "h2h_3_way") return normalizeHeadToHeadMarket(market, event);
  if (market.key === "h2h_h1") return normalizeHeadToHeadMarket(market, event, "1x2_1re_mi_temps", "1X2 - 1re mi-temps");
  if (market.key === "h2h_h2") return normalizeHeadToHeadMarket(market, event, "1x2_2e_mi_temps", "1X2 - 2e mi-temps");
  if (market.key === "double_chance") return normalizeDoubleChanceMarket(market, event);
  if (market.key === "spreads") return normalizeSpreadMarket(market, event, "handicap", "Handicap");
  if (market.key === "spreads_h1") return normalizeSpreadMarket(market, event, "handicap_1re_mi_temps", "Handicap - 1re mi-temps");
  if (market.key === "spreads_h2") return normalizeSpreadMarket(market, event, "handicap_2e_mi_temps", "Handicap - 2e mi-temps");
  if (market.key === "alternate_spreads") return normalizeSpreadMarket(market, event, "handicap_alternatif", "Handicap alternatif");
  if (market.key === "totals") return normalizeTotalsMarket(market, "total", "Total");
  if (market.key === "totals_h1") return normalizeTotalsMarket(market, "total_1re_mi_temps", "Total - 1re mi-temps");
  if (market.key === "totals_h2") return normalizeTotalsMarket(market, "total_2e_mi_temps", "Total - 2e mi-temps");
  if (market.key === "alternate_totals") return normalizeTotalsMarket(market, "total_alternatif", "Total alternatif");
  if (market.key === "team_totals") return normalizeTeamTotalsMarket(market, "total_equipe", "Total équipe");
  if (market.key === "alternate_team_totals") return normalizeTeamTotalsMarket(market, "total_equipe_alternatif", "Total équipe alternatif");
  if (market.key === "btts") return normalizeBooleanMarket(market, "btts", "Les deux équipes marquent");
  if (market.key === "btts_h1") return normalizeBooleanMarket(market, "btts_1re_mi_temps", "Les deux équipes marquent - 1re mi-temps");
  if (market.key === "btts_h2") return normalizeBooleanMarket(market, "btts_2e_mi_temps", "Les deux équipes marquent - 2e mi-temps");
  if (market.key === "draw_no_bet") return normalizeDrawNoBetMarket(market, event);
  return normalizeGenericMarket(market, event);
}

function normalizeHeadToHeadMarket(market: TheOddsMarket, event: TheOddsEvent, forcedKey?: string, forcedName?: string): ProviderEvent["markets"] {
  const hasDraw = market.outcomes.some((outcome) => isDrawOutcome(outcome.name));
  const selections = market.outcomes
    .filter((outcome) => outcome.price > 1)
    .map((outcome) => {
      const side = outcomeSide(outcome.name, event);
      return {
        key: side ?? slugify(outcome.name),
        name: h2hSelectionName(side, outcome.name),
        odds: String(outcome.price),
        status: "ACTIVE" as const
      };
    })
    .sort((a, b) => selectionRank(a.key) - selectionRank(b.key));

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

  return [
    {
      key: forcedKey ?? (hasDraw ? "1x2" : "match_winner"),
      name: forcedName ?? (hasDraw ? "1X2" : "Vainqueur"),
      type: hasDraw ? "THREE_WAY" : "MONEYLINE",
      selections
    }
  ];
}

function normalizeDoubleChanceMarket(market: TheOddsMarket, event: TheOddsEvent): ProviderEvent["markets"] {
  const selections = market.outcomes
    .filter((outcome) => outcome.price > 1)
    .map((outcome) => ({
      key: doubleChanceKey(outcome.name, event),
      name: translateDoubleChanceOutcome(outcome.name, event),
      odds: String(outcome.price),
      status: "ACTIVE" as const
    }))
    .sort((a, b) => doubleChanceRank(a.key) - doubleChanceRank(b.key));

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

  return [{ key: "double_chance", name: "Double chance", type: "DOUBLE_CHANCE", selections }];
}

function normalizeSpreadMarket(market: TheOddsMarket, event: TheOddsEvent, key: string, name: string): ProviderEvent["markets"] {
  const selections = market.outcomes
    .filter((outcome) => outcome.price > 1)
    .map((outcome) => {
      const side = outcomeSide(outcome.name, event) ?? slugify(outcome.name);
      const point = formatPoint(outcome.point);
      return {
        key: `${side}_${point}`,
        name: `${outcome.name} (${point})`,
        odds: String(outcome.price),
        status: "ACTIVE" as const
      };
    });

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

  return [
    {
      key,
      name,
      type: "HANDICAP",
      selections
    }
  ];
}

function normalizeTotalsMarket(market: TheOddsMarket, keyPrefix: string, label: string): ProviderEvent["markets"] {
  const byPoint = new Map<string, TheOddsOutcome[]>();

  for (const outcome of market.outcomes.filter((item) => item.price > 1)) {
    const point = formatPoint(outcome.point);
    byPoint.set(point, [...(byPoint.get(point) ?? []), outcome]);
  }

  return [...byPoint.entries()].map(([point, outcomes]) => ({
    key: `${keyPrefix}_${slugify(point)}`,
    name: `${label} ${point}`,
    type: "TOTAL",
    selections: outcomes.map((outcome) => ({
      key: slugify(outcome.name),
      name: translateTotalOutcome(outcome.name, point),
      odds: String(outcome.price),
      status: "ACTIVE" as const
    }))
  }));
}

function normalizeTeamTotalsMarket(market: TheOddsMarket, keyPrefix: string, label: string): ProviderEvent["markets"] {
  const byTeamAndPoint = new Map<string, TheOddsOutcome[]>();

  for (const outcome of market.outcomes.filter((item) => item.price > 1)) {
    const team = outcome.description || outcome.name;
    const point = formatPoint(outcome.point);
    const key = `${team}:${point}`;
    byTeamAndPoint.set(key, [...(byTeamAndPoint.get(key) ?? []), outcome]);
  }

  return [...byTeamAndPoint.entries()].map(([groupKey, outcomes]) => {
    const [team, point] = groupKey.split(":");
    return {
      key: `${keyPrefix}_${slugify(team)}_${slugify(point)}`,
      name: `${label} ${team} ${point}`,
      type: "TEAM_TOTAL",
      selections: outcomes.map((outcome) => ({
        key: slugify(outcome.name),
        name: translateTotalOutcome(outcome.name, point),
        odds: String(outcome.price),
        status: "ACTIVE" as const
      }))
    };
  });
}

function normalizeBooleanMarket(market: TheOddsMarket, key: string, name: string): ProviderEvent["markets"] {
  const selections = market.outcomes
    .filter((outcome) => outcome.price > 1)
    .map((outcome) => ({
      key: slugify(outcome.name),
      name: translateBooleanOutcome(outcome.name),
      odds: String(outcome.price),
      status: "ACTIVE" as const
    }));

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

  return [{ key, name, type: "BOOLEAN", selections }];
}

function normalizeDrawNoBetMarket(market: TheOddsMarket, event: TheOddsEvent): ProviderEvent["markets"] {
  const selections = market.outcomes
    .filter((outcome) => outcome.price > 1)
    .map((outcome) => {
      const side = outcomeSide(outcome.name, event) ?? slugify(outcome.name);
      return {
        key: side,
        name: outcome.name,
        odds: String(outcome.price),
        status: "ACTIVE" as const
      };
    })
    .sort((a, b) => selectionRank(a.key) - selectionRank(b.key));

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

  return [{ key: "draw_no_bet", name: "Remboursé si nul", type: "DRAW_NO_BET", selections }];
}

function normalizeGenericMarket(market: TheOddsMarket, event: TheOddsEvent): ProviderEvent["markets"] {
  const label = MARKET_LABELS[market.key] ?? { name: titleCase(market.key.replace(/_/g, "-")), type: "CUSTOM" };
  const byLine = new Map<string, TheOddsOutcome[]>();

  for (const outcome of market.outcomes.filter((item) => item.price > 1)) {
    const descriptor = outcome.description || "";
    const point = typeof outcome.point === "number" ? formatPoint(outcome.point) : "";
    const lineKey = [descriptor, point].filter(Boolean).join(":") || "main";
    byLine.set(lineKey, [...(byLine.get(lineKey) ?? []), outcome]);
  }

  return [...byLine.entries()].map(([lineKey, outcomes]) => {
    const [descriptor, point] = lineKey === "main" ? ["", ""] : lineKey.split(":");
    const suffix = [descriptor, point].filter(Boolean).join(" ");
    return {
      key: [slugify(market.key), slugify(descriptor), slugify(point)].filter(Boolean).join("_"),
      name: suffix ? `${label.name} ${suffix}` : label.name,
      type: label.type,
      selections: outcomes.map((outcome, index) => ({
        key: [slugify(outcome.description || ""), slugify(outcome.name), slugify(formatPoint(outcome.point)), String(index)].filter(Boolean).join("_"),
        name: formatGenericOutcomeName(outcome, event),
        odds: String(outcome.price),
        status: "ACTIVE" as const
      }))
    };
  });
}

function dedupeProviderMarkets(markets: ProviderEvent["markets"]) {
  const byKey = new Map<string, ProviderEvent["markets"][number]>();

  for (const market of markets) {
    const current = byKey.get(market.key);
    if (!current || market.selections.length > current.selections.length) {
      byKey.set(market.key, market);
    }
  }

  return [...byKey.values()];
}

function appendDerivedMarkets(markets: ProviderEvent["markets"], event: TheOddsEvent, config: TheOddsApiConfig) {
  if (mapSportSlug(event) !== "football") return markets;

  const byKey = new Map(markets.map((market) => [market.key, market]));
  const mainMarket = byKey.get("1x2");
  if (!mainMarket) return markets;

  const probabilities = footballProbabilitiesFromWinnerMarket(mainMarket);
  if (!probabilities) return markets;

  const derivedMargin = Math.max(config.pricing.minMarginPercent / 100, 0.035);

  if (!byKey.has("double_chance")) {
    markets.push({
      key: "double_chance",
      name: "Double chance",
      type: "DOUBLE_CHANCE",
      selections: [
        { key: "home_draw", name: "1X", odds: modelOdds(probabilities.home + probabilities.draw, derivedMargin), status: "ACTIVE" },
        { key: "home_away", name: "12", odds: modelOdds(probabilities.home + probabilities.away, derivedMargin), status: "ACTIVE" },
        { key: "draw_away", name: "X2", odds: modelOdds(probabilities.draw + probabilities.away, derivedMargin), status: "ACTIVE" }
      ]
    });
  }

  if (!byKey.has("draw_no_bet")) {
    const noDrawTotal = probabilities.home + probabilities.away;
    markets.push({
      key: "draw_no_bet",
      name: "Remboursé si nul",
      type: "DRAW_NO_BET",
      selections: [
        { key: "home", name: event.home_team, odds: modelOdds(probabilities.home / noDrawTotal, derivedMargin), status: "ACTIVE" },
        { key: "away", name: event.away_team, odds: modelOdds(probabilities.away / noDrawTotal, derivedMargin), status: "ACTIVE" }
      ]
    });
  }

  if (!byKey.has("btts")) {
    const yesProbability = clamp(0.47 + probabilities.draw * 0.22 - Math.abs(probabilities.home - probabilities.away) * 0.14, 0.37, 0.64);
    markets.push({
      key: "btts",
      name: "Les deux équipes marquent",
      type: "BOOLEAN",
      selections: [
        { key: "yes", name: "Oui", odds: modelOdds(yesProbability, derivedMargin), status: "ACTIVE" },
        { key: "no", name: "Non", odds: modelOdds(1 - yesProbability, derivedMargin), status: "ACTIVE" }
      ]
    });
  }

  if (!byKey.has("total_2_5")) {
    const overProbability = clamp(0.48 + (1 - probabilities.draw) * 0.16 - Math.abs(probabilities.home - probabilities.away) * 0.04, 0.38, 0.66);
    markets.push({
      key: "total_2_5",
      name: "Total 2.5",
      type: "TOTAL",
      selections: [
        { key: "over", name: "Plus 2.5", odds: modelOdds(overProbability, derivedMargin), status: "ACTIVE" },
        { key: "under", name: "Moins 2.5", odds: modelOdds(1 - overProbability, derivedMargin), status: "ACTIVE" }
      ]
    });
  }

  return markets;
}

function footballProbabilitiesFromWinnerMarket(market: ProviderEvent["markets"][number]) {
  const home = Number(market.selections.find((selection) => selection.key === "home")?.odds);
  const draw = Number(market.selections.find((selection) => selection.key === "draw")?.odds);
  const away = Number(market.selections.find((selection) => selection.key === "away")?.odds);

  if (!Number.isFinite(home) || !Number.isFinite(draw) || !Number.isFinite(away) || home <= 1 || draw <= 1 || away <= 1) {
    return null;
  }

  const impliedHome = 1 / home;
  const impliedDraw = 1 / draw;
  const impliedAway = 1 / away;
  const total = impliedHome + impliedDraw + impliedAway;

  return {
    home: impliedHome / total,
    draw: impliedDraw / total,
    away: impliedAway / total
  };
}

function modelOdds(probability: number, margin: number) {
  return String(roundOdds(1 / clamp(probability * (1 + margin), 0.01, 0.99)));
}

function clamp(value: number, min: number, max: number) {
  return Math.min(max, Math.max(min, value));
}

function orderedMarketKeys(bookmakers: TheOddsBookmaker[], configuredMarkets: string[]) {
  const present = new Set(bookmakers.flatMap((bookmaker) => bookmaker.markets.map((market) => market.key)));
  const configured = configuredMarkets.filter((market) => present.has(market));
  const extras = [...present].filter((market) => !configuredMarkets.includes(market));
  return unique([...configured, ...extras]);
}

function pickBookmakerMarket(bookmakers: TheOddsBookmaker[], marketKey: string, priorities: string[]) {
  const ordered = [
    ...priorities
      .map((key) => bookmakers.find((bookmaker) => bookmaker.key === key))
      .filter((bookmaker): bookmaker is TheOddsBookmaker => Boolean(bookmaker)),
    ...bookmakers.filter((bookmaker) => !priorities.includes(bookmaker.key))
  ];

  for (const bookmaker of ordered) {
    const market = bookmaker.markets.find((item) => item.key === marketKey);
    if (market?.outcomes.some((outcome) => outcome.price > 1)) {
      return { bookmaker, market };
    }
  }

  return null;
}

function mergeEventBookmakers(base: TheOddsEvent, details: TheOddsEvent[]) {
  if (details.length === 0) return base;

  const bookmakerMap = new Map<string, TheOddsBookmaker>();
  for (const bookmaker of base.bookmakers) {
    bookmakerMap.set(bookmaker.key, { ...bookmaker, markets: [...bookmaker.markets] });
  }

  for (const detail of details) {
    for (const bookmaker of detail.bookmakers) {
      const current = bookmakerMap.get(bookmaker.key) ?? { ...bookmaker, markets: [] };
      const marketMap = new Map(current.markets.map((market) => [market.key, market]));
      for (const market of bookmaker.markets) {
        marketMap.set(market.key, market);
      }
      bookmakerMap.set(bookmaker.key, { ...current, markets: [...marketMap.values()] });
    }
  }

  return { ...base, bookmakers: [...bookmakerMap.values()] };
}

function priceMarket(market: TheOddsMarket, pricing: OddsPricingConfig): TheOddsMarket {
  if (pricing.upliftPercent <= 0) return market;

  const nextOutcomes = market.outcomes.map((outcome) => ({ ...outcome }));
  const groups = new Map<string, number[]>();

  nextOutcomes.forEach((outcome, index) => {
    if (outcome.price <= 1) return;
    const key = pricingGroupKey(market, outcome);
    groups.set(key, [...(groups.get(key) ?? []), index]);
  });

  for (const indexes of groups.values()) {
    const priced = priceOutcomeGroup(
      indexes.map((index) => nextOutcomes[index]),
      pricing
    );

    priced.forEach((outcome, index) => {
      nextOutcomes[indexes[index]] = outcome;
    });
  }

  return { ...market, outcomes: nextOutcomes };
}

function pricingGroupKey(market: TheOddsMarket, outcome: TheOddsOutcome) {
  const point = typeof outcome.point === "number" ? formatPoint(outcome.point) : "main";
  const descriptor = outcome.description || "";
  if (market.key === "h2h" || market.key === "draw_no_bet" || market.key === "btts") return market.key;
  return `${market.key}:${descriptor}:${point}`;
}

function priceOutcomeGroup(outcomes: TheOddsOutcome[], pricing: OddsPricingConfig) {
  const uplift = pricing.upliftPercent / 100;
  const targetMargin = pricing.minMarginPercent / 100;
  const applyUplift = (scale: number) =>
    outcomes.map((outcome) => ({
      ...outcome,
      price: roundOdds(Math.min(outcome.price * (1 + uplift * scale), pricing.maxOdds))
    }));

  if (outcomes.length < 2) {
    return applyUplift(1);
  }

  if (marketMargin(applyUplift(1)) >= targetMargin) {
    return applyUplift(1);
  }

  if (marketMargin(outcomes) <= targetMargin) {
    return outcomes;
  }

  let low = 0;
  let high = 1;
  for (let index = 0; index < 24; index += 1) {
    const mid = (low + high) / 2;
    if (marketMargin(applyUplift(mid)) >= targetMargin) {
      low = mid;
    } else {
      high = mid;
    }
  }

  return applyUplift(low);
}

function marketMargin(outcomes: TheOddsOutcome[]) {
  return outcomes.reduce((sum, outcome) => sum + 1 / outcome.price, 0) - 1;
}

function roundOdds(value: number) {
  return Number(Math.max(1.01, value).toFixed(2));
}

function chunkMarkets(markets: string[], size: number) {
  const chunks: string[][] = [];
  for (let index = 0; index < markets.length; index += size) {
    chunks.push(markets.slice(index, index + size));
  }
  return chunks;
}

function unique<T>(values: T[]) {
  return [...new Set(values)];
}

async function mapWithConcurrency<T, R>(items: T[], limit: number, mapper: (item: T) => Promise<R>) {
  const results: R[] = [];
  let cursor = 0;

  async function worker() {
    while (cursor < items.length) {
      const index = cursor;
      cursor += 1;
      results[index] = await mapper(items[index]);
    }
  }

  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
  return results;
}

function mapSportSlug(sport: Pick<TheOddsSport, "key" | "group"> | Pick<TheOddsEvent, "sport_key">) {
  const key = "sport_key" in sport ? sport.sport_key : sport.key;
  const group = "group" in sport ? sport.group.toLowerCase() : "";

  if (key.startsWith("soccer_") || group.includes("soccer")) return "football";
  if (key.startsWith("tennis_") || group.includes("tennis")) return "tennis";
  if (key.startsWith("basketball_") || group.includes("basketball")) return "basketball";
  if (key.startsWith("icehockey_") || group.includes("ice hockey")) return "hockey";
  if (key.startsWith("rugbyleague_") || key.startsWith("rugbyunion_") || group.includes("rugby")) return "rugby";
  if (key.startsWith("mma_") || group.includes("mixed martial arts")) return "mma";
  if (key.startsWith("baseball_") || group.includes("baseball")) return "baseball";
  if (key.startsWith("americanfootball_") || group.includes("american football")) return "american-football";
  if (key.startsWith("cricket_") || group.includes("cricket")) return "cricket";
  if (key.startsWith("volleyball_") || group.includes("volleyball")) return "volleyball";
  if (key.startsWith("handball_") || group.includes("handball")) return "handball";
  if (key.startsWith("esports_") || group.includes("esports")) return "esports";
  return slugify(key.replace(/_/g, "-"));
}

function outcomeSide(name: string, event: TheOddsEvent) {
  if (name === event.home_team) return "home";
  if (name === event.away_team) return "away";
  if (isDrawOutcome(name)) return "draw";
  return undefined;
}

function h2hSelectionName(side: string | undefined, fallback: string) {
  if (side === "home") return "1";
  if (side === "draw") return "X";
  if (side === "away") return "2";
  return fallback;
}

function doubleChanceKey(name: string, event: TheOddsEvent) {
  const value = name.toLowerCase();
  const hasHome = value.includes(event.home_team.toLowerCase()) || /\b1\b|home/.test(value);
  const hasAway = value.includes(event.away_team.toLowerCase()) || /\b2\b|away/.test(value);
  const hasDraw = isDrawOutcome(value) || value.includes("draw") || value.includes("nul") || /\bx\b/.test(value);

  if (hasHome && hasDraw) return "home_draw";
  if (hasHome && hasAway) return "home_away";
  if (hasDraw && hasAway) return "draw_away";
  return slugify(name);
}

function translateDoubleChanceOutcome(name: string, event: TheOddsEvent) {
  const key = doubleChanceKey(name, event);
  if (key === "home_draw") return "1X";
  if (key === "home_away") return "12";
  if (key === "draw_away") return "X2";
  return name;
}

function doubleChanceRank(key: string) {
  if (key === "home_draw") return 1;
  if (key === "home_away") return 2;
  if (key === "draw_away") return 3;
  return 10;
}

function selectionRank(key: string) {
  if (key === "home") return 1;
  if (key === "draw") return 2;
  if (key === "away") return 3;
  return 10;
}

function isDrawOutcome(name: string) {
  return ["draw", "nul", "x"].includes(name.trim().toLowerCase());
}

function inferCountry(sportTitle: string, fallback?: string) {
  const parts = sportTitle.split(" - ").map((part) => part.trim());
  return parts.length > 1 ? parts.at(-1)! : fallback || "International";
}

function translateTotalOutcome(name: string, point: string) {
  const normalized = name.toLowerCase();
  if (normalized === "over") return `Plus ${point}`;
  if (normalized === "under") return `Moins ${point}`;
  return `${name} ${point}`;
}

function translateBooleanOutcome(name: string) {
  const normalized = name.trim().toLowerCase();
  if (["yes", "oui"].includes(normalized)) return "Oui";
  if (["no", "non"].includes(normalized)) return "Non";
  return name;
}

function formatGenericOutcomeName(outcome: TheOddsOutcome, event: TheOddsEvent) {
  const point = typeof outcome.point === "number" ? formatPoint(outcome.point) : "";
  const translated = point ? translateTotalOutcome(outcome.name, point) : translateBooleanOutcome(outcome.name);
  const descriptor = outcome.description && outcome.description !== event.home_team && outcome.description !== event.away_team ? outcome.description : "";
  return [descriptor, translated].filter(Boolean).join(" - ") || outcome.name;
}

function formatPoint(point?: number) {
  if (typeof point !== "number") return "0";
  return Number.isInteger(point) ? String(point) : point.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
}

function toTheOddsApiIso(value: Date) {
  return value.toISOString().replace(/\.\d{3}Z$/, "Z");
}

function slugify(value: string) {
  return value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "");
}

function titleCase(value: string) {
  return value
    .split(/[-_]/)
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join(" ");
}

function dedupeEvents(events: ProviderEvent[]) {
  const byId = new Map<string, ProviderEvent>();
  for (const event of events) {
    byId.set(event.externalId, event);
  }
  return [...byId.values()];
}

function parseExternalEventId(externalId: string) {
  if (!externalId.startsWith("the-odds-api:")) return null;
  const parts = externalId.split(":").filter(Boolean);
  return parts.at(-1) ?? null;
}
