document.addEventListener("DOMContentLoaded", () => {
  const backendBaseUrl =
    document.body.dataset.backendBaseUrl || "http://127.0.0.1:8000";
  const params = new URLSearchParams(window.location.search);
  const serverSlug = params.get("server") || "";
  const matchId = params.get("match") || "";
  const nodes = {
    title: document.getElementById("match-detail-title"),
    summary: document.getElementById("match-detail-summary"),
    note: document.getElementById("match-detail-note"),
    state: document.getElementById("match-detail-state"),
    grid: document.getElementById("match-detail-grid"),
    actions: document.getElementById("match-detail-actions"),
    playersSection: document.getElementById("match-detail-players-section"),
    playersNote: document.getElementById("match-detail-players-note"),
    playersState: document.getElementById("match-detail-players-state"),
    playerControls: document.getElementById("match-detail-player-controls"),
    playerSearch: document.getElementById("match-detail-player-search"),
    playerTeamFilters: [...document.querySelectorAll('input[name="match-detail-player-team-filter"]')],
    playerSort: document.getElementById("match-detail-player-sort"),
    playerSortDirection: document.getElementById("match-detail-player-sort-direction"),
    playersTableShell: document.getElementById("match-detail-players-table-shell"),
    playersBody: document.getElementById("match-detail-players-body"),
    timelineSection: document.getElementById("match-detail-timeline-section"),
    timelineNote: document.getElementById("match-detail-timeline-note"),
    timelineState: document.getElementById("match-detail-timeline-state"),
    timelineGrid: document.getElementById("match-detail-timeline-grid"),
    mapHero: document.getElementById("match-detail-map-hero"),
    mapImage: document.getElementById("match-detail-map-image"),
  };

  if (!serverSlug || !matchId) {
    nodes.title.textContent = "Partida no seleccionada";
    nodes.summary.textContent = "Vuelve al historico y abre una partida registrada.";
    nodes.note.textContent = "";
    setState(nodes.state, "No hay una partida seleccionada.", true);
    return;
  }

  void loadMatchDetail({ backendBaseUrl, serverSlug, matchId, nodes });
});

async function loadMatchDetail({ backendBaseUrl, serverSlug, matchId, nodes }) {
  try {
    const payload = await fetchJson(
      `${backendBaseUrl}/api/historical/matches/detail?server=${encodeURIComponent(
        serverSlug,
      )}&match=${encodeURIComponent(matchId)}`,
    );
    const data = payload?.data;
    const item = data?.item;
    if (!data?.found || !item) {
      nodes.title.textContent = "Detalle no disponible";
      nodes.summary.textContent =
        "La partida existe como enlace interno, pero todavia no hay detalle suficiente para mostrar.";
      nodes.note.textContent = "";
      setState(nodes.state, "Detalle no disponible para esta partida.");
      return;
    }

    renderMatchDetail(item, nodes);
  } catch (error) {
    nodes.title.textContent = "Detalle no disponible";
    nodes.summary.textContent = "No se pudo conectar con el backend local.";
    nodes.note.textContent = "";
    setState(nodes.state, "Error al cargar el detalle de la partida.", true);
  }
}

function renderMatchDetail(item, nodes) {
  const mapName = item.map?.pretty_name || item.map?.name || "Mapa no disponible";
  const serverName = item.server?.name || item.server?.slug || "Servidor no disponible";
  nodes.title.textContent = mapName;
  nodes.summary.textContent = serverName;
  nodes.note.textContent = "";
  renderMapHero(item, mapName, nodes);
  nodes.grid.innerHTML = renderScoreboardDetail(item, { mapName, serverName });
  renderPlayerSection(item, nodes);
  hideTimelineSection(nodes);
  renderActions(item, nodes.actions);
  nodes.state.hidden = true;
  nodes.grid.hidden = false;
}

