/**
 * Kanban - Drag-and-drop kanban board component
 * @countermeasure/web-components/visualization/kanban
 */

import { resolveContainer, uniqueId } from '../../primitives/utils'
import { setSafeIconHtml } from '../../utils/sanitize'
import type { KanbanCard, KanbanColumn, KanbanConfig } from './types'

export type {
  KanbanCard,
  KanbanColumn,
  KanbanConfig,
  KanbanAssignee,
  KanbanSwimlane,
  KanbanAction,
  KanbanAnimations,
} from './types'

/** Create a DOM element with a class name and optional text content */
function el<K extends keyof HTMLElementTagNameMap>(
  tag: K,
  className: string,
  text?: string
): HTMLElementTagNameMap[K] {
  const element = document.createElement(tag)
  element.className = className
  if (text !== undefined) {
    element.textContent = text
  }
  return element
}

/** Valid priority values for CSS modifier classes */
const VALID_PRIORITIES = new Set(['low', 'medium', 'high', 'critical'])

export class Kanban {
  private readonly container: HTMLElement
  private readonly config: KanbanConfig
  private readonly instanceId: string
  private columns: KanbanColumn[]
  private wrapperEl: HTMLElement | null = null
  private dragState: { cardId: string; fromColumnId: string } | null = null
  private readonly collapsedState = new Map<string, boolean>()

  constructor(config: KanbanConfig) {
    this.container = resolveContainer(config.container)
    this.config = config
    this.instanceId = uniqueId('cmm-kanban')
    this.columns = this.cloneColumns(config.columns)

    // Initialize collapsed state from column config
    for (const col of this.columns) {
      if (col.collapsible === true) {
        this.collapsedState.set(col.id, col.collapsed === true)
      }
    }

    this.render()
  }

  /** Replace all columns and re-render */
  update(columns: KanbanColumn[]): void {
    this.columns = this.cloneColumns(columns)
    this.render()
  }

  /** Add a card to a specific column */
  addCard(columnId: string, card: KanbanCard): void {
    const column = this.columns.find(c => c.id === columnId)
    if (column === undefined) {
      return
    }
    column.cards.push({ ...card })
    this.render()
  }

  /** Remove a card by ID from any column */
  removeCard(cardId: string): void {
    for (const column of this.columns) {
      const idx = column.cards.findIndex(c => c.id === cardId)
      if (idx >= 0) {
        column.cards.splice(idx, 1)
        break
      }
    }
    this.render()
  }

  /** Move a card to a target column at a specific index */
  moveCard(cardId: string, toColumnId: string, index: number): void {
    this.performMove(cardId, '', toColumnId, index)
  }

  /** Remove the component from the DOM and clean up */
  destroy(): void {
    this.container.innerHTML = ''
    this.wrapperEl = null
    this.dragState = null
  }

  /** Deep-clone columns to avoid mutating config */
  private cloneColumns(columns: KanbanColumn[]): KanbanColumn[] {
    return columns.map(col => ({
      ...col,
      cards: col.cards.map(card => ({ ...card })),
    }))
  }

  /** Check if animations are enabled */
  private isAnimationEnabled(): boolean {
    const { animations } = this.config
    if (animations === undefined) {
      return true // default enabled
    }
    if (typeof animations === 'boolean') {
      return animations
    }
    return animations.entrance !== false
  }

  /** Get the max description length */
  private getDescriptionMaxLength(): number {
    return this.config.descriptionMaxLength ?? 100
  }

  /** Get the max labels count */
  private getMaxLabels(): number {
    return this.config.maxLabels ?? 3
  }

  /** Get the max assignee avatars count */
  private getMaxAssigneeAvatars(): number {
    return this.config.maxAssigneeAvatars ?? 3
  }

  // ---------------------------------------------------------------------------
  // Board rendering
  // ---------------------------------------------------------------------------

