import {
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  ViewEncapsulation,
  computed,
  effect,
  inject,
  input,
} from '@angular/core';
import * as Plot from '@observablehq/plot';
import { chartId, findChartSvg, injectSvgTitleDesc, prefersReducedMotion } from '../_internal/a11y';
import type { ChartBaseInputs } from '../_internal/chart-types';
import { DEFAULT_BAR_FILL } from '../_internal/palette';

/**
 * `<lib-bar-chart>` — vertical bar chart over a categorical X axis.
 *
 * Backed by `@observablehq/plot`'s `barY` mark per ADR-0023. Every
 * row in `data` becomes one bar with its height encoding `yKey` and
 * its position encoding `xKey`.
 *
 * Every bar is filled with the same colour by default — a single-
 * series bar chart's X axis is typically a *label* (day, bucket,
 * name) rather than a category that meaningfully encodes colour, so
 * rotating through a categorical palette adds visual noise without
 * carrying signal. Consumers that genuinely want per-bar colouring
 * (a categorical axis where each bar IS a category with semantic
 * meaning) should use `<lib-donut-chart>` instead, or pass an
 * explicit `fillColor` if they really do want a different shade.
 *
 * Per the lib's a11y contract:
 *   - outer `<figure>` carries `role="img"` + `aria-labelledby` +
 *     `aria-describedby`,
 *   - SVG gets `<title>` + `<desc>` prepended (Plot doesn't emit
 *     them; the helper in `_internal/a11y.ts` does),
 *   - `<details>` disclosure with a `<table>` fallback ships every
 *     data point in a screen-reader-friendly form,
 *   - fill defaults to `DEFAULT_BAR_FILL` from
 *     `_internal/palette.ts` (a colour-blind-safe blue),
 *   - `prefers-reduced-motion` skips transitions.
 */

export interface BarChartInputs<T> extends ChartBaseInputs<T> {
  /** Key in `T` whose value provides the X-axis category. */
  readonly xKey: keyof T & string;
  /** Key in `T` whose value provides the bar height. */
  readonly yKey: keyof T & string;
  /** Human-readable axis labels (no implicit i18n inside the lib). */
  readonly xLabel?: string;
  readonly yLabel?: string;
  /**
   * Override the default single fill colour. Caller-owned — keep it
   * colour-blind-safe and contrasted against the surrounding surface.
   * Most consumers should leave it unset and accept the lib default.
   */
  readonly fillColor?: string;
}

@Component({
  selector: 'lib-bar-chart',
  templateUrl: './bar-chart.html',
  styleUrl: './bar-chart.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  // Plot renders into a host SVG outside Angular's template
  // (imperatively appended via `nativeElement.append(...)`). We
  // disable view encapsulation so the lib's stylesheet (axis ticks,
  // dark-mode tokens) applies to that injected DOM.
  encapsulation: ViewEncapsulation.None,
})
export class BarChart<T extends Record<string, unknown>> {
  private readonly host = inject(ElementRef<HTMLElement>);

  readonly data = input.required<readonly T[]>();
  readonly xKey = input.required<keyof T & string>();
  readonly yKey = input.required<keyof T & string>();
  readonly caption = input.required<string>();
  readonly description = input.required<string>();
  readonly ariaLabel = input.required<string>();
  readonly xLabel = input<string | undefined>(undefined);
  readonly yLabel = input<string | undefined>(undefined);
  readonly fillColor = input<string>(DEFAULT_BAR_FILL);

  protected readonly chartId = chartId();
  protected readonly titleId = `${this.chartId}-title`;
  protected readonly descId = `${this.chartId}-desc`;

  /** Tabular fallback rendered under `<details>`. One row per datum. */
  protected readonly tableRows = computed(() =>
    this.data().map((row) => ({
      x: String(row[this.xKey()]),
      y: Number(row[this.yKey()]),
    })),
  );

  constructor() {
    // Re-render whenever any input that affects the visual changes.
    // Plot's `Plot.plot()` returns a fresh DOM node each call — we
    // replace the previous one in the host's `.chart-canvas`.
    effect(() => {
      this.renderPlot();
    });
  }

  private renderPlot(): void {
    const canvas = (this.host.nativeElement as HTMLElement).querySelector<HTMLDivElement>(
      '.chart-canvas',
    );
    if (!canvas) {
      return;
    }
    const data = this.data();
    const xKey = this.xKey();
    const yKey = this.yKey();
    const xLabel = this.xLabel() ?? null;
    const yLabel = this.yLabel() ?? null;
    const fill = this.fillColor();

    const plot = Plot.plot({
      // Mirrors the host element's available width so the chart is
      // responsive. Plot computes a sensible height from the mark.
      width: canvas.clientWidth || 600,
      marginLeft: 56,
      marginBottom: 40,
      x: { label: xLabel, type: 'band' },
      y: { label: yLabel, grid: true, nice: true },
      marks: [
        Plot.barY(data as Plot.Data, {
          x: xKey,
          y: yKey,
          // Literal-string fill — Plot interprets non-key strings as
          // a fixed colour. Same shade on every bar, by design.
          fill,
          // Plot's default tooltip would require interaction wiring
          // we haven't shipped yet — the tabular fallback under
          // `<details>` is the v1 substitute for hover values.
        }),
        Plot.ruleY([0]),
      ],
      // Plot's CSS-in-style inline output respects our dark-mode
      // tokens via the host stylesheet (.chart-canvas .plot-d6a7b1).
    });

    // Honor the a11y contract — prepend <title> + <desc> to the
    // inner SVG before swapping it into the canvas, so screen
    // readers see them on first render rather than after a re-
    // attach. `findChartSvg` handles both Plot return shapes (bare
    // SVG or <figure> wrapping a legend + SVG).
    const svg = findChartSvg(plot);
    if (svg) {
      injectSvgTitleDesc(svg, this.caption(), this.description());
    }
    if (prefersReducedMotion()) {
      plot.setAttribute('data-no-transitions', '');
    }

    canvas.replaceChildren(plot);
  }
}