function renderMapHero(item, mapName, nodes) {
  if (!nodes.mapHero || !nodes.mapImage) {
    return;
  }

  const mapImagePath = resolveMapImagePath(item, mapName);
  if (!mapImagePath) {
    nodes.mapImage.removeAttribute("src");
    nodes.mapImage.alt = "";
    nodes.mapHero.hidden = true;
    return;
  }

  nodes.mapImage.src = mapImagePath;
  nodes.mapImage.alt = mapName;
  nodes.mapImage.onerror = () => {
    nodes.mapImage.removeAttribute("src");
    nodes.mapHero.hidden = true;
  };
  nodes.mapHero.hidden = false;
}

function renderScoreboardDetail(item, { mapName, serverName }) {
  const result = item.result || {};
  const alliedScore = Number.isFinite(Number(result.allied_score))
    ? formatNumber(result.allied_score)
    : "-";
  const axisScore = Number.isFinite(Number(result.axis_score))
    ? formatNumber(result.axis_score)
    : "-";
  const winner = String(item.winner || result.winner || "").toLowerCase();
  const isAlliedWinner = winner === "allies" || winner === "allied";
  const isAxisWinner = winner === "axis";
  const factions = resolveMatchFactions(item, mapName);
  const metadata = [
    ["Servidor", serverName],
    ["Mapa", mapName],
    ["Modo", formatGameMode(item.game_mode || item.gamestate?.game_mode)],
    ["Duracion", formatDuration(item.duration_seconds)],
    ["Inicio", formatMatchTimestamp(item, "start")],
  ];
  if (item.ended_at) {
    metadata.push(["Fin", formatMatchTimestamp(item, "end")]);
  }

  return `
    <section class="historical-scoreboard-layout" aria-label="Resumen de marcador de la partida">
      <div class="historical-scoreboard-layout__main">
        ${renderScoreboardSide({
          sideClass: "historical-scoreboard-side--allied",
          emblem: factions.allied.emblem,
          sideLabel: "Aliados",
          factionLabel: factions.allied.label,
          isWinner: isAlliedWinner,
        })}
        <div class="historical-scoreboard-center">
          <span class="historical-scoreboard-center__timer">${escapeHtml(formatDuration(item.duration_seconds))}</span>
          <strong class="historical-scoreboard-center__score">${escapeHtml(alliedScore)} : ${escapeHtml(axisScore)}</strong>
          <span class="historical-scoreboard-center__map">${escapeHtml(mapName)}</span>
          <span class="historical-scoreboard-center__mode">${escapeHtml(formatGameMode(item.game_mode || item.gamestate?.game_mode))}</span>
          <span class="historical-scoreboard-center__winner">${escapeHtml(formatWinner(winner))}</span>
        </div>
        ${renderScoreboardSide({
          sideClass: "historical-scoreboard-side--axis",
          emblem: factions.axis.emblem,
          sideLabel: "Eje",
          factionLabel: factions.axis.label,
          isWinner: isAxisWinner,
        })}
      </div>
      <div class="historical-scoreboard-layout__meta">
        ${metadata.map(([label, value]) => renderCompactMeta(label, value)).join("")}
      </div>
    </section>
  `;
}

function renderScoreboardSide({ sideClass, emblem, sideLabel, factionLabel, isWinner }) {
  const fallbackLabel = factionLabel || sideLabel;
  return `
    <div class="historical-scoreboard-side ${sideClass} ${isWinner ? "is-winner" : ""}">
      <img
        class="historical-scoreboard-side__emblem"
        src="${escapeHtml(emblem)}"
        alt="${escapeHtml(fallbackLabel)}"
        width="128"
        height="128"
        loading="lazy"
        decoding="async"
        onerror="this.hidden = true; this.closest('.historical-scoreboard-side').classList.add('is-emblem-missing');"
      />
      <div class="historical-scoreboard-side__text">
        <strong>${escapeHtml(sideLabel)}</strong>
        ${isWinner ? "<em>Ganador</em>" : ""}
      </div>
    </div>
  `;
}

function renderCompactMeta(label, value) {
  return `
    <article>
      <span>${escapeHtml(label)}</span>
      <strong>${escapeHtml(value || "No disponible")}</strong>
    </article>
  `;
}

function hideTimelineSection(nodes) {
  if (!nodes.timelineSection) {
    return;
  }
  nodes.timelineSection.hidden = true;
  nodes.timelineNote.textContent = "";
  nodes.timelineState.hidden = true;
  nodes.timelineGrid.hidden = true;
  nodes.timelineGrid.innerHTML = "";
}

