/**
 * <pipeline-builder> web component
 *
 * Visual DAG builder for release pipeline stages.
 * Syncs to a hidden textarea (data-target) as JSON.
 *
 * Stage format (matches Rust serde of PipelineStage):
 *   { "id": "stage-name", "depends_on": ["other"], "config": {"Deploy": {"environment": "prod"}} }
 *
 * Usage:
 *   <pipeline-builder data-target="pipeline-stages"></pipeline-builder>
 *   <textarea id="pipeline-stages" name="stages_json" hidden></textarea>
 */

class PipelineBuilder extends HTMLElement {
  connectedCallback() {
    this.stages = [];
    this._targetId = this.dataset.target;
    this._readonly = this.dataset.readonly === "true";
    this._mode = "builder"; // "builder" | "json"

    // Load initial value from target textarea
    const target = this._target();
    if (target && target.value.trim()) {
      try {
        const parsed = JSON.parse(target.value.trim());
        this.stages = this._parseStages(parsed);
      } catch (e) {
        this._rawJson = target.value.trim();
      }
    }

    this._render();
  }

  _target() {
    return this._targetId ? document.getElementById(this._targetId) : null;
  }

  // Extract the stage type string from a config object
  _stageType(config) {
    if (!config) return "deploy";
    if (config.Deploy !== undefined) return "deploy";
    if (config.Wait !== undefined) return "wait";
    return "deploy";
  }

  // Extract display info from config
  _configLabel(config) {
    if (!config) return "";
    if (config.Deploy) return config.Deploy.environment || "";
    if (config.Wait) return config.Wait.duration_seconds ? `${config.Wait.duration_seconds}s` : "";
    return "";
  }

  _normalizeStage(s) {
    // Handle the new typed format: {id, depends_on, config: {Deploy: {environment}}}
    if (s.id !== undefined) {
      return {
        id: s.id || "",
        depends_on: Array.isArray(s.depends_on) ? s.depends_on : [],
        config: s.config || { Deploy: { environment: "" } },
      };
    }
    // Legacy format: {name, type, depends_on}
    const type = s.type || "deploy";
    const config = type === "wait"
      ? { Wait: { duration_seconds: s.duration_seconds || 0 } }
      : { Deploy: { environment: s.environment || "" } };
    return {
      id: s.name || "",
      depends_on: Array.isArray(s.depends_on) ? s.depends_on : [],
      config,
    };
  }

  _parseStages(parsed) {
    if (Array.isArray(parsed)) {
      return parsed.map((s) => this._normalizeStage(s));
    }
    if (parsed.stages && Array.isArray(parsed.stages)) {
      return parsed.stages.map((s) => this._normalizeStage(s));
    }
    // Map format: { "id": { depends_on, config } }
    if (typeof parsed === "object" && parsed !== null) {
      return Object.entries(parsed).map(([id, val]) =>
        this._normalizeStage({ id, ...val })
      );
    }
    return [];
  }

  _sync() {
    const target = this._target();
    if (!target) return;
    if (this.stages.length === 0) {
      target.value = "";
      return;
    }
    // Filter out stages with no id
    const valid = this.stages.filter((s) => s.id.trim());
    target.value = JSON.stringify(valid, null, 2);
  }

  _validate() {
    const ids = this.stages.map((s) => s.id).filter(Boolean);
    const idSet = new Set(ids);
    const errors = [];

    if (ids.length !== idSet.size) {
      errors.push("Duplicate stage IDs detected");
    }

    for (const s of this.stages) {
      for (const dep of s.depends_on) {
        if (!idSet.has(dep)) {
          errors.push(`"${s.id}" depends on unknown stage "${dep}"`);
        }
      }
    }

    // Cycle detection (Kahn's algorithm)
    const inDegree = {};
    const adj = {};
    for (const s of this.stages) {
      if (!s.id) continue;
      inDegree[s.id] = 0;
      adj[s.id] = [];
    }
    for (const s of this.stages) {
      if (!s.id) continue;
      for (const dep of s.depends_on) {
        if (adj[dep]) {
          adj[dep].push(s.id);
          inDegree[s.id]++;
        }
      }
    }
    const queue = Object.keys(inDegree).filter((k) => inDegree[k] === 0);
    let visited = 0;
    while (queue.length > 0) {
      const node = queue.shift();
      visited++;
      for (const next of adj[node] || []) {
        inDegree[next]--;
        if (inDegree[next] === 0) queue.push(next);
      }
    }
    if (visited < Object.keys(inDegree).length) {
      errors.push("Cycle detected in stage dependencies");
    }

    for (let i = 0; i < this.stages.length; i++) {
      if (!this.stages[i].id.trim()) {
        errors.push(`Stage ${i + 1} has no ID`);
      }
    }

    return errors;
  }

