/** Public reputation rank. Score 1.0–9.9, four tiers. The argument is net points, not a count of votes. */

/** Profile / hover / Community chrome. Rank string stays `{tier} {score}`. */
export const REPUTATION_LABEL = "Lore";

export const REPUTATION_SCORE_MIN_TENTHS = 10;
export const REPUTATION_SCORE_MAX_TENTHS = 99;

export type ReputationTier = "Newbie" | "Geek" | "Wizard" | "GOAT";

/** Points of net score required to gain +1.0 score while inside the tier. */
export const REPUTATION_SCORE_STEP: Record<ReputationTier, number> = {
  Newbie: 10,
  Geek: 15,
  Wizard: 25,
  GOAT: 40,
};

/** Largest absolute dial a tier may cast. */
export function maxDialWeight(tier: ReputationTier): 1 | 3 | 10 {
  if (tier === "Newbie") return 1;
  if (tier === "Geek") return 3;
  return 10;
}

/**
 * Half-points required to hold each tenth. Index 0 is score 1.0.
 * A tenth costs STEP/10 points. Geek (1.5) and Wizard (2.5) stay exact in halves.
 * The step is paid at the tier of the tenth you are leaving.
 */
const THRESHOLD_HALVES: readonly number[] = (() => {
  const out: number[] = [0];
  for (let tenths = REPUTATION_SCORE_MIN_TENTHS; tenths < REPUTATION_SCORE_MAX_TENTHS; tenths++) {
    let half = 8;
    if (tenths <= 31) half = 2;
    else if (tenths <= 54) half = 3;
    else if (tenths <= 76) half = 5;
    out.push(out[out.length - 1] + half);
  }
  return out;
})();

function netForThresholdIndex(index: number): number {
  if (index <= 0) return 0;
  const capped = Math.min(index, THRESHOLD_HALVES.length - 1);
  return Math.ceil(THRESHOLD_HALVES[capped] / 2);
}

/** Tenths of the public score: 10 → 1.0, 99 → 9.9. `net` is the signed score sum. */
export function reputationTenths(net: number): number {
  if (!Number.isFinite(net) || net <= 0) return REPUTATION_SCORE_MIN_TENTHS;
  const halves = Math.trunc(net) * 2;
  let lo = 0;
  let hi = THRESHOLD_HALVES.length - 1;
  while (lo < hi) {
    const mid = Math.ceil((lo + hi) / 2);
    if (THRESHOLD_HALVES[mid] <= halves) lo = mid;
    else hi = mid - 1;
  }
  return REPUTATION_SCORE_MIN_TENTHS + lo;
}

export function getReputationTier(tenths: number): ReputationTier {
  if (tenths <= 31) return "Newbie";
  if (tenths <= 54) return "Geek";
  if (tenths <= 76) return "Wizard";
  return "GOAT";
}

/** Inclusive score ranges for the public table. */
export const REPUTATION_TIER_RANGES: ReadonlyArray<{
  tier: ReputationTier;
  fromTenths: number;
  toTenths: number;
}> = [
  { tier: "Newbie", fromTenths: 10, toTenths: 31 },
  { tier: "Geek", fromTenths: 32, toTenths: 54 },
  { tier: "Wizard", fromTenths: 55, toTenths: 76 },
  { tier: "GOAT", fromTenths: 77, toTenths: 99 },
];

export function formatScoreTenths(tenths: number): string {
  return (tenths / 10).toFixed(1);
}

/** Tenths already reached in each tier. The current tenth counts as filled. */
export function reputationTierFills(net: number): Array<{ tier: ReputationTier; span: number; filled: number }> {
  const tenths = reputationTenths(net);
  return REPUTATION_TIER_RANGES.map((row) => {
    const span = row.toTenths - row.fromTenths + 1;
    const filled = tenths >= row.toTenths
      ? span
      : tenths < row.fromTenths
        ? 0
        : tenths - row.fromTenths + 1;
    return { tier: row.tier, span, filled };
  });
}

export function reputationTierForRep(net: number) {
  const tenths = reputationTenths(net);
  for (const row of REPUTATION_TIER_RANGES) {
    if (tenths <= row.toTenths) return row;
  }
  return REPUTATION_TIER_RANGES[REPUTATION_TIER_RANGES.length - 1];
}

export function maxDialWeightForNet(net: number): 1 | 3 | 10 {
  return maxDialWeight(getReputationTier(reputationTenths(net)));
}

/** `Wizard 6.1`. */
export function formatReputationRank(net: number): string {
  const tenths = reputationTenths(net);
  return `${getReputationTier(tenths)} ${(tenths / 10).toFixed(1)}`;
}

/** Lowercase tier for a request: `geek`. */
export function reputationLore(net: number): string {
  return getReputationTier(reputationTenths(net)).toLowerCase();
}