function renderPlayerSection(item, nodes) {
  const players = Array.isArray(item.players) ? item.players : [];
  nodes.playersSection.hidden = false;
  if (players.length === 0) {
    nodes.playersNote.textContent =
      "Esta partida no tiene estadisticas por jugador disponibles en el detalle interno.";
    setState(
      nodes.playersState,
      "No hay filas de jugador registradas para esta partida.",
    );
    nodes.playerControls.hidden = true;
    nodes.playersTableShell.hidden = true;
    nodes.playersBody.innerHTML = "";
    return;
  }

  const state = {
    search: "",
    team: "all",
    sort: "kills",
    direction: "desc",
    isDefaultSort: true,
  };
  const renderRows = () => renderPlayerTable(item, players, state, nodes);

  nodes.playerSearch.value = "";
  nodes.playerTeamFilters.forEach((control) => {
    control.checked = control.value === state.team;
  });
  nodes.playerSort.value = state.sort;
  nodes.playerSortDirection.value = state.direction;
  bindPlayerTableControls(nodes, state, renderRows);
  renderRows();
  nodes.playerControls.hidden = false;
  nodes.playersTableShell.hidden = false;
}

function bindPlayerTableControls(nodes, state, renderRows) {
  nodes.playerControls.onsubmit = (event) => {
    event.preventDefault();
  };
  nodes.playerSearch.oninput = () => {
    closePlayerDetailRows(nodes.playersBody);
    state.search = nodes.playerSearch.value;
    renderRows();
  };
  nodes.playerTeamFilters.forEach((control) => {
    control.onchange = () => {
      closePlayerDetailRows(nodes.playersBody);
      state.team = control.value;
      renderRows();
    };
  });
  nodes.playerSort.onchange = () => {
    state.sort = nodes.playerSort.value;
    state.isDefaultSort = false;
    renderRows();
  };
  nodes.playerSortDirection.onchange = () => {
    state.direction = nodes.playerSortDirection.value;
    state.isDefaultSort = false;
    renderRows();
  };
}

function renderPlayerTable(item, players, state, nodes) {
  const visiblePlayers = getVisiblePlayers(players, item, state);
  nodes.playersNote.textContent =
    visiblePlayers.length === players.length
      ? `${formatNumber(players.length)} jugadores con estadisticas locales.`
      : `${formatNumber(visiblePlayers.length)} de ${formatNumber(players.length)} jugadores visibles.`;
  nodes.playersState.hidden = visiblePlayers.length > 0;
  if (!visiblePlayers.length) {
    nodes.playersState.textContent = "No hay jugadores que coincidan con los controles activos.";
  }
  nodes.playersBody.innerHTML = visiblePlayers
    .map((entry, index) => renderPlayerRows(entry.player, item, index, entry.inactive))
    .join("");
  bindPlayerDetailRows(nodes.playersBody);
}

function getVisiblePlayers(players, item, state) {
  const normalizedSearch = normalizeLookupText(state.search);
  return players
    .map((player) => ({
      player,
      inactive: isInactiveMatchPlayer(player),
      team: getTeamSideDisplay(player.team || player.team_side),
    }))
    .filter((entry) => {
      const matchesTeam = state.team === "all" || entry.team.key === state.team;
      const matchesName =
        !normalizedSearch ||
        normalizeLookupText(entry.player.player_name).includes(normalizedSearch);
      return matchesTeam && matchesName;
    })
    .sort((a, b) => comparePlayerEntries(a, b, item, state));
}

function comparePlayerEntries(a, b, item, state) {
  if (state.isDefaultSort) {
    return (
      compareInactivePriority(a, b) ||
      compareNumericStat(b.player.kills, a.player.kills) ||
      compareNumericStat(a.player.deaths, b.player.deaths) ||
      comparePlayerNames(a.player, b.player)
    );
  }

  if (!["name", "team"].includes(state.sort)) {
    const inactivePriority = compareInactivePriority(a, b);
    if (inactivePriority) {
      return inactivePriority;
    }
  }

  const direction = state.direction === "asc" ? 1 : -1;
  const compared = comparePlayerSortValue(a, b, item, state.sort);
  return compared * direction || comparePlayerNames(a.player, b.player);
}

