import { classMap } from 'lit/directives/class-map.js'
import { customElement, property, state } from 'lit/decorators.js'
import {
  formatISODate,
  newDate,
  newDateYMD,
  formatReadableDate,
  parseISODateString,
  todayInTz,
  newDateFromDate,
} from 'shared-utils/date-utils'
import { html, nothing, PropertyValues, TemplateResult } from 'lit'
import { PktElement } from '@/base-elements/element'
import converters from '../../helpers/converters'
import specs from 'componentSpecs/calendar.json'
import '@/components/icon'
import type { IDateConstraints, TDateRangeMap } from 'shared-types/calendar'
import {
  isDateExcluded,
  isDayDisabled as checkDayDisabled,
  isPrevMonthAllowed as checkPrevMonthAllowed,
  isNextMonthAllowed as checkNextMonthAllowed,
} from 'shared-utils/calendar/date-validation'
import {
  DAYS_PER_WEEK,
  calculateCalendarDimensions,
  getCellType,
  getDayNumber,
} from 'shared-utils/calendar/calendar-grid'
import {
  convertSelectedToDates,
  updateRangeMap as calculateRangeMap,
  isRangeAllowed as checkRangeAllowed,
  addToSelection,
  removeFromSelection,
  toggleSelection,
  handleRangeSelection,
} from 'shared-utils/calendar/selection-manager'
import {
  shouldIgnoreKeyboardEvent,
  findNextSelectableDate as findNextDate,
  getKeyDirection,
} from 'shared-utils/calendar/keyboard-navigation'

// Types

type TDayViewData = {
  currentDate: Date
  currentDateISO: string
  isToday: boolean
  isSelected: boolean
  isDisabled: boolean
  ariaLabel: string
  tabindex: string
}

export class PktCalendar extends PktElement {
  // Selection properties
  @property({ converter: converters.csvToArray })
  selected: string | string[] = []

  @property({ type: Boolean })
  multiple: boolean = specs.props.multiple.default

  @property({ type: Number, attribute: 'max-multiple' })
  maxMultiple: number = specs.props.maxMultiple.default

  @property({ type: Boolean })
  range: boolean = specs.props.range.default

  // Date constraints
  @property({ type: String })
  earliest: string | null = specs.props.earliest.default

  @property({ type: String })
  latest: string | null = specs.props.latest.default

  @property({ converter: converters.stringsToDate })
  excludedates: Date[] = []

  @property({ converter: converters.csvToArray })
  excludeweekdays: string[] = []

  // Display options
  @property({ type: Boolean })
  weeknumbers: boolean = specs.props.weeknumbers.default

  @property({ type: Boolean })
  withcontrols: boolean = specs.props.withcontrols.default

  @property({ converter: converters.stringToDate })
  currentmonth: Date | null = null

  @property({ type: String })
  today: string | null = null

  // Localization strings
  @property({ type: Array, attribute: 'day-strings' }) dayStrings: string[] = this.strings.dates.daysShort
  @property({ type: Array, attribute: 'day-strings-long' }) dayStringsLong: string[] = this.strings.dates.days
  @property({ type: Array, attribute: 'month-strings' }) monthStrings: string[] = this.strings.dates.months
  @property({ type: String, attribute: 'week-string' }) weekString: string = this.strings.dates.week
  @property({ type: String, attribute: 'prev-month-string' }) prevMonthString: string = this.strings.dates.prevMonth
  @property({ type: String, attribute: 'next-month-string' }) nextMonthString: string = this.strings.dates.nextMonth

  // Internal state - selection tracking
  @property({ type: Array }) private _selected: Date[] = []
  @state() private inRange: TDateRangeMap = {}
  @property({ type: Date }) private rangeHovered: Date | null = null

  private get todayDate(): Date {
    return this.today ? parseISODateString(this.today) : todayInTz()
  }

