import i18n from 'i18next';

interface ValueFormat {
  prefix?: string;
  suffix?: string;
  /** Show full integer (no k/M abbreviation) */
  raw?: boolean;
}

function usesChineseNumberUnits(): boolean {
  return i18n.language?.toLowerCase().startsWith('zh') ?? false;
}

function formatChineseCompactNumber(value: number): string | null {
  const abs = Math.abs(value);
  if (abs >= 100_000_000) return `${trimFixed(value / 100_000_000)}亿`;
  if (abs >= 10_000) return `${trimFixed(value / 10_000)}万`;
  return null;
}

function trimFixed(value: number): string {
  return value.toFixed(1).replace(/\.0$/, '');
}

export function formatValue(value: number, fmt?: ValueFormat): string {
  const p = fmt?.prefix ?? '';
  const s = fmt?.suffix ?? '';
  if (fmt?.raw) return `${p}${Math.round(value)}${s}`;
  if (usesChineseNumberUnits()) {
    const chineseCompactValue = formatChineseCompactNumber(value);
    if (chineseCompactValue) return `${p}${chineseCompactValue}${s}`;
    if (Number.isInteger(value)) return `${p}${value.toLocaleString()}${s}`;
    return `${p}${value.toFixed(1)}${s}`;
  }
  if (Math.abs(value) >= 1_000_000) return `${p}${(value / 1_000_000).toFixed(1)}M${s}`;
  if (Math.abs(value) >= 1_000) return `${p}${(value / 1_000).toFixed(1)}k${s}`;
  if (Number.isInteger(value)) return `${p}${value.toLocaleString()}${s}`;
  return `${p}${value.toFixed(1)}${s}`;
}

export function formatFilterValue(value: number, rawOrFmt?: boolean | ValueFormat): string {
  const fmt = typeof rawOrFmt === 'object' ? rawOrFmt : { raw: rawOrFmt };
  const p = fmt?.prefix ?? '';
  const s = fmt?.suffix ?? '';
  if (fmt?.raw) return `${p}${Math.round(value)}${s}`;
  if (usesChineseNumberUnits()) {
    const chineseCompactValue = formatChineseCompactNumber(value);
    if (chineseCompactValue) return `${p}${chineseCompactValue}${s}`;
    if (Number.isInteger(value)) return `${p}${value}${s}`;
    return `${p}${value.toFixed(2)}${s}`;
  }
  if (Math.abs(value) >= 1_000_000) return `${p}${(value / 1_000_000).toFixed(1)}M${s}`;
  if (Math.abs(value) >= 1_000) return `${p}${(value / 1_000).toFixed(1)}k${s}`;
  if (Number.isInteger(value)) return `${p}${value}${s}`;
  return `${p}${value.toFixed(2)}${s}`;
}

/** Parse a user-typed value like "250k", "1.2M", "£300000", "50 sqm" back to a number. */
export function parseInputValue(
  text: string,
  opts?: { prefix?: string; suffix?: string; step?: number }
): number | null {
  let s = text.trim();
  if (opts?.prefix) s = s.replace(new RegExp(`^\\${opts.prefix}`), '');
  if (opts?.suffix) s = s.replace(new RegExp(`${opts.suffix.trim()}$`), '');
  s = s.trim().replace(/[,，]/g, '');
  const m = s.match(/^(-?\d+\.?\d*)\s*([kKmM万亿億]?)$/);
  if (!m) return null;
  let val = parseFloat(m[1]);
  if (isNaN(val)) return null;
  const unit = m[2];
  if (unit === 'k') val *= 1_000;
  else if (unit === 'K') val *= 1_000;
  else if (unit === 'm' || unit === 'M') val *= 1_000_000;
  else if (unit === '万') val *= 10_000;
  else if (unit === '亿' || unit === '億') val *= 100_000_000;
  if (opts?.step) val = Math.round(val / opts.step) * opts.step;
  return val;
}

export function formatDuration(d: string): string {
  if (d === 'F' || d === 'L') {
    // These are server enum values — translate via ts()
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const { ts } = require('../i18n/server') as { ts: (v: string) => string };
    if (d === 'F') return ts('Freehold');
    return ts('Leasehold');
  }
  return d;
}

export function formatTransactionDate(fractionalYear: number): string {
  const year = Math.floor(fractionalYear);
  const monthIndex = Math.min(Math.round((fractionalYear - year) * 12), 11);
  const language = i18n.language || undefined;
  return new Intl.DateTimeFormat(language, { month: 'short', year: 'numeric' }).format(
    new Date(Date.UTC(year, monthIndex, 1))
  );
}

export function formatYearMonth(year: number, month: number): string {
  const monthIndex = Math.min(Math.max(month - 1, 0), 11);
  const language = i18n.language || undefined;
  return new Intl.DateTimeFormat(language, { month: 'short', year: 'numeric' }).format(
    new Date(Date.UTC(year, monthIndex, 1))
  );
}

export function formatAge(value: number, approximate = true): string {
  if (value >= 1000) return approximate ? `~${Math.round(value)}` : `${Math.round(value)}`;
  return Math.round(value).toString();
}

// Format number with optional decimals, used in PropertyCard
export function formatNumber(value: number | undefined, decimals = 0): string {
  if (value === undefined) return '';
  return decimals > 0 ? value.toFixed(decimals) : Math.round(value).toLocaleString();
}

/**
 * Compute percentages that always sum to exactly 100, using the largest-remainder
 * (Hamilton) method. Floors each raw percentage, then distributes the residual to
 * the segments with the largest fractional parts. Eliminates rounding drift where
 * three 33.3% segments would otherwise display as "33%, 33%, 33% = 99%".
 *
 * Assumes `total` equals (or closely equals) the sum of `values`.
 */