function comparePlayerSortValue(a, b, item, sort) {
  if (sort === "name") {
    return comparePlayerNames(a.player, b.player);
  }
  if (sort === "team") {
    return compareText(a.team.label, b.team.label);
  }
  if (sort === "deaths" || sort === "teamkills" || sort === "kills") {
    return compareNumericStat(a.player[sort], b.player[sort]);
  }
  if (sort === "kd") {
    return compareNumericStat(getKdRatioValue(a.player), getKdRatioValue(b.player));
  }
  return compareNumericStat(
    getKpmValue(a.player.kills, item.duration_seconds),
    getKpmValue(b.player.kills, item.duration_seconds),
  );
}

function compareInactivePriority(a, b) {
  return Number(a.inactive) - Number(b.inactive);
}

function comparePlayerNames(a, b) {
  return compareText(getPlayerName(a), getPlayerName(b));
}

function compareText(a, b) {
  return String(a || "").localeCompare(String(b || ""), "es", {
    sensitivity: "base",
  });
}

function compareNumericStat(a, b) {
  return toSortableNumber(a) - toSortableNumber(b);
}

function renderPlayerRows(player, item, index, inactive = false) {
  const team = getTeamSideDisplay(player.team || player.team_side);
  const rowId = `match-player-row-${index}`;
  const panelId = `match-player-panel-${index}`;
  const playerName = getPlayerName(player);
  const kpm = formatKpm(player.kills, item.duration_seconds);
  return `
    <tr
      class="historical-player-row historical-player-row--${team.key} ${inactive ? "is-inactive" : ""}"
      id="${escapeHtml(rowId)}"
    >
      <td>
        <button
          class="historical-player-row__details-button"
          type="button"
          aria-controls="${escapeHtml(panelId)}"
          aria-expanded="false"
          aria-label="Ver estadisticas ampliadas de ${escapeHtml(playerName)}"
        >
          <span>${escapeHtml(playerName)}</span>
          <span aria-hidden="true">i</span>
        </button>
      </td>
      <td class="historical-player-team-cell">
        <span class="historical-player-team-badge historical-player-team-badge--${team.key}">
          ${escapeHtml(team.label)}
        </span>
      </td>
      <td>${escapeHtml(formatOptionalNumber(player.kills))}</td>
      <td>${escapeHtml(formatOptionalNumber(player.deaths))}</td>
      <td>${escapeHtml(formatOptionalNumber(player.teamkills))}</td>
      <td>${escapeHtml(formatKdRatio(player))}</td>
      <td>${escapeHtml(kpm)}</td>
    </tr>
    <tr
      class="historical-player-detail-row"
      id="${escapeHtml(panelId)}"
      aria-labelledby="${escapeHtml(rowId)}"
    >
      <td colspan="7">
        ${renderPlayerStatsPanel(player, item, { team, playerName, kpm })}
      </td>
    </tr>
  `;
}

function bindPlayerDetailRows(playersBody) {
  const playerRows = [...playersBody.querySelectorAll(".historical-player-row")];
  const collapseRow = (row) => {
    const button = row.querySelector(".historical-player-row__details-button");
    const detailRow = row.nextElementSibling;
    if (!button || !detailRow?.classList.contains("historical-player-detail-row")) {
      return;
    }
    row.classList.remove("is-expanded");
    detailRow.classList.remove("is-open");
    button.setAttribute("aria-expanded", "false");
  };

  playerRows.forEach((row) => {
    const button = row.querySelector(".historical-player-row__details-button");
    const detailRow = row.nextElementSibling;
    if (!button || !detailRow?.classList.contains("historical-player-detail-row")) {
      return;
    }
    const setExpanded = (expanded) => {
      if (expanded) {
        playerRows.filter((candidate) => candidate !== row).forEach(collapseRow);
      }
      row.classList.toggle("is-expanded", expanded);
      detailRow.classList.toggle("is-open", expanded);
      button.setAttribute("aria-expanded", String(expanded));
    };
    const toggleExpanded = () => setExpanded(!detailRow.classList.contains("is-open"));

    button.addEventListener("click", () => {
      toggleExpanded();
    });
  });
}