  _computeLevels() {
    const byId = {};
    for (const s of this.stages) {
      if (s.id) byId[s.id] = s;
    }
    const levels = {};
    const visited = new Set();

    const getLevel = (id) => {
      if (levels[id] !== undefined) return levels[id];
      if (visited.has(id)) return 0;
      visited.add(id);
      const s = byId[id];
      if (!s || s.depends_on.length === 0) {
        levels[id] = 0;
        return 0;
      }
      let maxDep = 0;
      for (const dep of s.depends_on) {
        if (byId[dep]) {
          maxDep = Math.max(maxDep, getLevel(dep) + 1);
        }
      }
      levels[id] = maxDep;
      return maxDep;
    };

    for (const s of this.stages) {
      if (s.id) getLevel(s.id);
    }
    return levels;
  }

  _render() {
    const errors = this._validate();
    if (!this._readonly) this._sync();

    this.innerHTML = "";
    this.className = "block";

    // Readonly mode: just show the DAG
    if (this._readonly) {
      if (this.stages.length > 0) {
        const canvas = el("div", "dag-canvas overflow-x-auto");
        this._renderDag(canvas);
        this.append(canvas);
      } else {
        this.append(el("p", "text-xs text-gray-400 italic", "No stages defined"));
      }
      return;
    }

    // Mode toggle
    const toolbar = el("div", "flex items-center gap-2 mb-3");
    const builderBtn = el(
      "button",
      `text-xs px-2.5 py-1 rounded border ${this._mode === "builder" ? "bg-gray-900 text-white border-gray-900" : "border-gray-300 text-gray-600 hover:bg-gray-50"}`,
      "Builder"
    );
    builderBtn.type = "button";
    builderBtn.onclick = () => {
      if (this._mode === "json") {
        const ta = this.querySelector(".json-editor");
        if (ta) {
          try {
            const parsed = JSON.parse(ta.value);
            this.stages = this._parseStages(parsed);
            this._rawJson = null;
          } catch (e) {
            this._rawJson = ta.value;
          }
        }
        this._mode = "builder";
        this._render();
      }
    };
    const jsonBtn = el(
      "button",
      `text-xs px-2.5 py-1 rounded border ${this._mode === "json" ? "bg-gray-900 text-white border-gray-900" : "border-gray-300 text-gray-600 hover:bg-gray-50"}`,
      "JSON"
    );
    jsonBtn.type = "button";
    jsonBtn.onclick = () => {
      this._mode = "json";
      this._render();
    };
    toolbar.append(builderBtn, jsonBtn);

    if (this._mode === "builder" && this.stages.length > 0) {
      const stageCount = el("span", "text-xs text-gray-400 ml-auto", `${this.stages.length} stage${this.stages.length !== 1 ? "s" : ""}`);
      toolbar.append(stageCount);
    }

    this.append(toolbar);

    if (this._mode === "json") {
      this._renderJsonMode();
    } else {
      this._renderBuilderMode(errors);
    }
  }

  _renderJsonMode() {
    const target = this._target();
    const currentJson = this._rawJson || (target ? target.value : "") || "[]";

    const ta = el("textarea", "json-editor w-full border border-gray-300 rounded-md px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-gray-900 resize-y");
    ta.rows = 12;
    ta.value = currentJson;
    ta.spellcheck = false;
    ta.oninput = () => {
      const t = this._target();
      if (t) t.value = ta.value;
      this._updateJsonErrors(ta.value);
    };

    const errBox = el("div", "json-errors mt-2");
    this.append(ta, errBox);
    this._updateJsonErrors(currentJson);
  }

  _updateJsonErrors(value) {
    const errBox = this.querySelector(".json-errors");
    if (!errBox) return;
    errBox.innerHTML = "";
    if (!value.trim()) return;
    try {
      const parsed = JSON.parse(value);
      const stages = Array.isArray(parsed) ? parsed : (parsed.stages || []);
      const ids = stages.map((s) => s.id || s.name).filter(Boolean);
      if (new Set(ids).size !== ids.length) {
        errBox.append(el("p", "text-xs text-amber-600", "Warning: duplicate stage IDs"));
      }
    } catch (e) {
      errBox.append(el("p", "text-xs text-red-600", "Invalid JSON: " + e.message));
    }
  }

