import { arc, pie, type PieArcDatum } from 'd3-shape';
import {
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  ViewEncapsulation,
  computed,
  effect,
  inject,
  input,
} from '@angular/core';
import { chartId, injectSvgTitleDesc, prefersReducedMotion, resolveTheme } from '../_internal/a11y';
import type { ChartBaseInputs } from '../_internal/chart-types';
import { paletteFor } from '../_internal/palette';

/**
 * `<lib-donut-chart>` — categorical breakdown with a hollow centre.
 *
 * Plot ships marks for bar / line / area / dot but no first-class
 * "pie" or "donut" — its design philosophy is that those rarely
 * win against a bar chart for legibility. We still ship one
 * because audit-log outcome breakdown reads naturally as a donut
 * (the empty centre carries the total). The component is therefore
 * raw D3 (`d3-shape`'s `pie` + `arc` generators) wrapped in the
 * same a11y envelope as the other charts.
 *
 * Per ADR-0023's "same `<lib-…>` API regardless of underlying
 * technique" rule, the input shape mirrors the bar chart — only
 * the chart-specific keys differ.
 */

export interface DonutChartInputs<T> extends ChartBaseInputs<T> {
  /** Key carrying the category label. */
  readonly categoryKey: keyof T & string;
  /** Key carrying the numeric value. */
  readonly valueKey: keyof T & string;
  /** Optional centre-of-donut text (e.g. the total). */
  readonly centerLabel?: string;
  /**
   * Optional per-category fill override. When the keys on the X
   * axis carry intent (e.g. audit outcomes — `success` MUST read
   * as green, `denied` as orange), pass a map from category value
   * to colour. Categories not in the map fall back to the
   * colour-blind-safe categorical palette. Use the
   * `semanticStatusColors` export from this lib to stay within the
   * curated colour set (per ADR-0023).
   */
  readonly colorMap?: Readonly<Record<string, string>>;
}

@Component({
  selector: 'lib-donut-chart',
  templateUrl: './donut-chart.html',
  styleUrl: './donut-chart.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
})
export class DonutChart<T extends Record<string, unknown>> {
  private readonly host = inject(ElementRef<HTMLElement>);

  readonly data = input.required<readonly T[]>();
  readonly categoryKey = input.required<keyof T & string>();
  readonly valueKey = input.required<keyof T & string>();
  readonly caption = input.required<string>();
  readonly description = input.required<string>();
  readonly ariaLabel = input.required<string>();
  readonly centerLabel = input<string | undefined>(undefined);
  readonly colorScheme = input<'sequential' | 'categorical'>('categorical');
  readonly colorMap = input<Readonly<Record<string, string>> | undefined>(undefined);

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

  protected readonly tableRows = computed(() =>
    this.data().map((row) => ({
      category: String(row[this.categoryKey()]),
      value: Number(row[this.valueKey()]),
    })),
  );

  constructor() {
    effect(() => {
      this.renderDonut();
    });
  }

  private renderDonut(): void {
    const canvas = (this.host.nativeElement as HTMLElement).querySelector<HTMLDivElement>(
      '.chart-canvas',
    );
    if (!canvas) {
      return;
    }
    const data = this.data();
    const categoryKey = this.categoryKey();
    const valueKey = this.valueKey();
    const palette = paletteFor(this.colorScheme(), resolveTheme(), Math.max(1, data.length));
    const colorMap = this.colorMap();

    const width = canvas.clientWidth || 320;
    const height = 240;
    const radius = Math.min(width, height) / 2;
    const innerRadius = radius * 0.6;

    const svgNs = 'http://www.w3.org/2000/svg';
    const svg = canvas.ownerDocument.createElementNS(svgNs, 'svg');
    svg.setAttribute('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`);
    svg.setAttribute('width', String(width));
    svg.setAttribute('height', String(height));

    injectSvgTitleDesc(svg, this.caption(), this.description());

    const pieLayout = pie<T>()
      .value((d) => Number(d[valueKey]))
      .sort(null);
    const arcGen = arc<PieArcDatum<T>>().innerRadius(innerRadius).outerRadius(radius);

    const arcs = pieLayout(data as T[]);
    arcs.forEach((slice, i) => {
      const path = canvas.ownerDocument.createElementNS(svgNs, 'path');
      path.setAttribute('d', arcGen(slice) ?? '');
      // Semantic override first (when the consumer maps a category
      // to an intent-bearing colour), then palette cycle as a
      // fallback for any unmapped category. Final `#888` is a
      // defensive default that should never actually trigger.
      const category = String(slice.data[categoryKey]);
      const fill = colorMap?.[category] ?? palette[i % palette.length] ?? '#888';
      path.setAttribute('fill', fill);
      path.setAttribute('stroke', resolveTheme() === 'dark' ? '#111827' : '#ffffff');
      path.setAttribute('stroke-width', '1.5');
      // Each slice gets its own <title> child so hovering announces
      // the category + value to screen readers via the SVG tooltip
      // convention. Belt-and-suspenders to the top-level desc.
      const sliceTitle = canvas.ownerDocument.createElementNS(svgNs, 'title');
      sliceTitle.textContent = `${slice.data[categoryKey]}: ${slice.data[valueKey]}`;
      path.appendChild(sliceTitle);
      svg.appendChild(path);
    });

    if (this.centerLabel()) {
      const text = canvas.ownerDocument.createElementNS(svgNs, 'text');
      text.setAttribute('text-anchor', 'middle');
      text.setAttribute('dominant-baseline', 'central');
      text.setAttribute('class', 'donut-center-label');
      text.textContent = this.centerLabel() ?? '';
      svg.appendChild(text);
    }

    if (prefersReducedMotion()) {
      svg.setAttribute('data-no-transitions', '');
    }

    canvas.replaceChildren(svg);
  }
}
