/**
 * Shared utilities for PlebSaber tools
 */

// ============================================================================
// 1. Type Definitions
// ============================================================================

export type MapMeta = {
  songName?: string;
  key?: string;
  coverURL?: string;
  mapper?: string;
};

export type StarInfo = {
  stars?: number;
  accRating?: number;
  passRating?: number;
  techRating?: number;
  status?: number;
};

export type BeatLeaderScore = {
  timeset?: string | number;
  accuracy?: number;
  acc?: number;
  rank?: number;
  leaderboard?: {
    id?: string | number | null;
    leaderboardId?: string | number | null;
    song?: { hash?: string | null };
    difficulty?: { value?: number | string | null; modeName?: string | null };
  };
};

export type BeatLeaderScoresResponse = {
  data?: BeatLeaderScore[];
  metadata?: { page?: number; itemsPerPage?: number; total?: number };
};

export type BeatLeaderPlayerProfile = {
  id?: string;
  name?: string;
  avatar?: string | null;
  country?: string | null;
  rank?: number | null;
  countryRank?: number | null;
  techPp?: number | null;
  accPp?: number | null;
  passPp?: number | null;
};

export type RequirementContext = {
  adminRank?: number | null;
};

export type ToolRequirement = {
  minGlobalRank?: number;
  requiresBetterRankThanAdmin?: boolean;
  summary: string;
  lockedMessage?: string;
};

export type Difficulty = {
  name: string;
  characteristic: string;
};

export type TriangleCorners = {
  tech: { x: number; y: number };
  acc: { x: number; y: number };
  pass: { x: number; y: number };
};

export type TriangleNormalized = {
  tech: number;
  acc: number;
  pass: number;
};

export type TriangleData = {
  corners: TriangleCorners;
  normalized: TriangleNormalized;
} | null;

// ============================================================================
// 2. Constants
// ============================================================================

export const ONE_YEAR_SECONDS = 365 * 24 * 60 * 60;
export const PLEB_BEATLEADER_ID = '76561199407393962';
export const DEFAULT_ADMIN_RANK_FALLBACK = 3000;

export const DIFFICULTIES = ['Easy', 'Normal', 'Hard', 'Expert', 'ExpertPlus'] as const;

export const MODES = ['Standard', 'Lawless', 'OneSaber', 'NoArrows', 'Lightshow'] as const;

const DEFAULT_PRIVATE_TOOL_REQUIREMENT: ToolRequirement = {
  requiresBetterRankThanAdmin: true,
  summary: 'Ranked higher than pleb on BeatLeader',
  lockedMessage: 'You must either contribute to BL Patreon (or be ranked higher than pleb) to use this tool'
};

export const TOOL_REQUIREMENTS = {
  'compare-histories': DEFAULT_PRIVATE_TOOL_REQUIREMENT,
  'player-headtohead': DEFAULT_PRIVATE_TOOL_REQUIREMENT,
  'player-playlist-gaps': DEFAULT_PRIVATE_TOOL_REQUIREMENT
} as const satisfies Record<string, ToolRequirement>;

export type ToolKey = keyof typeof TOOL_REQUIREMENTS;

export function getToolRequirement(key: string): ToolRequirement | null {
  return TOOL_REQUIREMENTS[key as ToolKey] ?? null;
}

export function meetsToolRequirement(
  profile: BeatLeaderPlayerProfile | null | undefined,
  requirement: ToolRequirement | null | undefined,
  context?: RequirementContext
): boolean {
  if (!requirement) return true;
  const rank = profile?.rank ?? null;

  if (requirement.requiresBetterRankThanAdmin) {
    const baseline = resolveAdminBaseline(context);
    if (typeof rank !== 'number' || !Number.isFinite(rank) || rank <= 0) return false;
    return rank <= (baseline ?? DEFAULT_ADMIN_RANK_FALLBACK);
  }

  if (requirement.minGlobalRank !== undefined) {
    if (typeof rank !== 'number' || !Number.isFinite(rank) || rank <= 0) return false;
    return rank <= requirement.minGlobalRank;
  }
  return true;
}

function resolveAdminBaseline(context?: RequirementContext): number | null {
  const val = context?.adminRank;
  if (typeof val === 'number' && Number.isFinite(val) && val > 0) {
    return val;
  }
  return null;
}

