import { useId } from "react";
import type { KpiCardsProps, Metric, MetricFormat } from "./schema";

/**
 * スパークラインの viewBox。実表示幅はカード幅に合わせて伸縮させるため
 * preserveAspectRatio="none" で描き、線幅の歪みは non-scaling-stroke で打ち消す。
 */
export const SPARK_W = 100;
export const SPARK_H = 32;
/** 線が枠の上下に張り付かないための余白（viewBox 座標） */
const SPARK_PAD = 3;

/** エントランスの時差。間隔は global.css の --stagger-step で統一 */
const stagger = (i: number, baseMs = 0) =>
  `calc(${baseMs}ms + ${i} * var(--stagger-step))`;

/** 現在値 = 系列の最後の要素 */
export const currentValue = (series: number[]) => series[series.length - 1];

/** 前期の値 = 系列の最後から2番目。schema は2点以上を強制するが、
 *  1点しかないデータが来ても NaN を出さないよう先頭にフォールバックする */
export const previousValue = (series: number[]) =>
  series[Math.max(0, series.length - 2)];

/** 前期比の生の差分（現在値 − 前期の値） */
export const computeDelta = (series: number[]) =>
  currentValue(series) - previousValue(series);

const decimalsOf = (n: number) => {
  const s = String(n);
  const dot = s.indexOf(".");
  // 指数表記（1e-7 等）からは桁を読めないので0扱い
  return dot === -1 ? 0 : s.length - dot - 1;
};

/**
 * 値そのものが持つ小数桁。format.decimals 省略時の既定値に使う。
 * 0 に決め打つと 3.8 が "4" と表示され、同じ系列から描くスパークラインが
 * 見出しの数字と食い違う（元データに無い数値を載せることになる）。
 * 浮動小数の誤差（0.30000000000000004 など）は schema と同じ4桁で頭打ちにする。
 */
export const inferDecimals = (values: number[]) =>
  Math.min(4, Math.max(0, ...values.map(decimalsOf)));

/** この指標を何桁で表示するか。目標も同じ桁で出すため値域に含める */
export const resolveDecimals = (metric: Metric) =>
  metric.format?.decimals ??
  inferDecimals(
    metric.target === undefined
      ? metric.series
      : [...metric.series, metric.target],
  );

/**
 * 前期比として画面に出す数値・桁・単位を1箇所で決める。
 * 矢印・符号・色をすべてこの「丸めたあとの値」から導くので、
 * 「▲ +0」のように向きと数字が食い違う表示が構造的に作れない。
 * 変化率が定義できない場合（前期が0以下）は null。
 */
export function displayedDelta(
  metric: Metric,
): { value: number; decimals: number; suffix: string } | null {
  const { series, deltaMode = "relative", deltaSuffix = "" } = metric;
  const raw = computeDelta(series);

  if (deltaMode === "absolute") {
    return { value: raw, decimals: resolveDecimals(metric), suffix: deltaSuffix };
  }

  // 変化率は正の基準値があって初めて意味を持つ。前期が0なら Infinity、
  // 負なら符号が反転して「改善したのにマイナス%」になるため、どちらも表示しない
  const prev = previousValue(series);
  if (prev <= 0) return null;
  return { value: (raw / prev) * 100, decimals: 1, suffix: "%" };
}

/** 丸めたあとの前期比。表示・矢印・色はすべてこの値を見る */
const roundedDelta = (metric: Metric) => {
  const d = displayedDelta(metric);
  return d === null ? null : Number(d.value.toFixed(d.decimals));
};

/**
 * 増減の向き。「良し悪し」は betterWhen で決まる色が担当するので、ここでは向きだけ。
 * 前期比そのものが出せないとき（前期が0以下）は矢印も出さない —
 * "→ —" は「変化なし」と読めてしまい、0 から立ち上がった新規指標を
 * 「動いていない」と誤って伝える。
 */
