import { formatNumber } from '@carto/ps-utils'
import type { Tooltip, Position } from '@deck.gl-community/editable-layers'
import configureMeasurements from 'convert-units'
import length, { type LengthUnits } from 'convert-units/definitions/length'
import type { ControllerOptions } from 'node_modules/@deck.gl/core/dist/controllers/controller'
import type { ValueOf } from 'type-fest'
import type { DeckGLComponentProps } from '../../types'
import {
  CONVERSIONS_UNITS,
  CONVERSIONS_UNITS_AREA,
  CONVERSIONS_UNITS_FROM_LINEAR_TO_AREA,
  INTL_UNITS,
  MapMeasurementUnit,
  MapMeasurementUnitAuto,
} from './const'
import type { EditModeConfig, MeasurementToolsTextProps } from './types'
import type { AreaUnits } from 'convert-units/definitions/area'
import area from 'convert-units/definitions/area'

const convert = configureMeasurements({
  length,
})

const convertArea = configureMeasurements({
  area,
})

function validateUnitForAreaConversion(
  unit: string,
): asserts unit is keyof typeof CONVERSIONS_UNITS_FROM_LINEAR_TO_AREA {
  if (!(unit in CONVERSIONS_UNITS_FROM_LINEAR_TO_AREA)) {
    const validUnits = Object.keys(CONVERSIONS_UNITS_FROM_LINEAR_TO_AREA)
    throw new Error(
      `Unit "${unit}" cannot be converted to area. Valid units are: ${validUnits.join(', ')}`,
    )
  }
}

/**
 * Converts a distance in meters to the specified unit for either length or area dimensions.
 *
 * @param distance - The distance value in meters (or square meters for area).
 * @param unit - The target measurement unit.
 * @param dimension - Whether to convert as `'length'` or `'area'`.
 * @returns The converted distance value in the target unit.
 */
export function convertMetersTo(
  distance: number,
  unit: ValueOf<typeof MapMeasurementUnit>,
  dimension: 'length' | 'area',
) {
  switch (dimension) {
    case 'length': {
      const unitFrom: LengthUnits = CONVERSIONS_UNITS[MapMeasurementUnit.meter]
      const unitTo: LengthUnits = CONVERSIONS_UNITS[unit]

      return convert(distance).from(unitFrom).to(unitTo)
    }
    case 'area': {
      validateUnitForAreaConversion(unit)
      const unitFrom: AreaUnits =
        CONVERSIONS_UNITS_AREA[
          CONVERSIONS_UNITS_FROM_LINEAR_TO_AREA[MapMeasurementUnit.meter]
        ]
      const unitTo: AreaUnits =
        CONVERSIONS_UNITS_AREA[CONVERSIONS_UNITS_FROM_LINEAR_TO_AREA[unit]]

      return convertArea(distance).from(unitFrom).to(unitTo)
    }
    default: {
      throw new Error(
        `Invalid dimension: ${dimension as string}, valid dimensions are: length, area`,
      )
    }
  }
}

const DEFAULT_AUTO_SPLIT_NUMBER = 10000

/**
 * Formats a distance or area value in meters into a localized, unit-aware display string for measurement text layers.
 *
 * @remarks
 * Supports automatic unit switching (e.g., meters to kilometers) when using auto units like `'m-km'` or `'ft-mi'`.
 * A custom formatter function can be provided via `options.customFormatter`.
 *
 * @returns A formatted string with the value and unit (e.g., "1.50 km" or "3.25 mi").
 */
export function getTextLayerFormattedDistance({
  meters,
  unit,
  locale,
  options: { autoSplitNumber, customFormatter, ...intlOptions } = {
    autoSplitNumber: DEFAULT_AUTO_SPLIT_NUMBER,
  },
  dimension = 'length',
}: {
  meters: number
  unit: ValueOf<typeof MapMeasurementUnitAuto>
  locale: string
  options?: MeasurementToolsTextProps
  dimension?: 'length' | 'area'
}) {
  const isMetricSystem = unit === MapMeasurementUnitAuto['m-km']
  const isImperialSystem = unit === MapMeasurementUnitAuto['ft-mi']
  const isAuto = isMetricSystem || isImperialSystem

  let _unit
  let _number

  if (isAuto) {
    const isSmallDistance =
      meters < (autoSplitNumber ?? DEFAULT_AUTO_SPLIT_NUMBER)
    let convertUnit: ValueOf<typeof MapMeasurementUnit> = isSmallDistance
      ? MapMeasurementUnit.meter
      : MapMeasurementUnit.kilometer

    _unit = isSmallDistance ? INTL_UNITS.meter : INTL_UNITS.kilometer

    if (isImperialSystem) {
      _unit = isSmallDistance ? INTL_UNITS.feet : INTL_UNITS.mile
      convertUnit = isSmallDistance
        ? MapMeasurementUnit.feet
        : MapMeasurementUnit.mile
    }

    _number = convertMetersTo(meters, convertUnit, dimension)
  } else {
    _unit = INTL_UNITS[unit]
    _number = convertMetersTo(meters, unit, dimension)
  }

  let result = `${formatNumber(_number, locale, {
    style: 'unit',
    unit: _unit,
    unitDisplay: 'short',
    notation: 'standard',
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
    ...intlOptions,
  })}`

  if (dimension === 'area') {
    result += '2'
  }

  if (customFormatter) {
    return customFormatter(result)
  }

  return result
}

/**
 * Returns a deck.gl controller options object with the `doubleClickZoom` setting applied.
 *
 * @param controller - The current controller value (boolean or options object).
 * @param options - Object containing the desired `hasDoubleClickZoom` setting.
 * @returns A `ControllerOptions` object with double-click zoom configured.
 */
export function getDeckController(
  controller: DeckGLComponentProps['controller'],
  {
    hasDoubleClickZoom,
  }: {
    hasDoubleClickZoom: boolean
  },
): ControllerOptions {
  return typeof controller === 'boolean'
    ? { doubleClickZoom: hasDoubleClickZoom }
    : { ...controller, doubleClickZoom: hasDoubleClickZoom }
}

export function buildTooltip(
  value: number,
  position: Position,
  modeConfig: EditModeConfig,
): Tooltip[] {
  if (value > 0) {
    const { formatTooltip, turfOptions } = modeConfig ?? {
      turfOptions: {
        units: 'kilometers',
      },
    }

    const units = turfOptions?.units

    let text
    if (formatTooltip) {
      text = formatTooltip(value)
    } else {
      text = `${parseFloat(value.toString()).toFixed(2)} ${units}`
    }

    const { measurementCallback } = modeConfig as {
      measurementCallback?: (value: number) => void
    }

    if (measurementCallback) {
      measurementCallback(value)
    }

    return [
      {
        position,
        text,
      },
    ]
  }
  return []
}