export function formatToolRequirementSummary(
  requirement: ToolRequirement | null | undefined,
  context?: RequirementContext
): string {
  if (!requirement) return '';
  if (requirement.requiresBetterRankThanAdmin) {
    const baseline = resolveAdminBaseline(context);
    if (baseline) {
      return `BeatLeader global rank ≤ ${baseline.toLocaleString()} (pleb's current rank)`;
    }
    return requirement.summary || `BeatLeader global rank ≤ ${DEFAULT_ADMIN_RANK_FALLBACK.toLocaleString()}`;
  }
  if (requirement.summary) return requirement.summary;
  if (requirement.minGlobalRank !== undefined) {
    return `BeatLeader global rank ≤ ${requirement.minGlobalRank}`;
  }
  return '';
}

// ============================================================================
// 3. BeatSaver & BeatLeader API Functions
// ============================================================================

/**
 * Fetch BeatSaver metadata for a given song hash
 */
export async function fetchBeatSaverMeta(hash: string): Promise<MapMeta | null> {
  try {
    const res = await fetch(`https://api.beatsaver.com/maps/hash/${encodeURIComponent(hash)}`);
    if (!res.ok) throw new Error(String(res.status));
    const data: any = await res.json();
    const cover = data?.versions?.[0]?.coverURL ?? `https://cdn.beatsaver.com/${hash.toLowerCase()}.jpg`;
    return {
      songName: data?.metadata?.songName ?? data?.name ?? undefined,
      key: data?.id ?? undefined,
      coverURL: cover,
      mapper: data?.uploader?.name ?? undefined
    };
  } catch {
    // Fallback to CDN cover only
    return { coverURL: `https://cdn.beatsaver.com/${hash.toLowerCase()}.jpg` };
  }
}

/**
 * Fetch BeatLeader star ratings for a given song hash
 * Returns a map keyed by `${hash}|${difficultyName}|${modeName}`
 */
export async function fetchBeatLeaderStarsByHash(
  hash: string,
  normalizeDifficultyName: (value: number | string | null | undefined) => string
): Promise<Record<string, StarInfo>> {
  try {
    const res = await fetch(`/api/beatleader?path=/leaderboards/hash/${encodeURIComponent(hash)}`);
    if (!res.ok) return {};
    const data: any = await res.json();
    const leaderboards: any[] = Array.isArray(data?.leaderboards) ? data.leaderboards : Array.isArray(data) ? data : [];
    const result: Record<string, StarInfo> = {};
    for (const lb of leaderboards) {
      const diffName: string | undefined = lb?.difficulty?.difficultyName ?? lb?.difficulty?.name ?? undefined;
      const modeName: string | undefined = lb?.difficulty?.modeName ?? lb?.modeName ?? 'Standard';
      if (!diffName || !modeName) continue;
      const normalized = normalizeDifficultyName(diffName);
      const key = `${hash}|${normalized}|${modeName}`;
      const info: StarInfo = {
        stars: lb?.difficulty?.stars ?? lb?.stars,
        accRating: lb?.difficulty?.accRating,
        passRating: lb?.difficulty?.passRating,
        techRating: lb?.difficulty?.techRating,
        status: lb?.difficulty?.status
      };
      result[key] = info;
    }
    return result;
  } catch {
    return {};
  }
}

/**
 * Load metadata for a list of items with unique hashes
 * Only loads metadata that isn't already in the cache
 */
export async function loadMetaForHashes(
  hashes: string[],
  existingCache: Record<string, MapMeta>,
  onProgress?: (loaded: number, total: number) => void
): Promise<Record<string, MapMeta>> {
  const uniqueHashes = Array.from(new Set(hashes));
  const needed = uniqueHashes.filter((h) => !existingCache[h]);
  if (needed.length === 0) return existingCache;
  
  const newCache = { ...existingCache };
  for (let i = 0; i < needed.length; i++) {
    const h = needed[i];
    const meta = await fetchBeatSaverMeta(h);
    if (meta) newCache[h] = meta;
    if (onProgress) onProgress(i + 1, needed.length);
  }
  return newCache;
}

/**
 * Load star ratings for a list of hashes
 * Only loads ratings that aren't already in the cache
 */
export async function loadStarsForHashes(
  hashes: string[],
  existingCache: Record<string, StarInfo>,
  normalizeFn: (value: number | string | null | undefined) => string,
  onProgress?: (loaded: number, total: number) => void
): Promise<Record<string, StarInfo>> {
  const uniqueHashes = Array.from(new Set(hashes));
  // Check if we need to load stars for these hashes
  const needed = uniqueHashes.filter((h) => {
    // Check if we have any star data for this hash
    return !Object.keys(existingCache).some(key => key.startsWith(`${h}|`));
  });
  
  if (needed.length === 0) return existingCache;
  
  const newCache = { ...existingCache };
  for (let i = 0; i < needed.length; i++) {
    const h = needed[i];
    const stars = await fetchBeatLeaderStarsByHash(h, normalizeFn);
    Object.assign(newCache, stars);
    if (onProgress) onProgress(i + 1, needed.length);
  }
  return newCache;
}