  // Internal state - navigation and display
  @property({ type: Number }) private year: number = 0
  @property({ type: Number }) private month: number = 0
  @property({ type: Number }) private week: number = 0
  @state() private currentmonthtouched: boolean = false

  // Internal state - keyboard navigation and focus management
  @state() private focusedDate: string | null = null
  private selectableDates: {
    currentDateISO: string
    isDisabled: boolean
    tabindex: string
  }[] = []
  private tabIndexSet: number = 0

  /**
   * Lifecycle methods
   */
  protected firstUpdated(_changedProperties: PropertyValues): void {
    this.addEventListener('keydown', this.handleKeydown)
  }

  disconnectedCallback(): void {
    this.removeEventListener('keydown', this.handleKeydown)
    super.disconnectedCallback()
  }

  updated(changedProperties: PropertyValues): void {
    super.updated(changedProperties)
    if (changedProperties.has('selected')) {
      this.convertSelected()
    }
  }

  /**
   * Date and selection management
   */
  private convertSelected() {
    if (typeof this.selected === 'string') {
      this.selected = this.selected.split(',')
    }
    if (this.selected.length === 1 && this.selected[0] === '') {
      this.selected = []
    }

    this._selected = convertSelectedToDates(this.selected)

    if (this.range && this.selected.length === 2) {
      this.inRange = calculateRangeMap(this._selected[0], this._selected[1])
    }

    this.setCurrentMonth()
  }

  private setCurrentMonth() {
    if (this.currentmonth === null && !this.currentmonthtouched) {
      this.currentmonthtouched = true
    }
    if (this.selected.length && this.selected[0] !== '') {
      const d = parseISODateString(this.selected[this.selected.length - 1])
      this.currentmonth = isNaN(d.getTime()) ? newDateFromDate(this.todayDate) : d
    } else if (this.currentmonth === null) {
      this.currentmonth = newDateFromDate(this.todayDate)
    }
    if (!this.currentmonth || isNaN(this.currentmonth.getTime())) {
      this.currentmonth = newDateFromDate(this.todayDate)
    }
    this.year = this.currentmonth.getFullYear()
    this.month = this.currentmonth.getMonth()
  }

  /**
   * Keyboard navigation
   */
  private handleKeydown(e: KeyboardEvent) {
    const direction = getKeyDirection(e.key)
    if (direction !== null) {
      this.handleArrowKey(e, direction)
    }
  }

  private handleArrowKey(e: KeyboardEvent, direction: number) {
    const target = e.target as HTMLElement
    if (shouldIgnoreKeyboardEvent(target)) return

    e.preventDefault()

    if (!this.focusedDate) {
      this.focusOnCurrentDate()
      return
    }

    const date = this.focusedDate ? newDate(this.focusedDate) : newDateYMD(this.year, this.month, 1)
    const nextDate = findNextDate(date, direction, this.querySelector.bind(this))

    if (nextDate) {
      const el = this.querySelector(`button[data-date="${formatISODate(nextDate)}"]`)
      if (el instanceof HTMLButtonElement && !el.dataset.disabled) {
        this.focusedDate = formatISODate(nextDate)
        el.focus()
      }
    }
  }
  /**
   * Rendering methods
   */
  render() {
    return html`
      <div
        class="pkt-calendar ${this.weeknumbers ? 'pkt-cal-weeknumbers' : ''}"
        @focusout=${this.closeEvent}
        @keydown=${(e: KeyboardEvent) => {
          if (e.key === 'Escape') {
            e.preventDefault()
            this.close()
          }
        }}
      >
        <nav class="pkt-cal-month-nav">
          ${this.renderMonthNavButton('prev')}
          ${this.renderMonthNav()}
          ${this.renderMonthNavButton('next')}
        </nav>
        <table
          class="pkt-cal-days pkt-txt-12-medium pkt-calendar__body"
          role="grid"
          ?aria-multiselectable=${this.range || this.multiple}
        >
          <thead>
            ${this.renderDayNames()}
          </thead>
          <tbody>
            ${this.renderCalendarBody()}
          </tbody>
        </table>
      </div>
    `
  }