  /** Render the entire board */
  private render(): void {
    this.container.innerHTML = ''

    const wrapper = el('div', 'cmm-kanban')
    wrapper.id = this.instanceId

    if (this.config.className !== undefined) {
      wrapper.classList.add(this.config.className)
    }

    if (this.config.density === 'compact') {
      wrapper.classList.add('cmm-kanban--compact')
    }

    if (this.config.swimlaneGroups !== undefined && this.config.swimlaneGroups.length > 0) {
      this.renderWithSwimlanes(wrapper)
    } else {
      for (const column of this.columns) {
        wrapper.appendChild(this.renderColumn(column))
      }
    }

    this.wrapperEl = wrapper
    this.container.appendChild(wrapper)
  }

  /** Render board with swimlane grouping */
  private renderWithSwimlanes(wrapper: HTMLElement): void {
    const swimlaneGroups = this.config.swimlaneGroups
    if (swimlaneGroups === undefined) {
      return
    }

    for (const swimlane of swimlaneGroups) {
      const laneEl = el('div', 'cmm-kanban__swimlane')
      laneEl.dataset.swimlaneId = swimlane.id

      // Count total cards matching this swimlane across all columns
      let totalCards = 0
      for (const column of this.columns) {
        const matching = column.cards.filter(card => swimlane.filter(card))
        totalCards += matching.length
      }

      // Swimlane header
      const headerEl = el('div', 'cmm-kanban__swimlane-header')
      headerEl.appendChild(el('span', 'cmm-kanban__swimlane-title', swimlane.label))
      headerEl.appendChild(el('span', 'cmm-kanban__swimlane-count', String(totalCards)))
      laneEl.appendChild(headerEl)

      // Render columns within this swimlane, filtering cards
      const columnsRow = el('div', 'cmm-kanban__swimlane-columns')
      columnsRow.style.display = 'flex'
      columnsRow.style.gap = '1rem'

      for (const column of this.columns) {
        const filteredCards = column.cards.filter(card => swimlane.filter(card))
        const swimlaneColumn: KanbanColumn = {
          ...column,
          cards: filteredCards,
        }
        columnsRow.appendChild(this.renderColumn(swimlaneColumn))
      }

      laneEl.appendChild(columnsRow)
      wrapper.appendChild(laneEl)
    }
  }

  // ---------------------------------------------------------------------------
  // Column rendering
  // ---------------------------------------------------------------------------

  /** Render a single column */
  private renderColumn(column: KanbanColumn): HTMLElement {
    const colEl = el('div', 'cmm-kanban__column')
    colEl.dataset.columnId = column.id

    const isCollapsed = this.collapsedState.get(column.id) === true
    const isAtLimit = column.limit !== undefined && column.cards.length >= column.limit

    if (isAtLimit) {
      colEl.classList.add('cmm-kanban__column--at-limit')
    }

    if (isCollapsed) {
      colEl.classList.add('cmm-kanban__column--collapsed')
    }

    // Header
    const header = el('div', 'cmm-kanban__column-header')

    if (column.color !== undefined) {
      header.style.borderTopColor = column.color
    }

    // Column icon
    if (column.icon !== undefined) {
      const iconEl = el('span', 'cmm-kanban__column-icon')
      setSafeIconHtml(iconEl, column.icon)
      header.appendChild(iconEl)
    }

    header.appendChild(el('span', 'cmm-kanban__column-title', column.title))

    const countEl = el('span', 'cmm-kanban__column-count', String(column.cards.length))

    if (column.limit !== undefined) {
      countEl.textContent += ` / ${String(column.limit)}`

      if (isAtLimit) {
        countEl.classList.add('cmm-kanban__column-count--warning')
      }
    }

    header.appendChild(countEl)

    // Collapse button
    if (column.collapsible === true) {
      const collapseBtn = el('button', 'cmm-kanban__column-collapse', isCollapsed ? '+' : '-')
      collapseBtn.addEventListener('click', () => {
        const currentState = this.collapsedState.get(column.id) === true
        const newState = !currentState
        this.collapsedState.set(column.id, newState)
        this.config.onColumnCollapse?.(column.id, newState)
        this.render()
      })
      header.appendChild(collapseBtn)
    }

    // Add button
    if (column.addButton === true) {
      const addBtn = el('button', 'cmm-kanban__add-btn', '+')
      addBtn.addEventListener('click', () => {
        this.config.onAddCard?.(column.id)
      })
      header.appendChild(addBtn)
    }

    colEl.appendChild(header)

    // Card list (hidden when collapsed)
    if (!isCollapsed) {
      const cardList = el('div', 'cmm-kanban__card-list')
      cardList.dataset.columnId = column.id

      if (this.isDraggable()) {
        this.setupDropZone(cardList, column.id)
      }

      if (column.cards.length === 0 && column.emptyMessage !== undefined) {
        cardList.appendChild(el('div', 'cmm-kanban__empty-state', column.emptyMessage))
      } else {
        for (let i = 0; i < column.cards.length; i++) {
          const card = column.cards[i]
          if (card !== undefined) {
            cardList.appendChild(this.renderCard(card, column.id, i))
          }
        }
      }

      colEl.appendChild(cardList)
    }

    return colEl
  }