export function deltaArrow(metric: Metric) {
  const rounded = roundedDelta(metric);
  if (rounded === null) return "";
  if (rounded === 0) return "→";
  return rounded > 0 ? "▲" : "▼";
}

const signed = (n: number, decimals: number) => {
  const body = Math.abs(n).toFixed(decimals);
  // 丸めた結果が0なら符号を付けない（"-0.0" は減少とも無変化とも読めてしまう）
  return `${Number(body) === 0 ? "" : n >= 0 ? "+" : "-"}${body}`;
};

/**
 * 系列（と target）を viewBox 座標に写す。
 * - y は上下反転する（SVG は下向きが正、指標は上向きが「大きい」）
 * - 定数列（min === max）は0除算せず中央に水平線を引く
 * - target を値域に含めるので、系列の外側にある目標でも破線が枠内に収まる
 */
export function sparklineGeometry(series: number[], target?: number) {
  const domain = target === undefined ? series : [...series, target];
  const min = Math.min(...domain);
  const max = Math.max(...domain);
  const span = max - min;
  const inner = SPARK_H - SPARK_PAD * 2;
  const toY = (v: number) =>
    span === 0 ? SPARK_H / 2 : SPARK_H - SPARK_PAD - ((v - min) / span) * inner;
  const toX = (i: number) =>
    series.length < 2 ? SPARK_W / 2 : (i / (series.length - 1)) * SPARK_W;
  const round = (n: number) => Number(n.toFixed(2));

  return {
    points: series.map((v, i) => `${round(toX(i))},${round(toY(v))}`).join(" "),
    targetY: target === undefined ? null : round(toY(target)),
    lastX: round(toX(series.length - 1)),
    lastY: round(toY(currentValue(series))),
  };
}

/** 現在値の表示。負号は prefix の外側に出す（¥-5 ではなく -¥5） */
export function formatValue(n: number, format?: MetricFormat) {
  const {
    prefix = "",
    suffix = "",
    decimals = 0,
    showSign = false,
  } = format ?? {};
  const sign = n < 0 ? "-" : showSign ? "+" : "";
  return `${sign}${prefix}${Math.abs(n).toFixed(decimals)}${suffix}`;
}

/**
 * 前期比の表示。
 * relative は変化率(%)、absolute は実数差 + deltaSuffix。
 * 率の指標（3.3% → 3.8%）を relative で出すと「+15%」という別物の数字になるため、
 * その場合は absolute + deltaSuffix "pt" を使う（= "+0.5pt"）。
 */
export function formatDelta(metric: Metric): string {
  const d = displayedDelta(metric);
  if (d === null) return "—";
  return `${signed(d.value, d.decimals)}${d.suffix}`;
}

/** 前期比の良し悪し。betterWhen: "down" では減少が改善（解約率など）。
 *  表示上0に丸まる変化は neutral（見えない差を色で主張しない） */
export function deltaTone(metric: Metric): "positive" | "negative" | "neutral" {
  const rounded = roundedDelta(metric);
  if (rounded === null || rounded === 0) return "neutral";
  const improving =
    (metric.betterWhen ?? "up") === "up" ? rounded > 0 : rounded < 0;
  return improving ? "positive" : "negative";
}

/**
 * 目標達成判定。betterWhen: "down" では target 以下が達成。target 無しは null。
 * 画面に出るのは丸めた値なので、判定も同じ桁で行う — 生値で比べると
 * 現在値 11.96 / 目標 12.04 が「12.0 に対して目標 12.0 なのに未達」という
 * 読みようのないカードになる（前期比の矢印と同じ、表示と判定の食い違い）。
 */
export function isAchieved(metric: Metric): boolean | null {
  if (metric.target === undefined) return null;
  const decimals = resolveDecimals(metric);
  const current = Number(currentValue(metric.series).toFixed(decimals));
  const target = Number(metric.target.toFixed(decimals));
  return (metric.betterWhen ?? "up") === "up"
    ? current >= target
    : current <= target;
}