function closePlayerDetailRows(playersBody) {
  [...playersBody.querySelectorAll(".historical-player-row")].forEach((row) => {
    const button = row.querySelector(".historical-player-row__details-button");
    const detailRow = row.nextElementSibling;
    if (!button || !detailRow?.classList.contains("historical-player-detail-row")) {
      return;
    }
    row.classList.remove("is-expanded");
    detailRow.classList.remove("is-open");
    button.setAttribute("aria-expanded", "false");
  });
}

function getPlayerName(player) {
  return player.player_name || player.name || "Jugador no identificado";
}

function isInactiveMatchPlayer(player) {
  const team = getTeamSideDisplay(player.team || player.team_side);
  return (
    team.key === "unknown" &&
    toSortableNumber(player.kills) === 0 &&
    toSortableNumber(player.deaths) === 0 &&
    toSortableNumber(player.teamkills) === 0 &&
    getKdRatioValue(player) === 0 &&
    !hasNamedCounts(player.top_weapons) &&
    !hasNamedCounts(player.most_killed) &&
    !hasNamedCounts(player.death_by)
  );
}

function renderPlayerStatsPanel(player, item, context) {
  const matchups = buildPlayerDirectMatchups(player);
  const hasExpandedStats =
    hasNamedCounts(player.top_weapons) ||
    hasNamedCounts(player.most_killed) ||
    hasNamedCounts(player.death_by) ||
    matchups.length > 0;

  return `
    <section class="historical-player-stats-panel" aria-label="Estadisticas ampliadas de ${escapeHtml(context.playerName)}">
      <div class="historical-player-stats-panel__header">
        <div>
          <p>${escapeHtml(context.team.label)}</p>
          <h4>${escapeHtml(context.playerName)}</h4>
        </div>
        <div class="historical-player-stats-panel__summary">
          ${renderPlayerStatChip("Kills", formatOptionalNumber(player.kills))}
          ${renderPlayerStatChip("Muertes", formatOptionalNumber(player.deaths))}
          ${renderPlayerStatChip("TK", formatOptionalNumber(player.teamkills))}
          ${renderPlayerStatChip("KD", formatKdRatio(player))}
          ${renderPlayerStatChip("KPM", context.kpm)}
        </div>
      </div>
      ${renderExternalProfilesSection(player)}
      ${
        hasExpandedStats
          ? `
            <div class="historical-player-stats-panel__grid">
              ${renderNamedCountSection("Armas", player.top_weapons)}
              ${renderNamedCountSection("Mas abatido", player.most_killed)}
              ${renderNamedCountSection("Muere por", player.death_by)}
              ${renderDirectMatchupsSection(matchups)}
            </div>
          `
          : `<p class="historical-player-stats-panel__empty">Sin estadisticas ampliadas disponibles.</p>`
      }
    </section>
  `;
}

function renderExternalProfilesSection(player) {
  const links = [
    ["steam", "Steam"],
    ["hellor", "Hellor"],
    ["hll_records", "HLL Records"],
    ["helo", "Helo"],
  ]
    .map(([key, label]) => [label, player.external_profile_links?.[key]])
    .filter(([, href]) => typeof href === "string" && href.trim());

  return `
    <article class="historical-player-stats-panel__section historical-player-stats-panel__profiles">
      <h5>Perfiles externos</h5>
      ${
        links.length
          ? `
            <div class="historical-player-profile-links">
              ${links
                .map(
                  ([label, href]) => `
                    <a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">
                      ${escapeHtml(label)}
                    </a>
                  `,
                )
                .join("")}
            </div>
          `
          : renderExternalProfilesUnavailable(player)
      }
    </article>
  `;
}