  // ---------------------------------------------------------------------------
  // Card rendering
  // ---------------------------------------------------------------------------

  /** Render a single card */
  private renderCard(card: KanbanCard, columnId: string, cardIndex: number): HTMLElement {
    const cardEl = el('div', 'cmm-kanban__card')
    cardEl.dataset.cardId = card.id

    if (card.priority !== undefined && VALID_PRIORITIES.has(card.priority)) {
      cardEl.classList.add(`cmm-kanban__card--${card.priority}`)
    }

    // Animation
    if (this.isAnimationEnabled()) {
      cardEl.classList.add('cmm-kanban__card--enter')
      cardEl.style.animationDelay = `${String(cardIndex * 50)}ms`
    }

    if (this.isDraggable()) {
      cardEl.draggable = true
      this.setupDragSource(cardEl, card.id, columnId)
    }

    cardEl.addEventListener('click', () => {
      this.config.onCardClick?.(card)
    })

    cardEl.setAttribute('tabindex', '0')
    cardEl.setAttribute('role', 'listitem')

    this.renderIssueRow(cardEl, card)

    // Title
    cardEl.appendChild(el('div', 'cmm-kanban__card-title', card.title))

    this.renderContextChips(cardEl, card)
    this.renderDescription(cardEl, card)
    this.renderProgressBar(cardEl, card)
    this.renderDueDate(cardEl, card)
    this.renderStats(cardEl, card)
    this.renderAssignees(cardEl, card)
    this.renderLabels(cardEl, card)
    this.renderActions(cardEl, card, columnId)

    return cardEl
  }

  /** Render the issue row (issueKey, issueType, storyPoints, rank) */
  private renderIssueRow(cardEl: HTMLElement, card: KanbanCard): void {
    const hasIssueRow =
      card.issueKey !== undefined ||
      card.issueType !== undefined ||
      card.storyPoints !== undefined ||
      card.rank !== undefined

    if (!hasIssueRow) {
      return
    }

    const issueRow = el('div', 'cmm-kanban__card-issue')

    if (card.issueKey !== undefined) {
      issueRow.appendChild(el('span', 'cmm-kanban__issue-key', card.issueKey))
    }

    if (card.issueType !== undefined) {
      const prefix = card.issueTypeIcon ?? ''
      issueRow.appendChild(el('span', 'cmm-kanban__issue-type', prefix + card.issueType))
    }

    if (card.storyPoints !== undefined) {
      issueRow.appendChild(el('span', 'cmm-kanban__story-points', String(card.storyPoints)))
    }

    if (card.rank !== undefined) {
      issueRow.appendChild(el('span', 'cmm-kanban__rank', String(card.rank)))
    }

    cardEl.appendChild(issueRow)
  }

  /** Render context chips (epic, sprint, parent) */
  private renderContextChips(cardEl: HTMLElement, card: KanbanCard): void {
    const hasContext =
      card.epicName !== undefined ||
      card.sprintName !== undefined ||
      (card.parentId !== undefined && card.parentLabel !== undefined)

    if (!hasContext) {
      return
    }

    const contextRow = el('div', 'cmm-kanban__card-context')

    if (card.epicName !== undefined) {
      contextRow.appendChild(
        el('span', 'cmm-kanban__context-chip cmm-kanban__context-chip--epic', card.epicName)
      )
    }

    if (card.sprintName !== undefined) {
      contextRow.appendChild(
        el('span', 'cmm-kanban__context-chip cmm-kanban__context-chip--sprint', card.sprintName)
      )
    }

    if (card.parentId !== undefined && card.parentLabel !== undefined) {
      contextRow.appendChild(
        el('span', 'cmm-kanban__context-chip cmm-kanban__context-chip--parent', card.parentLabel)
      )
    }

    cardEl.appendChild(contextRow)
  }