const TONE_CLASS: Record<ReturnType<typeof deltaTone>, string> = {
  positive: "text-success",
  negative: "text-error",
  neutral: "text-secondary",
};

export type MetricRecord = { metric: Metric; index: number };

/**
 * highlight は装飾フラグではなく、画面の結論階層を決める契約として扱う。
 * 指定が無い入力では先頭を暗黙に代表指標へしない。データに無い優先順位を
 * レイアウト側で捏造せず、全件を同格の比較ledgerとして残す。
 */
export function metricHierarchy(metrics: Metric[]): {
  primary: MetricRecord | null;
  supporting: MetricRecord[];
} {
  const primaryIndex = metrics.findIndex((metric) => metric.highlight);
  return {
    primary:
      primaryIndex === -1
        ? null
        : { metric: metrics[primaryIndex], index: primaryIndex },
    supporting: metrics
      .map((metric, index) => ({ metric, index }))
      .filter(({ index }) => index !== primaryIndex),
  };
}

/**
 * 実データ系列から描くスパークライン。target は同じ値域に写した破線で重ねるので、
 * 「線が破線より上/下にあるか」がそのまま達成状況になる（下がるほど良い指標でも
 * 意味が反転しない — プログレスバーの塗り面積では表せない性質）。
 */
function Sparkline({
  metric,
  gradientId,
  size,
}: {
  metric: Metric;
  gradientId: string;
  size: "hero" | "compact";
}) {
  const { points, targetY, lastX, lastY } = sparklineGeometry(
    metric.series,
    metric.target,
  );

  return (
    <div
      data-kpi-chart-size={size}
      className={`relative ${size === "hero" ? "mt-5 h-24 lg:h-28" : "mt-4 h-8"}`}
    >
      <svg
        viewBox={`0 0 ${SPARK_W} ${SPARK_H}`}
        preserveAspectRatio="none"
        className="h-full w-full"
        aria-hidden="true"
      >
        {metric.highlight && (
          <defs>
            <linearGradient id={gradientId} x1="0" y1="0" x2="1" y2="0">
              <stop offset="0%" stopColor="var(--color-tertiary)" />
              <stop offset="100%" stopColor="var(--color-accent-violet)" />
            </linearGradient>
          </defs>
        )}
        <polygon
          data-kpi-area=""
          points={`0,${SPARK_H} ${points} ${SPARK_W},${SPARK_H}`}
          className={metric.highlight ? "fill-tertiary/15" : "fill-tertiary/10"}
        />
        {targetY !== null && (
          <line
            data-kpi-target=""
            x1="0"
            y1={targetY}
            x2={SPARK_W}
            y2={targetY}
            className="stroke-secondary"
            strokeWidth="1"
            strokeDasharray="3 2"
            vectorEffect="non-scaling-stroke"
          />
        )}
        <polyline
          data-kpi-trace=""
          data-kpi-primary-trace={metric.highlight ? "" : undefined}
          points={points}
          fill="none"
          strokeWidth={size === "hero" ? "2.5" : "2"}
          strokeLinecap="round"
          strokeLinejoin="round"
          vectorEffect="non-scaling-stroke"
          stroke={metric.highlight ? `url(#${gradientId})` : undefined}
          className={metric.highlight ? undefined : "stroke-tertiary"}
        />
      </svg>
      {/* 現在値のマーカーは HTML 側で置く。SVG 内の circle は
          preserveAspectRatio="none" で横に引き伸ばされ楕円になるため */}
      <span
        data-kpi-current-marker=""
        className={`absolute -translate-x-1/2 -translate-y-1/2 rounded-full ring-2 ring-surface ${
          size === "hero" ? "size-3" : "size-2"
        } ${
          metric.highlight ? "bg-accent-violet" : "bg-tertiary"
        }`}
        style={{
          left: `${(lastX / SPARK_W) * 100}%`,
          top: `${(lastY / SPARK_H) * 100}%`,
        }}
      />
    </div>
  );
}

