import type { LineChartInput, PieChartInput, BarChartInput, HeatmapChartInput } from '@/types/chart'

export type Table = { columns: string[]; data: any[][] }

/** Convert {columns, data} => array of records */
export function toRecords(table: Table): Record<string, any>[] {
  const { columns, data } = table
  return data.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])))
}

/* ----------------------------- Utilities ----------------------------- */

const uniq = <T>(arr: T[]) => Array.from(new Set(arr))

function toNumber(v: any): number {
  if (typeof v === 'number') return v
  if (v == null || v === '') return 0
  const n = Number(v)
  return Number.isFinite(n) ? n : 0
}

/** Natural/date-aware ordering helpers */
function orderX(values: any[], xOrder?: string[], xIsDate?: boolean): string[] {
  const raw = uniq(values).filter((v) => v != null)
  if (xOrder && xOrder.length) return xOrder
  if (xIsDate) {
    return raw
      .slice()
      .sort((a: any, b: any) => new Date(a).getTime() - new Date(b).getTime())
      .map(String)
  }
  return raw.map(String)
}

/* --------------------------- LINE TRANSFORMS -------------------------- */

/**
 * Discriminated union for line transforms to avoid the “missing series” pitfall.
 * One path uses explicit numeric columns (series), the other uses groupBy.
 */
export type LineFromTableOpts =
  | {
      // Wide format: one series per numeric column
      title?: string
      x: string
      series: string[] // required in this path
      yUnit?: string
      smooth?: boolean
      stacked?: boolean
      xOrder?: string[]
      xIsDate?: boolean
      groupBy?: never
    }
  | {
      // Long format: one series per distinct key
      title?: string
      x: string
      groupBy: { keyCol: string; valueCol: string; sortKeys?: string[] }
      yUnit?: string
      smooth?: boolean
      stacked?: boolean
      xOrder?: string[]
      xIsDate?: boolean
      series?: never
    }

/** Build a LineChartInput from array of records (already materialized) */
export function lineFromRecords(
  records: Record<string, any>[],
  opts: LineFromTableOpts
): LineChartInput {
  const { x, title, yUnit, smooth, stacked, xOrder, xIsDate } = opts

  const xVals = records.map((r) => r[x])
  const orderedX = orderX(xVals, xOrder, xIsDate)

  // Path A: explicit numeric columns => one series per column
  if ('series' in opts) {
    const seriesDefs = opts.series!.map((col) => ({
      name: col,
      data: orderedX.map((xv) => {
        const row = records.find((r) => String(r[x]) === String(xv))
        return toNumber(row?.[col])
      }),
    }))

    return {
      kind: 'line',
      title,
      x: orderedX,
      xIsDate: xIsDate,
      series: seriesDefs,
      yUnit,
      smooth,
      stacked,
    }
  }

  // Path B: groupBy => one series per distinct key
  const { keyCol, valueCol, sortKeys } = opts.groupBy
  const keys = uniq(records.map((r) => r[keyCol])).map(String)
  const orderedKeys = sortKeys && sortKeys.length ? sortKeys : keys

  const seriesDefs = orderedKeys.map((k) => ({
    name: String(k),
    data: orderedX.map((xv) => {
      const row = records.find(
        (r) => String(r[x]) === String(xv) && String(r[keyCol]) === String(k)
      )
      return toNumber(row?.[valueCol])
    }),
  }))

  return {
    kind: 'line',
    title,
    x: orderedX,
    series: seriesDefs,
    xIsDate: xIsDate,
    yUnit,
    smooth,
    stacked,
  }
}

/** Build a LineChartInput directly from the table */
export function lineFromTable(table: Table, opts: LineFromTableOpts): LineChartInput {
  return lineFromRecords(toRecords(table), opts)
}

/* ---------------------------- PIE TRANSFORMS -------------------------- */

export type PieFromTableOpts = {
  title?: string
  label: string // category column
  value: string // numeric column
  aggregate?: 'sum' | 'avg'
  donut?: boolean
}