function renderExternalProfilesUnavailable(player) {
  const platform = String(player.platform || "").toLowerCase();
  const epicId = typeof player.epic_id === "string" ? player.epic_id.trim() : "";

  if (platform === "epic") {
    return epicId
      ? `<p>Jugador detectado como Epic. ID capturado: <code>${escapeHtml(epicId)}</code>. Sin enlaces externos compatibles confirmados para este proveedor.</p>`
      : "<p>Jugador detectado como Epic. Sin enlaces externos compatibles confirmados para este proveedor.</p>";
  }

  return "<p>Perfiles externos no disponibles.</p>";
}

function renderPlayerStatChip(label, value) {
  return `
    <article>
      <span>${escapeHtml(label)}</span>
      <strong>${escapeHtml(value)}</strong>
    </article>
  `;
}

function renderNamedCountSection(title, items) {
  if (!hasNamedCounts(items)) {
    return `
      <article class="historical-player-stats-panel__section">
        <h5>${escapeHtml(title)}</h5>
        <p>No disponible</p>
      </article>
    `;
  }
  return `
    <article class="historical-player-stats-panel__section">
      <h5>${escapeHtml(title)}</h5>
      <ol>
        ${items
          .map((stat) => {
            const name = stat.name || stat.label || "Sin nombre";
            const count = stat.count ?? stat.total ?? 0;
            return `<li><span>${escapeHtml(name)}</span><strong>${escapeHtml(formatNumber(count))}</strong></li>`;
          })
          .join("")}
      </ol>
    </article>
  `;
}

function renderDirectMatchupsSection(matchups) {
  if (!matchups.length) {
    return `
      <article class="historical-player-stats-panel__section historical-player-stats-panel__section--wide">
        <h5>Duelo directo</h5>
        <p>No disponible</p>
      </article>
    `;
  }
  return `
    <article class="historical-player-stats-panel__section historical-player-stats-panel__section--wide">
      <h5>Duelo directo</h5>
      <div class="historical-player-matchups" role="table" aria-label="Duelos directos">
        <div role="row">
          <span role="columnheader">Rival</span>
          <span role="columnheader">Abatidos</span>
          <span role="columnheader">Muertes</span>
          <span role="columnheader">Balance</span>
        </div>
        ${matchups
          .map(
            (matchup) => `
              <div role="row">
                <span role="cell">${escapeHtml(matchup.name)}</span>
                <strong role="cell">${escapeHtml(formatNumber(matchup.kills))}</strong>
                <strong role="cell">${escapeHtml(formatNumber(matchup.deaths))}</strong>
                <strong role="cell">${escapeHtml(formatSignedNumber(matchup.balance))}</strong>
              </div>
            `,
          )
          .join("")}
      </div>
    </article>
  `;
}

function buildPlayerDirectMatchups(player) {
  const byName = new Map();
  const addStats = (items, key) => {
    if (!Array.isArray(items)) {
      return;
    }
    items.forEach((item) => {
      const name = item.name || item.label;
      if (!name) {
        return;
      }
      const normalizedName = String(name);
      const current = byName.get(normalizedName) || {
        name: normalizedName,
        kills: 0,
        deaths: 0,
      };
      current[key] += Number(item.count ?? item.total ?? 0) || 0;
      byName.set(normalizedName, current);
    });
  };

  addStats(player.most_killed, "kills");
  addStats(player.death_by, "deaths");
  return [...byName.values()]
    .map((matchup) => ({
      ...matchup,
      balance: matchup.kills - matchup.deaths,
      involvement: matchup.kills + matchup.deaths,
    }))
    .sort((a, b) => b.involvement - a.involvement || a.name.localeCompare(b.name, "es"))
    .slice(0, 8);
}

function renderActions(item, actionsNode) {
  const matchUrl = normalizeSafePublicScoreboardMatchUrl(item.match_url);
  if (!matchUrl) {
    actionsNode.innerHTML = "";
    actionsNode.hidden = true;
    return;
  }
  actionsNode.innerHTML = `
    <a
      class="historical-match-card__link"
      data-match-detail-scoreboard-link
      href="${escapeHtml(matchUrl)}"
      target="_blank"
      rel="noopener noreferrer"
    >
      Ver en Scoreboard
    </a>
  `;
  actionsNode.hidden = false;
}

