/** Responsive sensor-history chart for the task detail view.
 *
 * Replaces the fixed 300x140 sparkline: renders at the card's real pixel
 * width (ResizeObserver), draws round y-ticks with gridlines, shades the
 * danger zone beyond a threshold (with the in-zone line segments in the
 * error color via clip paths), pins a target line for counters, moves
 * completion markers into a calm bottom lane, and offers a crosshair that
 * follows the pointer (works for touch) plus 7d/30d/90d/1y range chips.
 *
 * The component is deliberately dumb: it plots the points and reference
 * lines it is given. All task semantics (delta baselines, cumulative
 * clamping, projections) live in renderers/sparkline.ts.
 */

import { LitElement, html, css, svg, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { t } from "../styles";
import { niceTicks, fmtNum, fmtVal, fmtDateTick, fmtDateTime, timeTicks, needsYear } from "../renderers/chart-utils";

export interface ChartPoint {
  ts: number;
  val: number;
  min?: number;
  max?: number;
}

export interface ChartEvent {
  ts: number;
  type: string; // completed | skipped | reset
}

const H = 210;
const PAD_L = 46;
const PAD_R = 14;
const PAD_T = 12;
const LANE_H = 14; // completion-marker lane between plot and x labels
const PAD_B = 20 + LANE_H;

const RANGES = [
  { days: 7, key: "chart_range_7d" },
  { days: 30, key: "chart_range_30d" },
  { days: 90, key: "chart_range_90d" },
  { days: 365, key: "chart_range_1y" },
] as const;

export class MaintenanceTriggerChart extends LitElement {
  @property({ attribute: false }) public points: ChartPoint[] = [];
  @property({ attribute: false }) public events: ChartEvent[] = [];
  @property() public unit = "";
  @property() public lang = "en";
  @property({ attribute: false }) public thresholdAbove: number | null = null;
  @property({ attribute: false }) public thresholdBelow: number | null = null;
  /** Absolute y of a counter target line (points are pre-transformed). */
  @property({ attribute: false }) public targetValue: number | null = null;
  @property({ type: Boolean }) public forceZero = false;
  /** Optional dashed projection segment (e.g. degradation trend). */
  @property({ attribute: false }) public projection: ChartPoint[] | null = null;
  @property({ attribute: false }) public rangeDays = 30;
  @property({ type: Boolean }) public showRange = true;
  @property({ type: Boolean }) public busy = false;
  /** Reflects the parent's "hide outliers" state (filtering happens upstream). */
  @property({ type: Boolean }) public hideOutliers = false;
  /** Show the outlier toggle chip (only where a raw oscillating series exists). */
  @property({ type: Boolean }) public showOutlierToggle = true;

  @state() private _width = 0;
  @state() private _hover: { x: number; y: number; p: ChartPoint } | null = null;

  private _ro: ResizeObserver | null = null;

  connectedCallback(): void {
    super.connectedCallback();
    this._ro = new ResizeObserver((entries) => {
      const w = Math.floor(entries[0]?.contentRect?.width || 0);
      if (w && Math.abs(w - this._width) > 2) this._width = w;
    });
    this._ro.observe(this);
  }

  disconnectedCallback(): void {
    super.disconnectedCallback();
    this._ro?.disconnect();
    this._ro = null;
  }

  private _emitRange(days: number): void {
    if (days === this.rangeDays) return;
    this.dispatchEvent(new CustomEvent("range-change", { detail: { days }, bubbles: true, composed: true }));
  }

  private _toggleOutliers(): void {
    this.dispatchEvent(new CustomEvent("outlier-toggle", {
      detail: { hide: !this.hideOutliers }, bubbles: true, composed: true,
    }));
  }

  render() {
    const W = this._width || 320;
    const pts = [...this.points].sort((a, b) => a.ts - b.ts);
    const L = this.lang;

    return html`
      <div class="chart-wrap">
        ${this.showRange
          ? html`<div class="range-chips" role="group">
              ${this.showOutlierToggle
                ? html`<button
                    class="range-chip outlier-chip ${this.hideOutliers ? "active" : ""}"
                    ?disabled=${this.busy}
                    title=${t("hide_outliers", L)}
                    @click=${() => this._toggleOutliers()}
                  ><ha-icon icon="mdi:filter-variant"></ha-icon></button>`
                : nothing}
              ${RANGES.map(
                (r) => html`<button
                  class="range-chip ${this.rangeDays === r.days ? "active" : ""}"
                  ?disabled=${this.busy}
                  @click=${() => this._emitRange(r.days)}
                >${t(r.key, L)}</button>`,
              )}
            </div>`
          : nothing}
        ${pts.length < 2
          ? html`<div class="chart-empty">
              <ha-icon icon="mdi:chart-line"></ha-icon> ${t("loading_chart", L)}
            </div>`
          : this._renderSvg(W, pts)}
      </div>
    `;
  }

  private _renderSvg(W: number, pts: ChartPoint[]) {
    const L = this.lang;
    const plotW = W - PAD_L - PAD_R;
    const plotB = H - PAD_B;
    const plotH = plotB - PAD_T;

    // Domain: data ∪ min/max band ∪ reference lines ∪ optional zero floor.
    let lo = Infinity;
    let hi = -Infinity;
    for (const p of pts) {
      lo = Math.min(lo, p.min ?? p.val);
      hi = Math.max(hi, p.max ?? p.val);
    }
    if (this.thresholdAbove != null) { lo = Math.min(lo, this.thresholdAbove); hi = Math.max(hi, this.thresholdAbove); }
    if (this.thresholdBelow != null) { lo = Math.min(lo, this.thresholdBelow); hi = Math.max(hi, this.thresholdBelow); }
    if (this.targetValue != null) { lo = Math.min(lo, this.targetValue); hi = Math.max(hi, this.targetValue); }
    if (this.forceZero) lo = Math.min(lo, 0);
    const pad = (hi - lo || 1) * 0.06;
    // A progress axis never dips below zero — padding must not create a -50 tick.
    const loPadded = this.forceZero && lo >= 0 ? 0 : lo - pad;
    let { ticks, niceMin, niceMax } = niceTicks(loPadded, hi + pad, 4);
    if (this.forceZero && lo >= 0 && niceMin < 0) {
      niceMin = 0;
      ticks = ticks.filter((v) => v >= 0);
    }

    const tsMin = pts[0].ts;
    const tsMax = pts[pts.length - 1].ts;
    const tsSpan = tsMax - tsMin || 1;
    const withYear = needsYear(tsMin, tsMax);

    const toX = (ts: number) => PAD_L + ((ts - tsMin) / tsSpan) * plotW;
    const toY = (v: number) => PAD_T + (1 - (v - niceMin) / (niceMax - niceMin || 1)) * plotH;

    const linePts = pts.map((p) => `${toX(p.ts).toFixed(1)},${toY(p.val).toFixed(1)}`).join(" ");
    const areaPath =
      `M${toX(pts[0].ts).toFixed(1)},${plotB} ` +
      pts.map((p) => `L${toX(p.ts).toFixed(1)},${toY(p.val).toFixed(1)}`).join(" ") +
      ` L${toX(pts[pts.length - 1].ts).toFixed(1)},${plotB} Z`;

    // Optional min/max band behind the line.
    let bandPath = "";
    const band = pts.filter((p) => p.min != null && p.max != null);
    if (band.length >= 2) {
      const up = band.map((p) => `${toX(p.ts).toFixed(1)},${toY(p.max!).toFixed(1)}`);
      const dn = [...band].reverse().map((p) => `${toX(p.ts).toFixed(1)},${toY(p.min!).toFixed(1)}`);
      bandPath = `M${up[0]} ` + up.slice(1).map((x) => `L${x}`).join(" ") + ` L${dn.join(" L")} Z`;
    }

    // Danger zones: rect (shading + line-color clip), the threshold line y,
    // and where its label sits (just inside the zone).
    const zones: { y: number; h: number; lineY: number; label: string; labelY: number }[] = [];
    if (this.thresholdBelow != null) {
      const zy = toY(this.thresholdBelow);
      zones.push({ y: zy, h: Math.max(0, plotB - zy), lineY: zy, label: `▼ ${fmtNum(this.thresholdBelow)}`, labelY: Math.min(plotB - 4, zy + 13) });
    }
    if (this.thresholdAbove != null) {
      const zy = toY(this.thresholdAbove);
      zones.push({ y: PAD_T, h: Math.max(0, zy - PAD_T), lineY: zy, label: `▲ ${fmtNum(this.thresholdAbove)}`, labelY: Math.max(PAD_T + 11, zy - 5) });
    }

    const lastP = pts[pts.length - 1];
    const events = (this.events || []).filter((e) => e.ts >= tsMin && e.ts <= tsMax);
    const xTicks = timeTicks(tsMin, tsMax, Math.max(2, Math.min(5, Math.floor(plotW / 110) + 1)));
    const hover = this._hover;

    return html`
      <div class="svg-holder">
        <svg
          class="chart-svg"
          viewBox="0 0 ${W} ${H}"
          width=${W}
          height=${H}
          role="img"
          aria-label=${t("chart_sparkline", L)}
          @pointermove=${(e: PointerEvent) => this._onPointer(e, pts, toX, toY, W)}
          @pointerdown=${(e: PointerEvent) => this._onPointer(e, pts, toX, toY, W)}
          @pointerleave=${() => (this._hover = null)}
        >
          <defs>
            <clipPath id="plot"><rect x="${PAD_L}" y="${PAD_T}" width="${plotW}" height="${plotH}" /></clipPath>
            ${zones.length
              ? svg`<clipPath id="danger">${zones.map((z) => svg`<rect x="${PAD_L}" y="${z.y.toFixed(1)}" width="${plotW}" height="${z.h.toFixed(1)}" />`)}</clipPath>`
              : nothing}
            <!-- Diagonal hatch so the danger zone reads without relying on the
                 red tint alone (dark-theme contrast + colour-blind support). -->
            <pattern id="dangerHatch" patternUnits="userSpaceOnUse" width="7" height="7" patternTransform="rotate(45)">
              <rect width="7" height="7" fill="var(--error-color, #f44336)" opacity="0.10" />
              <line x1="0" y1="0" x2="0" y2="7" stroke="var(--error-color, #f44336)" stroke-width="1.4" opacity="0.5" />
            </pattern>
          </defs>

          ${ticks.map((v) => {
            const y = toY(v);
            if (y < PAD_T - 1 || y > plotB + 1) return nothing;
            return svg`
              <line x1="${PAD_L}" y1="${y.toFixed(1)}" x2="${W - PAD_R}" y2="${y.toFixed(1)}"
                stroke="var(--divider-color)" stroke-width="1" opacity="0.6" />
              <text x="${PAD_L - 7}" y="${(y + 3.5).toFixed(1)}" text-anchor="end" class="tick-label">${fmtNum(v)}</text>`;
          })}

          ${zones.map(
            (z) => svg`<rect x="${PAD_L}" y="${z.y.toFixed(1)}" width="${plotW}" height="${z.h.toFixed(1)}"
              fill="url(#dangerHatch)" />`,
          )}

          ${bandPath ? svg`<path d="${bandPath}" fill="var(--primary-color)" opacity="0.08" clip-path="url(#plot)" />` : nothing}
          <path d="${areaPath}" fill="var(--primary-color)" opacity="0.10" clip-path="url(#plot)" />
          <polyline points="${linePts}" fill="none" stroke="var(--primary-color)" stroke-width="2"
            stroke-linejoin="round" stroke-linecap="round" clip-path="url(#plot)" />
          ${zones.length
            ? svg`<polyline points="${linePts}" fill="none" stroke="var(--error-color, #f44336)" stroke-width="2"
                stroke-linejoin="round" stroke-linecap="round" clip-path="url(#danger)" />`
            : nothing}

          ${zones.map(
            (z) => svg`
              <line x1="${PAD_L}" y1="${z.lineY.toFixed(1)}" x2="${W - PAD_R}" y2="${z.lineY.toFixed(1)}"
                stroke="var(--error-color, #f44336)" stroke-width="1.5" stroke-dasharray="6,4" />
              <text x="${W - PAD_R - 4}" y="${z.labelY.toFixed(1)}" text-anchor="end" class="zone-label">${z.label}</text>`,
          )}

          ${this.targetValue != null
            ? svg`<line x1="${PAD_L}" y1="${toY(this.targetValue).toFixed(1)}" x2="${W - PAD_R}" y2="${toY(this.targetValue).toFixed(1)}"
                stroke="var(--error-color, #f44336)" stroke-width="1.5" stroke-dasharray="6,4" />
              <text x="${W - PAD_R - 4}" y="${(toY(this.targetValue) - 5).toFixed(1)}" text-anchor="end" class="zone-label">◆ ${fmtNum(this.targetValue)} ${this.unit}</text>`
            : nothing}

          ${this.projection && this.projection.length === 2
            ? svg`<line x1="${toX(this.projection[0].ts).toFixed(1)}" y1="${toY(this.projection[0].val).toFixed(1)}"
                x2="${Math.min(toX(this.projection[1].ts), W - PAD_R).toFixed(1)}" y2="${toY(Math.max(niceMin, Math.min(niceMax, this.projection[1].val))).toFixed(1)}"
                stroke="var(--warning-color, #ff9800)" stroke-width="1.5" stroke-dasharray="4,3" opacity="0.8" />`
            : nothing}

          ${xTicks.map((ts, i) => {
            const x = toX(ts);
            const anchor = i === 0 ? "start" : i === xTicks.length - 1 ? "end" : "middle";
            return svg`<text x="${x.toFixed(1)}" y="${H - 5}" text-anchor="${anchor}" class="tick-label">${fmtDateTick(ts, L, withYear)}</text>`;
          })}

          <line x1="${PAD_L}" y1="${plotB}" x2="${W - PAD_R}" y2="${plotB}" stroke="var(--divider-color)" stroke-width="1" />

          ${events.map((e) => {
            const x = toX(e.ts);
            const color =
              e.type === "completed"
                ? "var(--success-color, #4caf50)"
                : e.type === "skipped"
                  ? "var(--warning-color, #ff9800)"
                  : "var(--info-color, #2196f3)";
            return svg`
              <line x1="${x.toFixed(1)}" y1="${PAD_T}" x2="${x.toFixed(1)}" y2="${plotB}" stroke="${color}" stroke-width="1" opacity="0.14" />
              <rect x="${(x - 1.5).toFixed(1)}" y="${plotB + 3}" width="3" height="${LANE_H - 6}" rx="1.5" fill="${color}">
                <title>${fmtDateTime(e.ts, L)}</title>
              </rect>`;
          })}

          ${hover
            ? svg`
                <line x1="${hover.x.toFixed(1)}" y1="${PAD_T}" x2="${hover.x.toFixed(1)}" y2="${plotB}"
                  stroke="var(--secondary-text-color)" stroke-width="1" stroke-dasharray="3,3" opacity="0.7" />
                <circle cx="${hover.x.toFixed(1)}" cy="${hover.y.toFixed(1)}" r="4.5" fill="var(--primary-color)"
                  stroke="var(--card-background-color, #fff)" stroke-width="2" />`
            : svg`<circle cx="${toX(lastP.ts).toFixed(1)}" cy="${toY(lastP.val).toFixed(1)}" r="4" fill="var(--primary-color)"
                stroke="var(--card-background-color, #fff)" stroke-width="1.5" />`}
        </svg>
        ${hover
          ? html`<div
              class="hover-chip"
              style="left:${Math.min(Math.max(hover.x, 70), W - 70)}px"
            >
              <div class="hover-date">${fmtDateTime(hover.p.ts, L)}</div>
              <div class="hover-val">
                ${fmtVal(hover.p.val, this.unit, L)}
                ${hover.p.min != null && hover.p.max != null
                  ? html`<span class="hover-range">(${fmtNum(hover.p.min)}–${fmtNum(hover.p.max)})</span>`
                  : nothing}
              </div>
            </div>`
          : nothing}
      </div>
    `;
  }

  private _onPointer(
    e: PointerEvent,
    pts: ChartPoint[],
    toX: (ts: number) => number,
    toY: (v: number) => number,
    W: number,
  ): void {
    const svgEl = e.currentTarget as SVGSVGElement;
    const rect = svgEl.getBoundingClientRect();
    const px = ((e.clientX - rect.left) / rect.width) * W;
    if (px < PAD_L - 8 || px > W - PAD_R + 8) {
      this._hover = null;
      return;
    }
    let best = pts[0];
    let bestD = Infinity;
    for (const p of pts) {
      const d = Math.abs(toX(p.ts) - px);
      if (d < bestD) {
        bestD = d;
        best = p;
      }
    }
    this._hover = { x: toX(best.ts), y: toY(best.val), p: best };
  }

  static styles = css`
    :host { display: block; width: 100%; }
    .chart-wrap { position: relative; }
    .range-chips { display: flex; gap: 4px; justify-content: flex-end; margin-bottom: 2px; }
    .range-chip {
      font: inherit; font-size: 11.5px; padding: 2px 9px; border-radius: 12px; cursor: pointer;
      border: 1px solid var(--divider-color); background: transparent;
      color: var(--secondary-text-color);
    }
    /* Outlier toggle sits left of the range chips as an icon button. */
    .outlier-chip { margin-right: auto; padding: 2px 7px; display: inline-flex; align-items: center; }
    .outlier-chip ha-icon { --mdc-icon-size: 15px; }
    .range-chip.active {
      background: var(--primary-color); border-color: var(--primary-color);
      color: var(--text-primary-color, #fff);
    }
    .range-chip[disabled] { opacity: 0.5; pointer-events: none; }
    .svg-holder { position: relative; }
    .chart-svg { display: block; touch-action: pan-y; }
    .tick-label { fill: var(--secondary-text-color); font-size: 10.5px; }
    .zone-label { fill: var(--error-color, #f44336); font-size: 11px; font-weight: 600; }
    .chart-empty {
      display: flex; align-items: center; justify-content: center; gap: 8px; height: 120px;
      color: var(--secondary-text-color); font-size: 12.5px;
    }
    .chart-empty ha-icon { --mdc-icon-size: 17px; }
    .hover-chip {
      position: absolute; top: 0; transform: translateX(-50%);
      background: var(--card-background-color, #fff); border: 1px solid var(--divider-color);
      border-radius: 8px; padding: 4px 9px; pointer-events: none; white-space: nowrap;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); z-index: 3;
    }
    .hover-date { font-size: 10.5px; color: var(--secondary-text-color); }
    .hover-val { font-size: 12.5px; font-weight: 600; color: var(--primary-text-color); }
    .hover-range { font-weight: 400; color: var(--secondary-text-color); font-size: 11px; }
  `;
}

if (!customElements.get("maintenance-trigger-chart")) {
  customElements.define("maintenance-trigger-chart", MaintenanceTriggerChart);
}
