/**
 * Download data as CSV file.
 *
 * @param headers - Array of header names. If omitted and data is array of objects, keys will be taken from first object.
 * @param data - Array of rows. Each row can be either an object (key -> value) or an array of values.
 * @param filename - optional file name to use for downloaded file
 * @param delimiter - csv delimiter (default is comma)
 * @param addBOM - add UTF-8 BOM to the file to make Excel detect UTF-8 correctly
 * Usage examples:
 * 1) With headers and array of objects
 * downloadCsv(['name', 'age'], [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.csv');
 * 2) Without headers (automatically uses object keys)
 * downloadCsv(null, [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.csv');
 * 3) With array-of-arrays
 * downloadCsv(['col1', 'col2'], [['a', 'b'], ['c', 'd']], 'matrix.csv');
 */
export function downloadCsv(
  headers: string[] | null,
  headerLabels: string[],
  data: Array<Record<string, any> | any[]>,
  filename = 'data.csv',
  delimiter = ',',
  addBOM = true,
) {
  if (!Array.isArray(data) || data.length === 0) {
    // still create an empty CSV containing only headers
    const csvHeader = headers ? headers.join(delimiter) : ''
    const csvString = addBOM ? '\uFEFF' + csvHeader : csvHeader
    const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' })
    const link = document.createElement('a')
    link.href = URL.createObjectURL(blob)
    link.setAttribute('download', filename)
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    return
  }

  // if headers not provided and rows are objects, take keys from first object
  let _headers: string[] | null = headers
  if (!_headers) {
    const firstRow = data[0]
    if (typeof firstRow === 'object' && !Array.isArray(firstRow)) {
      _headers = Object.keys(firstRow)
    } else if (Array.isArray(firstRow)) {
      // if rows are arrays and no headers provided, we won't add header row
      _headers = null
    }
  }

  const escape = (val: unknown) => {
    if (val === null || typeof val === 'undefined') return ''
    const str = String(val)
    const needsQuoting = str.includes(delimiter) || str.includes('\n') || str.includes('\r') || str.includes('"')
    if (!needsQuoting) return str
    return '"' + str.replace(/"/g, '""') + '"'
  }

  const rows: string[] = data.map((row) => {
    if (Array.isArray(row)) {
      return row.map(escape).join(delimiter)
    }
    // object row - map using headers if available, otherwise use object values
    if (_headers && Array.isArray(_headers)) {
      return _headers.map((h) => escape((row as Record<string, any>)[h])).join(delimiter)
    }
    return Object.values(row).map(escape).join(delimiter)
  })

  const headerRow = headerLabels ? headerLabels.join(delimiter) : _headers ? _headers.join(delimiter) : null
  const csvString = (addBOM ? '\uFEFF' : '') + [headerRow, ...rows].filter(Boolean).join('\r\n')

  const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' })
  const link = document.createElement('a')
  link.href = URL.createObjectURL(blob)
  link.setAttribute('download', filename)
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
}

/**
 * Download data as XLS (Excel) file using xlsx library.
 *
 * @param headers - Array of header names. If omitted and data is array of objects, keys will be taken from first object.
 * @param data - Array of rows. Each row can be either an object (key -> value) or an array of values.
 * @param filename - optional file name to use for downloaded file (default: 'data.xlsx')
 * @param sheetName - optional sheet name in workbook (default: 'Sheet1')
 * Usage examples:
 * 1) With headers and array of objects
 * await downloadXls(['name', 'age'], [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.xlsx');
 * 2) Without headers (automatically uses object keys)
 * await downloadXls(null, [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.xlsx');
 * 3) With custom sheet name
 * await downloadXls(['col1', 'col2'], [['a', 'b'], ['c', 'd']], 'matrix.xlsx', 'MyData');
 */
export async function downloadXls(
  headers: string[] | null,
  headerLabels: string[],
  data: Array<Record<string, any> | any[]>,
  filename = 'data.xlsx',
  sheetName = 'Sheet1',
) {
  // Dynamically import xlsx to avoid server-side issues
  const { utils, write } = await import('xlsx')
  const { saveAs } = await import('file-saver')

  if (!Array.isArray(data) || data.length === 0) {
    // Create empty sheet with headers only
    const ws = utils.aoa_to_sheet(headers ? [headers] : [[]])
    const wb = utils.book_new()
    utils.book_append_sheet(wb, ws, sheetName)
    const wbout = write(wb, { bookType: 'xlsx', type: 'array' })
    saveAs(new Blob([wbout], { type: 'application/octet-stream' }), filename)
    return
  }

  // if headers not provided and rows are objects, take keys from first object
  let _headers: string[] | null = headers
  if (!_headers) {
    const firstRow = data[0]
    if (typeof firstRow === 'object' && !Array.isArray(firstRow)) {
      _headers = Object.keys(firstRow)
    } else if (Array.isArray(firstRow)) {
      _headers = null
    }
  }

  // Convert data rows to 2D array
  const rows: any[][] = data.map((row) => {
    if (Array.isArray(row)) {
      return row
    }
    // object row - map using headers if available, otherwise use object values
    if (_headers && Array.isArray(_headers)) {
      return _headers.map((h) => (row as Record<string, any>)[h] ?? '')
    }
    return Object.values(row)
  })

  // Combine headers/labels and rows for sheet
  // If caller provided headerLabels (as display labels), prefer them.
  const sheetHeader = headerLabels ? headerLabels : _headers ? _headers : null
  const sheetData = sheetHeader ? [sheetHeader, ...rows] : rows

  // Create worksheet and workbook
  const ws = utils.aoa_to_sheet(sheetData)
  const wb = utils.book_new()
  utils.book_append_sheet(wb, ws, sheetName)

  // Write and save file
  const wbout = write(wb, { bookType: 'xlsx', type: 'array' })
  saveAs(new Blob([wbout], { type: 'application/octet-stream' }), filename)
}