function Verdict({ achieved }: { achieved: boolean | null }) {
  if (achieved === null) return null;
  return (
    <span
      className={`shrink-0 whitespace-nowrap rounded-full px-2 py-0.5 text-label ${
        achieved ? "bg-success/10 text-success" : "bg-error/10 text-error"
      }`}
    >
      {achieved ? "目標達成" : "未達"}
    </span>
  );
}

function DeltaLine({ metric }: { metric: Metric }) {
  return (
    <p
      data-kpi-delta=""
      className={`mt-1 text-label ${TONE_CLASS[deltaTone(metric)]}`}
    >
      {deltaArrow(metric)} {formatDelta(metric)}
      <span className="ml-1 text-secondary">前期比</span>
    </p>
  );
}

function TargetLine({
  metric,
  format,
}: {
  metric: Metric;
  format: MetricFormat;
}) {
  if (metric.target === undefined) return null;
  return (
    <p data-kpi-target-label="" className="mt-2 text-label text-secondary">
      目標 {formatValue(metric.target, format)}
    </p>
  );
}

function PrimaryMetric({
  record,
  gradientId,
}: {
  record: MetricRecord;
  gradientId: string;
}) {
  const { metric, index } = record;
  const achieved = isAchieved(metric);
  const format = { ...metric.format, decimals: resolveDecimals(metric) };

  return (
    <article
      data-kpi-primary="true"
      data-priority="true"
      data-kpi-record={index}
      tabIndex={0}
      aria-label={`${metric.label}、代表指標`}
      className="group/metric animate-rise grid gap-7 bg-surface-subtle p-7 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tertiary lg:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] lg:items-end lg:p-8"
      style={{ animationDelay: stagger(index) }}
    >
      <div>
        <div className="flex items-center justify-between gap-3">
          <p className="font-mono text-label tracking-wide text-tertiary">
            代表指標
          </p>
          <Verdict achieved={achieved} />
        </div>
        <h2 className="mt-4 text-h2 text-secondary">{metric.label}</h2>
        <p data-kpi-value="" className="mt-2 text-display">
          {formatValue(currentValue(metric.series), format)}
        </p>
        <DeltaLine metric={metric} />
      </div>

      <div className="min-w-0 border-t border-outline pt-5 lg:border-l lg:border-t-0 lg:pl-8 lg:pt-0">
        <div className="flex items-center justify-between gap-3">
          <p className="text-label text-secondary">実績推移</p>
          <TargetLine metric={metric} format={format} />
        </div>
        <Sparkline metric={metric} gradientId={gradientId} size="hero" />
      </div>
    </article>
  );
}

function SupportingMetric({
  record,
  gradientId,
}: {
  record: MetricRecord;
  gradientId: string;
}) {
  const { metric, index } = record;
  const achieved = isAchieved(metric);
  const format = { ...metric.format, decimals: resolveDecimals(metric) };

  return (
    <article
      data-kpi-supporting="true"
      data-kpi-record={index}
      tabIndex={0}
      aria-label={`${metric.label}を強調`}
      className="group/metric animate-rise min-w-0 bg-surface p-5 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tertiary"
      style={{ animationDelay: stagger(index) }}
    >
      <div className="flex min-h-10 items-start justify-between gap-2">
        <h2 className="text-label text-secondary">{metric.label}</h2>
        <Verdict achieved={achieved} />
      </div>
      <p data-kpi-value="" className="mt-2 text-h2">
        {formatValue(currentValue(metric.series), format)}
      </p>
      <DeltaLine metric={metric} />
      <Sparkline metric={metric} gradientId={gradientId} size="compact" />
      <TargetLine metric={metric} format={format} />
    </article>
  );
}