  private renderMonthNavButton(direction: 'prev' | 'next'): TemplateResult {
    const isPrev = direction === 'prev'
    const isAllowed = isPrev ? this.isPrevMonthAllowed() : this.isNextMonthAllowed()
    const label = isPrev ? this.prevMonthString : this.nextMonthString
    const iconName = isPrev ? 'chevron-thin-left' : 'chevron-thin-right'
    const className = isPrev ? 'pkt-calendar__prev-month' : 'pkt-calendar__next-month'
    const onClick = isPrev ? () => this.prevMonth() : () => this.nextMonth()

    return html`<div>
      <button
        type="button"
        aria-label="${label}"
        @click=${() => isAllowed && onClick()}
        @keydown=${(e: KeyboardEvent) => {
          if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault()
            isAllowed && onClick()
          }
        }}
        class="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--icon-only ${className} ${isAllowed ? '' : 'pkt-invisible'}"
        .data-disabled=${!isAllowed ? 'disabled' : nothing}
        ?aria-disabled=${!isAllowed}
        tabindex=${isAllowed ? '0' : '-1'}
      >
        <pkt-icon class="pkt-btn__icon" name="${iconName}"></pkt-icon>
        <span class="pkt-btn__text">${label}</span>
      </button>
    </div>`
  }

  private renderDayNames(): TemplateResult {
    const days: TemplateResult[] = []

    if (this.weeknumbers) {
      days.push(html`<th><div class="pkt-calendar__week-number">${this.weekString}</div></th>`)
    }

    for (let i = 0; i < this.dayStrings.length; i++) {
      days.push(
        html`<th>
          <div class="pkt-calendar__day-name" aria-label="${this.dayStringsLong[i]}">
            ${this.dayStrings[i]}
          </div>
        </th>`,
      )
    }

    return html`<tr class="pkt-cal-week-row">${days}</tr>`
  }

  private renderMonthNav(): TemplateResult {
    if (this.withcontrols) {
      return html`<div class="pkt-cal-month-picker">
        <label for="${this.id}-monthnav" class="pkt-hide">${this.strings.dates.month}</label>
        <select
          aria-label="${this.strings.dates.month}"
          class="pkt-input pkt-input-compact"
          id="${this.id}-monthnav"
          @change=${(e: Event) => {
            e.stopImmediatePropagation()
            const target = e.target as HTMLSelectElement
            this.changeMonth(this.year, parseInt(target.value))
          }}
        >
          ${this.monthStrings.map(
            (month, index) =>
              html`<option value=${index} ?selected=${this.month === index}>${month}</option>`,
          )}
        </select>
        <label for="${this.id}-yearnav" class="pkt-hide">${this.strings.dates.year}</label>
        <input
          aria-label="${this.strings.dates.year}"
          class="pkt-input pkt-cal-input-year pkt-input-compact"
          id="${this.id}-yearnav"
          type="number"
          size="4"
          placeholder="0000"
          @change=${(e: Event) => {
            e.stopImmediatePropagation()
            const target = e.target as HTMLInputElement
            this.changeMonth(parseInt(target.value), this.month)
          }}
          .value=${this.year}
        />
      </div>`
    }

    return html`<div class="pkt-txt-16-medium pkt-calendar__month-title" aria-live="polite">
      ${this.monthStrings[this.month]} ${this.year}
    </div>`
  }

  private getDayViewData(dayCounter: number, today: Date): TDayViewData {
    const currentDate = newDateYMD(this.year, this.month, dayCounter)
    const currentDateISO = formatISODate(currentDate)
    const isToday = currentDateISO === formatISODate(today)
    const isSelected = this.selected.includes(currentDateISO)
    const isDisabled = this.isDayDisabled(currentDate, isSelected)
    const tabindex = this.calculateTabIndex(currentDateISO, isDisabled, dayCounter)

    return {
      currentDate,
      currentDateISO,
      isToday,
      isSelected,
      isDisabled,
      ariaLabel: formatReadableDate(currentDate),
      tabindex,
    }
  }

