<script setup>
import { computed, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Effect, Option } from "effect";
import { Fetch } from "fx-fetch";
import { CurveType } from "@unovis/ts";
import {
  VisArea,
  VisAxis,
  VisBulletLegend,
  VisCrosshair,
  VisGroupedBar,
  VisLine,
  VisScatter,
  VisStackedBar,
  VisTooltip,
  VisXYContainer
} from "@unovis/vue";
import { Markdown } from "../../../../ui/markdown";
import { cel } from "../../../../../utils/cel";
import { celBindings, injectCELContext } from "../../../../../utils/cel-context";
import { getLocalizedText } from "../../../../../share/locale";
import { fetchJsonOption } from "../../../../../share/request";
defineOptions({ name: "ShwfedBlockChartXyRuntime" });
const block = defineModel("block", { type: null, ...{ required: true } });
const { locale } = useI18n();
const inheritedContext = injectCELContext();
const mounted = ref(false);
const dataValues = ref({});
const fetchError = ref(null);
const CURVE = {
  linear: CurveType.Linear,
  monotoneX: CurveType.MonotoneX,
  natural: CurveType.Natural,
  basis: CurveType.Basis,
  cardinal: CurveType.Cardinal,
  catmullRom: CurveType.CatmullRom,
  step: CurveType.Step,
  stepAfter: CurveType.StepAfter,
  stepBefore: CurveType.StepBefore
};
const PALETTE = [
  "#4e79a7",
  "#f28e2b",
  "#e15759",
  "#76b7b2",
  "#59a14f",
  "#edc948",
  "#b07aa1",
  "#ff9da7",
  "#9c755f",
  "#bab0ac"
];
const seriesColors = computed(
  () => block.value.y.map(
    (s, i) => s.color && s.color.length > 0 ? s.color : PALETTE[i % PALETTE.length]
  )
);
const caption = computed(() => getLocalizedText(block.value.caption, locale.value) || "");
function seriesLabel(s) {
  return getLocalizedText(s.label, locale.value) || null;
}
function baseContext() {
  return celBindings(inheritedContext);
}
async function fetchAll() {
  const ctx = baseContext();
  const out = {};
  const failed = [];
  for (const ds of block.value.dataSources) {
    const program = Effect.gen(function* () {
      let jsonOpt = Option.none();
      if (ds.request) {
        jsonOpt = yield* fetchJsonOption(yield* cel(ds.request, ctx));
      }
      return yield* cel(ds.data, { ...ctx, json: jsonOpt });
    });
    try {
      out[ds.name] = await Effect.runPromise(Effect.provide(program, Fetch.layer));
    } catch (e) {
      console.warn("[shwfed-chart-xy] data source failed", ds.name, e);
      failed.push(ds.name);
      out[ds.name] = [];
    }
  }
  dataValues.value = out;
  fetchError.value = failed.length > 0 ? `\u6570\u636E\u6E90\u52A0\u8F7D\u5931\u8D25\uFF1A${failed.join("\u3001")}` : null;
}
onMounted(() => {
  mounted.value = true;
  void fetchAll();
});
watch(
  () => block.value.dataSources.map((d) => `${d.name}|${d.request ?? ""}|${d.data}`).join("\n"),
  () => {
    void fetchAll();
  }
);
function isFiniteNum(v) {
  return typeof v === "number" && Number.isFinite(v);
}
function toY(v) {
  if (isFiniteNum(v)) return v;
  if (typeof v === "string" && v.trim() !== "") {
    const n = Number(v);
    return Number.isFinite(n) ? n : void 0;
  }
  return void 0;
}
function evalList(expression, ctx) {
  try {
    const r = Effect.runSync(cel(expression, ctx));
    return Array.isArray(r) ? r : [];
  } catch {
    return [];
  }
}
function evalNumber(expression, ctx) {
  try {
    return toY(Effect.runSync(cel(expression, ctx)));
  } catch {
    return void 0;
  }
}
const dataCtx = computed(() => ({
  ...baseContext(),
  ...dataValues.value
}));
const chartData = computed(() => {
  const ctx = dataCtx.value;
  const xRaw = evalList(block.value.x, ctx);
  const yLists = block.value.y.map((s) => evalList(s.value, ctx));
  const sizeLists = block.value.y.map((s) => s.size ? evalList(s.size, ctx) : null);
  const xMode = xRaw.length === 0 ? "ordinal" : xRaw.every((v) => v instanceof Date) ? "time" : xRaw.every((v) => isFiniteNum(v)) ? "number" : "ordinal";
  const xLabels = /* @__PURE__ */ new Map();
  const records = xRaw.map((xv, i) => {
    const rec = { __x: i };
    if (xMode === "time") rec.__x = xv.getTime();
    else if (xMode === "number") rec.__x = xv;
    else xLabels.set(i, xv === null || xv === void 0 ? "" : String(xv));
    yLists.forEach((list, k) => {
      rec[`y${k}`] = toY(list[i]);
    });
    sizeLists.forEach((list, k) => {
      if (list) rec[`sz${k}`] = toY(list[i]);
    });
    return rec;
  });
  return { records, xMode, xLabels };
});
const seriesDash = computed(
  () => block.value.y.map((s) => {
    if (!s.dash) return void 0;
    const arr = evalList(s.dash, dataCtx.value).filter(isFiniteNum);
    return arr.length > 0 ? arr : void 0;
  })
);
const seriesWidth = computed(
  () => block.value.y.map((s) => s.width ? evalNumber(s.width, dataCtx.value) : void 0)
);
const xAccessor = (d) => d.__x;
const yAccessors = computed(
  () => block.value.y.map((_, k) => (d) => d[`y${k}`])
);
const sizeAccessors = computed(
  () => block.value.y.map((s, k) => s.size ? (d) => d[`sz${k}`] : void 0)
);
const seriesEntries = computed(
  () => block.value.y.map((s, k) => ({ k, s, color: seriesColors.value[k] }))
);
const lineSeries = computed(() => seriesEntries.value.filter((e) => e.s.chart === "line"));
const areaSeries = computed(() => seriesEntries.value.filter((e) => e.s.chart === "area"));
const scatterSeries = computed(() => seriesEntries.value.filter((e) => e.s.chart === "scatter"));
function barGroup(chart) {
  const entries = seriesEntries.value.filter((e) => e.s.chart === chart);
  return {
    entries,
    accessors: entries.map((e) => yAccessors.value[e.k]),
    colors: entries.map((e) => e.color),
    // One width per merged component — take it from the first such series.
    width: entries[0] ? seriesWidth.value[entries[0].k] : void 0
  };
}
const groupedBars = computed(() => barGroup("groupedBar"));
const stackedBars = computed(() => barGroup("stackedBar"));
const legendItems = computed(() => {
  const items = [];
  block.value.y.forEach((s, i) => {
    const label = seriesLabel(s);
    if (label) items.push({ name: label, color: seriesColors.value[i] });
  });
  return items;
});
const xTickFormat = computed(() => {
  const mode = chartData.value.xMode;
  if (mode === "ordinal") {
    const labels = chartData.value.xLabels;
    return (tick) => labels.get(tick) ?? "";
  }
  if (mode === "time") return (tick) => new Date(tick).toLocaleDateString();
  return void 0;
});
const xTickValues = computed(() => {
  if (chartData.value.xMode !== "ordinal") return void 0;
  const keys = [...chartData.value.xLabels.keys()];
  if (keys.length <= 12) return keys;
  const stride = Math.ceil(keys.length / 10);
  return keys.filter((_, i) => i % stride === 0);
});
function formatNumber(n) {
  return Number.isInteger(n) ? String(n) : n.toFixed(2);
}
function escapeHtml(s) {
  return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c] ?? c);
}
function crosshairTemplate(d) {
  const mode = chartData.value.xMode;
  const xLabel = mode === "ordinal" ? chartData.value.xLabels.get(d.__x) ?? "" : mode === "time" ? new Date(d.__x).toLocaleDateString() : String(d.__x);
  const rows = [];
  block.value.y.forEach((s, k) => {
    const label = seriesLabel(s);
    if (!label) return;
    const v = d[`y${k}`];
    rows.push(
      `<div style="display:flex;align-items:center;gap:6px;padding:1px 0"><span style="width:8px;height:8px;border-radius:9999px;background:${seriesColors.value[k]}"></span><span style="color:#3f3f46">${escapeHtml(label)}</span><span style="margin-left:auto;font-variant-numeric:tabular-nums;color:#18181b">${v === void 0 ? "\u2014" : formatNumber(v)}</span></div>`
    );
  });
  const lines = rows.join("");
  return `<div style="font-size:12px;min-width:160px"><div style="font-weight:600;color:#18181b;margin-bottom:4px">${escapeHtml(xLabel)}</div>${lines}</div>`;
}
const placeholder = computed(() => {
  if (fetchError.value) return fetchError.value;
  if (chartData.value.records.length === 0) return "\u6682\u65E0\u6570\u636E";
  return null;
});
</script>