export function roundedPercentages(values: number[], total: number, decimals = 0): number[] {
  if (total <= 0 || values.length === 0) return values.map(() => 0);
  const scale = 10 ** decimals;
  const targetSum = 100 * scale;
  const raw = values.map((v) => (v / total) * 100 * scale);
  const floors = raw.map((r) => Math.floor(r));
  const result = floors.slice();
  let diff = targetSum - floors.reduce((a, b) => a + b, 0);
  const order = raw.map((r, i) => ({ i, frac: r - floors[i] })).sort((a, b) => b.frac - a.frac);
  for (let k = 0; k < order.length && diff > 0; k++) {
    result[order[k].i] += 1;
    diff -= 1;
  }
  return result.map((v) => v / scale);
}

export function formatRelativeTime(isoDate: string): string {
  // eslint-disable-next-line @typescript-eslint/no-var-requires
  const i18n = require('../i18n').default as {
    t: (key: string, opts?: Record<string, unknown>) => string;
  };
  const now = Date.now();
  const then = new Date(isoDate).getTime();
  const diffMs = now - then;
  const diffSec = Math.floor(diffMs / 1000);
  if (diffSec < 60) return i18n.t('format.justNow');
  const diffMin = Math.floor(diffSec / 60);
  if (diffMin < 60) return i18n.t('format.minutesAgo', { count: diffMin });
  const diffHr = Math.floor(diffMin / 60);
  if (diffHr < 24) return i18n.t('format.hoursAgo', { count: diffHr });
  const diffDay = Math.floor(diffHr / 24);
  if (diffDay < 30) return i18n.t('format.daysAgo', { count: diffDay });
  return new Date(isoDate).toLocaleDateString();
}

// Percentile-based scale: maps between percentile space (0–100) and absolute values
// using the histogram's CDF. Each percentile step = 1% of data.
export interface PercentileScale {
  toValue: (percentile: number) => number;
  toPercentile: (value: number) => number;
}

export function buildPercentileScale(hist: {
  min: number;
  max: number;
  p1: number;
  p99: number;
  counts: number[];
}): PercentileScale {
  const n = hist.counts.length;
  const total = hist.counts.reduce((a, b) => a + b, 0);

  if (n === 0 || total === 0) {
    const range = hist.max - hist.min || 1;
    return {
      toValue: (p) => hist.min + (p / 100) * range,
      toPercentile: (v) => ((v - hist.min) / range) * 100,
    };
  }

  // Bin boundaries: [min, p1, ..middle edges.., p99, max]
  const boundaries: number[] = [];
  if (n === 1) {
    boundaries.push(hist.min, hist.max);
  } else {
    boundaries.push(hist.min, hist.p1);
    if (n > 2) {
      const middleWidth = (hist.p99 - hist.p1) / (n - 2);
      for (let i = 1; i < n - 1; i++) {
        boundaries.push(hist.p1 + i * middleWidth);
      }
    }
    boundaries.push(hist.max);
  }

  // Cumulative fraction: cumFrac[0]=0, cumFrac[n]=1
  const cumFrac: number[] = [0];
  for (let i = 0; i < n; i++) {
    cumFrac.push(cumFrac[i] + hist.counts[i] / total);
  }
  cumFrac[n] = 1; // ensure exact 1.0

  return {
    toValue(percentile: number): number {
      const target = Math.max(0, Math.min(1, percentile / 100));
      if (target <= 0) return boundaries[0];
      if (target >= 1) return boundaries[n];
      let i = 0;
      for (; i < n - 1; i++) {
        if (cumFrac[i + 1] > target) break;
      }
      const binFrac = cumFrac[i + 1] - cumFrac[i];
      const t = binFrac > 0 ? (target - cumFrac[i]) / binFrac : 0;
      return boundaries[i] + t * (boundaries[i + 1] - boundaries[i]);
    },

    toPercentile(value: number): number {
      if (value <= boundaries[0]) return 0;
      if (value >= boundaries[n]) return 100;
      let i = 0;
      for (; i < n - 1; i++) {
        if (boundaries[i + 1] > value) break;
      }
      const binWidth = boundaries[i + 1] - boundaries[i];
      const t = binWidth > 0 ? (value - boundaries[i]) / binWidth : 0;
      return (cumFrac[i] + t * (cumFrac[i + 1] - cumFrac[i])) * 100;
    },
  };
}

// Calculate weighted mean from histogram with outlier bins.
// Bin 0 = [min, p1), bins 1..n-2 = [p1, p99) evenly, bin n-1 = [p99, max].
export function calculateHistogramMean(histogram: {
  min: number;
  max: number;
  p1: number;
  p99: number;
  counts: number[];
}): number | undefined {
  const n = histogram.counts.length;
  if (n === 0) return undefined;
  const totalCount = histogram.counts.reduce((a, b) => a + b, 0);
  if (totalCount === 0) return undefined;

  const { min, max, p1, p99 } = histogram;
  const middleBins = Math.max(n - 2, 0);
  const middleWidth = middleBins > 0 && p99 > p1 ? (p99 - p1) / middleBins : 0;

  let weightedSum = 0;
  for (let i = 0; i < n; i++) {
    let center: number;
    if (i === 0) center = (min + p1) / 2;
    else if (i === n - 1) center = (p99 + max) / 2;
    else center = p1 + (i - 0.5) * middleWidth;
    weightedSum += center * histogram.counts[i];
  }
  return weightedSum / totalCount;
}
