<script lang="ts" setup>
import dayjs from 'dayjs'
import { type ColumnType, PermissionEntity, PermissionKey, UITypes } from 'nocodb-sdk'
import type { Row } from '~/lib/types'

const emit = defineEmits(['expandRecord', 'newRecord'])

const {
  calDataType,
  selectedDate,
  selectedTime,
  formattedData,
  calendarRange,
  formattedSideBarData,
  updateRowProperty,
  displayField,
  sideBarFilterOption,
  showSideMenu,
  updateFormat,
  timezoneDayjs,
  timezone,
  isSyncedFromColumn,
} = useCalendarViewStoreOrThrow()

const { isSyncedTable } = useSmartsheetStoreOrThrow()

const { $e } = useNuxtApp()

const container = ref<null | HTMLElement>(null)

const { width: containerWidth } = useElementSize(container)

const { isUIAllowed } = useRoles()

const meta = inject(MetaInj, ref())

const fields = inject(FieldsInj, ref())

const isPublic = inject(IsPublicInj, ref(false))

const { fields: _fields } = useViewColumnsOrThrow()

const fieldStyles = computed(() => {
  if (!_fields.value) return new Map()
  return new Map(
    _fields.value.map((field) => [
      field.fk_column_id,
      {
        underline: field.underline,
        bold: field.bold,
        italic: field.italic,
      },
    ]),
  )
})

const getFieldStyle = (field: ColumnType) => {
  return fieldStyles.value.get(field.id)
}

const hours = computed(() => {
  const hours: Array<dayjs.Dayjs> = []

  // Force the date part only (no time), to reset time to 00:00 in target TZ
  const baseDateStr = dayjs(selectedDate.value).format('YYYY-MM-DD')

  // Parse in timezone with NO time part — this ensures it's midnight in TZ
  const startOfDay = timezoneDayjs.dayjsTz(baseDateStr, 'YYYY-MM-DD').hour(0).minute(0).second(0)

  for (let i = 0; i < 24; i++) {
    hours.push(startOfDay.clone().add(i, 'hour'))
  }
  return hours
})

const currTime = ref(timezoneDayjs.dayjsTz())

const overlayTop = computed(() => {
  const perRecordHeight = 52

  const minutes = currTime.value.minute() + currTime.value.hour() * 60

  const top = (perRecordHeight / 60) * minutes - 9

  return top
})

const shouldEnableOverlay = computed(() => {
  return !isPublic.value && timezoneDayjs.dayjsTz().isSame(selectedDate.value, 'day')
})

onMounted(() => {
  const intervalId = setInterval(() => {
    currTime.value = timezoneDayjs.dayjsTz()
  }, 10000) // 10000 ms = 10 seconds

  // Clean up the interval when the component is unmounted
  onUnmounted(() => {
    clearInterval(intervalId)
  })
})

const calculateNewDates = useMemoize(
  ({
    endDate,
    startDate,
    scheduleStart,
    scheduleEnd,
  }: {
    endDate: dayjs.Dayjs
    startDate: dayjs.Dayjs
    scheduleStart: dayjs.Dayjs
    scheduleEnd: dayjs.Dayjs
  }) => {
    // If there is no end date, we add 15 minutes to the start date and use that as the end date
    if (!endDate.isValid()) {
      endDate = startDate.clone().add(15, 'minutes')
    }

    if (endDate.diff(startDate, 'minute') < 15) {
      endDate = startDate.clone().add(15, 'minutes')
    }

    if (endDate.diff(startDate, 'minute') === 60) {
      endDate = startDate.clone().add(59, 'minutes')
    }

    // If the start date is before the opened date, we use the schedule start as the start date
    // This is to ensure the generated style of the record is not outside the bounds of the calendar
    if (startDate.isSameOrBefore(scheduleStart)) {
      startDate = scheduleStart
    }

    // If the end date is after the schedule end, we use the schedule end as the end date
    // This is to ensure the generated style of the record is not outside the bounds of the calendar
    if (endDate.isAfter(scheduleEnd)) {
      endDate = scheduleEnd
    }

    return { endDate, startDate }
  },
)

const getGridTime = (date: dayjs.Dayjs, round = false) => {
  const gridCalc = date.hour() * 60 + date.minute()
  if (round) {
    return Math.ceil(gridCalc)
  } else {
    return Math.floor(gridCalc)
  }
}

const getGridTimeSlots = (from: dayjs.Dayjs, to: dayjs.Dayjs) => {
  return {
    from: getGridTime(from, false),
    to: getGridTime(to, true) - 1,
  }
}

