import { endOfToday, isToday, startOfToday } from "date-fns";
import type { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { BarSeriesOption } from "echarts/charts";
import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
import { getEnergyColor } from "./common/color";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import "../../../../components/ha-card";
import "./common/hui-energy-graph-chip";
import type {
  EnergyData,
  EnergySumData,
  EnergyConsumptionData,
  GridSourceTypeEnergyPreference,
} from "../../../../data/energy";
import {
  computeConsumptionData,
  getEnergyDataCollection,
  getSuggestedPeriod,
  getSummedData,
  validateEnergyCollectionKey,
} from "../../../../data/energy";
import type { Statistics, StatisticsMetaData } from "../../../../data/recorder";
import { getStatisticLabel } from "../../../../data/recorder";
import type { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceCard } from "../../types";
import type { EnergyUsageGraphCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed";
import {
  type EnergyDataPoint,
  fillDataGapsAndRoundCaps,
  generateFillBuckets,
  getCommonOptions,
  getCompareTransform,
  getPeriodMidpointOffset,
} from "./common/energy-chart-options";
import type { HaECOption } from "../../../../resources/echarts/echarts";
import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base";

const colorPropertyMap = {
  to_grid: "--energy-grid-return-color",
  to_battery: "--energy-battery-in-color",
  from_grid: "--energy-grid-consumption-color",
  used_grid: "--energy-grid-consumption-color",
  used_solar: "--energy-solar-color",
  used_battery: "--energy-battery-out-color",
};

const stackOrder = {
  to_battery: 1,
  to_grid: 2,
  used_solar: 3,
  used_battery: 4,
  from_grid: 5,
  used_grid: 5,
};

@customElement("hui-energy-usage-graph-card")
export class HuiEnergyUsageGraphCard
  extends SubscribeMixin(LitElement)
  implements LovelaceCard
{
  public static async getConfigElement() {
    await import("../../editor/config-elements/hui-energy-graph-card-editor");
    return document.createElement("hui-energy-graph-card-editor");
  }

  @property({ attribute: false }) public hass!: HomeAssistant;

  @state() private _config?: EnergyUsageGraphCardConfig;

  public static getStubConfig(
    _hass: HomeAssistant,
    _entities: string[],
    _entitiesFill: string[]
  ): EnergyUsageGraphCardConfig {
    return {
      type: "energy-usage-graph",
    };
  }

  @state() private _chartData: BarSeriesOption[] = [];

  @state() private _yAxisFractionDigits = 1;

  @state() private _legendData?: CustomLegendOption["data"];

  @state() private _start = startOfToday();

  @state() private _end = endOfToday();

  @state() private _compareStart?: Date;

  @state() private _compareEnd?: Date;

  @state() private _total?: number;

  protected hassSubscribeRequiredHostProps = ["_config"];

  public hassSubscribe(): UnsubscribeFunc[] {
    return [
      getEnergyDataCollection(this.hass, {
        key: this._config?.collection_key,
      }).subscribe((data) => this._getStatistics(data)),
    ];
  }

  public getCardSize(): Promise<number> | number {
    return 3;
  }

  public setConfig(config: EnergyUsageGraphCardConfig): void {
    if (config.collection_key) {
      validateEnergyCollectionKey(config.collection_key);
    }
    this._config = config;
  }

  protected shouldUpdate(changedProps: PropertyValues<this>): boolean {
    return (
      hasConfigChanged(this, changedProps) ||
      changedProps.size > 1 ||
      !changedProps.has("hass")
    );
  }

  protected render() {
    if (!this.hass || !this._config) {
      return nothing;
    }

    return html`
      <ha-card>
        ${
          this._config.title
            ? html` <div class="card-header">
                <span>${this._config.title}</span>
                ${
                  this._total
                    ? html`<hui-energy-graph-chip
                        .tooltip=${this._formatTotal(this._total)}
                      >
                        ${this.hass.localize(
                          "ui.panel.lovelace.cards.energy.energy_usage_graph.total_usage",
                          { num: formatNumber(this._total, this.hass.locale) }
                        )}
                      </hui-energy-graph-chip>`
                    : nothing
                }
              </div>`
            : nothing
        }
        <div
          class="content ${classMap({
            "has-header": !!this._config.title,
          })}"
        >
          <ha-chart-base
            .hass=${this.hass}
            .data=${this._chartData}
            .options=${this._createOptions(
              this._start,
              this._end,
              this.hass.locale,
              this.hass.config,
              this._compareStart,
              this._compareEnd,
              this._yAxisFractionDigits,
              this._legendData
            )}
            chart-type="bar"
            .expandLegend=${this._config.expand_legend}
          ></ha-chart-base>
          ${
            !this._chartData.some((dataset) => dataset.data!.length)
              ? html`<div class="no-data">
                  ${
                    isToday(this._start)
                      ? this.hass.localize(
                          "ui.panel.lovelace.cards.energy.no_data"
                        )
                      : this.hass.localize(
                          "ui.panel.lovelace.cards.energy.no_data_period"
                        )
                  }
                </div>`
              : ""
          }
        </div>
      </ha-card>
    `;
  }

  private _formatTotal = (total: number) =>
    this.hass.localize(
      "ui.panel.lovelace.cards.energy.energy_usage_graph.total_consumed",
      { num: formatNumber(total, this.hass.locale) }
    );

  private _createOptions = memoizeOne(
    (
      start: Date,
      end: Date,
      locale: FrontendLocaleData,
      config: HassConfig,
      compareStart: Date | undefined,
      compareEnd: Date | undefined,
      yAxisFractionDigits: number,
      legendData?: CustomLegendOption["data"]
    ): HaECOption => {
      const commonOptions = getCommonOptions(
        start,
        end,
        locale,
        config,
        "kWh",
        compareStart,
        compareEnd,
        this._formatTotal,
        false,
        yAxisFractionDigits
      );
      const tooltip = commonOptions.tooltip;
      const baseFormatter =
        tooltip &&
        !Array.isArray(tooltip) &&
        typeof tooltip.formatter === "function"
          ? tooltip.formatter
          : undefined;
      const options: HaECOption = {
        ...commonOptions,
        legend: {
          show: this._config?.show_legend !== false,
          type: "custom",
          data: legendData,
        },
        tooltip: {
          ...commonOptions.tooltip,
          formatter: (params: TopLevelFormatterParams) => {
            if (!Array.isArray(params)) {
              return nothing;
            }
            const sorted = [...params].sort((a, b) => {
              const aValue = (a.value as number[])?.[1];
              const bValue = (b.value as number[])?.[1];
              if (aValue > 0 && bValue < 0) {
                return -1;
              }
              if (bValue > 0 && aValue < 0) {
                return 1;
              }
              if (aValue > 0) {
                return b.componentIndex - a.componentIndex;
              }
              return a.componentIndex - b.componentIndex;
            });
            return baseFormatter ? baseFormatter(sorted) : nothing;
          },
        },
      };
      return options;
    }
  );

  private async _getStatistics(energyData: EnergyData): Promise<void> {
    const datasets: BarSeriesOption[] = [];

    let yMin = Infinity;
    let yMax = -Infinity;
    const trackY = (v: number) => {
      if (v < yMin) yMin = v;
      if (v > yMax) yMax = v;
    };

    const statIds: {
      to_grid?: string[];
      from_grid?: string[];
      solar?: string[];
      to_battery?: string[];
      from_battery?: string[];
    } = {};

    const statLabels: {
      to_grid: Record<string, string>;
      from_grid: Record<string, string>;
      to_battery: Record<string, string>;
    } = {
      to_grid: {},
      from_grid: {},
      to_battery: {},
    };

    // Grid sources can be import-only or export-only; assign color indices by
    // position in the user's grid sources config so this card matches the
    // energy sources table, which uses positional indices.
    const colorIndices: Record<string, Record<string, number>> = {};
    Object.keys(colorPropertyMap).forEach((key) => {
      colorIndices[key] = {};
    });
    let gridIdx = 0;

    for (const source of energyData.prefs.energy_sources) {
      if (source.type === "solar") {
        if (statIds.solar) {
          statIds.solar.push(source.stat_energy_from);
        } else {
          statIds.solar = [source.stat_energy_from];
        }
        continue;
      }

      if (source.type === "battery") {
        if (statIds.to_battery) {
          statIds.to_battery.push(source.stat_energy_to);
          statIds.from_battery!.push(source.stat_energy_from);
        } else {
          statIds.to_battery = [source.stat_energy_to];
          statIds.from_battery = [source.stat_energy_from];
        }
        if (source.name) {
          statLabels.to_battery[source.stat_energy_to] = this.hass.localize(
            "ui.panel.lovelace.cards.energy.energy_sources_table.named_battery_charged",
            { name: source.name }
          );
        }
        continue;
      }

      if (source.type !== "grid") {
        continue;
      }

      const gridSource = source as GridSourceTypeEnergyPreference;
      if (gridSource.stat_energy_from) {
        if (statIds.from_grid) {
          statIds.from_grid.push(gridSource.stat_energy_from);
        } else {
          statIds.from_grid = [gridSource.stat_energy_from];
        }
        colorIndices.from_grid[gridSource.stat_energy_from] = gridIdx;
        if (gridSource.name) {
          statLabels.from_grid[gridSource.stat_energy_from] =
            gridSource.stat_energy_to
              ? this.hass.localize(
                  "ui.panel.lovelace.cards.energy.energy_usage_graph.named_grid_consumed",
                  { name: gridSource.name }
                )
              : gridSource.name;
        }
      }
      if (gridSource.stat_energy_to) {
        if (statIds.to_grid) {
          statIds.to_grid.push(gridSource.stat_energy_to);
        } else {
          statIds.to_grid = [gridSource.stat_energy_to];
        }
        colorIndices.to_grid[gridSource.stat_energy_to] = gridIdx;
        if (gridSource.name) {
          statLabels.to_grid[gridSource.stat_energy_to] =
            gridSource.stat_energy_from
              ? this.hass.localize(
                  "ui.panel.lovelace.cards.energy.energy_usage_graph.named_grid_exported",
                  { name: gridSource.name }
                )
              : gridSource.name;
        }
      }
      gridIdx++;
    }

    const computedStyles = getComputedStyle(this);

    Object.keys(colorPropertyMap).forEach((key) => {
      if (
        key === "used_grid" ||
        key === "used_solar" ||
        key === "used_battery" ||
        key === "from_grid" ||
        key === "to_grid"
      ) {
        return;
      }
      if (statIds[key]) {
        Object.values(statIds[key]).forEach((id, idx) => {
          colorIndices[key][id as string] = idx;
        });
      }
    });

    const typeLabels = {
      used_grid: this.hass.localize(
        "ui.panel.lovelace.cards.energy.energy_usage_graph.combined_from_grid"
      ),
      used_solar: this.hass.localize(
        "ui.panel.lovelace.cards.energy.energy_usage_graph.consumed_solar"
      ),
      used_battery: this.hass.localize(
        "ui.panel.lovelace.cards.energy.energy_usage_graph.consumed_battery"
      ),
    };

    this._start = energyData.start;
    this._end = energyData.end || endOfToday();

    this._compareStart = energyData.startCompare;
    this._compareEnd = energyData.endCompare;

    const { summedData, compareSummedData } = getSummedData(energyData);
    const { consumption, compareConsumption } = computeConsumptionData(
      summedData,
      compareSummedData
    );

    if (energyData.statsCompare) {
      datasets.push(
        ...this._processDataSet(
          energyData.statsCompare,
          energyData.statsMetadata,
          compareSummedData!,
          compareConsumption!,
          statIds,
          colorIndices,
          computedStyles,
          typeLabels,
          statLabels,
          trackY,
          true
        )
      );
    }

    // add empty dataset so compare bars are first
    // `stack: usage` so it doesn't take up space yet
    datasets.push({
      id: "compare-placeholder",
      type: "bar",
      stack: energyData.statsCompare ? "compare" : "usage",
      data: [],
      // @ts-expect-error
      order: 0,
    });

    datasets.push(
      ...this._processDataSet(
        energyData.stats,
        energyData.statsMetadata,
        summedData,
        consumption,
        statIds,
        colorIndices,
        computedStyles,
        typeLabels,
        statLabels,
        trackY,
        false
      )
    );

    // @ts-expect-error
    datasets.sort((a, b) => a.order - b.order);
    fillDataGapsAndRoundCaps(
      datasets,
      true,
      generateFillBuckets(
        datasets,
        this._start,
        this._end,
        getSuggestedPeriod(this._start, this._end)
      )
    );
    this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax, true);
    this._chartData = datasets;
    this._legendData = this._getLegendData(datasets);
    this._total = this._processTotal(consumption);
  }

  private _getLegendData(
    datasets: BarSeriesOption[]
  ): CustomLegendOption["data"] {
    // Each main series gets a legend item, and its matching compare
    // series (if any) is attached as a secondary id so toggling
    // the legend item shows/hides both at once.
    const compareIds = new Set(
      datasets
        .map((dataset) => dataset.id as string)
        .filter((id) => id?.startsWith("compare-"))
    );
    return datasets
      .filter(
        (dataset) =>
          dataset.id && !(dataset.id as string).startsWith("compare-")
      )
      .map((dataset) => {
        const id = dataset.id as string;
        const compareId = `compare-${id}`;
        return {
          id,
          secondaryIds: compareIds.has(compareId) ? [compareId] : [],
          name: dataset.name as string,
        };
      });
  }

  private _processTotal(consumption: EnergyConsumptionData) {
    return consumption.total.used_total > 0
      ? consumption.total.used_total
      : undefined;
  }

  private _processDataSet(
    statistics: Statistics,
    statisticsMetaData: Record<string, StatisticsMetaData>,
    summedData: EnergySumData,
    consumptionData: EnergyConsumptionData,
    statIdsByCat: {
      to_grid?: string[] | undefined;
      from_grid?: string[] | undefined;
      solar?: string[] | undefined;
      to_battery?: string[] | undefined;
      from_battery?: string[] | undefined;
    },
    colorIndices: Record<string, Record<string, number>>,
    computedStyles: CSSStyleDeclaration,
    typeLabels: {
      used_grid: string;
      used_solar: string;
      used_battery: string;
    },
    statLabels: {
      to_grid: Record<string, string>;
      from_grid: Record<string, string>;
      to_battery: Record<string, string>;
    },
    trackY: (v: number) => void,
    compare = false
  ) {
    const data: BarSeriesOption[] = [];

    const combinedData: {
      to_grid?: Record<string, Record<number, number>>;
      to_battery?: Record<string, Record<number, number>>;
      from_grid?: Record<string, Record<number, number>>;
      used_grid?: Record<string, Record<number, number>>;
      used_solar?: Record<string, Record<number, number>>;
      used_battery?: Record<string, Record<number, number>>;
    } = {};

    Object.entries(statIdsByCat).forEach(([key, statIds]) => {
      if (!["to_grid", "from_grid", "to_battery"].includes(key)) {
        return;
      }
      const sets: Record<string, Record<number, number>> = {};
      statIds!.forEach((id) => {
        const stats = statistics[id];
        if (!stats) {
          return;
        }

        const set = {};
        stats.forEach((stat) => {
          if (stat.change === null || stat.change === undefined) {
            return;
          }
          const val = stat.change;
          if (!(stat.start in set)) {
            set[stat.start] = val;
          }
        });
        sets[id] = set;
      });
      combinedData[key] = sets;
    });

    // Only add solar/battery consumption series when such a source is
    // actually configured, otherwise the legend shows empty solar/battery
    // entries for grid-only setups.
    if (statIdsByCat.solar) {
      combinedData.used_solar = { used_solar: consumptionData.used_solar };
    }
    if (statIdsByCat.from_battery) {
      combinedData.used_battery = {
        used_battery: consumptionData.used_battery,
      };
    }

    if (combinedData.from_grid && summedData.to_battery) {
      const used_grid = {};
      // If we have to_battery and multiple grid sources in the same period, we
      // can't determine which source was used. So delete all the individual
      // sources and replace with a 'combined from grid' value.
      for (const [start, grid_to_battery] of Object.entries(
        consumptionData.grid_to_battery
      )) {
        if (!grid_to_battery) {
          continue;
        }
        let noOfSources = 0;
        let source: string;
        for (const [key, stats] of Object.entries(combinedData.from_grid)) {
          if (stats[start]) {
            source = key;
            noOfSources++;
          }
          if (noOfSources > 1) {
            break;
          }
        }
        if (noOfSources === 1) {
          combinedData.from_grid[source!][start] =
            consumptionData.used_grid[start];
        } else {
          Object.values(combinedData.from_grid).forEach((stats) => {
            delete stats[start];
          });
          used_grid[start] = consumptionData.used_grid[start];
        }
      }
      combinedData.used_grid = { used_grid };
    }

    const uniqueKeys = summedData.timestamps;

    // Only start timestamps available here, so center sub-daily bars from the
    // gap between the first two entries, clamped to the nominal period so
    // sparse or lone buckets stay centered on the same grid as dense data.
    const period = getSuggestedPeriod(this._start, this._end);
    const periodOffset = getPeriodMidpointOffset(
      period,
      uniqueKeys.length >= 2 ? uniqueKeys[1] - uniqueKeys[0] : undefined
    );

    const compareTransform = getCompareTransform(
      this._start,
      this._compareStart!
    );

    Object.entries(combinedData).forEach(([type, sources]) => {
      Object.entries(sources).forEach(([statId, source]) => {
        const points: BarSeriesOption["data"] = [];
        // Process chart data.
        for (const key of uniqueKeys) {
          const value = source[key] || 0;
          const y =
            value && ["to_grid", "to_battery"].includes(type)
              ? -1 * value
              : value;
          const dataPoint: EnergyDataPoint = [key + periodOffset, y, key];
          if (compare) {
            dataPoint[0] =
              compareTransform(new Date(key)).getTime() + periodOffset;
          }
          points.push(dataPoint);
          trackY(y);
        }

        data.push({
          id: `${compare ? "compare-" : ""}${statId}-${type}`,
          type: "bar",
          cursor: "default",
          name:
            type in typeLabels
              ? typeLabels[type]
              : statLabels[type]?.[statId] ||
                getStatisticLabel(
                  this.hass,
                  statId,
                  statisticsMetaData[statId]
                ),
          // @ts-expect-error
          order: stackOrder[type] ?? Object.keys(combinedData).length,
          barMaxWidth: 50,
          itemStyle: {
            borderColor: getEnergyColor(
              computedStyles,
              this.hass.themes.darkMode,
              false,
              compare,
              colorPropertyMap[type],
              colorIndices[type]?.[statId]
            ),
          },
          color: getEnergyColor(
            computedStyles,
            this.hass.themes.darkMode,
            true,
            compare,
            colorPropertyMap[type],
            colorIndices[type]?.[statId]
          ),
          stack: compare ? "compare" : "usage",
          data: points,
        });
      });
    });
    return data;
  }

  static styles = css`
    ha-card {
      height: 100%;
    }
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding-bottom: 0;
    }
    .content {
      padding: 16px;
    }
    .has-header {
      padding-top: 0;
    }
    .no-data {
      position: absolute;
      height: 100%;
      top: 0;
      left: 0;
      right: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 20%;
      margin-left: 32px;
      margin-inline-start: 32px;
      margin-inline-end: initial;
      box-sizing: border-box;
    }
  `;
}

declare global {
  interface HTMLElementTagNameMap {
    "hui-energy-usage-graph-card": HuiEnergyUsageGraphCard;
  }
}