export default function KpiCards({
  title,
  subtitle,
  metrics,
  instanceId,
}: KpiCardsProps) {
  const autoId = useId();
  // useId() は ":r0:" のようにコロンを含む。SVG の url(#...) 参照で使うため
  // ID に使える文字だけに絞る（同一ページの複数インスタンスで衝突させない）。
  // 全部落ちた場合（記号だけの instanceId）は先頭が "-" の不正なIDになるので退避する
  const sanitized = (instanceId ?? autoId).replace(
      // 捨てる操作は単射でないので、schema を通る別々の値が同じ id になる
      // （`sales:a` と `sales/a` がどちらも `salesa`）。同じ文書へ 2 つ置くと
      // id と radio の name が衝突して、片方の操作がもう片方に効く。
      // 逃がし文字（`_`）自身も逃がすので、この変換は単射。
      /[^a-zA-Z0-9-]/g,
      (c) => `_${c.charCodeAt(0).toString(16)}_`,
    );
  const uid = /^[a-zA-Z_]/.test(sanitized) ? sanitized : `id${sanitized}`;
  const notes = metrics.filter((metric) => metric.note);
  const hierarchy = metricHierarchy(metrics);

  return (
    <section className="mx-auto max-w-slide bg-surface p-12 font-sans text-on-surface">
      <style>{`
        [data-kpi-layout]:has([data-kpi-supporting="true"]:is(:hover, :focus)) [data-kpi-primary-trace] {
          opacity: 0.28;
        }
        [data-kpi-supporting="true"]:is(:hover, :focus) [data-kpi-trace] {
          stroke: var(--color-on-surface);
        }
      `}</style>
      <header className="mb-8">
        <h1 className="text-h1">{title}</h1>
        {subtitle && (
          <p className="mt-2 text-body-sm text-secondary">{subtitle}</p>
        )}
      </header>

      {hierarchy.primary ? (
        <div
          data-kpi-layout="hierarchy"
          className="overflow-hidden rounded-lg border border-outline bg-surface shadow-card"
        >
          <PrimaryMetric
            record={hierarchy.primary}
            gradientId={`${uid}-spark-${hierarchy.primary.index}`}
          />
          {hierarchy.supporting.length > 0 && (
            <div
              data-kpi-supporting-ledger="true"
              className="grid gap-px border-t border-outline bg-outline sm:grid-cols-2 lg:grid-cols-3"
            >
              {hierarchy.supporting.map((record) => (
                <SupportingMetric
                  key={record.index}
                  record={record}
                  gradientId={`${uid}-spark-${record.index}`}
                />
              ))}
            </div>
          )}
        </div>
      ) : (
        <div
          data-kpi-layout="comparison"
          className="grid gap-px overflow-hidden rounded-lg border border-outline bg-outline shadow-card sm:grid-cols-2 lg:grid-cols-3"
        >
          {hierarchy.supporting.map((record) => (
            <SupportingMetric
              key={record.index}
              record={record}
              gradientId={`${uid}-spark-${record.index}`}
            />
          ))}
        </div>
      )}

      {notes.length > 0 && (
        <details data-inline-disclosure="true" className="group mt-6 rounded-lg border border-outline bg-surface-subtle">
          <summary className="flex cursor-pointer list-none items-center px-4 py-3 text-label text-secondary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tertiary">
            <span>補足を確認</span>
            <span className="ml-auto font-mono">{notes.length}件</span>
            <span aria-hidden="true" className="ml-2 text-tertiary transition-transform group-open:rotate-45">＋</span>
          </summary>
          <div className="grid gap-3 border-t border-outline p-4 sm:grid-cols-2">
            {notes.map((metric, index) => (
              <article key={index} data-note-item="true" className="rounded-md bg-surface p-4">
                <h3 className="text-label">{metric.label}</h3>
                <p className="mt-2 text-body-sm">{metric.note}</p>
              </article>
            ))}
          </div>
        </details>
      )}
    </section>
  );
}