const hasSlotForRecord = (
  columnArray: Row[],
  dates: {
    fromDate: dayjs.Dayjs
    toDate: dayjs.Dayjs
  },
) => {
  const { fromDate, toDate } = dates

  if (!fromDate || !toDate) return false

  for (const column of columnArray) {
    const columnFromCol = column.rowMeta.range?.fk_from_col
    const columnToCol = column.rowMeta.range?.fk_to_col

    if (!columnFromCol) return false

    const { startDate: columnFromDate, endDate: columnToDate } = calculateNewDates({
      startDate: timezoneDayjs.timezonize(column.row[columnFromCol.title!]),
      endDate:
        columnToCol && dayjs(column.row[columnToCol.title!])?.isValid()
          ? timezoneDayjs.timezonize(column.row[columnToCol.title!])
          : timezoneDayjs.timezonize(column.row[columnFromCol.title!]).add(1, 'hour').subtract(1, 'minute'),
      scheduleStart: timezoneDayjs.dayjsTz(selectedDate.value).startOf('day'),
      scheduleEnd: timezoneDayjs.dayjsTz(selectedDate.value).endOf('day'),
    })

    if (
      fromDate.isBetween(columnFromDate, columnToDate, null, '[]') ||
      toDate.isBetween(columnFromDate, columnToDate, null, '[]')
    ) {
      return false
    }
  }
  return true
}
const getMaxOverlaps = ({
  row,
  columnArray,
  graph,
}: {
  row: Row
  columnArray: Array<Array<Row>>
  graph: Map<string, Set<string>>
}) => {
  const visited: Set<string> = new Set()

  const dfs = (id: string): number => {
    visited.add(id)
    let maxOverlaps = 1
    const neighbors = graph.get(id)
    if (neighbors) {
      for (const neighbor of neighbors) {
        if (maxOverlaps >= columnArray.length) return maxOverlaps
        if (!visited.has(neighbor)) {
          maxOverlaps = Math.min(Math.max(maxOverlaps, dfs(neighbor) + 1), columnArray.length)
        }
      }
    }
    return maxOverlaps
  }

  const id = row.rowMeta.id as string
  if (graph.has(id)) {
    dfs(id)
  }

  const overlapIterations: Array<number> = []

  columnArray
    .flat()
    .filter((record) => visited.has(record.rowMeta.id!))
    .forEach((record) => {
      overlapIterations.push(record.rowMeta.overLapIteration!)
    })

  return Math.max(...overlapIterations)
}

const dragRecord = ref<Row | null>(null)

const isDragging = ref(false)

const dragElement = ref<HTMLElement | null>(null)

const resizeDirection = ref<'right' | 'left' | null>()

const resizeRecord = ref<Row | null>(null)

const dragTimeout = ref<ReturnType<typeof setTimeout>>()

const hoverRecord = ref<string | null>(null)