  private getDateConstraints(): IDateConstraints {
    return {
      earliest: this.earliest,
      latest: this.latest,
      excludedates: this.excludedates,
      excludeweekdays: this.excludeweekdays,
    }
  }

  private isDayDisabled(date: Date, isSelected: boolean): boolean {
    return checkDayDisabled(date, isSelected, this.getDateConstraints(), {
      multiple: this.multiple,
      maxMultiple: this.maxMultiple,
      selectedCount: this.selected.length,
    })
  }

  private calculateTabIndex(dateISO: string, isDisabled: boolean, dayCounter: number): string {
    if (this.focusedDate) {
      return this.focusedDate === dateISO && !isDisabled ? '0' : '-1'
    }

    if (!isDisabled && this.tabIndexSet === 0) {
      this.tabIndexSet = dayCounter
      return '0'
    }

    return this.tabIndexSet === dayCounter ? '0' : '-1'
  }

  private getDayCellClasses(data: TDayViewData) {
    const { currentDateISO, isToday, isSelected } = data
    const isRangeStart = this.range &&
      (this.selected.length === 2 || this.rangeHovered !== null) &&
      currentDateISO === this.selected[0]
    const isRangeEnd = this.range && this.selected.length === 2 && currentDateISO === this.selected[1]

    return {
      'pkt-cal-today': isToday,
      'pkt-cal-selected': isSelected,
      'pkt-cal-in-range': this.inRange[currentDateISO],
      'pkt-cal-excluded': this.isExcluded(data.currentDate),
      'pkt-cal-in-range-first': isRangeStart,
      'pkt-cal-in-range-last': isRangeEnd,
      'pkt-cal-range-hover':
        this.rangeHovered !== null && currentDateISO === formatISODate(this.rangeHovered),
    }
  }

  private getDayButtonClasses(data: TDayViewData) {
    const { currentDateISO, isToday, isSelected, isDisabled } = data
    const isRangeStart = this.range &&
      (this.selected.length === 2 || this.rangeHovered !== null) &&
      currentDateISO === this.selected[0]
    const isRangeEnd = this.range && this.selected.length === 2 && currentDateISO === this.selected[1]

    return {
      'pkt-calendar__date': true,
      'pkt-calendar__date--today': isToday,
      'pkt-calendar__date--selected': isSelected,
      'pkt-calendar__date--disabled': isDisabled,
      'pkt-calendar__date--in-range': this.inRange[currentDateISO],
      'pkt-calendar__date--in-range-hover':
        this.rangeHovered !== null && currentDateISO === formatISODate(this.rangeHovered),
      'pkt-calendar__date--range-start': isRangeStart,
      'pkt-calendar__date--range-end': isRangeEnd,
    }
  }

  private handleDayFocus(date: Date, dateISO: string): void {
    if (this.range && !this.isExcluded(date)) {
      this.handleRangeHover(date)
    }
    this.focusedDate = dateISO
  }