  _renderBuilderMode(errors) {
    if (this.stages.length > 0) {
      const dagBox = el("div", "mb-4 border border-gray-200 rounded-lg overflow-hidden");
      const canvas = el("div", "dag-canvas p-4 bg-gray-50 overflow-x-auto");
      canvas.style.minHeight = "80px";
      this._renderDag(canvas);
      dagBox.append(canvas);
      this.append(dagBox);
    }

    const list = el("div", "space-y-2 mb-3");
    for (let i = 0; i < this.stages.length; i++) {
      list.append(this._renderStageCard(i));
    }
    this.append(list);

    if (errors.length > 0) {
      const errBox = el("div", "mb-3 p-3 bg-red-50 border border-red-200 rounded-md");
      for (const err of errors) {
        errBox.append(el("p", "text-xs text-red-700", err));
      }
      this.append(errBox);
    }

    const addBtn = el("button", "text-sm px-3 py-1.5 rounded border border-dashed border-gray-300 text-gray-500 hover:border-gray-400 hover:text-gray-700 w-full", "+ Add stage");
    addBtn.type = "button";
    addBtn.onmousedown = (e) => e.preventDefault();
    addBtn.onclick = () => {
      clearTimeout(this._blurTimer);
      this.stages.push({ id: "", depends_on: [], config: { Deploy: { environment: "" } } });
      this._render();
      requestAnimationFrame(() => {
        const inputs = this.querySelectorAll('input[data-field="id"]');
        if (inputs.length) inputs[inputs.length - 1].focus();
      });
    };
    this.append(addBtn);
  }