const recordsAcrossAllRange = computed<{
  record: Row[]
  spanningRecords: Row[]
  gridTimeMap: Map<
    number,
    {
      count: number
      id: string[]
      overflowRecords: Row[]
    }
  >
}>(() => {
  if (!calendarRange.value || !formattedData.value) return { record: [], count: {} }

  const scheduleStart = timezoneDayjs.dayjsTz(selectedDate.value).startOf('day')
  const scheduleEnd = timezoneDayjs.dayjsTz(selectedDate.value).endOf('day')

  const perRecordHeight = 52

  const columnArray: Array<Array<Row>> = [[]]
  const gridTimeMap = new Map<
    number,
    {
      count: number
      id: string[]
      overflowRecords: Row[]
    }
  >()

  const recordsByRange: Array<Row> = []
  const recordSpanningDays: Array<Row> = []

  calendarRange.value.forEach((range) => {
    const { fk_from_col: fromCol, fk_to_col: endCol } = range

    // We fetch all the records that match the calendar ranges in a single time.
    // But not all fetched records are valid for the certain range, so we filter them out & sort them
    const sortedFormattedData = [...formattedData.value]
      .filter((record) => {
        const fromDate = fromCol?.title && record.row[fromCol.title] ? timezoneDayjs.timezonize(record.row[fromCol.title]) : null

        if (fromCol && endCol) {
          const toDate = record.row[endCol.title!] ? timezoneDayjs.timezonize(record.row[endCol.title!]) : null

          if (fromDate?.isValid() && toDate?.isValid()) {
            if (!fromDate.isSame(toDate, 'day')) {
              // TODO: If multiple range is introduced, we have to make sure no duplicate records are inserted
              recordSpanningDays.push(record)
              return false
            }
            return fromDate.isSameOrBefore(toDate)
          }
          return true
        }

        return fromCol ? !!fromDate : false
      })
      .sort((a, b) =>
        timezoneDayjs.timezonize(a.row[fromCol!.title!]).isBefore(timezoneDayjs.timezonize(b.row[fromCol!.title!])) ? 1 : -1,
      )

    for (const record of sortedFormattedData) {
      const id = record.rowMeta.id ?? generateRandomNumber()

      if (fromCol && endCol) {
        const { endDate, startDate } = calculateNewDates({
          endDate: dayjs(record.row[endCol.title!])?.isValid()
            ? timezoneDayjs.timezonize(record.row[endCol.title!])
            : dayjs(record.row[endCol.title!]),
          startDate: timezoneDayjs.timezonize(record.row[fromCol.title!]),
          scheduleStart,
          scheduleEnd,
        })
        // The top of the record is calculated based on the start hour and minute
        const topInPixels = (startDate.minute() / 60 + startDate.hour()) * perRecordHeight

        // A minimum height of 52px is set for each record
        // The height of the record is calculated based on the difference between the start and end date
        const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 52, perRecordHeight)

        const style: Partial<CSSStyleDeclaration> = {
          height: `${heightInPixels - 2}px`,
          top: `${topInPixels + 1}px`,
        }

        recordsByRange.push({
          ...record,
          rowMeta: {
            ...record.rowMeta,
            style,
            id,
            range: range as any,
          },
        })
      } else if (fromCol) {
        const { startDate, endDate } = calculateNewDates({
          startDate: timezoneDayjs.timezonize(record.row[fromCol.title!]),
          endDate: timezoneDayjs.timezonize(record.row[fromCol.title!]).add(1, 'hour').subtract(1, 'minute'),
          scheduleStart,
          scheduleEnd,
        })
        let style: Partial<CSSStyleDeclaration> = {}

        // The top of the record is calculated based on the start hour
        // Update such that it is also based on Minutes
        const topInPixels = (startDate.minute() / 60 + startDate.hour()) * perRecordHeight

        // A minimum height of 80px is set for each record
        const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 52, perRecordHeight)
        style = {
          ...style,
          top: `${topInPixels + 1}px`,
          height: `${heightInPixels - 2}px`,
        }

        recordsByRange.push({
          ...record,
          rowMeta: {
            ...record.rowMeta,
            range: range as any,
            style,
            id,
          },
        })
      }
    }
  })

  recordsByRange.sort((a, b) => {
    const fromColA = a.rowMeta.range?.fk_from_col
    const fromColB = b.rowMeta.range?.fk_from_col
    if (!fromColA || !fromColB) return 0
    return timezoneDayjs.timezonize(a.row[fromColA.title!]).isBefore(timezoneDayjs.timezonize(b.row[fromColB.title!])) ? -1 : 1
  })

  for (const record of recordsByRange) {
    const fromCol = record.rowMeta.range?.fk_from_col
    const toCol = record.rowMeta.range?.fk_to_col

    if (!fromCol) continue

    const { startDate, endDate } = calculateNewDates({
      startDate: timezoneDayjs.timezonize(record.row[fromCol.title!]),
      endDate:
        toCol && dayjs(record.row[toCol.title])?.isValid()
          ? timezoneDayjs.timezonize(record.row[toCol.title!])
          : timezoneDayjs.timezonize(record.row[fromCol.title!]).add(1, 'hour').subtract(1, 'minute'),
      scheduleStart,
      scheduleEnd,
    })

    const gridTimes = getGridTimeSlots(startDate, endDate)

    for (let gridCounter = gridTimes.from; gridCounter <= gridTimes.to; gridCounter++) {
      if (!gridTimeMap.has(gridCounter)) {
        gridTimeMap.set(gridCounter, {
          count: 1,
          id: [record.rowMeta.id!],
          overflowRecords: [],
        })
      } else {
        gridTimeMap.set(gridCounter, {
          count: gridTimeMap.get(gridCounter)!.count + 1,
          id: [...gridTimeMap.get(gridCounter)!.id, record.rowMeta.id!],
          overflowRecords: [],
        })
      }
    }

    let foundAColumn = false

    for (const column in columnArray) {
      if (
        hasSlotForRecord(columnArray[column], {
          fromDate: startDate,
          toDate: endDate,
        })
      ) {
        columnArray[column].push(record)
        foundAColumn = true
        break
      }
    }

    if (!foundAColumn) {
      columnArray.push([record])
    }
  }
  for (const columnIndex in columnArray) {
    for (const record of columnArray[columnIndex]) {
      record.rowMeta.overLapIteration = parseInt(columnIndex) + 1
    }
  }

  const graph = new Map<string, Set<string>>()

  // Build the graph
  for (const [_gridTime, { id: ids }] of gridTimeMap) {
    for (const id1 of ids) {
      if (!graph.has(id1)) {
        graph.set(id1, new Set())
      }
      for (const id2 of ids) {
        if (id1 !== id2) {
          graph.get(id1)!.add(id2)
        }
      }
    }
  }

  for (const record of recordsByRange) {
    const numberOfOverlaps = getMaxOverlaps({
      row: record,
      columnArray,
      graph,
    })

    record.rowMeta.numberOfOverlaps = numberOfOverlaps

    let width
    let left = 100
    let display = 'block'

    const isRecordDraggingOrResizeState =
      record.rowMeta.id === dragRecord.value?.rowMeta.id || record.rowMeta.id === resizeRecord.value?.rowMeta.id

    if (isRecordDraggingOrResizeState || !numberOfOverlaps || numberOfOverlaps <= 0) {
      record.rowMeta.style = {
        ...record.rowMeta.style,
        zIndex: 10,
      }
      left = 0
      width = '100%'
    } else {
      const overlapIndex = record.rowMeta.overLapIteration

      if (overlapIndex > 8) {
        display = 'none'

        // Add overflowing record to gridTimeMap
        const fromCol = record.rowMeta.range?.fk_from_col
        const toCol = record.rowMeta.range?.fk_to_col

        if (fromCol) {
          const { startDate, endDate } = calculateNewDates({
            startDate: timezoneDayjs.timezonize(record.row[fromCol.title!]),
            endDate:
              toCol && dayjs(record.row[toCol.title!])?.isValid()
                ? timezoneDayjs.timezonize(record.row[toCol.title!])
                : timezoneDayjs.timezonize(record.row[fromCol.title!]).add(1, 'hour').subtract(1, 'minute'),
            scheduleStart,
            scheduleEnd,
          })

          const gridTimes = getGridTimeSlots(startDate, endDate)

          for (let gridCounter = gridTimes.from; gridCounter <= gridTimes.to; gridCounter++) {
            if (gridTimeMap.has(gridCounter)) {
              const currentSlot = gridTimeMap.get(gridCounter)!
              if (!currentSlot.overflowRecords.some((r) => r.rowMeta.id === record.rowMeta.id)) {
                currentSlot.overflowRecords.push(record)
                gridTimeMap.set(gridCounter, currentSlot)
              }
            }
          }
        }
      } else {
        const availableWidth = containerWidth.value - 70
        width = Math.max(availableWidth / Math.min(numberOfOverlaps, 8), 180)

        const spacing = (availableWidth - width * Math.min(numberOfOverlaps, 8)) / Math.max(Math.min(numberOfOverlaps, 8) - 1, 1)
        left = (width + spacing) * (overlapIndex - 1)

        if (left + width > availableWidth) {
          width = Math.max(availableWidth - left - 1.5, 180)
        }
      }
    }

    record.rowMeta.style = {
      ...record.rowMeta.style,
      display,
      width: typeof width === 'number' ? `${width}px` : width,
      left: `${left}px`,
    }
  }

  return {
    gridTimeMap,
    record: recordsByRange,
    spanningRecords: recordSpanningDays,
  }
})