  private renderDayView(dayCounter: number, today: Date) {
    const data = this.getDayViewData(dayCounter, today)
    const { currentDate, currentDateISO, isSelected, isDisabled, ariaLabel, tabindex } = data

    // Track selectable dates for keyboard navigation
    this.selectableDates.push({ currentDateISO, isDisabled, tabindex })

    const cellClasses = this.getDayCellClasses(data)
    const buttonClasses = this.getDayButtonClasses(data)

    return html`<td class=${classMap(cellClasses)}>
      <button
        type="button"
        aria-pressed=${isSelected ? 'true' : 'false'}
        ?disabled=${isDisabled}
        class="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--label-only ${classMap(buttonClasses)}"
        @mouseover=${() => this.range && !this.isExcluded(currentDate) && this.handleRangeHover(currentDate)}
        @focus=${() => this.handleDayFocus(currentDate, currentDateISO)}
        aria-label="${ariaLabel}"
        tabindex=${tabindex}
        data-disabled=${isDisabled ? 'disabled' : nothing}
        data-date=${currentDateISO}
        @keydown=${(e: KeyboardEvent) => {
          if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault()
            this.handleDateSelect(currentDate)
          }
        }}
        @click=${(e: MouseEvent) => {
          if (!isDisabled) {
            e.preventDefault()
            this.handleDateSelect(currentDate)
          }
        }}
      >
        <span class="pkt-btn__text pkt-txt-14-light">${dayCounter}</span>
      </button>
    </td>`
  }

  private renderEmptyDayCell(day: number): TemplateResult {
    return html`<td class="pkt-cal-other">
      <div
        class="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--label-only"
        data-disabled="disabled"
      >
        <span class="pkt-btn__text pkt-txt-14-light">${day}</span>
      </div>
    </td>`
  }

  private renderWeekRow(cells: TemplateResult[]): TemplateResult {
    return html`<tr class="pkt-cal-week-row" role="row">${cells}</tr>`
  }

  private renderCalendarBody() {
    const today = this.todayDate
    const dimensions = calculateCalendarDimensions(this.year, this.month)

    this.selectableDates = []
    this.tabIndexSet = 0

    let dayCounter = 1
    this.week = dimensions.initialWeek

    const rows: TemplateResult[] = []

    for (let i = 0; i < dimensions.numRows; i++) {
      const cells: TemplateResult[] = []

      // Add week number if enabled
      if (this.weeknumbers) {
        cells.push(html`<td class="pkt-cal-week">${this.week}</td>`)
      }
      this.week++

      // Render each day in the week
      for (let j = 0; j < DAYS_PER_WEEK; j++) {
        const cellType = getCellType(i, j, dayCounter, dimensions)

        if (cellType === 'current-month') {
          cells.push(this.renderDayView(dayCounter, today))
          dayCounter++
        } else {
          const dayNumber = getDayNumber(cellType, j, dayCounter, dimensions)
          cells.push(this.renderEmptyDayCell(dayNumber))
          if (cellType === 'next-month') {
            dayCounter++
          }
        }
      }

      rows.push(this.renderWeekRow(cells))
    }

    return rows
  }

  /**
   * Date validation
   */
  private isExcluded(date: Date): boolean {
    return isDateExcluded(date, this.getDateConstraints())
  }

  /**
   * Month navigation
   */
  isPrevMonthAllowed(): boolean {
    return checkPrevMonthAllowed(this.year, this.month, this.earliest)
  }

  private prevMonth() {
    const newMonth = this.month === 0 ? 11 : this.month - 1
    const newYear = this.month === 0 ? this.year - 1 : this.year
    this.changeMonth(newYear, newMonth)
  }

  isNextMonthAllowed(): boolean {
    return checkNextMonthAllowed(this.year, this.month, this.latest)
  }

  private nextMonth() {
    const newMonth = this.month === 11 ? 0 : this.month + 1
    const newYear = this.month === 11 ? this.year + 1 : this.year
    this.changeMonth(newYear, newMonth)
  }

  private changeMonth(year: number, month: number): void {
    this.year = typeof year === 'string' ? parseInt(year) : year
    this.month = typeof month === 'string' ? parseInt(month) : month
    this.currentmonth = newDateFromDate(new Date(this.year, this.month, 1))
    this.tabIndexSet = 0
    this.focusedDate = null
    this.selectableDates = []
  }

  /**
   * Date selection logic
   */
  private emptySelected(): void {
    this.selected = []
    this._selected = []
    this.inRange = {}
  }