function resolveMatchFactions(item, mapName) {
  const normalizedMap = normalizeLookupText(
    `${item.map?.name || ""} ${item.map?.pretty_name || ""} ${mapName || ""}`,
  );

  if (/(kursk|stalingrad|kharkov)/.test(normalizedMap)) {
    return {
      allied: {
        label: "Sovieticos",
        emblem: "./assets/img/factions/soviets.webp",
      },
      axis: {
        label: "Eje",
        emblem: "./assets/img/factions/germany.webp",
      },
    };
  }

  if (/(driel|elalamein|el alamein|tobruk)/.test(normalizedMap)) {
    return {
      allied: {
        label: "Britanicos",
        emblem: "./assets/img/factions/britain.webp",
      },
      axis: {
        label: normalizedMap.includes("tobruk") || normalizedMap.includes("elalamein")
          ? "Afrika Korps"
          : "Eje",
        emblem: "./assets/img/factions/germany.webp",
      },
    };
  }

  return {
    allied: {
      label: "USA",
      emblem: "./assets/img/factions/us.webp",
    },
    axis: {
      label: "Eje",
      emblem: "./assets/img/factions/germany.webp",
    },
  };
}

function resolveMapImagePath(item, mapName) {
  const normalizedMap = normalizeLookupText(
    `${item.map?.name || ""} ${item.map?.pretty_name || ""} ${mapName || ""}`,
  ).replaceAll(" ", "");
  const mapAssetByKey = {
    carentan: "carentan-day.webp",
    driel: "driel-day.webp",
    elalamein: "elalamein-day.webp",
    elsenbornridge: "elsenbornridge-day.webp",
    foy: "foy-day.webp",
    hill400: "hill400-day.webp",
    hurtgenforest: "hurtgenforest-day.webp",
    kharkov: "kharkov-day.webp",
    kursk: "kursk-day.webp",
    mortain: "mortain-day.webp",
    omahabeach: "omahabeach-day.webp",
    purpleheartlane: "purpleheartlane-rain.webp",
    smolensk: "smolensk-day.webp",
    stmariedumont: "stmariedumont-day.webp",
    stmereeglise: "stmereeglise-day.webp",
    tobrukdawn: "tobruk-dawn.webp",
    tobruk: "tobruk-day.webp",
    utahbeach: "utahbeach-day.webp",
  };
  const matchedKey = Object.keys(mapAssetByKey).find((key) =>
    normalizedMap.includes(key),
  );
  return matchedKey ? `./assets/img/maps/${mapAssetByKey[matchedKey]}` : "";
}

function normalizeLookupText(value) {
  return String(value || "")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, " ")
    .trim();
}

function formatTeamSide(value) {
  return getTeamSideDisplay(value).label;
}

function getTeamSideDisplay(value) {
  const normalized = String(value || "")
    .trim()
    .toLowerCase();
  if (normalized === "allies" || normalized === "allied" || normalized === "aliados") {
    return { key: "allies", label: "Aliados" };
  }
  if (normalized === "axis" || normalized === "eje") {
    return { key: "axis", label: "Eje" };
  }
  return { key: "unknown", label: "No disponible" };
}

function formatGameMode(value) {
  if (!value) {
    return "Modo no disponible";
  }
  const normalized = String(value).replaceAll("_", " ").replaceAll("-", " ");
  return normalized.charAt(0).toUpperCase() + normalized.slice(1);
}

function formatDuration(value) {
  const seconds = Number(value);
  if (!Number.isFinite(seconds) || seconds <= 0) {
    return "Duracion no disponible";
  }
  const minutes = Math.round(seconds / 60);
  if (minutes < 60) {
    return `${formatNumber(minutes)} min`;
  }
  const hours = Math.floor(minutes / 60);
  const remainingMinutes = minutes % 60;
  return `${formatNumber(hours)} h ${formatNumber(remainingMinutes)} min`;
}

function formatWinner(value) {
  const normalized = String(value || "").toLowerCase();
  if (normalized === "allies" || normalized === "allied") {
    return "Ganador: Aliados";
  }
  if (normalized === "axis") {
    return "Ganador: Eje";
  }
  if (normalized === "draw") {
    return "Empate";
  }
  return "Resultado no disponible";
}