<template>
  <div class="flex h-full min-h-[200px] w-full flex-col gap-2">
    <div
      v-if="placeholder"
      class="flex flex-1 items-center justify-center text-sm text-zinc-400"
    >
      {{ placeholder }}
    </div>

    <template v-else-if="mounted">
      <VisXYContainer
        :data="chartData.records"
        :margin="{ top: 8, right: 16, bottom: 4, left: 8 }"
        class="min-h-0 flex-1"
      >
        <VisStackedBar
          v-if="stackedBars.entries.length > 0"
          :x="xAccessor"
          :y="stackedBars.accessors"
          :color="stackedBars.colors"
          :bar-width="stackedBars.width"
        />
        <VisGroupedBar
          v-if="groupedBars.entries.length > 0"
          :x="xAccessor"
          :y="groupedBars.accessors"
          :color="groupedBars.colors"
          :group-width="groupedBars.width"
        />
        <VisArea
          v-for="e in areaSeries"
          :key="e.s.id"
          :x="xAccessor"
          :y="yAccessors[e.k]"
          :color="e.color"
          :line="true"
          :line-color="e.s.lineColor || e.color"
          :line-width="e.s.lineWidth"
        />
        <VisLine
          v-for="e in lineSeries"
          :key="e.s.id"
          :x="xAccessor"
          :y="yAccessors[e.k]"
          :color="e.color"
          :curve-type="CURVE[e.s.curveType]"
          :line-width="e.s.lineWidth"
          :line-dash-array="seriesDash[e.k]"
        />
        <VisScatter
          v-for="e in scatterSeries"
          :key="e.s.id"
          :x="xAccessor"
          :y="yAccessors[e.k]"
          :color="e.color"
          :size="sizeAccessors[e.k]"
        />
        <VisAxis
          type="x"
          :tick-format="xTickFormat"
          :tick-values="xTickValues"
          :tick-text-hide-overlapping="true"
          :grid-line="false"
        />
        <VisAxis type="y" />
        <VisCrosshair
          :x="xAccessor"
          :y="yAccessors"
          :color="seriesColors"
          :template="crosshairTemplate"
        />
        <VisTooltip />
      </VisXYContainer>

      <VisBulletLegend
        v-if="legendItems.length > 0"
        :items="legendItems"
        class="mx-auto w-fit shrink-0 text-xs"
      />
      <Markdown
        v-if="caption"
        :source="caption"
        block
        class="prose prose-sm prose-zinc max-w-none shrink-0 text-center"
      />
    </template>
  </div>
</template>