/** Build a PieChartInput from array of records */
export function pieFromRecords(
  records: Record<string, any>[],
  opts: PieFromTableOpts
): PieChartInput {
  const { title, label, value, aggregate = 'sum', donut } = opts

  const map = new Map<string, { total: number; count: number }>()
  for (const r of records) {
    const key = String(r[label])
    const v = toNumber(r[value])
    const acc = map.get(key) || { total: 0, count: 0 }
    acc.total += v
    acc.count += 1
    map.set(key, acc)
  }

  const series = Array.from(map.entries()).map(([name, { total, count }]) => ({
    name,
    value: aggregate === 'avg' ? (count ? total / count : 0) : total,
  }))

  return {
    kind: 'pie',
    title,
    series,
    donut,
  }
}

/** Build a PieChartInput directly from the table */
export function pieFromTable(table: Table, opts: PieFromTableOpts): PieChartInput {
  return pieFromRecords(toRecords(table), opts)
}

export type BarFromTableOpts = {
  title?: string | null
  x: string // category axis column (e.g., "product")
  valueCol: string // numeric column (e.g., "total_quantity")
  groupCol?: string | null // optional: one series per distinct group (e.g., "region")
  stacked?: boolean | null
  yUnit?: string
  xOrder?: string[] | null
}

function cleanGroupName(v: any): string | null {
  if (v === undefined || v === null) return null
  const s = String(v).trim()
  if (!s || s.toLowerCase() === 'undefined' || s.toLowerCase() === 'null') return null
  return s
}

export function barFromTable(table: Table, opts: BarFromTableOpts): BarChartInput {
  const records = toRecords(table)
  const { x, valueCol, groupCol, title, stacked, yUnit, xOrder } = opts

  const hasGroupCol = !!groupCol && Array.isArray(table.columns) && table.columns.includes(groupCol)

  const xCats = orderX(
    records.map((r) => r[x]),
    xOrder ?? undefined,
    false
  )

  // Aggregate values by (x, group)
  const matrix = new Map<string, number>() // key = `${x}|${group}`
  const groupsSet = new Set<string>()

  for (const r of records) {
    const xKey = String(r[x])
    const gRaw = hasGroupCol ? cleanGroupName(r[groupCol!]) : null
    const gKey = gRaw ?? '__single__'
    const val = toNumber(r[valueCol])

    groupsSet.add(gKey)
    matrix.set(`${xKey}|${gKey}`, (matrix.get(`${xKey}|${gKey}`) ?? 0) + val)
  }

  const onlySingle = groupsSet.size === 1 && groupsSet.has('__single__')

  const groups = onlySingle
    ? ['__single__']
    : Array.from(groupsSet).filter((g) => g !== '__single__')

  const series = groups.map((g) => ({
    name: g === '__single__' ? valueCol : g,
    data: xCats.map((xv) => matrix.get(`${String(xv)}|${g}`) ?? 0),
  }))

  return {
    kind: 'bar',
    title: title ?? undefined,
    x: xCats,
    series,
    stacked: !!stacked && !onlySingle,
    yUnit,
  }
}

// --- HEATMAP ---
export type HeatmapFromTableOpts = {
  title?: string
  xCol: string // x categories
  yCol: string // y categories
  valueCol: string // numeric cell value
  xOrder?: string[]
  yOrder?: string[]
}

export function heatmapFromTable(table: Table, opts: HeatmapFromTableOpts): HeatmapChartInput {
  const records = toRecords(table)
  const { xCol, yCol, valueCol, title, xOrder, yOrder } = opts

  const xs = orderX(
    records.map((r) => r[xCol]),
    xOrder,
    false
  )
  const ys = orderX(
    records.map((r) => r[yCol]),
    yOrder,
    false
  )

  const xIndex = new Map(xs.map((v, i) => [String(v), i]))
  const yIndex = new Map(ys.map((v, i) => [String(v), i]))

  const grid: Array<[number, number, number]> = []
  for (const r of records) {
    const xi = xIndex.get(String(r[xCol]))
    const yi = yIndex.get(String(r[yCol]))
    if (xi == null || yi == null) continue
    grid.push([xi, yi, toNumber(r[valueCol])])
  }

  return { kind: 'heatmap', title, x: xs, y: ys, data: grid }
}