  private normalizeSelected(): string[] {
    if (typeof this.selected === 'string') {
      return this.selected.split(',')
    }
    return this.selected
  }

  public addToSelected(selectedDate: Date): void {
    this.selected = addToSelection(selectedDate, this.normalizeSelected())
    this._selected = convertSelectedToDates(this.selected)

    if (this.range && this.selected.length === 2) {
      this.convertSelected()
      this.close()
    }
  }

  public removeFromSelected(selectedDate: Date): void {
    this.selected = removeFromSelection(selectedDate, this.normalizeSelected())
    this._selected = convertSelectedToDates(this.selected)
  }

  public toggleSelected(selectedDate: Date): void {
    this.selected = toggleSelection(selectedDate, this.normalizeSelected(), this.maxMultiple)
    this._selected = convertSelectedToDates(this.selected)
  }

  private isRangeAllowed(date: Date): boolean {
    return checkRangeAllowed(date, this._selected, this.excludedates, this.excludeweekdays)
  }

  private handleRangeSelect(selectedDate: Date): Promise<void> {
    this.selected = handleRangeSelection(selectedDate, this.normalizeSelected(), {
      excludedates: this.excludedates,
      excludeweekdays: this.excludeweekdays,
    })
    this._selected = convertSelectedToDates(this.selected)

    if (this.selected.length === 2) {
      this.convertSelected()
      this.close()
    } else if (this.selected.length === 1) {
      // Clear inRange markers when starting a new range
      this.inRange = {}
    }

    return Promise.resolve()
  }

  private handleRangeHover(date: Date): void {
    if (
      !this.range ||
      this._selected.length !== 1 ||
      !this.isRangeAllowed(date) ||
      this._selected[0] >= date
    ) {
      this.rangeHovered = null
      return
    }

    this.rangeHovered = date
    this.inRange = calculateRangeMap(this._selected[0], date)
  }

  public handleDateSelect(selectedDate: Date | null): Promise<void> {
    if (!selectedDate) return Promise.resolve()
    if (this.range) {
      this.handleRangeSelect(selectedDate)
    } else if (this.multiple) {
      this.toggleSelected(selectedDate)
    } else {
      if (this.selected.includes(formatISODate(selectedDate))) {
        this.emptySelected()
      } else {
        this.emptySelected()
        this.addToSelected(selectedDate)
      }
      this.close()
    }
    this.dispatchEvent(
      new CustomEvent('date-selected', {
        detail: this.selected,
        bubbles: true,
        composed: true,
      }),
    )
    return Promise.resolve()
  }

  /**
   * Focus management and event handlers
   */
  public focusOnCurrentDate(): void {
    const currentDateISO = formatISODate(newDateFromDate(this.todayDate))
    const el = this.querySelector(`button[data-date="${currentDateISO}"]`)

    if (el instanceof HTMLButtonElement) {
      this.focusedDate = currentDateISO
      el.focus()
      return
    }

    // Fall back to first selectable date
    const firstSelectable = this.selectableDates.find((x) => !x.isDisabled)
    if (firstSelectable) {
      const firstSelectableEl = this.querySelector(
        `button[data-date="${firstSelectable.currentDateISO}"]`,
      )
      if (firstSelectableEl instanceof HTMLButtonElement) {
        this.focusedDate = firstSelectable.currentDateISO
        firstSelectableEl.focus()
      }
    }
  }

  public closeEvent(e: FocusEvent): void {
    if (
      !this.contains(e.relatedTarget as Node) &&
      !(e.target as Element).classList.contains('pkt-invisible')
    ) {
      this.close()
    }
  }

  public close(): void {
    this.dispatchEvent(
      new CustomEvent('close', {
        detail: true,
        bubbles: true,
        composed: true,
      }),
    )
  }
}

try {
  customElement('pkt-calendar')(PktCalendar)
} catch (e) {
  console.warn('Forsøker å definere <pkt-calendar>, men den er allerede definert')
}