/** Score 1.0–9.9, separate from the tier. */
export function reputationRank(net: number): number {
  return Number((reputationTenths(net) / 10).toFixed(1));
}

/** Net required for the next 0.1. At 9.9 returns `net`. */
export function getNextScoreAt(net: number): number {
  const n = Number.isFinite(net) ? Math.trunc(net) : 0;
  const tenths = reputationTenths(n);
  if (tenths >= REPUTATION_SCORE_MAX_TENTHS) return n;
  return netForThresholdIndex(tenths - REPUTATION_SCORE_MIN_TENTHS + 1);
}

/** 0–100 fill inside the current tenth. 100 at 9.9. */
export function reputationStepProgress(net: number): number {
  const n = Number.isFinite(net) ? Math.trunc(net) : 0;
  const tenths = reputationTenths(n);
  if (tenths >= REPUTATION_SCORE_MAX_TENTHS) return 100;
  const index = tenths - REPUTATION_SCORE_MIN_TENTHS;
  const start = netForThresholdIndex(index);
  const end = netForThresholdIndex(index + 1);
  if (end <= start) return 100;
  return Math.min(100, Math.max(0, ((n - start) / (end - start)) * 100));
}

export const REPUTATION_TIERS: readonly ReputationTier[] = REPUTATION_TIER_RANGES.map((row) => row.tier);

const TIER_SLUG: Record<ReputationTier, string> = {
  Newbie: "newbie",
  Geek: "geek",
  Wizard: "wizard",
  GOAT: "goat",
};

const TIER_FROM_SLUG: Record<string, ReputationTier> = {
  newbie: "Newbie",
  geek: "Geek",
  wizard: "Wizard",
  goat: "GOAT",
};

/** Smallest net that displays this tenth. Score 1.0 is 0. */
export function netToReachTenths(tenths: number): number {
  const index = Math.max(0, Math.min(THRESHOLD_HALVES.length - 1, tenths - REPUTATION_SCORE_MIN_TENTHS));
  return netForThresholdIndex(index);
}

function maxNetHoldingTenths(tenths: number): number | null {
  if (tenths >= REPUTATION_SCORE_MAX_TENTHS) return null;
  return netForThresholdIndex(tenths - REPUTATION_SCORE_MIN_TENTHS + 1) - 1;
}

/** Inclusive net span whose public badge is this tier. Newbie has no floor (negatives stay Newbie). */
export function receivedRangeForTier(tier: ReputationTier): { minReceived: number | null; maxReceived: number | null } {
  const row = REPUTATION_TIER_RANGES.find((item) => item.tier === tier) ?? REPUTATION_TIER_RANGES[0];
  return {
    minReceived: row.fromTenths <= REPUTATION_SCORE_MIN_TENTHS ? null : netToReachTenths(row.fromTenths),
    maxReceived: maxNetHoldingTenths(row.toTenths),
  };
}

/** Merged net spans for selected tiers. Empty = no filter (all tiers or none selected). */
export function receivedRangesForTiers(
  tiers: readonly ReputationTier[],
): Array<{ minReceived: number | null; maxReceived: number | null }> {
  const wanted = new Set(tiers);
  if (wanted.size === 0 || wanted.size === REPUTATION_TIERS.length) return [];
  const ranges: Array<{ minReceived: number | null; maxReceived: number | null }> = [];
  for (const row of REPUTATION_TIER_RANGES) {
    if (!wanted.has(row.tier)) continue;
    const next = receivedRangeForTier(row.tier);
    const last = ranges[ranges.length - 1];
    if (
      last
      && last.maxReceived != null
      && next.minReceived != null
      && next.minReceived === last.maxReceived + 1
    ) {
      last.maxReceived = next.maxReceived;
    } else {
      ranges.push({ minReceived: next.minReceived, maxReceived: next.maxReceived });
    }
  }
  return ranges;
}

export function parseReputationTiers(raw: string | null | undefined): ReputationTier[] {
  if (!raw) return [];
  const seen = new Set<ReputationTier>();
  for (const part of raw.split(",")) {
    const tier = TIER_FROM_SLUG[part.trim().toLowerCase()];
    if (tier) seen.add(tier);
  }
  return REPUTATION_TIERS.filter((tier) => seen.has(tier));
}

/** Comma slugs in tier order. Null when empty or all four (no filter). */
export function formatReputationTiers(tiers: readonly ReputationTier[]): string | null {
  const ordered = REPUTATION_TIERS.filter((tier) => tiers.includes(tier));
  if (ordered.length === 0 || ordered.length === REPUTATION_TIERS.length) return null;
  return ordered.map((tier) => TIER_SLUG[tier]).join(",");
}