  /** Render truncated description */
  private renderDescription(cardEl: HTMLElement, card: KanbanCard): void {
    if (card.description === undefined) {
      return
    }

    const maxLen = this.getDescriptionMaxLength()
    const text =
      card.description.length > maxLen
        ? card.description.slice(0, maxLen) + '...'
        : card.description
    cardEl.appendChild(el('div', 'cmm-kanban__card-description', text))
  }

  /** Render subtask / completion progress bar */
  private renderProgressBar(cardEl: HTMLElement, card: KanbanCard): void {
    const hasProgress =
      (card.subtaskCount !== undefined && card.subtaskCount > 0) ||
      card.completionPercentage !== undefined

    if (!hasProgress) {
      return
    }

    let percentage = 0
    if (card.completionPercentage !== undefined) {
      percentage = card.completionPercentage
    } else if (card.subtaskCount !== undefined && card.subtaskCount > 0) {
      percentage = ((card.subtaskCompleted ?? 0) / card.subtaskCount) * 100
    }

    const progressEl = el('div', 'cmm-kanban__card-progress')
    const trackEl = el('div', 'cmm-kanban__card-progress-track')
    const fillEl = el('div', 'cmm-kanban__card-progress-fill')
    fillEl.style.width = `${String(Math.round(percentage))}%`

    trackEl.appendChild(fillEl)
    progressEl.appendChild(trackEl)
    cardEl.appendChild(progressEl)
  }

