import { TZDate } from "@date-fns/tz";
import { consume, type ContextType } from "@lit/context";
import type { ActionDetail } from "@material/mwc-list";
import { mdiCalendarToday } from "@mdi/js";
import "cally";
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import {
  formatCallyDateRange,
  formatCallyMonthYear,
  formatDateMonthYear,
  formatISODateOnly,
} from "../../common/datetime/format_date";
import { transform } from "../../common/decorators/transform";
import { fireEvent } from "../../common/dom/fire_event";
import type {
  HASSDomEvent,
  HASSDomTargetEvent,
} from "../../common/dom/fire_event";
import { configContext, internationalizationContext } from "../../data/context";
import { TimeZone } from "../../data/translation";
import { MobileAwareMixin } from "../../mixins/mobile-aware-mixin";
import { haStyleScrollbar } from "../../resources/styles";
import type { HomeAssistantConfig, ValueChangedEvent } from "../../types";
import "../chips/ha-chip-set";
import "../chips/ha-filter-chip";
import type { HaFilterChip } from "../chips/ha-filter-chip";
import type { HaBaseTimeInput } from "../ha-base-time-input";
import "../ha-icon-button";
import "../ha-icon-button-next";
import "../ha-icon-button-prev";
import "../ha-list";
import "../ha-list-item";
import "../ha-time-input";
import type { HaTimeInput } from "../ha-time-input";
import type { DateRangePickerRanges } from "./ha-date-range-picker";
import { datePickerStyles, dateRangePickerStyles } from "./styles";

@customElement("date-range-picker")
export class DateRangePicker extends MobileAwareMixin(LitElement) {
  @property({ attribute: false }) public ranges?: DateRangePickerRanges | false;

  @property({ attribute: false }) public startDate?: Date;

  @property({ attribute: false }) public endDate?: Date;

  @property({ attribute: "time-picker", type: Boolean })
  public timePicker = false;

  @state()
  @consume({ context: internationalizationContext, subscribe: true })
  private _i18n!: ContextType<typeof internationalizationContext>;

  @state()
  @consume({ context: configContext, subscribe: true })
  @transform<HomeAssistantConfig, HassConfig>({
    transformer: ({ config }) => config,
  })
  private _hassConfig!: HassConfig;

  /** used to show month and year in calendar-range header */
  @state() private _pickerMonthYear?: string;

  /**
   * used for today to navigate focus in calendar-range
   *
   * Always mirrors the day calendar-range has focused, never cleared: once this
   * has held a date, rendering `undefined` over it makes cally fall back to the
   * selected range and page the calendar away from where the user is.
   */
  @state() private _focusDate?: string;

  @state() private _dateValue?: string;

  @state() private _timeValue = {
    from: { hours: 0, minutes: 0 },
    to: { hours: 23, minutes: 59 },
  };

  @queryAll("ha-time-input") private _timeInputs?: NodeListOf<HaTimeInput>;

  public connectedCallback() {
    super.connectedCallback();

    const date = this.startDate || new Date();

    this._dateValue =
      this.startDate && this.endDate
        ? formatCallyDateRange(
            this.startDate,
            this.endDate,
            this._i18n?.locale,
            this._hassConfig
          )
        : undefined;
    this._pickerMonthYear = formatDateMonthYear(
      date,
      this._i18n.locale,
      this._hassConfig
    );

    if (this.timePicker && this.startDate && this.endDate) {
      this._timeValue = {
        from: {
          hours: this.startDate.getHours(),
          minutes: this.startDate.getMinutes(),
        },
        to: {
          hours: this.endDate.getHours(),
          minutes: this.endDate.getMinutes(),
        },
      };
    }
  }

  private _renderRanges() {
    if (this._isMobileSize) {
      return html`
        <ha-chip-set class="ha-scrollbar">
          ${Object.entries(this.ranges!).map(
            ([name, range], index) => html`
              <ha-filter-chip
                .index=${index}
                .range=${range}
                @click=${this._clickDateRangeChip}
              >
                ${name}
              </ha-filter-chip>
            `
          )}
        </ha-chip-set>
      `;
    }

    return html`
      <ha-list @action=${this._setDateRange} activatable>
        ${Object.keys(this.ranges!).map(
          (name) => html`<ha-list-item>${name}</ha-list-item>`
        )}
      </ha-list>
    `;
  }