const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[], isDelete: boolean) => {
  updateRowProperty(row, updateProperty, isDelete)
}, 500)

// When the user is dragging a record, we calculate the new start and end date based on the mouse position
const calculateNewRow = (event: MouseEvent, skipChangeCheck?: boolean) => {
  if (!container.value || !dragRecord.value) return { newRow: null, updateProperty: [] }

  const { top } = container.value.getBoundingClientRect()

  const { scrollHeight } = container.value

  // We calculate the percentage of the mouse position in the scroll container
  const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight

  const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
  const toCol = dragRecord.value.rowMeta.range?.fk_to_col

  // We calculate the hour based on the percentage of the mouse position in the scroll container
  // It can be between 0 and 23 (inclusive)
  const hour = Math.max(Math.floor(percentY * 23), 0)
  const minutes = Math.min(Math.max(Math.round(Math.floor((percentY * 23 - hour) * 60) / 15) * 15, 0), 60)
  // We calculate the new startDate by adding the hour to the start of the selected date
  const newStartDate = timezoneDayjs.timezonize(selectedDate.value.startOf('day')).add(hour, 'hour').add(minutes, 'minute')
  if (!newStartDate || !fromCol) return { newRow: null, updateProperty: [] }

  let endDate

  const newRow = {
    ...dragRecord.value,
    row: {
      ...dragRecord.value.row,
      [fromCol.title!]: timezoneDayjs.dayjsTz(newStartDate).format(updateFormat.value),
    },
  }

  const updateProperty = [fromCol.title!]

  if (toCol) {
    const fromDate = dragRecord.value.row[fromCol.title!] ? timezoneDayjs.timezonize(dragRecord.value.row[fromCol.title!]) : null
    const toDate = dragRecord.value.row[toCol.title!] ? timezoneDayjs.timezonize(dragRecord.value.row[toCol.title!]) : null

    // If there is an end date, we calculate the new end date based on the new start date and add the difference between the start and end date to the new start date
    if (fromDate && toDate) {
      endDate = timezoneDayjs.dayjsTz(newStartDate).add(toDate.diff(fromDate, 'hour'), 'hour')
    } else if (fromDate && !toDate) {
      // If there is no end date, we set the end date to the end of the day
      endDate = timezoneDayjs.dayjsTz(newStartDate).endOf('hour')
    } else if (!fromDate && toDate) {
      // If there is no start date, we set the end date to the end of the day
      endDate = timezoneDayjs.dayjsTz(newStartDate).endOf('hour')
    } else {
      endDate = newStartDate.clone()
    }

    newRow.row[toCol.title!] = timezoneDayjs.dayjsTz(endDate).format(updateFormat.value)

    updateProperty.push(toCol.title!)
  }

  // If from and to columns of the dragRecord and the newRow are the same, we don't manipulate the formattedRecords and formattedSideBarData. This removes unwanted computation
  if (dragRecord.value.row[fromCol.title!] === newRow.row[fromCol.title!] && !skipChangeCheck) {
    return { newRow: null, updateProperty: [] }
  }

  if (!newRow) {
    return { newRow: null, updateProperty: [] }
  }

  // We use the primary key of the new row to find the old row in the formattedData array
  // If the old row is found, we replace it with the new row in the formattedData array
  const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
  if (dragElement.value) {
    formattedData.value = formattedData.value.map((r) => {
      const pk = extractPkFromRow(r.row, meta.value!.columns!)
      return pk === newPk ? newRow : r
    })
  } else {
    // If the old row is not found, we add the new row to the formattedData array and remove the old row from the formattedSideBarData array
    formattedData.value = [...formattedData.value, newRow]
    formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
      const pk = extractPkFromRow(r.row, meta.value!.columns!)
      return pk !== newPk
    })

    dragRecord.value = {
      ...dragRecord.value,
      row: newRow.row,
    }
  }
  return { newRow, updateProperty }
}