  _renderStageCard(index) {
    const stage = this.stages[index];
    const type = this._stageType(stage.config);
    const otherIds = this.stages
      .map((s, i) => (i !== index && s.id.trim() ? s.id.trim() : null))
      .filter(Boolean);

    const card = el("div", "border border-gray-200 rounded-md bg-white");

    // Header row
    const header = el("div", "flex items-center gap-2 px-3 py-2");
    const badge = el("span", "text-xs font-mono text-gray-400 w-5 shrink-0", `${index + 1}`);

    // ID input
    const idInput = el("input", "flex-1 border border-gray-200 rounded px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-gray-400 min-w-0");
    idInput.type = "text";
    idInput.value = stage.id;
    idInput.placeholder = "stage id";
    idInput.dataset.field = "id";
    idInput.oninput = () => {
      this.stages[index].id = idInput.value.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-");
      idInput.value = this.stages[index].id;
      this._sync();
      this._renderDagIfPresent();
    };
    idInput.onblur = () => {
      this._blurTimer = setTimeout(() => this._render(), 150);
    };

    // Type select (deploy / wait)
    const typeSelect = el("select", "border border-gray-200 rounded px-2 py-1 text-xs bg-white shrink-0");
    for (const t of ["deploy", "wait"]) {
      const opt = document.createElement("option");
      opt.value = t;
      opt.textContent = t;
      opt.selected = type === t;
      typeSelect.append(opt);
    }
    typeSelect.onmousedown = (e) => e.stopPropagation();
    typeSelect.onchange = () => {
      clearTimeout(this._blurTimer);
      if (typeSelect.value === "wait") {
        this.stages[index].config = { Wait: { duration_seconds: 0 } };
      } else {
        this.stages[index].config = { Deploy: { environment: "" } };
      }
      this._render();
    };

    // Remove button
    const removeBtn = el("button", "text-gray-400 hover:text-red-500 shrink-0 p-1");
    removeBtn.type = "button";
    removeBtn.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>`;
    removeBtn.title = "Remove stage";
    removeBtn.onmousedown = (e) => e.preventDefault();
    removeBtn.onclick = () => {
      clearTimeout(this._blurTimer);
      const removedId = this.stages[index].id;
      this.stages.splice(index, 1);
      for (const s of this.stages) {
        s.depends_on = s.depends_on.filter((d) => d !== removedId);
      }
      this._render();
    };

    header.append(badge, idInput, typeSelect, removeBtn);
    card.append(header);

    // Config row (type-specific fields)
    const configRow = el("div", "px-3 pb-2 flex items-center gap-2 flex-wrap");
    if (type === "deploy") {
      const envLabel = el("span", "text-xs text-gray-500 shrink-0", "env:");
      const envInput = el("input", "border border-gray-200 rounded px-2 py-1 text-xs w-32 focus:outline-none focus:ring-1 focus:ring-gray-400");
      envInput.type = "text";
      envInput.value = (stage.config.Deploy && stage.config.Deploy.environment) || "";
      envInput.placeholder = "environment";
      envInput.onmousedown = (e) => e.stopPropagation();
      envInput.oninput = () => {
        if (!this.stages[index].config.Deploy) this.stages[index].config = { Deploy: { environment: "" } };
        this.stages[index].config.Deploy.environment = envInput.value.trim();
        this._sync();
      };
      envInput.onblur = () => {
        this._blurTimer = setTimeout(() => this._render(), 150);
      };
      configRow.append(envLabel, envInput);
    } else if (type === "wait") {
      const durLabel = el("span", "text-xs text-gray-500 shrink-0", "wait:");
      const durInput = el("input", "border border-gray-200 rounded px-2 py-1 text-xs w-20 focus:outline-none focus:ring-1 focus:ring-gray-400");
      durInput.type = "number";
      durInput.min = "0";
      durInput.value = (stage.config.Wait && stage.config.Wait.duration_seconds) || 0;
      durInput.placeholder = "seconds";
      durInput.onmousedown = (e) => e.stopPropagation();
      durInput.oninput = () => {
        if (!this.stages[index].config.Wait) this.stages[index].config = { Wait: { duration_seconds: 0 } };
        this.stages[index].config.Wait.duration_seconds = parseInt(durInput.value) || 0;
        this._sync();
      };
      durInput.onblur = () => {
        this._blurTimer = setTimeout(() => this._render(), 150);
      };
      const secLabel = el("span", "text-xs text-gray-400", "seconds");
      configRow.append(durLabel, durInput, secLabel);
    }
    card.append(configRow);

    // Dependencies row
    if (otherIds.length > 0) {
      const depsRow = el("div", "px-3 pb-2 flex items-center gap-2 flex-wrap");
      const label = el("span", "text-xs text-gray-500 shrink-0", "after:");
      depsRow.append(label);

      for (const dep of otherIds) {
        const isSelected = stage.depends_on.includes(dep);
        const chip = el(
          "button",
          `text-xs px-2 py-0.5 rounded-full border transition-colors ${isSelected ? "bg-gray-900 text-white border-gray-900" : "border-gray-300 text-gray-500 hover:border-gray-400"}`,
          dep
        );
        chip.type = "button";
        chip.onmousedown = (e) => e.preventDefault();
        chip.onclick = () => {
          clearTimeout(this._blurTimer);
          if (isSelected) {
            this.stages[index].depends_on = this.stages[index].depends_on.filter((d) => d !== dep);
          } else {
            this.stages[index].depends_on.push(dep);
          }
          this._render();
        };
        depsRow.append(chip);
      }
      card.append(depsRow);
    }

    return card;
  }

  _renderDagIfPresent() {
    const canvas = this.querySelector(".dag-canvas");
    if (canvas) this._renderDag(canvas);
  }

  _renderDag(canvas) {
    canvas.innerHTML = "";
    const named = this.stages.filter((s) => s.id.trim());
    if (named.length === 0) {
      canvas.append(el("p", "text-xs text-gray-400 italic", "Add stages to see the pipeline graph"));
      return;
    }

    const levels = this._computeLevels();
    const maxLevel = Math.max(0, ...Object.values(levels));

    const columns = [];
    for (let l = 0; l <= maxLevel; l++) columns.push([]);
    for (const s of named) {
      const lvl = levels[s.id] || 0;
      columns[lvl].push(s);
    }

    const svgNS = "http://www.w3.org/2000/svg";
    const NODE_W = 120;
    const NODE_H = 40;
    const COL_GAP = 60;
    const ROW_GAP = 12;

    const positions = {};
    let totalW = 0;
    let totalH = 0;

    for (let col = 0; col <= maxLevel; col++) {
      const stages = columns[col];
      for (let row = 0; row < stages.length; row++) {
        const x = col * (NODE_W + COL_GAP);
        const y = row * (NODE_H + ROW_GAP);
        positions[stages[row].id] = { x, y };
        totalW = Math.max(totalW, x + NODE_W);
        totalH = Math.max(totalH, y + NODE_H);
      }
    }

    const PAD = 8;
    const svgW = totalW + PAD * 2;
    const svgH = totalH + PAD * 2;

    const svg = document.createElementNS(svgNS, "svg");
    svg.setAttribute("width", svgW);
    svg.setAttribute("height", svgH);
    svg.style.display = "block";

    // Arrowhead marker
    const defs = document.createElementNS(svgNS, "defs");
    const marker = document.createElementNS(svgNS, "marker");
    marker.setAttribute("id", "pb-arrow");
    marker.setAttribute("viewBox", "0 0 10 10");
    marker.setAttribute("refX", "10");
    marker.setAttribute("refY", "5");
    marker.setAttribute("markerWidth", "6");
    marker.setAttribute("markerHeight", "6");
    marker.setAttribute("orient", "auto-start-reverse");
    const arrowPath = document.createElementNS(svgNS, "path");
    arrowPath.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
    arrowPath.setAttribute("fill", "#9ca3af");
    marker.append(arrowPath);
    defs.append(marker);
    svg.append(defs);

    // Draw edges
    for (const s of named) {
      const to = positions[s.id];
      if (!to) continue;
      for (const dep of s.depends_on) {
        const from = positions[dep];
        if (!from) continue;
        const line = document.createElementNS(svgNS, "line");
        line.setAttribute("x1", from.x + NODE_W + PAD);
        line.setAttribute("y1", from.y + NODE_H / 2 + PAD);
        line.setAttribute("x2", to.x + PAD);
        line.setAttribute("y2", to.y + NODE_H / 2 + PAD);
        line.setAttribute("stroke", "#d1d5db");
        line.setAttribute("stroke-width", "2");
        line.setAttribute("marker-end", "url(#pb-arrow)");
        svg.append(line);
      }
    }

    // Draw nodes
    const TYPE_COLORS = {
      deploy: { bg: "#dbeafe", border: "#93c5fd", text: "#1e40af" },
      wait: { bg: "#fef3c7", border: "#fcd34d", text: "#92400e" },
    };

    for (const s of named) {
      const pos = positions[s.id];
      if (!pos) continue;
      const type = this._stageType(s.config);
      const colors = TYPE_COLORS[type] || TYPE_COLORS.deploy;
      const label = this._configLabel(s.config);

      const rect = document.createElementNS(svgNS, "rect");
      rect.setAttribute("x", pos.x + PAD);
      rect.setAttribute("y", pos.y + PAD);
      rect.setAttribute("width", NODE_W);
      rect.setAttribute("height", NODE_H);
      rect.setAttribute("rx", "6");
      rect.setAttribute("fill", colors.bg);
      rect.setAttribute("stroke", colors.border);
      rect.setAttribute("stroke-width", "1.5");
      svg.append(rect);

      // Stage ID text
      const text = document.createElementNS(svgNS, "text");
      text.setAttribute("x", pos.x + NODE_W / 2 + PAD);
      text.setAttribute("y", pos.y + NODE_H / 2 + PAD + (label ? -4 : 0));
      text.setAttribute("text-anchor", "middle");
      text.setAttribute("dominant-baseline", "middle");
      text.setAttribute("fill", colors.text);
      text.setAttribute("font-size", "12");
      text.setAttribute("font-weight", "600");
      text.textContent = s.id.length > 14 ? s.id.slice(0, 13) + "…" : s.id;
      svg.append(text);

      // Config label (environment or duration)
      if (label) {
        const sub = document.createElementNS(svgNS, "text");
        sub.setAttribute("x", pos.x + NODE_W / 2 + PAD);
        sub.setAttribute("y", pos.y + NODE_H / 2 + 10 + PAD);
        sub.setAttribute("text-anchor", "middle");
        sub.setAttribute("dominant-baseline", "middle");
        sub.setAttribute("fill", colors.text);
        sub.setAttribute("font-size", "9");
        sub.setAttribute("opacity", "0.7");
        sub.textContent = label;
        svg.append(sub);
      }
    }

    canvas.append(svg);
  }
}

function el(tag, className, text) {
  const e = document.createElement(tag);
  if (className) e.className = className;
  if (text) e.textContent = text;
  return e;
}

customElements.define("pipeline-builder", PipelineBuilder);
