const state = {
  manifest: null,
  graph: null,
  trace: null,
  report: null,
  filter: "all",
};

const ids = {
  manifestUrl: document.getElementById("manifest-url"),
  manifestUrlLoad: document.getElementById("manifest-url-load"),
  manifestFile: document.getElementById("manifest-file"),
  graphFile: document.getElementById("graph-file"),
  traceFile: document.getElementById("trace-file"),
  reportFile: document.getElementById("report-file"),
  summaryStatus: document.getElementById("summary-status"),
  graphStatus: document.getElementById("graph-status"),
  traceStatus: document.getElementById("trace-status"),
  stats: document.getElementById("stats"),
  analysisMetrics: document.getElementById("analysis-metrics"),
  nodeChips: document.getElementById("node-chips"),
  edgeList: document.getElementById("edge-list"),
  traceList: document.getElementById("trace-list"),
  traceTemplate: document.getElementById("trace-card-template"),
  filterButtons: Array.from(document.querySelectorAll(".filter")),
};

ids.manifestUrlLoad.addEventListener("click", () => loadManifestFromUrl());
ids.manifestUrl.addEventListener("keydown", (event) => {
  if (event.key === "Enter") {
    event.preventDefault();
    loadManifestFromUrl();
  }
});
ids.manifestFile.addEventListener("change", (event) => loadJsonFile(event, "manifest"));
ids.graphFile.addEventListener("change", (event) => loadJsonFile(event, "graph"));
ids.traceFile.addEventListener("change", (event) => loadJsonFile(event, "trace"));
ids.reportFile.addEventListener("change", (event) => loadJsonFile(event, "report"));

for (const button of ids.filterButtons) {
  button.addEventListener("click", () => {
    state.filter = button.dataset.filter ?? "all";
    syncFilterButtons();
    renderTrace();
  });
}

syncFilterButtons();
renderSummary();
renderGraph();
renderTrace();