const onResize = (event: MouseEvent) => {
  if (!isUIAllowed('dataEdit') || !container.value || !resizeRecord.value) return
  if (resizeRecord.value.rowMeta.range?.is_readonly) return

  const { top, bottom } = container.value.getBoundingClientRect()
  const { scrollHeight } = container.value

  // If the mouse position is near the top or bottom of the scroll container, we scroll the container
  if (event.clientY > bottom - 20) {
    container.value.scrollTop += 10
  } else if (event.clientY < top + 20) {
    container.value.scrollTop -= 10
  }

  const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight

  const fromCol = resizeRecord.value.rowMeta.range?.fk_from_col
  const toCol = resizeRecord.value.rowMeta.range?.fk_to_col

  if (!fromCol || !toCol) return

  const ogEndDate = timezoneDayjs.timezonize(resizeRecord.value.row[toCol.title!])
  const ogStartDate = timezoneDayjs.timezonize(resizeRecord.value.row[fromCol.title!])

  const minutes = Math.round((percentY * 24 * 60) / 15) * 15 // Round to nearest 15 minutes

  let newRow: Row | null = null
  let updateProperty: string[] = []

  if (resizeDirection.value === 'right') {
    // If the user is resizing the record to the right, we calculate the new end date based on the mouse position
    let newEndDate = timezoneDayjs.timezonize(selectedDate.value.startOf('day')).add(minutes, 'minute')

    updateProperty = [toCol.title!]

    // If the new end date is before the start date, we set the new end date to the start date
    // This is to ensure the end date is always same or after the start date
    if (timezoneDayjs.dayjsTz(newEndDate).isBefore(ogStartDate.add(1, 'hour'))) {
      newEndDate = ogStartDate.clone().add(1, 'hour')
    }

    if (!newEndDate.isValid()) return

    newRow = {
      ...resizeRecord.value,
      row: {
        ...resizeRecord.value.row,
        [toCol.title!]: newEndDate.format('YYYY-MM-DD HH:mm:ssZ'),
      },
    }
  } else if (resizeDirection.value === 'left') {
    let newStartDate = timezoneDayjs.dayjsTz(selectedDate.value).startOf('day').add(minutes, 'minute')

    updateProperty = [fromCol.title!]

    // If the new start date is after the end date, we set the new start date to the end date
    // This is to ensure the start date is always before or same the end date
    if (timezoneDayjs.dayjsTz(newStartDate).isAfter(ogEndDate.subtract(1, 'hour'))) {
      newStartDate = timezoneDayjs.dayjsTz(ogEndDate).clone().add(-1, 'hour')
    }
    if (!newStartDate) return

    newRow = {
      ...resizeRecord.value,
      row: {
        ...resizeRecord.value.row,
        [fromCol.title!]: timezoneDayjs.dayjsTz(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
      },
    }
  }
  if (!newRow) return

  const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
  formattedData.value = formattedData.value.map((r) => {
    const pk = extractPkFromRow(r.row, meta.value!.columns!)
    return pk === newPk ? newRow : r
  }) as Row[]
  useDebouncedRowUpdate(newRow, updateProperty, false)
}

const onResizeEnd = () => {
  resizeDirection.value = null
  resizeRecord.value = null
  document.removeEventListener('mousemove', onResize)
  document.removeEventListener('mouseup', onResizeEnd)
}

const onResizeStart = (direction: 'right' | 'left', _event: MouseEvent, record: Row) => {
  if (!isUIAllowed('dataEdit')) return
  if (record.rowMeta.range?.is_readonly) return

  resizeDirection.value = direction
  resizeRecord.value = record
  document.addEventListener('mousemove', onResize)
  document.addEventListener('mouseup', onResizeEnd)
}

const onDrag = (event: MouseEvent) => {
  if (!isUIAllowed('dataEdit') || !container.value || !dragRecord.value) return
  const { top, bottom } = container.value.getBoundingClientRect()

  if (event.clientY > bottom - 20) {
    container.value.scrollTop += 10
  } else if (event.clientY < top + 20) {
    container.value.scrollTop -= 10
  }
  calculateNewRow(event)
}

const stopDrag = (event: MouseEvent) => {
  event.preventDefault()
  clearTimeout(dragTimeout.value!)
  if (!isUIAllowed('dataEdit') || !isDragging.value || !container.value || !dragRecord.value) return

  const { newRow, updateProperty } = calculateNewRow(event, true)
  if (!newRow && !updateProperty) return

  if (dragElement.value) {
    dragElement.value.style.boxShadow = 'none'
    dragElement.value = null
  }

  if (dragRecord.value) {
    dragRecord.value = null
  }

  if (!newRow) return
  updateRowProperty(newRow, updateProperty, false)

  $e('c:calendar:day:drag-record')

  document.removeEventListener('mousemove', onDrag)
  document.removeEventListener('mouseup', stopDrag)
}

const dragStart = (event: MouseEvent, record: Row) => {
  if (isSyncedFromColumn.value) return

  let target = event.target as HTMLElement

  isDragging.value = false

  // We use a timeout to determine if the user is dragging or clicking on the record
  dragTimeout.value = setTimeout(() => {
    if (!isUIAllowed('dataEdit')) return
    if (record.rowMeta.range?.is_readonly) return

    isDragging.value = true
    while (!target.classList.contains('draggable-record')) {
      target = target.parentElement as HTMLElement
    }

    dragRecord.value = record
    dragElement.value = target

    document.addEventListener('mousemove', onDrag)
    document.addEventListener('mouseup', stopDrag)
  }, 200)

  const onMouseUp = () => {
    clearTimeout(dragTimeout.value!)
    document.removeEventListener('mouseup', onMouseUp)
    if (!isDragging.value) {
      emit('expandRecord', record)
    }
  }

  document.addEventListener('mouseup', onMouseUp)
}

// We support drag and drop from the sidebar to the day view of the date field
const dropEvent = (event: DragEvent) => {
  if (!isUIAllowed('dataEdit') || !container.value) return
  event.preventDefault()
  const data = event.dataTransfer?.getData('text/plain')
  if (data) {
    const {
      record,
      isWithoutDates,
    }: {
      record: Row
      initialClickOffsetY: number
      initialClickOffsetX: number
      isWithoutDates: boolean
    } = parseProp(data)

    // Not a valid record
    if (!record?.rowMeta) return

    if (record.rowMeta.range?.is_readonly) return

    const fromCol = record.rowMeta.range?.fk_from_col
    const toCol = record.rowMeta.range?.fk_to_col

    if (!fromCol) return

    const { top } = container.value.getBoundingClientRect()

    const { scrollHeight } = container.value

    // We calculate the percentage of the mouse position in the scroll container
    const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight

    const hour = Math.max(Math.floor(percentY * 23), 0)
    const minutes = Math.min(Math.max(Math.round(Math.floor((percentY * 23 - hour) * 60) / 15) * 15, 0), 60)

    const newStartDate = timezoneDayjs.dayjsTz(selectedDate.value).startOf('day').add(hour, 'hour').add(minutes, 'minute')

    let endDate

    const newRow = {
      ...record,
      row: {
        ...record.row,
        [fromCol.title!]: timezoneDayjs.dayjsTz(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
      },
    }

    const updateProperty = [fromCol.title!]

    if (toCol) {
      const fromDate = record.row[fromCol.title!] ? timezoneDayjs.timezonize(record.row[fromCol.title!]) : null
      const toDate = record.row[toCol.title!] ? timezoneDayjs.timezonize(record.row[toCol.title!]) : null

      if (fromDate && toDate) {
        endDate = timezoneDayjs.dayjsTz(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
      } else if (fromDate && !toDate) {
        endDate = timezoneDayjs.dayjsTz(newStartDate).endOf('day')
      } else if (!fromDate && toDate) {
        endDate = timezoneDayjs.dayjsTz(newStartDate).endOf('day')
      } else {
        endDate = newStartDate.clone()
      }
      newRow.row[toCol.title!] = timezoneDayjs.dayjsTz(endDate).format('YYYY-MM-DD HH:mm:ssZ')
      updateProperty.push(toCol.title!)
    }

    if (!newRow) return

    const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)

    if (dragElement.value && !isWithoutDates) {
      formattedData.value = formattedData.value.map((r) => {
        const pk = extractPkFromRow(r.row, meta.value!.columns!)
        return pk === newPk ? newRow : r
      })
    } else {
      formattedData.value = [...formattedData.value, newRow]
      if (sideBarFilterOption.value !== 'allRecords') {
        formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
          return extractPkFromRow(r.row, meta.value!.columns!) !== newPk
        })
      }
    }

    if (dragElement.value) {
      dragElement.value.style.boxShadow = 'none'
      dragElement.value = null
    }
    updateRowProperty(newRow, updateProperty, false)
    $e('c:calendar:day:drag-record')
  }
}