function formatOptionalNumber(value) {
  return value === null || value === undefined ? "No disponible" : formatNumber(value);
}

function formatKdRatio(player) {
  if (
    !Number.isFinite(Number(player.kd_ratio)) &&
    (!Number.isFinite(Number(player.kills)) || !Number.isFinite(Number(player.deaths)))
  ) {
    return "No disponible";
  }
  return formatDecimal(getKdRatioValue(player), 2);
}

function getKdRatioValue(player) {
  if (Number.isFinite(Number(player.kd_ratio))) {
    return Number(player.kd_ratio);
  }
  const kills = Number(player.kills);
  const deaths = Number(player.deaths);
  if (!Number.isFinite(kills) || !Number.isFinite(deaths)) {
    return 0;
  }
  return deaths > 0 ? kills / deaths : kills;
}

function formatKpm(kills, durationSeconds) {
  return formatDecimal(getKpmValue(kills, durationSeconds), 2);
}

function getKpmValue(kills, durationSeconds) {
  const parsedKills = Number(kills);
  const parsedDurationSeconds = Number(durationSeconds);
  if (
    !Number.isFinite(parsedKills) ||
    !Number.isFinite(parsedDurationSeconds) ||
    parsedDurationSeconds <= 0
  ) {
    return 0;
  }
  return parsedKills / (parsedDurationSeconds / 60);
}

function formatNamedCounts(items) {
  if (!Array.isArray(items) || items.length === 0) {
    return "No disponible";
  }
  return items
    .slice(0, 3)
    .map((item) => {
      const name = item.name || item.label || "Sin nombre";
      const count = item.count ?? item.total ?? 0;
      return `${name} (${formatNumber(count)})`;
    })
    .join(" / ");
}

function hasNamedCounts(items) {
  return Array.isArray(items) && items.length > 0;
}

function formatNumber(value) {
  const parsedValue = Number(value);
  if (!Number.isFinite(parsedValue)) {
    return "0";
  }
  return new Intl.NumberFormat("es-ES").format(parsedValue);
}

function toSortableNumber(value) {
  const parsedValue = Number(value);
  return Number.isFinite(parsedValue) ? parsedValue : 0;
}

function formatSignedNumber(value) {
  const parsedValue = Number(value);
  if (!Number.isFinite(parsedValue) || parsedValue === 0) {
    return "0";
  }
  return `${parsedValue > 0 ? "+" : ""}${formatNumber(parsedValue)}`;
}

function formatDecimal(value, fractionDigits = 1) {
  const parsedValue = Number(value);
  if (!Number.isFinite(parsedValue)) {
    return "0";
  }
  return new Intl.NumberFormat("es-ES", {
    minimumFractionDigits: fractionDigits,
    maximumFractionDigits: fractionDigits,
  }).format(parsedValue);
}

function formatTimestamp(timestamp) {
  if (!timestamp) {
    return "Fecha no disponible";
  }

  const value = new Date(timestamp);
  if (Number.isNaN(value.getTime())) {
    return "Fecha no disponible";
  }

  return new Intl.DateTimeFormat("es-ES", {
    dateStyle: "short",
    timeStyle: "short",
  }).format(value);
}

function formatMatchTimestamp(item, kind) {
  const timestamp = kind === "start" ? item.started_at : item.ended_at;
  if (timestamp) {
    return formatTimestamp(timestamp);
  }
  return "No disponible";
}

function normalizeSafePublicScoreboardMatchUrl(value) {
  if (typeof value !== "string" || !value.trim()) {
    return "";
  }
  try {
    const url = new URL(value.trim());
    const allowedOrigins = new Set([
      "https://scoreboard.comunidadhll.es",
      "https://scoreboard.comunidadhll.es:5443",
    ]);
    const isAllowedPath = url.pathname === "/games" || url.pathname.startsWith("/games/");
    return allowedOrigins.has(url.origin) && isAllowedPath ? url.href : "";
  } catch (error) {
    return "";
  }
}

async function fetchJson(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Request failed with ${response.status}`);
  }
  return response.json();
}

function setState(node, message, isError = false) {
  node.textContent = message;
  node.hidden = false;
  node.classList.toggle("is-error", isError);
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}