  render() {
    return html`<div class="picker">
        ${
          this.ranges !== false && this.ranges
            ? html`<div class="date-range-ranges">${this._renderRanges()}</div>`
            : nothing
        }
        <div class="range">
          <calendar-range
            .value=${this._dateValue}
            .locale=${this._i18n.locale.language}
            .focusedDate=${this._focusDate}
            @focusday=${this._focusChanged}
            @change=${this._handleChange}
            show-outside-days
            .firstDayOfWeek=${firstWeekdayIndex(this._i18n.locale)}
          >
            <ha-icon-button-prev
              tabindex="-1"
              slot="previous"
            ></ha-icon-button-prev>
            <div class="heading" slot="heading">
              <span class="month-year">${this._pickerMonthYear}</span>
              <ha-icon-button
                @click=${this._focusToday}
                .path=${mdiCalendarToday}
                .label=${this._i18n.localize("ui.dialogs.date-picker.today")}
              ></ha-icon-button>
            </div>
            <ha-icon-button-next
              tabindex="-1"
              slot="next"
            ></ha-icon-button-next>
            <calendar-month dir=${this.dir}></calendar-month>
          </calendar-range>
          ${
            this.timePicker
              ? html`
                  <div class="times">
                    <ha-time-input
                      .value=${`${this._timeValue.from.hours}:${this._timeValue.from.minutes}`}
                      .locale=${this._i18n.locale}
                      @value-changed=${this._handleChangeTime}
                      .label=${this._i18n.localize(
                        "ui.components.date-range-picker.time_from"
                      )}
                      id="from"
                      placeholder-labels
                      auto-validate
                    ></ha-time-input>
                    <ha-time-input
                      .value=${`${this._timeValue.to.hours}:${this._timeValue.to.minutes}`}
                      .locale=${this._i18n.locale}
                      @value-changed=${this._handleChangeTime}
                      .label=${this._i18n.localize(
                        "ui.components.date-range-picker.time_to"
                      )}
                      id="to"
                      placeholder-labels
                      auto-validate
                    ></ha-time-input>
                  </div>
                `
              : nothing
          }
        </div>
      </div>
      <div class="footer">
        <ha-button appearance="plain" @click=${this._cancel}
          >${this._i18n.localize("ui.common.cancel")}</ha-button
        >
        <ha-button .disabled=${!this._dateValue} @click=${this._save}
          >${this._i18n.localize(
            "ui.components.date-range-picker.select"
          )}</ha-button
        >
      </div>`;
  }

  private _focusToday() {
    const date = new Date();
    this._focusDate = formatISODateOnly(
      date,
      this._i18n.locale,
      this._hassConfig
    );
    this._pickerMonthYear = formatDateMonthYear(
      date,
      this._i18n.locale,
      this._hassConfig
    );
  }

  private _cancel() {
    fireEvent(this, "cancel-date-picker");
  }

  private _save() {
    if (!this._dateValue) {
      return;
    }

    const dates = this._dateValue.split("/");
    let startDate = new Date(`${dates[0]}T00:00:00`);
    let endDate = new Date(`${dates[1]}T23:59:00`);

    if (this.timePicker) {
      const timeInputs = this._timeInputs;
      if (
        timeInputs &&
        ![...timeInputs].every((input) => input.reportValidity())
      ) {
        // If we have time inputs, and they don't all report valid, don't save
        return;
      }
      startDate.setHours(this._timeValue.from.hours);
      startDate.setMinutes(this._timeValue.from.minutes);
      endDate.setHours(this._timeValue.to.hours);
      endDate.setMinutes(this._timeValue.to.minutes);

      startDate.setSeconds(0);
      startDate.setMilliseconds(0);
      endDate.setSeconds(0);
      endDate.setMilliseconds(0);

      if (endDate <= startDate) {
        endDate.setDate(startDate.getDate() + 1);
      }
    }

    if (this._i18n.locale.time_zone === TimeZone.server) {
      startDate = new Date(
        new TZDate(startDate, this._hassConfig.time_zone).getTime()
      );
      endDate = new Date(
        new TZDate(endDate, this._hassConfig.time_zone).getTime()
      );
    }

    if (
      startDate.getHours() !== this._timeValue.from.hours ||
      startDate.getMinutes() !== this._timeValue.from.minutes ||
      endDate.getHours() !== this._timeValue.to.hours ||
      endDate.getMinutes() !== this._timeValue.to.minutes
    ) {
      this._timeValue.from.hours = startDate.getHours();
      this._timeValue.from.minutes = startDate.getMinutes();
      this._timeValue.to.hours = endDate.getHours();
      this._timeValue.to.minutes = endDate.getMinutes();
    }

    fireEvent(this, "value-changed", {
      value: {
        startDate,
        endDate,
      },
    });
  }