const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
  if (!recordsAcrossAllRange.value || !recordsAcrossAllRange.value.gridTimeMap) return { isOverflow: false, overflowCount: 0 }
  const { gridTimeMap } = recordsAcrossAllRange.value
  const startMinute = hour.hour() * 60 + hour.minute()
  const endMinute = hour.hour() * 60 + hour.minute() + 59
  let overflowCount = 0

  for (let minute = startMinute; minute <= endMinute; minute++) {
    const recordCount = gridTimeMap.get(minute)?.count ?? 0
    overflowCount = Math.max(overflowCount, recordCount)
  }

  return { isOverflow: overflowCount - 8 > 0, overflowCount: overflowCount - 8 }
}

const getOverflowRecords = (hour: dayjs.Dayjs) => {
  if (!recordsAcrossAllRange.value || !recordsAcrossAllRange.value.gridTimeMap) return { isOverflow: false, overflowCount: 0 }
  const { gridTimeMap } = recordsAcrossAllRange.value

  const startMinute = hour.hour() * 60 + hour.minute()
  const endMinute = hour.hour() * 60 + hour.minute() + 59

  const uniqueRecords: Row[] = []
  const uniqueRecordIds = new Set<string>()

  for (let minute = startMinute; minute <= endMinute; minute++) {
    const records = gridTimeMap?.get(minute)?.overflowRecords ?? []
    for (const rec of records) {
      if (!uniqueRecordIds.has(rec.rowMeta?.id)) {
        uniqueRecords.push(rec)
        uniqueRecordIds.add(rec.rowMeta?.id)
      }
    }
  }

  return uniqueRecords
}

const viewMore = (hour: dayjs.Dayjs) => {
  sideBarFilterOption.value = 'selectedHours'
  selectedTime.value = hour
  showSideMenu.value = true
}

const selectHour = (hour: dayjs.Dayjs) => {
  selectedTime.value = hour
  dragRecord.value = null
}

// TODO: Add Support for multiple ranges when multiple ranges are supported
const newRecord = (hour: dayjs.Dayjs) => {
  if (!isUIAllowed('dataEdit') || !calendarRange.value?.length || isSyncedTable.value) return
  const record = {
    row: {
      [calendarRange.value[0].fk_from_col!.title!]: hour.format('YYYY-MM-DD HH:mm:ssZ'),
    },
  }
  emit('newRecord', record)
}