  /** Render due date with overdue/today/soon modifiers */
  private renderDueDate(cardEl: HTMLElement, card: KanbanCard): void {
    if (card.dueDate === undefined) {
      return
    }

    const dueEl = el('span', 'cmm-kanban__card-due')

    let dueDateNorm: Date
    if (card.dueDate instanceof Date) {
      dueDateNorm = new Date(card.dueDate)
      dueDateNorm.setHours(0, 0, 0, 0)
    } else {
      // Parse YYYY-MM-DD string as local date (not UTC)
      const parts = card.dueDate.split('-')
      dueDateNorm = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]))
      dueDateNorm.setHours(0, 0, 0, 0)
    }

    const today = new Date()
    today.setHours(0, 0, 0, 0)

    const diffMs = dueDateNorm.getTime() - today.getTime()
    const diffDays = diffMs / (1000 * 60 * 60 * 24)

    if (diffDays < 0) {
      dueEl.classList.add('cmm-kanban__card-due--overdue')
    } else if (diffDays === 0) {
      dueEl.classList.add('cmm-kanban__card-due--today')
    } else if (diffDays <= 3) {
      dueEl.classList.add('cmm-kanban__card-due--soon')
    }

    dueEl.textContent = dueDateNorm.toLocaleDateString()
    cardEl.appendChild(dueEl)
  }

  /** Render comment and attachment count stats */
  private renderStats(cardEl: HTMLElement, card: KanbanCard): void {
    const hasComments = card.commentCount !== undefined && card.commentCount > 0
    const hasAttachments = card.attachmentCount !== undefined && card.attachmentCount > 0

    if (!hasComments && !hasAttachments) {
      return
    }

    const statsRow = el('div', 'cmm-kanban__card-stats')

    if (hasComments) {
      statsRow.appendChild(el('span', 'cmm-kanban__card-stat', String(card.commentCount)))
    }

    if (hasAttachments) {
      statsRow.appendChild(el('span', 'cmm-kanban__card-stat', String(card.attachmentCount)))
    }

    cardEl.appendChild(statsRow)
  }

  /** Render assignee avatars (array format) or legacy single assignee */
  private renderAssignees(cardEl: HTMLElement, card: KanbanCard): void {
    // New array format takes precedence over old string
    if (card.assignees !== undefined && card.assignees.length > 0) {
      const assigneesEl = el('div', 'cmm-kanban__card-assignees')

      const maxAvatars = this.getMaxAssigneeAvatars()
      const visibleAssignees = card.assignees.slice(0, maxAvatars)
      const overflowCount = card.assignees.length - maxAvatars

      for (const assignee of visibleAssignees) {
        const avatarEl = el('span', 'cmm-kanban__card-avatar')
        avatarEl.title = assignee.name
        if (assignee.avatarUrl !== undefined) {
          avatarEl.style.backgroundImage = `url(${assignee.avatarUrl})`
        } else {
          avatarEl.textContent = assignee.name.charAt(0).toUpperCase()
        }
        assigneesEl.appendChild(avatarEl)
      }

      if (overflowCount > 0) {
        assigneesEl.appendChild(
          el('span', 'cmm-kanban__card-assignees-overflow', `+${String(overflowCount)}`)
        )
      }

      cardEl.appendChild(assigneesEl)
    } else if (card.assignee !== undefined) {
      // Legacy single assignee
      const footerEl = el('div', 'cmm-kanban__card-footer')
      footerEl.appendChild(el('span', 'cmm-kanban__card-assignee', card.assignee))
      cardEl.appendChild(footerEl)
    }
  }

  /** Render label pills with overflow */
  private renderLabels(cardEl: HTMLElement, card: KanbanCard): void {
    if (card.labels === undefined || card.labels.length === 0) {
      return
    }

    const labelsEl = el('div', 'cmm-kanban__card-labels')

    const maxLabels = this.getMaxLabels()
    const visibleLabels = card.labels.slice(0, maxLabels)
    const overflowCount = card.labels.length - maxLabels

    for (const label of visibleLabels) {
      const pill = el('span', 'cmm-kanban__card-label', label.text)
      pill.style.backgroundColor = label.color
      labelsEl.appendChild(pill)
    }

    if (overflowCount > 0) {
      labelsEl.appendChild(
        el('span', 'cmm-kanban__card-labels-overflow', `+${String(overflowCount)}`)
      )
    }

    cardEl.appendChild(labelsEl)
  }

  /** Render quick action buttons */
  private renderActions(cardEl: HTMLElement, card: KanbanCard, columnId: string): void {
    if (this.config.actions === undefined || this.config.actions.length === 0) {
      return
    }

    const visibleActions = this.config.actions.filter(action => {
      if (action.visible === undefined) {
        return true
      }
      return action.visible(card, columnId)
    })

    if (visibleActions.length === 0) {
      return
    }

    const actionsEl = el('div', 'cmm-kanban__card-actions')

    for (const action of visibleActions) {
      const prefix = action.icon ?? ''
      const btn = el('button', 'cmm-kanban__action-btn', prefix + action.label)
      btn.dataset.actionId = action.id
      btn.addEventListener('click', (e: Event) => {
        e.stopPropagation()
        void this.config.onCardAction?.(action.id, card)
      })
      actionsEl.appendChild(btn)
    }

    cardEl.appendChild(actionsEl)
  }

  // ---------------------------------------------------------------------------
  // Drag and drop
  // ---------------------------------------------------------------------------

  /** Whether drag-and-drop is enabled */
  private isDraggable(): boolean {
    return this.config.draggable !== false
  }

  /** Set up drag events on a card element */
  private setupDragSource(cardEl: HTMLElement, cardId: string, columnId: string): void {
    cardEl.addEventListener('dragstart', (e: DragEvent) => {
      this.dragState = { cardId, fromColumnId: columnId }
      cardEl.classList.add('cmm-kanban__card--dragging')

      if (e.dataTransfer !== null) {
        e.dataTransfer.effectAllowed = 'move'
        e.dataTransfer.setData('text/plain', cardId)
      }
    })

    cardEl.addEventListener('dragend', () => {
      cardEl.classList.remove('cmm-kanban__card--dragging')
      this.dragState = null

      // Remove all drop indicators
      if (this.wrapperEl !== null) {
        const indicators = this.wrapperEl.querySelectorAll('.cmm-kanban__card-list--drag-over')
        for (const ind of indicators) {
          ind.classList.remove('cmm-kanban__card-list--drag-over')
        }
      }
    })
  }

  /** Set up drop zone on a card list element */
  private setupDropZone(listEl: HTMLElement, columnId: string): void {
    listEl.addEventListener('dragover', (e: DragEvent) => {
      e.preventDefault()
      if (e.dataTransfer !== null) {
        e.dataTransfer.dropEffect = 'move'
      }
      listEl.classList.add('cmm-kanban__card-list--drag-over')
    })

    listEl.addEventListener('dragleave', (e: DragEvent) => {
      const related = e.relatedTarget
      if (related instanceof Node && listEl.contains(related)) {
        return
      }
      listEl.classList.remove('cmm-kanban__card-list--drag-over')
    })

    listEl.addEventListener('drop', (e: DragEvent) => {
      e.preventDefault()
      listEl.classList.remove('cmm-kanban__card-list--drag-over')

      if (this.dragState === null) {
        return
      }

      const { cardId, fromColumnId } = this.dragState

      // Calculate drop index based on mouse position
      const dropIndex = this.calculateDropIndex(listEl, e.clientY)

      this.dragState = null

      // Perform the move
      this.performMove(cardId, fromColumnId, columnId, dropIndex)
    })
  }

  /** Calculate the index to insert the card at based on mouse Y position */
  private calculateDropIndex(listEl: HTMLElement, clientY: number): number {
    const cards = listEl.querySelectorAll('.cmm-kanban__card')
    let index = cards.length

    for (let i = 0; i < cards.length; i++) {
      const cardEl = cards[i]
      if (cardEl === undefined) {
        continue
      }
      const rect = cardEl.getBoundingClientRect()
      const midY = rect.top + rect.height / 2

      if (clientY < midY) {
        index = i
        break
      }
    }

    return index
  }

  // ---------------------------------------------------------------------------
  // Card movement
  // ---------------------------------------------------------------------------

  /** Extract a card from its current column, returning the card and source column id */
  private extractCard(cardId: string): { card: KanbanCard; fromColumnId: string } | undefined {
    for (const column of this.columns) {
      const idx = column.cards.findIndex(c => c.id === cardId)
      if (idx >= 0) {
        const removed = column.cards.splice(idx, 1)
        const card = removed[0]
        if (card !== undefined) {
          return { card, fromColumnId: column.id }
        }
      }
    }
    return undefined
  }

  /** Perform a card move between columns, with async revert support */
  private performMove(
    cardId: string,
    _fromColumnId: string,
    toColumnId: string,
    dropIndex: number
  ): void {
    const extracted = this.extractCard(cardId)
    if (extracted === undefined) {
      return
    }

    const { card, fromColumnId } = extracted

    const toColumn = this.columns.find(c => c.id === toColumnId)
    if (toColumn === undefined) {
      return
    }

    const clampedIndex = Math.min(dropIndex, toColumn.cards.length)
    toColumn.cards.splice(clampedIndex, 0, card)

    const result = this.config.onCardMove?.(cardId, fromColumnId, toColumnId, clampedIndex)
    this.render()

    // Handle async result with revert
    if (result instanceof Promise) {
      const savedCard = { ...card }
      result
        .then(() => {
          const movedEl = this.container.querySelector(`[data-card-id="${cardId}"]`)
          if (movedEl !== null) {
            movedEl.classList.add('cmm-kanban__card--landed')
          }
        })
        .catch(() => {
          const targetCol = this.columns.find(c => c.id === toColumnId)
          if (targetCol !== undefined) {
            const removeIdx = targetCol.cards.findIndex(c => c.id === cardId)
            if (removeIdx >= 0) {
              targetCol.cards.splice(removeIdx, 1)
            }
          }
          const sourceCol = this.columns.find(c => c.id === fromColumnId)
          if (sourceCol !== undefined) {
            sourceCol.cards.push(savedCard)
          }
          this.render()
        })
    }
  }
}