  private _focusChanged(
    ev: HASSDomEvent<Date> &
      HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
  ) {
    this._pickerMonthYear = formatCallyMonthYear(ev.detail, this._i18n.locale);
    this._focusDate = ev.target.focusedDate;
  }

  private _handleChange(
    ev: HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
  ) {
    const dateElement = ev.target as HTMLElementTagNameMap["calendar-range"];
    this._dateValue = dateElement.value;
    this._focusDate = dateElement.focusedDate;
  }

  private _clickDateRangeChip(
    ev: HASSDomTargetEvent<
      HaFilterChip & { index: number; range: [Date, Date] }
    >
  ) {
    const chip = ev.target as HaFilterChip & {
      index: number;
      range: [Date, Date];
    };
    this._saveDateRangePreset(chip.range, chip.index);
  }

  private _setDateRange(ev: HASSDomEvent<ActionDetail>) {
    const dateRange: [Date, Date] = Object.values(this.ranges!)[
      ev.detail.index
    ];
    this._saveDateRangePreset(dateRange, ev.detail.index);
  }

  private _saveDateRangePreset(range: [Date, Date], index: number) {
    fireEvent(this, "value-changed", {
      value: {
        startDate: range[0],
        endDate: range[1],
      },
    });
    fireEvent(this, "preset-selected", {
      index,
    });
  }

  private _handleChangeTime(
    ev: ValueChangedEvent<string> & HASSDomTargetEvent<HaBaseTimeInput>
  ) {
    ev.stopPropagation();
    const time = ev.detail.value;
    const target = ev.target as HaBaseTimeInput;
    const type = target.id;
    if (time) {
      if (!this._timeValue) {
        this._timeValue = {
          from: { hours: 0, minutes: 0 },
          to: { hours: 23, minutes: 59 },
        };
      }
      const [hours, minutes] = time.split(":").map(Number);
      this._timeValue[type].hours = hours;
      this._timeValue[type].minutes = minutes;
    }
  }

  static styles = [
    datePickerStyles,
    dateRangePickerStyles,
    haStyleScrollbar,
    css`
      .picker {
        display: flex;
        flex-direction: row;
      }

      .date-range-ranges {
        border-right: var(--ha-border-width-sm) solid var(--divider-color);
        min-width: 140px;
        flex: 0 1 30%;
      }

      .range {
        display: flex;
        flex-direction: column;
        align-items: center;
        flex: 1;
        padding: var(--ha-space-3);
        overflow-x: hidden;
      }

      @media all and (max-width: 450px), all and (max-height: 500px) {
        .picker {
          flex-direction: column;
        }

        .date-range-ranges {
          border-bottom: 1px solid var(--divider-color);
          margin-top: var(--ha-space-5);
          overflow: visible;
        }

        ha-chip-set {
          padding: var(--ha-space-3);
          flex-wrap: nowrap;
          overflow-x: auto;
        }

        .range {
          flex-basis: fit-content;
        }
      }

      .times {
        display: flex;
        flex-direction: column;
        gap: var(--ha-space-2);
      }

      .footer {
        display: flex;
        justify-content: flex-end;
        padding: var(--ha-space-2);
        border-top: 1px solid var(--divider-color);
      }
    `,
  ];
}

declare global {
  interface HTMLElementTagNameMap {
    "date-range-picker": DateRangePicker;
  }

  interface HASSDomEvents {
    "cancel-date-picker": undefined;
    "preset-selected": { index: number };
  }
}