watch(
  () => recordsAcrossAllRange.value,
  () => {
    setTimeout(() => {
      if (isDragging.value) return
      const records = document.querySelectorAll('.draggable-record')
      if (records.length) records.item(0)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
      else document.querySelectorAll('.nc-calendar-day-hour').item(9)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
    }, 100)
  },
  { immediate: true },
)

watch([() => timezone], () => {
  currTime.value = timezoneDayjs.dayjsTz()
})

const expandRecord = (record: Row) => {
  emit('expandRecord', record)
}
</script>

<template>
  <div class="h-[calc(100vh-5.3rem)] overflow-y-auto nc-scrollbar-md">
    <SmartsheetCalendarDateTimeSpanningContainer
      v-if="
        calendarRange.some((range) => range.fk_to_col !== null && range.fk_to_col !== undefined) &&
        recordsAcrossAllRange.spanningRecords?.length
      "
      :records="recordsAcrossAllRange.spanningRecords"
      @expand-record="expandRecord"
    />
    <div ref="container" class="w-full flex relative no-selection" data-testid="nc-calendar-day-view" @drop="dropEvent">
      <div
        v-if="shouldEnableOverlay"
        class="absolute ml-0.5 pointer-events-none w-full z-4"
        :style="{
          top: `${overlayTop}px`,
        }"
      >
        <div class="flex w-full items-center">
          <span
            class="text-nc-content-inverted-primary bg-nc-content-brand text-xs font-bold rounded-md pointer-events-auto leading-3.5 p-0.5 cursor-pointer"
            @click="newRecord(currTime)"
          >
            {{ currTime.format('hh:mm A') }}
          </span>
          <div class="flex-1 relative ml-1 nc-calendar-border-line border-b-2 border-nc-border-brand"></div>
        </div>
      </div>

      <div>
        <div
          v-for="(hour, index) in hours"
          :key="index"
          class="flex h-13 relative border-1 group hover:bg-nc-bg-gray-extralight border-nc-base-white"
          data-testid="nc-calendar-day-hour"
          @click="selectHour(hour)"
          @dblclick="newRecord(hour)"
        >
          <div class="w-16 border-b-0 pr-2 pl-2 text-right text-xs text-nc-content-gray-disabled font-semibold h-13">
            {{ timezoneDayjs.dayjsTz(hour).format('hh a') }}
          </div>
        </div>
      </div>
      <div class="w-full">
        <div
          v-for="(hour, index) in hours"
          :key="index"
          :class="{
            'selected-hour': hour.isSame(selectedTime),
          }"
          class="flex w-full border-l-nc-border-gray-light h-13 transition nc-calendar-day-hour relative border-1 group hover:bg-nc-bg-gray-extralight border-nc-base-white border-b-nc-border-gray-light"
          data-testid="nc-calendar-day-hour"
          @click="selectHour(hour)"
          @dblclick="newRecord(hour)"
        >
          <NcDropdown
            v-if="calendarRange.length > 1 && !isPublic"
            :class="{
              '!block': hour.isSame(selectedTime),
              '!hidden': !hour.isSame(selectedTime),
            }"
            auto-close
          >
            <NcButton
              class="!group-hover:block mr-10 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute"
              size="xsmall"
              type="secondary"
            >
              <component :is="iconMap.plus" class="h-4 w-4" />
            </NcButton>
            <template #overlay>
              <NcMenu class="w-64">
                <NcMenuItem> Select date field to add </NcMenuItem>
                <template v-for="(range, calIndex) in calendarRange" :key="calIndex">
                  <NcMenuItem
                    v-if="!range.is_readonly"
                    class="text-nc-content-gray font-semibold text-sm"
                    @click="
                () => {
                  let record = {
                    row: {
                      [range.fk_from_col!.title!]: hour.format('YYYY-MM-DD HH:mm:ssZ'),
                    },
                  }
                  if (range.fk_to_col) {
                    record = {
                      row: {
                        ...record.row,
                        [range.fk_to_col!.title!]: hour.add(1, 'hour').format('YYYY-MM-DD HH:mm:ssZ'),
                      },
                    }
                  }
                  emit('newRecord', record)
                }
              "
                  >
                    <div class="flex items-center gap-1">
                      <LazySmartsheetHeaderIcon :column="range.fk_from_col" />

                      <span class="ml-1">{{ range.fk_from_col!.title! }}</span>
                    </div>
                  </NcMenuItem>
                </template>
              </NcMenu>
            </template>
          </NcDropdown>

          <div
            v-else-if="
              !isPublic && isUIAllowed('dataEdit') && [UITypes.DateTime, UITypes.Date].includes(calDataType) && !isSyncedTable
            "
            :class="{
              '!block': hour.isSame(selectedTime),
              '!hidden': !hour.isSame(selectedTime),
            }"
            class="!group-hover:block mr-10 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block relative"
          >
            <PermissionsTooltip
              :entity="PermissionEntity.TABLE"
              :entity-id="meta?.id"
              :permission="PermissionKey.TABLE_RECORD_ADD"
              placement="left"
            >
              <template #default="{ isAllowed }">
                <NcButton
                  size="xsmall"
                  type="secondary"
                  :disabled="!isAllowed"
                  @click="
                  () => {
                    let record = {
                      row: {
                        [calendarRange[0].fk_from_col!.title!]: hour.format('YYYY-MM-DD HH:mm:ssZ'),
                      },
                    }

                    if (calendarRange[0].fk_to_col) {
                      record = {
                        row: {
                          ...record.row,
                          [calendarRange[0].fk_to_col!.title!]: hour.add(1, 'hour').format('YYYY-MM-DD HH:mm:ssZ'),
                        },
                      }
                    }
                    emit('newRecord', record)
                }"
                >
                  <component :is="iconMap.plus" class="h-4 w-4" />
                </NcButton>
              </template>
            </PermissionsTooltip>
          </div>

          <NcDropdown v-if="isOverflowAcrossHourRange(hour).isOverflow">
            <NcButton
              v-e="`['c:calendar:week-view-more']`"
              class="!absolute bottom-2 text-center w-15 mx-auto inset-x-0 z-3 text-nc-content-gray-muted"
              size="xxsmall"
              type="secondary"
              @click="viewMore(hour)"
            >
              <span class="text-xs">
                +
                {{ isOverflowAcrossHourRange(hour).overflowCount }}
                more
              </span>
            </NcButton>

            <template #overlay>
              <div class="bg-nc-bg-default px-4 gap-3 flex flex-col py-4 max-h-70 overflow-y-auto">
                <LazySmartsheetCalendarSideRecordCard
                  v-for="(record, idx) in getOverflowRecords(hour)"
                  :key="idx"
                  :draggable="false"
                  class="w-64"
                  :from-date="timezoneDayjs.timezonize(record.row[record.rowMeta.range.fk_from_col.title!]).format('D MMM • h:mm A')"
                  :invalid="false"
                  :row="record"
                  :to-date="record?.rowMeta?.range?.fk_to_col?.title && record.row[record.rowMeta.range!.fk_to_col.title!] ?  timezoneDayjs.timezonize(record.row[record.rowMeta.range!.fk_to_col.title!]).format('DD MMM • HH:mm A') : null"
                  data-testid="nc-sidebar-record-card"
                  @click="expandRecord(record)"
                >
                  <template v-if="!isRowEmpty(record, displayField)">
                    <LazySmartsheetPlainCell v-model="record.row[displayField!.title!]" :column="displayField" />
                  </template>
                  <template v-else>
                    <span class="text-nc-content-gray-muted"> - </span>
                  </template>
                </LazySmartsheetCalendarSideRecordCard>
              </div>
            </template>
          </NcDropdown>
        </div>
      </div>
      <div class="absolute inset-0 pointer-events-none">
        <div
          class="relative !ml-[68px] !mr-1 z-2 nc-calendar-day-record-container"
          data-testid="nc-calendar-day-record-container"
        >
          <template v-for="record in recordsAcrossAllRange.record" :key="record.rowMeta.id">
            <div
              v-if="record.rowMeta.style?.display !== 'none'"
              :data-testid="`nc-calendar-day-record-${record.row[displayField!.title!]}`"
              :data-unique-id="record.rowMeta.id"
              :style="{
                ...record.rowMeta.style,
                opacity:
                  (dragRecord === null || record.rowMeta.id === dragRecord?.rowMeta.id) &&
                  (resizeRecord === null || record.rowMeta.id === resizeRecord?.rowMeta.id)
                    ? 1
                    : 0.3,
              }"
              class="absolute draggable-record transition group cursor-pointer pointer-events-auto"
              @mousedown="dragStart($event, record)"
              @mouseleave="hoverRecord = null"
              @mouseover="hoverRecord = record.rowMeta.id as string"
              @dragover.prevent
            >
              <LazySmartsheetRow :row="record">
                <LazySmartsheetCalendarVRecordCard
                  :hover="hoverRecord === record.rowMeta.id"
                  :selected="record.rowMeta.id === dragRecord?.rowMeta?.id"
                  :record="record"
                  :dragging="record.rowMeta.id === dragRecord?.rowMeta?.id || record.rowMeta.id === resizeRecord?.rowMeta?.id"
                  :resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
                  @resize-start="onResizeStart"
                >
                  <template v-for="(field, id) in fields" :key="id">
                    <LazySmartsheetPlainCell
                      v-if="!isRowEmpty(record, field!)"
                      v-model="record.row[field!.title!]"
                      class="text-xs font-medium"
                      :bold="getFieldStyle(field).bold"
                      :column="field"
                      :italic="getFieldStyle(field).italic"
                      :underline="getFieldStyle(field).underline"
                    />
                  </template>
                  <template #time>
                    <div class="text-xs font-medium text-nc-content-gray-disabled">
                      {{ timezoneDayjs.timezonize(record.row[record.rowMeta.range?.fk_from_col!.title!]).format('h:mm a') }}
                    </div>
                  </template>
                </LazySmartsheetCalendarVRecordCard>
              </LazySmartsheetRow>
            </div>
          </template>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.no-selection {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

.selected-hour {
  @apply relative !bg-nc-bg-brand !border-nc-bg-brand;
}

.nc-calendar-border-line::after {
  @apply absolute bg-nc-content-brand w-0.5 h-3;
  content: '';
  top: -5px;
  bottom: -6px;
  left: 0px;
}
</style>