async function loadJsonFile(event, kind) {
  const input = event.currentTarget;
  const file = input.files?.[0];
  if (!file) {
    return;
  }

  try {
    const text = await file.text();
    const parsed = JSON.parse(text);
    if (kind === "manifest") {
      validateManifest(parsed);
      state.manifest = parsed;
      ids.summaryStatus.textContent =
        `Manifest loaded: schema ${parsed.schema_version}. ` +
        "For full auto-load, use the manifest URL field while serving the artifacts over HTTP.";
      return;
    }
    const envelope = validateEnvelope(parsed, expectedArtifactType(kind));
    state[kind] = envelope.payload;
    if (kind === "graph") {
      renderGraph();
    } else if (kind === "trace") {
      renderTrace();
    } else if (kind === "report") {
      renderSummary();
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    window.alert(`Failed to load ${kind} JSON: ${message}`);
  } finally {
    input.value = "";
  }
}

async function loadManifestFromUrl() {
  const manifestUrl = ids.manifestUrl.value.trim();
  if (!manifestUrl) {
    window.alert("Enter a manifest URL first.");
    return;
  }

  try {
    const manifestResponse = await fetch(manifestUrl);
    if (!manifestResponse.ok) {
      throw new Error(`Manifest request failed with status ${manifestResponse.status}`);
    }
    const manifest = await manifestResponse.json();
    validateManifest(manifest);
    state.manifest = manifest;

    const baseUrl = new URL(manifestUrl, window.location.href);
    const parent = new URL("./", baseUrl);
    const artifactMap = Object.fromEntries(
      manifest.artifacts.map((artifact) => [artifact.artifact_type, artifact.file_name])
    );

    const graph = await fetchArtifactJson(parent, artifactMap.graph_schema, "graph_schema");
    const trace = await fetchArtifactJson(
      parent,
      artifactMap.execution_trace,
      "execution_trace"
    );
    const report = await fetchArtifactJson(parent, artifactMap.run_report, "run_report");

    state.graph = graph.payload;
    state.trace = trace.payload;
    state.report = report.payload;

    ids.summaryStatus.textContent = `Loaded manifest and artifacts from ${baseUrl.origin}.`;
    renderGraph();
    renderTrace();
    renderSummary();
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    window.alert(`Failed to load manifest URL: ${message}`);
  }
}

async function fetchArtifactJson(baseUrl, fileName, expectedType) {
  if (!fileName) {
    throw new Error(`Manifest does not include ${expectedType}`);
  }
  const artifactUrl = new URL(fileName, baseUrl);
  const response = await fetch(artifactUrl);
  if (!response.ok) {
    throw new Error(
      `${expectedType} request failed with status ${response.status}`
    );
  }
  const json = await response.json();
  return validateEnvelope(json, expectedType);
}

function expectedArtifactType(kind) {
  if (kind === "graph") return "graph_schema";
  if (kind === "trace") return "execution_trace";
  if (kind === "report") return "run_report";
  return kind;
}

function validateEnvelope(value, expectedType) {
  if (
    !value ||
    typeof value !== "object" ||
    typeof value.artifact_type !== "string" ||
    typeof value.schema_version !== "string" ||
    !("payload" in value) ||
    typeof value.metadata !== "object"
  ) {
    throw new Error("Invalid artifact envelope");
  }
  if (value.artifact_type !== expectedType) {
    throw new Error(
      `Unexpected artifact type: expected ${expectedType}, got ${value.artifact_type}`
    );
  }
  return value;
}

function validateManifest(value) {
  if (
    !value ||
    typeof value !== "object" ||
    typeof value.schema_version !== "string" ||
    !Array.isArray(value.artifacts)
  ) {
    throw new Error("Invalid artifact manifest");
  }
}

function syncFilterButtons() {
  for (const button of ids.filterButtons) {
    const active = button.dataset.filter === state.filter;
    button.classList.toggle("is-active", active);
  }
}

function renderSummary() {
  ids.stats.replaceChildren();
  ids.analysisMetrics.replaceChildren();

  if (!state.report) {
    ids.summaryStatus.textContent = "Load a report to populate metrics.";
    return;
  }

  ids.summaryStatus.textContent = "Report loaded.";
  appendStat("Accepted", state.report.accepted_count, ids.stats);
  appendStat("Attempts", state.report.attempt_count, ids.stats);
  appendStat(
    "Avg / Accept",
    formatNumber(state.report.average_attempts_per_accept),
    ids.stats
  );
  appendStat("Seconds", formatNumber(state.report.total_seconds), ids.stats);

  const analysis = state.report.sequence_analysis ?? {};
  if (Object.keys(analysis).length === 0) {
    const empty = document.createElement("div");
    empty.className = "empty-note";
    empty.textContent = "No sequence analysis in this report.";
    ids.analysisMetrics.append(empty);
    return;
  }

  for (const [key, value] of Object.entries(analysis)) {
    appendMetric(key, value, ids.analysisMetrics);
  }
}

function renderGraph() {
  ids.nodeChips.replaceChildren();
  ids.edgeList.replaceChildren();

  if (!state.graph) {
    ids.graphStatus.textContent = "Load a graph schema to inspect node wiring.";
    return;
  }

  ids.graphStatus.textContent = `${state.graph.nodes.length} nodes, ${state.graph.edges.length} edges.`;

  for (const node of state.graph.nodes) {
    const chip = document.createElement("article");
    chip.className = "chip";
    chip.innerHTML = `
      <p class="chip-type">${escapeHtml(node.node_type)}</p>
      <h3>${escapeHtml(node.node_id)}</h3>
      <p>${escapeHtml(node.input_names.join(", ") || "no inputs")} -> ${escapeHtml(
      node.output_names.join(", ") || "no outputs"
    )}</p>
    `;
    ids.nodeChips.append(chip);
  }

  for (const edge of state.graph.edges) {
    const item = document.createElement("li");
    item.innerHTML = `<code>${escapeHtml(edge.source_node_id)}.${escapeHtml(
      edge.source_output
    )}</code> -> <code>${escapeHtml(edge.target_node_id)}.${escapeHtml(
      edge.target_input
    )}</code>`;
    ids.edgeList.append(item);
  }
}

function renderTrace() {
  ids.traceList.replaceChildren();

  if (!state.trace) {
    ids.traceStatus.textContent =
      "Load a trace to inspect candidate-by-candidate behavior.";
    return;
  }

  const attempts = state.trace.attempts ?? [];
  const filtered = attempts.filter((attempt) => {
    if (state.filter === "accepted") {
      return attempt.accepted;
    }
    if (state.filter === "rejected") {
      return !attempt.accepted;
    }
    return true;
  });

  ids.traceStatus.textContent = `${filtered.length} shown of ${attempts.length} attempts.`;

  filtered.forEach((trace, index) => {
    const fragment = ids.traceTemplate.content.cloneNode(true);
    const card = fragment.querySelector(".trace-card");
    const traceLabel = fragment.querySelector(".trace-label");
    const traceTitle = fragment.querySelector(".trace-title");
    const badge = fragment.querySelector(".badge");
    const metrics = fragment.querySelector(".metric-list");
    const metadata = fragment.querySelector(".json-block");
    const transition = fragment.querySelector(".transition-block");

    card.dataset.accepted = String(trace.accepted);
    traceLabel.textContent = `Attempt ${index + 1}`;
    traceTitle.textContent = `Candidate ${JSON.stringify(trace.candidate)}`;
    badge.textContent = trace.accepted ? "Accepted" : "Rejected";
    badge.classList.toggle("accepted", trace.accepted);
    badge.classList.toggle("rejected", !trace.accepted);

    appendMetric("elapsed_seconds", trace.elapsed_seconds, metrics);

    const score =
      trace.metadata?.critique?.outputs?.[0] ?? trace.metadata?.decision?.accepted;
    if (score !== undefined) {
      appendMetric("signal", score, metrics);
    }

    metadata.textContent = JSON.stringify(trace.metadata, null, 2);
    transition.textContent = JSON.stringify(
      {
        previous_state: trace.previous_state,
        next_state: trace.next_state,
      },
      null,
      2
    );

    ids.traceList.append(fragment);
  });
}

function appendStat(label, value, container) {
  const article = document.createElement("article");
  article.className = "stat-card";
  article.innerHTML = `<p>${escapeHtml(label)}</p><h3>${escapeHtml(String(value))}</h3>`;
  container.append(article);
}

function appendMetric(label, value, container) {
  const dt = document.createElement("dt");
  dt.textContent = label;
  const dd = document.createElement("dd");
  dd.textContent =
    typeof value === "number" ? formatNumber(value) : JSON.stringify(value);
  container.append(dt, dd);
}

function formatNumber(value) {
  if (typeof value !== "number") {
    return String(value);
  }
  if (Number.isInteger(value)) {
    return String(value);
  }
  return value.toFixed(6).replace(/0+$/, "").replace(/\.$/, "");
}

function escapeHtml(value) {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
}
