import { customElement, property, state } from 'lit/decorators.js'
import { formatISODate, fromISOToDate, parseISODateString } from 'shared-utils/date-utils'
import { html, nothing, PropertyValues } from 'lit'
import { PktCalendar } from '@/components/calendar/calendar'
import { PktInputElement } from '@/base-elements/input-element'
import { Ref, createRef, ref } from 'lit/directives/ref.js'
import converters from '@/helpers/converters'
import specs from 'componentSpecs/datepicker.json'
import '@/components/calendar'
import '@/components/icon'
import '@/components/input-wrapper'
import './date-tags'
import './datepicker-popup'
import './datepicker-single'
import './datepicker-range'
import './datepicker-multiple'
import { slotContent } from '@/directives/slot-content'
import {
  valueUtils,
  inputTypeUtils,
  calendarUtils,
  cssUtils,
  dateProcessingUtils,
  formUtils,
} from './datepicker-utils'
import { valueToArray, arrayToCsv } from 'shared-utils/value-utils'
import { sleep } from 'shared-utils/utils'
import { isIOS } from 'shared-utils/device-utils'
import { PktDatepickerPopup } from './datepicker-popup'
import { PktDatepickerSingle } from './datepicker-single'
import { PktDatepickerRange } from './datepicker-range'
import { PktDatepickerMultiple } from './datepicker-multiple'
import { ElementProps } from '@/types/typeUtils'

type Props = ElementProps<
  PktDatepicker,
  | 'label'
  | 'dateformat'
  | 'multiple'
  | 'maxlength'
  | 'range'
  | 'showRangeLabels'
  | 'min'
  | 'max'
  | 'weeknumbers'
  | 'withcontrols'
  | 'excludedates'
  | 'excludeweekdays'
  | 'currentmonth'
  | 'calendarOpen'
  | 'timezone'
>

export class PktDatepicker extends PktInputElement<Props> {
  /**
   * Element attributes and properties
   */
  private _valueProperty: string = ''
  private _valueProcessing = false
  datepickerPopupRef: Ref<PktDatepickerPopup> = createRef()

  @property({ type: String, reflect: true })
  get value(): string {
    return this._valueProperty
  }

  set value(newValue: string | string[]) {
    const oldValue = this._valueProperty
    this._valueProperty = Array.isArray(newValue) ? newValue.join(',') : newValue || ''
    this.valueChanged(this._valueProperty, oldValue)
    this.requestUpdate('value', oldValue)
  }

  _value: string[] = []

  @property({ type: String, reflect: true })
  label: string = 'Datovelger'

  @property({ type: String })
  dateformat: string = specs.props.dateformat.default

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

  @property({ type: Number, reflect: true })
  maxlength: number | null = null

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

  @property({ type: Boolean, attribute: 'show-range-labels' })
  showRangeLabels: boolean = false

  @property({ type: String, reflect: true })
  min: string | null = null

  @property({ type: String, reflect: true })
  max: string | null = null

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

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

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

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

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

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

  @property({ type: Boolean, reflect: true, attribute: 'calendar-open' })
  calendarOpen: boolean = false

  @property({ type: String })
  timezone: string = 'Europe/Oslo'

  @state() inputClasses = {}

  /**
   * Computed properties
   */
  get inputType(): string {
    return inputTypeUtils.getInputType()
  }

  /**
   * Housekeeping / lifecycle methods
   */

  connectedCallback() {
    super.connectedCallback()
    if (this.timezone && this.timezone !== window.pktTz) {
      window.pktTz = this.timezone
    }
  }

  disconnectedCallback(): void {
    super.disconnectedCallback()
  }

  onInput(): void {
    this.dispatchEvent(new Event('input', { bubbles: true }))
  }

  valueChanged(newValue: string | null, oldValue: string | null): void {
    if (this._valueProcessing || newValue === oldValue) return
    this._valueProcessing = true
    try {
      const parsedValue = valueToArray(newValue)

      // For multiple dates, filter out invalid ones to prevent accumulating bad dates
      // For single/range dates, preserve user input for validation feedback
      const filteredValue =
        this.multiple && parsedValue.length > 1
          ? valueUtils.filterSelectableDates(
              parsedValue,
              this.min,
              this.max,
              this.excludedates,
              this.excludeweekdays,
            )
          : parsedValue

      if (this.range && !valueUtils.validateRangeOrder(filteredValue)) {
        this._value = []
        this._valueProperty = ''
        super.valueChanged('', oldValue)
        return
      }

      this._value = filteredValue

      const parsedValueString = arrayToCsv(filteredValue)
      if (this._valueProperty !== parsedValueString) {
        this._valueProperty = parsedValueString
      }

      // In range mode, don't dispatch events until both dates are selected
      if (this.range && filteredValue.length < 2) return

      super.valueChanged(parsedValueString, oldValue)
    } finally {
      this._valueProcessing = false
    }
  }

  attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
    if (name === 'value' && this.value !== _old) {
      this.valueChanged(value, _old)
    }

    if (name === 'excludedates' && typeof this.excludedates === 'string') {
      this.excludedates = valueToArray(value || '')
    }

    if (name === 'excludeweekdays' && typeof this.excludeweekdays === 'string') {
      this.excludeweekdays = valueToArray(value || '')
    }
    super.attributeChangedCallback(name, _old, value)
  }

  updated(changedProperties: PropertyValues): void {
    // Re-process value only when multiple/range also changed in the same update cycle,
    // since attribute initialization may process value before multiple/range are set.
    // When value is set via JS setter after init, multiple/range are already correct
    // and the setter's valueChanged() call is sufficient.
    if (
      changedProperties.has('value') &&
      (changedProperties.has('multiple') || changedProperties.has('range'))
    ) {
      const newValue = Array.isArray(this.value) ? this.value.join(',') : this.value
      const oldValue = changedProperties.get('value')
      const oldValueStr = Array.isArray(oldValue) ? oldValue.join(',') : oldValue
      this.valueChanged(newValue, oldValueStr)
    }
    if (changedProperties.has('multiple')) {
      if (this.multiple && !Array.isArray(this._value)) {
        this._value = valueToArray(this.value)
      } else if (!this.multiple && Array.isArray(this._value)) {
        this._value = this._value.filter(Boolean)
      }
      if (!this.multiple && !this.range && Array.isArray(this._value)) {
        this._value = [this._value[0] ?? '']
      }
    }
    super.updated(changedProperties)
  }

  /**
   * Element references
   */

  // Override the inputRef and inputRefTo for compatibility
  get inputRef(): Ref<HTMLInputElement> {
    const element = this.currentInputElement
    return { value: element } as Ref<HTMLInputElement>
  }

  get inputRefTo(): Ref<HTMLInputElement> {
    const element = this.currentInputElementTo
    return { value: element } as Ref<HTMLInputElement>
  }

  calRef: Ref<PktCalendar> = createRef()
  popupRef: Ref<HTMLDivElement> = createRef()

  // Child component refs
  singleInputRef: Ref<PktDatepickerSingle> = createRef()
  rangeInputRef: Ref<PktDatepickerRange> = createRef()
  multipleInputRef: Ref<PktDatepickerMultiple> = createRef()

  // Getters for backward compatibility with input refs
  get currentInputElement(): HTMLInputElement | undefined {
    if (this.multiple) {
      return this.multipleInputRef.value?.inputElement
    } else if (this.range) {
      return this.rangeInputRef.value?.inputElement
    } else {
      return this.singleInputRef.value?.inputElement
    }
  }

  get currentInputElementTo(): HTMLInputElement | undefined {
    if (this.range) {
      return this.rangeInputRef.value?.inputElementTo
    }
    return undefined
  }

  get currentButtonElement(): HTMLButtonElement | undefined {
    if (this.multiple) {
      return this.multipleInputRef.value?.buttonElement
    } else if (this.range) {
      return this.rangeInputRef.value?.buttonElement
    } else {
      return this.singleInputRef.value?.buttonElement
    }
  }

  // Override btnRef for compatibility
  get btnRef(): Ref<HTMLButtonElement> {
    const element = this.currentButtonElement
    return { value: element } as Ref<HTMLButtonElement>
  }

  /**
   * Rendering
   */
  renderInput() {
    return html`
      <pkt-datepicker-single
        .value=${this._value[0] ?? ''}
        .inputType=${this.inputType}
        .id=${this.id}
        .min=${this.min}
        .max=${this.max}
        .placeholder=${this.placeholder}
        .readonly=${this.readonly}
        .disabled=${this.disabled}
        .inputClasses=${this.inputClasses}
        .internals=${this.internals}
        .strings=${this.strings}
        @toggle-calendar=${(e: CustomEvent) => this.toggleCalendar(e.detail)}
        @input-change=${() => this.onInput()}
        @input-focus=${() => this.onFocus()}
        @input-blur=${(e: CustomEvent) => {
          if (!this.calRef.value?.contains(e.detail.relatedTarget as Node)) {
            this.onBlur()
          }
        }}
        @manage-validity=${(e: CustomEvent) => this.manageValidity(e.detail)}
        @value-change=${(e: CustomEvent) => {
          this.value = e.detail
        }}
        @input-changed=${() => {
          this.touched = true
        }}
        ${ref(this.singleInputRef)}
      ></pkt-datepicker-single>
    `
  }

  renderRangeInput() {
    return html`
      <pkt-datepicker-range
        .value=${this._value}
        .inputType=${this.inputType}
        .id=${this.id}
        .label=${this.label}
        .min=${this.min}
        .max=${this.max}
        .placeholder=${this.placeholder}
        .readonly=${this.readonly}
        .disabled=${this.disabled}
        .showRangeLabels=${this.showRangeLabels}
        .inputClasses=${this.inputClasses}
        .internals=${this.internals}
        .strings=${this.strings}
        @toggle-calendar=${(e: CustomEvent) => this.toggleCalendar(e.detail)}
        @input-change=${() => this.onInput()}
        @input-focus=${() => this.onFocus()}
        @input-blur=${(e: CustomEvent) => {
          if (!this.calRef.value?.contains(e.detail.relatedTarget as Node)) {
            this.onBlur()
          }
        }}
        @range-blur=${(e: CustomEvent) => {
          const inputFrom = this.currentInputElement
          const inputTo = this.currentInputElementTo

          // Update _value with current input values to sync with calendar
          if (inputFrom && inputTo) {
            const fromValue = inputFrom.value
            const toValue = inputTo.value

            // If from date is after to date, clear the to date
            if (fromValue && toValue && fromValue > toValue) {
              inputTo.value = ''
              this._value = [fromValue]
              this.value = fromValue
            } else {
              const newValues = [fromValue, toValue].filter(Boolean)
              if (
                newValues.length > 0 &&
                (newValues[0] !== this._value[0] || newValues[1] !== this._value[1])
              ) {
                this._value = newValues
                this.value = newValues.join(',')
              }
            }
          }

          dateProcessingUtils.processRangeBlur(
            e.detail.event,
            e.detail.values,
            this.calRef,
            () => this.clearInputValue(),
            (input) => this.manageValidity(input),
          )
        }}
        @manage-validity=${(e: CustomEvent) => this.manageValidity(e.detail)}
        @validate-date-input=${(e: CustomEvent) => {
          formUtils.validateDateInput(e.detail, this.internals, this.min, this.max, this.strings)
        }}
        @handle-date-select=${(e: CustomEvent) => {
          const date = fromISOToDate(e.detail)
          if (date) {
            const formattedDate = formatISODate(date)
            // Only update calendar if the date is different from current values
            if (this._value[0] !== formattedDate && this._value[1] !== formattedDate) {
              this.calRef?.value?.handleDateSelect(date)
            }
          }
        }}
        @input-changed=${() => {
          this.touched = true
        }}
        ${ref(this.rangeInputRef)}
      ></pkt-datepicker-range>
    `
  }

  renderMultipleInput() {
    return html`
      <pkt-datepicker-multiple
        .value=${this._value}
        .inputType=${this.inputType}
        .id=${this.id}
        .min=${this.min}
        .max=${this.max}
        .placeholder=${this.placeholder}
        .readonly=${this.readonly}
        .disabled=${this.disabled}
        .maxlength=${this.maxlength}
        .inputClasses=${this.inputClasses}
        .internals=${this.internals}
        .strings=${this.strings}
        @toggle-calendar=${(e: CustomEvent) => this.toggleCalendar(e.detail)}
        @input-change=${() => this.onInput()}
        @input-focus=${() => this.onFocus()}
        @input-blur=${(e: CustomEvent) => {
          if (!this.calRef.value?.contains(e.detail.relatedTarget as Node)) {
            this.onBlur()
          }
        }}
        @add-to-selected=${(e: CustomEvent) => this.addToSelected(e.detail)}
        @input-changed=${() => {
          this.touched = true
        }}
        ${ref(this.multipleInputRef)}
      ></pkt-datepicker-multiple>
    `
  }

  renderCalendar() {
    return html`
      <pkt-datepicker-popup
        class="pkt-contents"
        ?open=${this.calendarOpen}
        ?multiple=${this.multiple}
        ?range=${this.range}
        ?weeknumbers=${this.weeknumbers}
        ?withcontrols=${this.withcontrols}
        .maxMultiple=${this.maxlength}
        .selected=${this._value}
        .earliest=${this.min}
        .latest=${this.max}
        .excludedates=${Array.isArray(this.excludedates)
          ? this.excludedates
          : (this.excludedates as string).split(',')}
        .excludeweekdays=${this.excludeweekdays}
        .currentmonth=${this.currentmonth ? parseISODateString(this.currentmonth) : null}
        .today=${this.today}
        @date-selected=${(e: CustomEvent) => {
          this.value = dateProcessingUtils.processDateSelection(e.detail, this.multiple, this.range)
          this._value = e.detail
          dateProcessingUtils.updateInputValues(
            this.inputRef,
            this.inputRefTo,
            this._value,
            this.range,
            this.multiple,
            (input) => this.manageValidity(input),
          )
        }}
        @close=${() => {
          this.onBlur()
          this.hideCalendar()
        }}
        ${ref(this.datepickerPopupRef)}
      ></pkt-datepicker-popup>
    `
  }

  render() {
    this.inputClasses = cssUtils.getInputClasses(
      this.fullwidth,
      this.showRangeLabels,
      this.multiple,
      this.range,
      this.readonly,
      this.inputType,
    )

    return html`
      <pkt-input-wrapper
        label="${this.label}"
        forId="${this.id}-input"
        ?counter=${this.multiple && !!this.maxlength}
        .counterCurrent=${this.value ? this._value.length : 0}
        .counterMaxLength=${this.maxlength}
        ?disabled=${this.disabled}
        ?hasError=${this.hasError}
        ?hasFieldset=${this.hasFieldset}
        ?inline=${this.inline}
        ?required=${this.required}
        ?optionalTag=${this.optionalTag}
        ?requiredTag=${this.requiredTag}
        useWrapper=${this.useWrapper}
        .optionalText=${this.optionalText}
        .requiredText=${this.requiredText}
        .tagText=${this.tagText}
        .errorMessage=${this.errorMessage}
        .helptext=${this.helptext}
        .helptextDropdown=${this.helptextDropdown}
        .helptextDropdownButton=${this.helptextDropdownButton}
        .ariaDescribedBy=${this.ariaDescribedBy}
        class="pkt-datepicker"
      >
        <div class="pkt-contents" slot="helptext">${slotContent(this, 'helptext')}</div>
        ${this.multiple
          ? html`<pkt-date-tags
              .dates=${this._value}
              dateformat=${this.dateformat}
              strings=${this.strings}
              id-base=${this.id}
              @date-tag-removed=${(e: CustomEvent) => {
                const popup = this.datepickerPopupRef.value
                const date = fromISOToDate(e.detail)
                if (popup && date && typeof popup.handleDateSelect === 'function') {
                  popup.handleDateSelect(date)
                } else {
                  this.calRef.value?.handleDateSelect(date)
                }
              }}
            ></pkt-date-tags>`
          : nothing}
        <div class="pkt-datepicker__inputs ${this.range ? 'pkt-input__range-inputs' : ''}">
          ${this.range
            ? this.renderRangeInput()
            : this.multiple
              ? this.renderMultipleInput()
              : this.renderInput()}
        </div>
      </pkt-input-wrapper>
      ${this.renderCalendar()}
    `
  }

  /**
   * Handlers
   */

  handleCalendarPosition() {
    const hasCounter = this.multiple && !!this.maxlength
    calendarUtils.handleCalendarPosition(this.popupRef, this.inputRef, hasCounter)
  }

  addToSelected = (e: Event | KeyboardEvent) => {
    const popup = this.datepickerPopupRef.value
    if (popup && typeof popup.addToSelected === 'function') {
      return popup.addToSelected(e, this.min, this.max)
    }
    return calendarUtils.addToSelected(e, this.calRef, this.min, this.max)
  }

  public async showCalendar() {
    const popup = this.datepickerPopupRef.value
    this.calendarOpen = true
    if (popup && typeof popup.show === 'function') {
      popup.show()
      if (isIOS()) popup.focusOnCurrentDate()
      return
    }
    await sleep(20)
    this.handleCalendarPosition()
    if (isIOS()) {
      this.calRef.value?.focusOnCurrentDate()
    }
  }

  public hideCalendar() {
    const popup = this.datepickerPopupRef.value
    this.calendarOpen = false
    if (popup && typeof popup.hide === 'function') return popup.hide()
  }

  public async toggleCalendar(e: Event) {
    e.preventDefault()
    const popup = this.datepickerPopupRef.value
    if (popup && typeof popup.toggle === 'function') {
      const wasOpen = !!popup.open
      popup.toggle()
      this.calendarOpen = !wasOpen
      return
    }
    this.calendarOpen ? this.hideCalendar() : this.showCalendar()
  }

  public clearInputValue() {
    this._value = []
    this.value = ''
    this.internals.setFormValue(this.value)
    this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))
    this.dispatchEvent(
      new CustomEvent('value-change', {
        detail: this._value,
        bubbles: true,
        composed: true,
      }),
    )
  }
}

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