/**
 * Fetch recent scores for a player filtered by difficulty
 */
export async function fetchAllRecentScoresForDiff(
  playerId: string,
  cutoffEpoch: number,
  reqDiff: string,
  maxPages = 100
): Promise<BeatLeaderScore[]> {
  const qs = new URLSearchParams({ diff: reqDiff, cutoffEpoch: String(cutoffEpoch), maxPages: String(maxPages) });
  const url = `/api/beatleader-cache/player/${encodeURIComponent(playerId)}?${qs.toString()}`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Failed to fetch scores for ${playerId}: ${res.status}`);
  const data = (await res.json()) as BeatLeaderScoresResponse;
  return data.data ?? [];
}

/**
 * Fetch recent scores for a player across all difficulties
 */
export async function fetchAllRecentScoresAllDiffs(
  playerId: string,
  cutoffEpoch: number,
  normalizeFn: (value: number | string | null | undefined) => string,
  parseFn: (ts: string | number | undefined) => number
): Promise<BeatLeaderScore[]> {
  const arrays = await Promise.all(
    DIFFICULTIES.map((d) => fetchAllRecentScoresForDiff(playerId, cutoffEpoch, d))
  );
  // Merge and dedupe by leaderboard key (hash|diff|mode) and timeset
  const merged = new Map<string, BeatLeaderScore>();
  for (const arr of arrays) {
    for (const s of arr) {
      const rawHash = s.leaderboard?.song?.hash ?? undefined;
      const modeName = s.leaderboard?.difficulty?.modeName ?? 'Standard';
      if (!rawHash) continue;
      const hashLower = String(rawHash).toLowerCase();
      const diffName = normalizeFn(s.leaderboard?.difficulty?.value ?? undefined);
      const key = `${hashLower}|${diffName}|${modeName}`;
      const prev = merged.get(key);
      if (!prev || parseFn(prev.timeset) < parseFn(s.timeset)) merged.set(key, s);
    }
  }
  return Array.from(merged.values());
}

/**
 * Fetch all scores for a player (no time limit) by paginating through the BeatLeader API
 * Useful for playlist gap analysis where we need to check all historical plays
 */
export async function fetchAllPlayerScores(playerId: string, maxPages = 200): Promise<BeatLeaderScore[]> {
  const pageSize = 100;
  let page = 1;
  const all: BeatLeaderScore[] = [];
  while (page <= maxPages) {
    const url = `/api/beatleader/player/${encodeURIComponent(playerId)}?scores=1&count=${pageSize}&page=${page}&sortBy=date&order=desc`;
    const res = await fetch(url);
    if (!res.ok) throw new Error(`Failed to fetch scores for ${playerId}: ${res.status}`);
    const data = (await res.json()) as BeatLeaderScoresResponse;
    const batch = data.data ?? [];
    if (batch.length === 0) break;
    all.push(...batch);
    page += 1;
  }
  return all;
}

// ============================================================================
// 4. Data Processing Helpers
// ============================================================================

/**
 * Normalize difficulty names to standard format
 */
export function normalizeDifficultyName(value: number | string | null | undefined): string {
  if (value === null || value === undefined) return 'ExpertPlus';
  if (typeof value === 'string') {
    const v = value.toLowerCase();
    if (v.includes('expertplus') || v === 'expertplus' || v === 'ex+' || v.includes('ex+')) return 'ExpertPlus';
    if (v.includes('expert')) return 'Expert';
    if (v.includes('hard')) return 'Hard';
    if (v.includes('normal')) return 'Normal';
    if (v.includes('easy')) return 'Easy';
    return value;
  }
  switch (value) {
    case 1: return 'Easy';
    case 3: return 'Normal';
    case 5: return 'Hard';
    case 7: return 'Expert';
    case 9: return 'ExpertPlus';
    default: return 'ExpertPlus';
  }
}

/**
 * Parse a timeset value to a number
 */
export function parseTimeset(ts: string | number | undefined): number {
  if (ts === undefined) return 0;
  if (typeof ts === 'number') return ts;
  const n = Number(ts);
  return Number.isFinite(n) ? n : 0;
}

/**
 * Get cutoff epoch timestamp from months ago
 */
export function getCutoffEpochFromMonths(months: number | string): number {
  const m = Number(months) || 0;
  const seconds = Math.max(0, m) * 30 * 24 * 60 * 60; // approx 30 days per month
  return Math.floor(Date.now() / 1000) - seconds;
}

/**
 * Normalize accuracy value to percentage (0-100)
 */
export function normalizeAccuracy(value: number | undefined): number | null {
  if (value === undefined || value === null) return null;
  return value <= 1 ? value * 100 : value;
}

/**
 * Build a map of latest scores by key (hash|diff|mode)
 */
export function buildLatestByKey(
  scores: BeatLeaderScore[],
  cutoffEpoch: number,
  normalizeFn: (value: number | string | null | undefined) => string,
  parseFn: (ts: string | number | undefined) => number
): Map<string, BeatLeaderScore> {
  const byKey = new Map<string, BeatLeaderScore>();
  for (const s of scores) {
    const t = parseFn(s.timeset);
    if (!t || t < cutoffEpoch - ONE_YEAR_SECONDS) continue; // sanity guard
    const rawHash = s.leaderboard?.song?.hash ?? undefined;
    const modeName = s.leaderboard?.difficulty?.modeName ?? 'Standard';
    if (!rawHash) continue;
    const hashLower = String(rawHash).toLowerCase();
    const diffName = normalizeFn(s.leaderboard?.difficulty?.value ?? undefined);
    const key = `${hashLower}|${diffName}|${modeName}`;
    const prev = byKey.get(key);
    if (!prev || parseFn(prev.timeset) < t) byKey.set(key, s);
  }
  return byKey;
}

// ============================================================================
// 5. Statistical Functions
// ============================================================================

/**
 * Calculate the mean (average) of an array of numbers
 */
export function mean(values: number[]): number {
  if (!values.length) return 0;
  return values.reduce((a, b) => a + b, 0) / values.length;
}

/**
 * Calculate the median of an array of numbers
 */
export function median(values: number[]): number {
  if (!values.length) return 0;
  const v = [...values].sort((a, b) => a - b);
  const mid = Math.floor(v.length / 2);
  return v.length % 2 ? v[mid] : (v[mid - 1] + v[mid]) / 2;
}

/**
 * Calculate a percentile of an array of numbers
 */
export function percentile(values: number[], p: number): number {
  if (!values.length) return 0;
  const v = [...values].sort((a, b) => a - b);
  const idx = Math.min(v.length - 1, Math.max(0, Math.floor((p / 100) * (v.length - 1))));
  return v[idx];
}

// ============================================================================
// 6. Skill Triangle Calculations
// ============================================================================

const DEFAULT_MAX_TECH_PP = 1300;
const DEFAULT_MAX_ACC_PP = 15000;
const DEFAULT_MAX_PASS_PP = 6000;
const GYRON_LENGTH = 57.74;

const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);

/**
 * Calculate skill triangle data from player's PP values
 * Returns corner coordinates and normalized values for rendering the skill triangle
 */
export function calculateSkillTriangle(
  techPp: number | null | undefined,
  accPp: number | null | undefined,
  passPp: number | null | undefined
): TriangleData {
  const tech = typeof techPp === 'number' ? techPp : 0;
  const acc = typeof accPp === 'number' ? accPp : 0;
  const pass = typeof passPp === 'number' ? passPp : 0;

  // If all values are zero, return null
  if (tech === 0 && acc === 0 && pass === 0) {
    return null;
  }

  // Calculate triangle scale
  const triangleScale = Math.max(
    1,
    tech > 0 ? tech / DEFAULT_MAX_TECH_PP : 0,
    acc > 0 ? acc / DEFAULT_MAX_ACC_PP : 0,
    pass > 0 ? pass / DEFAULT_MAX_PASS_PP : 0
  );

  const maxTechPp = DEFAULT_MAX_TECH_PP * triangleScale;
  const maxAccPp = DEFAULT_MAX_ACC_PP * triangleScale;
  const maxPassPp = DEFAULT_MAX_PASS_PP * triangleScale;

  // Normalize PP values
  const normalizedTechPp = maxTechPp ? clamp(tech / maxTechPp, 0, 1) : 0;
  const normalizedAccPp = maxAccPp ? clamp(acc / maxAccPp, 0, 1) : 0;
  const normalizedPassPp = maxPassPp ? clamp(pass / maxPassPp, 0, 1) : 0;

  // Calculate corner positions
  const cornerTech = {
    x: (GYRON_LENGTH - normalizedTechPp * GYRON_LENGTH) * 0.866,
    y: 86.6 - (GYRON_LENGTH - normalizedTechPp * GYRON_LENGTH) / 2
  };
  const cornerAcc = {
    x: 100 - (GYRON_LENGTH - normalizedAccPp * GYRON_LENGTH) * 0.866,
    y: 86.6 - (GYRON_LENGTH - normalizedAccPp * GYRON_LENGTH) / 2
  };
  const cornerPass = {
    x: 50,
    y: (86.6 - GYRON_LENGTH / 2) * (1 - normalizedPassPp)
  };

  return {
    corners: {
      tech: cornerTech,
      acc: cornerAcc,
      pass: cornerPass
    },
    normalized: {
      tech: normalizedTechPp,
      acc: normalizedAccPp,
      pass: normalizedPassPp
    }
  };
}

// ============================================================================
// 7. Playlist Generation
// ============================================================================

/**
 * Increment and return the playlist count for a given key
 */
export function incrementPlaylistCount(key: string): number {
  try {
    const raw = localStorage.getItem('playlist_counts');
    const obj = raw ? (JSON.parse(raw) as Record<string, number>) : {};
    const next = (obj[key] ?? 0) + 1;
    obj[key] = next;
    localStorage.setItem('playlist_counts', JSON.stringify(obj));
    return next;
  } catch {
    return 1;
  }
}

/**
 * Convert a list of songs to Beat Saber playlist JSON format
 */
export function toPlaylistJson(
  songs: Array<{ hash: string; difficulties: Difficulty[] }>,
  playlistKey: string,
  description: string
): unknown {
  const count = incrementPlaylistCount(playlistKey);
  const playlistTitle = `${playlistKey}-${String(count).padStart(2, '0')}`;
  return {
    playlistTitle,
    playlistAuthor: 'SaberList Tool',
    songs: songs.map((s) => ({
      hash: s.hash,
      difficulties: s.difficulties,
    })),
    description,
    allowDuplicates: false,
    customData: {}
  };
}

/**
 * Download a playlist as a .bplist file
 */
export function downloadPlaylist(playlistData: unknown): void {
  const title = (playlistData as any).playlistTitle ?? 'playlist';
  const blob = new Blob([JSON.stringify(playlistData, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `${title}.bplist`;
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}

// ============================================================================
// 8. Pagination Utilities
// ============================================================================

export type PaginationResult<T> = {
  pageItems: T[];
  totalPages: number;
  validPage: number;
};

/**
 * Calculate pagination for a list of items
 */
export function calculatePagination<T>(
  items: T[],
  page: number,
  pageSize: number
): PaginationResult<T> {
  const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
  const validPage = Math.min(Math.max(1, page), totalPages);
  const startIdx = (validPage - 1) * pageSize;
  const endIdx = startIdx + pageSize;
  const pageItems = items.slice(startIdx, endIdx);
  
  return {
    pageItems,
    totalPages,
    validPage
  };
}

// ============================================================================
// 9. URL Parameter Utilities
// ============================================================================

/**
 * Parse URL search parameters with type safety
 */
export function getUrlParam(
  searchParams: URLSearchParams,
  key: string,
  defaultValue?: string
): string | undefined {
  return searchParams.get(key) ?? defaultValue;
}

/**
 * Parse a URL parameter as a number
 */
export function getUrlParamAsNumber(
  searchParams: URLSearchParams,
  key: string,
  defaultValue: number
): number {
  const value = searchParams.get(key);
  if (!value) return defaultValue;
  const num = Number(value);
  return Number.isFinite(num) ? num : defaultValue;
}

/**
 * Parse a URL parameter as one of a set of allowed values
 */
export function getUrlParamAsEnum<T extends string>(
  searchParams: URLSearchParams,
  key: string,
  allowedValues: readonly T[],
  defaultValue: T
): T {
  const value = searchParams.get(key);
  if (!value) return defaultValue;
  return (allowedValues as readonly string[]).includes(value) ? (value as T) : defaultValue;
}

/**
 * Update URL without triggering navigation
 */
export function updateUrlParams(
  params: Record<string, string | number | boolean | undefined | null>,
  replace = true
): void {
  const sp = new URLSearchParams(window.location.search);
  
  for (const [key, value] of Object.entries(params)) {
    if (value === undefined || value === null || value === '' || value === false) {
      sp.delete(key);
    } else {
      sp.set(key, String(value));
    }
  }
  
  const qs = sp.toString();
  const url = window.location.pathname + (qs ? `?${qs}` : '');
  
  if (replace) {
    window.history.replaceState(null, '', url);
  } else {
    window.history.pushState(null, '', url);
  }
}

