export type WeightDimKey =
  | 'total_score'
  | 'ext_pop'
  | 'plat_sust_pop'
  | 'plat_ly_pop'
  | 'recent_pop'
  | 'real_rov_7d'
  | 'real_vov_7d'

export interface WeightDimMeta {
  key: WeightDimKey
  label: string
}

export type NodeWeights = Partial<Record<WeightDimKey, number | null>>
export type NodeCounts = Record<WeightDimKey, number>

export interface CategoryNode {
  id: number
  name: string | null
  level: number | null
  description: string | null
  weights?: Partial<NodeWeights>
  counts?: Partial<NodeCounts>
  hung_word_count?: number
  children: CategoryNode[]
}

export interface CategoryTreeResponse {
  biz_dt: string | null
  dims: WeightDimMeta[]
  nodes: CategoryNode[]
}

export const DEFAULT_DIMS: WeightDimMeta[] = [
  { key: 'total_score', label: '全局热度' },
  { key: 'ext_pop', label: '外部热度' },
  { key: 'plat_sust_pop', label: '平台持续热度' },
  { key: 'plat_ly_pop', label: '去年同期热度' },
  { key: 'recent_pop', label: '近期热度' },
  { key: 'real_rov_7d', label: 'ROV相对差(7日)' },
  { key: 'real_vov_7d', label: 'VOV相对差(7日)' },
]

export function maxTreeDepth(nodes: CategoryNode[], depth = 1): number {
  if (!nodes.length) return 0
  let max = depth
  for (const node of nodes) {
    if (node.children.length) {
      max = Math.max(max, maxTreeDepth(node.children, depth + 1))
    }
  }
  return max
}

/** Keep nodes that have data on `dim`, or have such a descendant. */
export function filterTreeByDim(
  nodes: CategoryNode[],
  dim: WeightDimKey,
): CategoryNode[] {
  const out: CategoryNode[] = []
  for (const node of nodes) {
    const children = filterTreeByDim(node.children, dim)
    const hasData = (node.counts?.[dim] ?? 0) > 0
    if (hasData || children.length) {
      out.push({ ...node, children })
    }
  }
  return out
}

export function collectWeights(
  nodes: CategoryNode[],
  dim: WeightDimKey,
  out: number[] = [],
): number[] {
  for (const node of nodes) {
    const w = node.weights?.[dim]
    const c = node.counts?.[dim] ?? 0
    if (typeof w === 'number' && c > 0) out.push(w)
    if (node.children.length) collectWeights(node.children, dim, out)
  }
  return out
}

/** Build ascending-sorted values for percentile / min-max lookup in this tab. */
export function buildWeightScale(values: number[]): number[] {
  if (!values.length) return []
  return values.slice().sort((a, b) => a - b)
}

/** Percentile heat t in [0, 1] among nodes with data in this tab. */
export function weightHeatT(weight: number, sortedScale: number[]): number | null {
  if (!sortedScale.length) return null
  const n = sortedScale.length
  let lo = 0
  let hi = n
  while (lo < hi) {
    const mid = (lo + hi) >> 1
    if (sortedScale[mid] < weight) lo = mid + 1
    else hi = mid
  }
  if (n === 1) return 1
  return Math.min(1, Math.max(0, lo / n))
}

/** Linear heat t in [0, 1]: min→0, max→1 (used by 全局热度 tab). */
export function weightHeatTLinear(weight: number, sortedScale: number[]): number | null {
  if (!sortedScale.length) return null
  const min = sortedScale[0]
  const max = sortedScale[sortedScale.length - 1]
  if (max === min) return 0.5
  return Math.min(1, Math.max(0, (weight - min) / (max - min)))
}

/** Heat t for the active dim; scale is built per tab. */
export function scoreHeatT(
  dim: WeightDimKey,
  weight: number,
  sortedScale: number[],
): number | null {
  if (dim === 'total_score') {
    return weightHeatTLinear(weight, sortedScale)
  }
  return weightHeatT(weight, sortedScale)
}

/** Red (冷) ← White (中) → Green (热) */
export function heatColor(t: number | null): string {
  if (t == null) return '#f1f5f9'
  const clamp = Math.min(1, Math.max(0, t))
  if (clamp <= 0.5) {
    const u = clamp / 0.5
    const r = Math.round(239 + (255 - 239) * u)
    const g = Math.round(68 + (255 - 68) * u)
    const b = Math.round(68 + (255 - 68) * u)
    return `rgb(${r}, ${g}, ${b})`
  }
  const u = (clamp - 0.5) / 0.5
  const r = Math.round(255 + (34 - 255) * u)
  const g = Math.round(255 + (197 - 255) * u)
  const b = Math.round(255 + (94 - 255) * u)
  return `rgb(${r}, ${g}, ${b})`
}

/** Darker text on mid/light, lighter on deep red/green. */
export function heatTextColor(t: number | null): string {
  if (t == null) return '#64748b'
  if (t < 0.18 || t > 0.82) return '#0f172a'
  return '#0f172a'
}

export const POSTERIOR_EFFECT_ACCEPTABLE_FLOOR = -0.2

export function classifyPosteriorEffect(value: number | null | undefined): string | null {
  if (value == null || Number.isNaN(value)) return null
  if (value > 0) return '效果非常好'
  if (value >= POSTERIOR_EFFECT_ACCEPTABLE_FLOOR) return '可接受'
  return '效果不佳'
}

export function formatPosteriorDiff(value: number | null | undefined): string {
  if (value == null || Number.isNaN(value)) return '—'
  const sign = value > 0 ? '+' : ''
  const effect = classifyPosteriorEffect(value)
  const base = `${sign}${value.toFixed(4)}`
  return effect ? `${base} [${effect}]` : base
}

export function formatAvgScore(avg: number | undefined | null, count?: number): string {
  if (count != null && count <= 0) return '—'
  if (avg == null || Number.isNaN(avg)) return '—'
  if (avg >= 100) return avg.toFixed(0)
  if (avg >= 1) return avg.toFixed(1)
  return avg.toFixed(2)
}

export function formatWeightScore(
  dim: WeightDimKey,
  score: number | undefined | null,
  count?: number,
): string {
  if (count != null && count <= 0) return '—'
  if (score == null || Number.isNaN(score)) return '—'
  if (dim === 'total_score') return score.toFixed(2)
  if (dim === 'real_rov_7d' || dim === 'real_vov_7d') {
    return formatPosteriorDiff(score)
  }
  return formatAvgScore(score, count)
}
