// DAEV transform layer — added in Slice 6.4.5 wiring brief
// Translates backend audit response into UI fixture shape
// (drop-in replacement for window.DAEV_FIXTURES[id]).
//
// Backend shape verified via Probe Pack A (audits/{id} response):
//   audit.graph                  = { "<scope_key>": { nodes, edges, scope, anchor_node_id, schema_version } }
//   audit.graph[scope].nodes[]   = { id, line, end_byte, filepath, metadata, start_byte, vuln_class, entity_type, taint_label: float, code_snippet }
//   audit.graph[scope].edges[]   = { metadata, edge_type, source_id, target_id, confidence }
//   audit.report.findings[]      = { title, category, severity (lc), finding_id, agent_votes, causal_path[], description, blast_radius, debate_round, symbolic_hits[], dag_node_count, review_required, causal_fit_method, council_confidence, intervention_recommendation }
//   audit.report.regime          = { mode, arbiter, language, severity_hint }
//   audit.report.metadata.cognitive_signals = { miller_score, zipf_slope, system_mode (s1|s2) }
//
// UI fixture shape (per Probe B3 — FIX_SQLI canonical):
//   { id, lang, label, audit_id, lines[], finding: {...}, graph: { nodes, edges, positions } }
//   nodes[] = { id, label, kind, role, line, taint }
//   edges[] = { id, from, to, kind, taint }

(function() {
  // ─── Backend entity_type → UI kind map ─────────────────────────
  const ENTITY_TYPE_MAP = {
    'source': 'source',
    'sink': 'sink',
    'assignment': 'definition',  // best fit per probe A3 observed values
    // Future-proofing for backend additions:
    'transform': 'transform',
    'use': 'use',
    'gate': 'gate',
    'sanitizer': 'sanitizer',
    'definition': 'definition',
  };

  // ─── Synthesize UI label from backend node ─────────────────────
  function synthLabel(node) {
    const snippet = (node.code_snippet || '').trim();
    if (!snippet) return node.id ? node.id.split(':').pop() : 'node';
    return snippet.length > 40 ? snippet.slice(0, 38) + '…' : snippet;
  }

  // ─── Synthesize UI role hint ───────────────────────────────────
  function synthRole(node) {
    const et = node.entity_type;
    const meta = node.metadata || {};
    const snippet = node.code_snippet || '';
    if (et === 'source' && meta.param) return 'http_input';
    if (et === 'source') return 'input';
    if (et === 'sink' && /execute|cursor|query/i.test(snippet)) return 'db_query';
    if (et === 'sink' && /subprocess|popen|shell|exec/i.test(snippet)) return 'shell_call';
    if (et === 'sink' && /open|read|write/i.test(snippet)) return 'file_op';
    if (et === 'sink') return 'sink';
    if (et === 'assignment') return 'binding';
    return '';
  }

  // ─── Derive verdict from findings (backend persists "unknown") ─
  function deriveVerdict(findings) {
    if (!findings || findings.length === 0) return 'SAFE';
    const sevs = findings.map(f => (f.severity || '').toLowerCase());
    if (sevs.includes('critical') || sevs.includes('high')) return 'UNSAFE';
    if (sevs.includes('medium')) return 'NEEDS_REVIEW';
    return 'SAFE';
  }

  // ─── Pick highest-severity finding for UI's single-finding render
  function pickPrimaryFinding(findings) {
    if (!findings || findings.length === 0) return null;
    const sev_order = { critical: 4, high: 3, medium: 2, low: 1 };
    return [...findings].sort((a, b) =>
      (sev_order[(b.severity || '').toLowerCase()] || 0) -
      (sev_order[(a.severity || '').toLowerCase()] || 0)
    )[0];
  }

  // ─── Derive edge taint from endpoint nodes ─────────────────────
  function deriveEdgeTaint(edge, nodes) {
    const src = nodes.find(n => n.id === edge.source_id);
    const tgt = nodes.find(n => n.id === edge.target_id);
    const srcTainted = src && src.taint_label != null && src.taint_label > 0.3;
    const tgtTainted = tgt && tgt.taint_label != null && tgt.taint_label > 0.3;
    return srcTainted || tgtTainted;
  }

  // ─── Extract cognitive signals from report.metadata ────────────
  // Slice 6.4.5.2 Phase 8 / Phase 11 (BUG-81): static fallback for legacy
  // audits that predate the metadata-persistence fix. Real values from the
  // backend (after Phase 1 deploy) take precedence.
  const STATIC_SIGNALS_BY_ID = {
    sqli:      { miller: 3.2, zipf: -1.08, system: 'S2' },
    cmdi:      { miller: 2.8, zipf: -0.97, system: 'S2' },
    pathtrav:  { miller: 3.5, zipf: -1.12, system: 'S2' },
    hardcoded: { miller: 1.5, zipf: -0.82, system: 'S1' },
  };

  function extractSignals(report, fallback_id) {
    const md = (report && report.metadata) || {};
    const cs = md.cognitive_signals || {};
    const has_real = typeof cs.miller_score === 'number' || typeof cs.zipf_slope === 'number';
    if (has_real) {
      return {
        miller: typeof cs.miller_score === 'number' ? cs.miller_score : 0,
        zipf: typeof cs.zipf_slope === 'number' ? cs.zipf_slope : 0,
        system: cs.system_mode === 's2' ? 'S2' : 'S1',
      };
    }
    // Static fallback for legacy audits — keeps the Signals beat coherent.
    return STATIC_SIGNALS_BY_ID[fallback_id] || { miller: 2.0, zipf: -0.9, system: 'S2' };
  }

  // Slice 6.4.5.2 Phase 8 (BUG-83): UI-side CWE inference fallback used only
  // when the backend's finding.cwe_id is null/empty. Phase 2 backend now
  // surfaces cwe_id directly with a server-side title-regex fallback, so
  // most findings will have it populated.
  function inferCwe(title) {
    const t = (title || '').toLowerCase();
    if (t.includes('sql injection')) return 'CWE-89';
    if (t.includes('command injection')) return 'CWE-78';
    if (t.includes('path traversal')) return 'CWE-22';
    if (t.includes('xss') || t.includes('cross-site')) return 'CWE-79';
    if (t.includes('hardcoded') || t.includes('credential') || t.includes('secret')) return 'CWE-798';
    if (t.includes('toctou') || t.includes('race')) return 'CWE-367';
    if (t.includes('deserialization')) return 'CWE-502';
    if (t.includes('md5') || t.includes('sha1') || t.includes('weak crypt')) return 'CWE-327';
    return '';
  }

  // ─── Pick first scope from scope-keyed graph ───────────────────
  function pickFirstScope(graph) {
    if (!graph || typeof graph !== 'object') return null;
    const keys = Object.keys(graph);
    if (keys.length === 0) return null;
    return graph[keys[0]];
  }

  // Slice 6.4.5.2 Phase 13 (BUG-86): when a /audits/from-github audit returns
  // a graph too large for the demo viewport (e.g., pallets/click → 9310 nodes),
  // BFS from the finding's causal_path nodes to extract a relevant subgraph.
  // Falls back to top-N-by-taint if no causal_path is present.
  const MAX_DISPLAY_NODES = 80;

  function downsampleGraph(rawNodes, rawEdges, causalPath) {
    if (!Array.isArray(rawNodes) || rawNodes.length <= MAX_DISPLAY_NODES) {
      return { nodes: rawNodes || [], edges: rawEdges || [], truncated: false };
    }
    // BFS frontier seeded by the causal_path (or top-3 tainted nodes if absent).
    const seeds = (Array.isArray(causalPath) && causalPath.length > 0)
      ? causalPath.slice()
      : rawNodes
          .filter(n => n.taint_label != null && n.taint_label > 0.3)
          .slice(0, 3)
          .map(n => n.id);
    if (seeds.length === 0) {
      // No semantic anchor — just take the first MAX nodes deterministically.
      const truncatedNodes = rawNodes.slice(0, MAX_DISPLAY_NODES);
      const ids = new Set(truncatedNodes.map(n => n.id));
      const truncatedEdges = (rawEdges || []).filter(e =>
        ids.has(e.source_id) && ids.has(e.target_id),
      );
      return { nodes: truncatedNodes, edges: truncatedEdges, truncated: true };
    }
    // Build adjacency (both directions).
    const adj = {};
    rawNodes.forEach(n => { adj[n.id] = []; });
    (rawEdges || []).forEach(e => {
      if (adj[e.source_id]) adj[e.source_id].push(e.target_id);
      if (adj[e.target_id]) adj[e.target_id].push(e.source_id);
    });
    const keep = new Set(seeds);
    const queue = seeds.slice();
    while (queue.length > 0 && keep.size < MAX_DISPLAY_NODES) {
      const id = queue.shift();
      const neighbors = adj[id] || [];
      for (const nb of neighbors) {
        if (!keep.has(nb)) {
          keep.add(nb);
          if (keep.size >= MAX_DISPLAY_NODES) break;
          queue.push(nb);
        }
      }
    }
    const keptNodes = rawNodes.filter(n => keep.has(n.id));
    const keptEdges = (rawEdges || []).filter(e =>
      keep.has(e.source_id) && keep.has(e.target_id),
    );
    return { nodes: keptNodes, edges: keptEdges, truncated: true };
  }

  // ─── MAIN TRANSFORM ────────────────────────────────────────────
  function apiAuditToFixtureShape(api_audit, source_lines = null, fallback_id = 'wired') {
    if (!api_audit || typeof api_audit !== 'object') {
      console.warn('[daev-transform] empty audit input; returning null');
      return null;
    }

    const report = api_audit.report || {};
    const findings = Array.isArray(report.findings) ? report.findings : [];
    const primary = pickPrimaryFinding(findings);
    const sg = pickFirstScope(api_audit.graph) || { nodes: [], edges: [] };

    // Slice 6.4.5.2 Phase 13 (BUG-86): downsample huge graphs (e.g.,
    // pallets/click → 9310 nodes from /audits/from-github) before mapping
    // into UI shape. The original_node_count is preserved in
    // ui_finding.graph.original_node_count so the truncation banner can
    // surface the real backend count.
    let rawNodes = sg.nodes || [];
    let rawEdges = sg.edges || [];

    // Slice 6.4.7 Phase 4 (HC-52): the F-204 backend fix landed in
    // Phase 1 — github audits now persist a multi-scope graph_json
    // with real nodes via per-file ``build_module_dag``. The slice
    // 6.4.5.3 causal_path → pseudo-node synthesis below is now a
    // DEFENSIVE fallback, not the happy path. If the backend graph is
    // empty AND we can synthesize from causal_path, we do — but emit a
    // ``console.warn`` so investigators can spot the case during
    // Phase 7 browser walkthrough. Post-slice-6.4.7 this should be
    // RARE; persistent warnings indicate a regression in the per-file
    // refactor or a non-Python audit (Phase 1 only Pythoned the fix).
    if (rawNodes.length === 0 && primary && Array.isArray(primary.causal_path)) {
      const cp = primary.causal_path;
      if (cp.length > 0) {
        console.warn(
          `[daev-transform] graph empty for ${fallback_id} — falling back to ` +
          `causal_path synthesis (slice 6.4.5.3 band-aid demoted in Phase 4 ` +
          `but still active). Post-slice-6.4.7 this should be RARE; investigate ` +
          `if it persists.`
        );
        const synthNodes = cp.map((step, i) => {
          const s = String(step);
          const colonIdx = s.lastIndexOf(':');
          const filepath = colonIdx > 0 ? s.slice(0, colonIdx) : s;
          const lineStr = colonIdx > 0 ? s.slice(colonIdx + 1) : '';
          const lineNum = parseInt(lineStr, 10);
          return {
            id: s,
            line: Number.isFinite(lineNum) ? lineNum : 0,
            filepath,
            entity_type: i === 0 ? 'source' : (i === cp.length - 1 ? 'sink' : 'assignment'),
            taint_label: 1.0,
            code_snippet: filepath ? `${filepath.split('/').pop() || filepath}:${lineNum || '?'}` : s,
          };
        });
        const synthEdges = [];
        for (let i = 0; i < cp.length - 1; i++) {
          synthEdges.push({
            source_id: cp[i],
            target_id: cp[i + 1],
            edge_type: 'causal-flow',
            confidence: 1.0,
          });
        }
        rawNodes = synthNodes;
        rawEdges = synthEdges;
      }
    }
    const causalPath = (primary && Array.isArray(primary.causal_path))
      ? primary.causal_path
      : null;
    const ds = downsampleGraph(rawNodes, rawEdges, causalPath);

    // Map nodes
    const nodes = ds.nodes.map(n => ({
      id: n.id,
      label: synthLabel(n),
      kind: ENTITY_TYPE_MAP[n.entity_type] || 'use',
      role: synthRole(n),
      line: n.line || 0,
      filepath: n.filepath || '',
      taint: (n.taint_label != null) && (n.taint_label > 0.3),
    }));

    // Map edges (from/to RENAME from source_id/target_id; synthesize id)
    const edges = ds.edges.map((e, idx) => ({
      id: `e_${idx}_${(e.source_id || '').slice(-8)}_${(e.target_id || '').slice(-8)}`,
      from: e.source_id,
      to: e.target_id,
      kind: e.edge_type || 'def-use',
      taint: deriveEdgeTaint(e, ds.nodes),
    }));

    // Slice 6.4.5.3 Phase 5 (Item 7): connect causal_path nodes with
    // synthetic taint edges so the LLM-identified taint flow is always
    // visible as a connected chain. Investigation in /tmp/phase5-graph-
    // diagnosis.md showed the prestaged sqli/cmdi/pathtrav/hardcoded
    // graphs come back from /switzerland/gate as multiple disconnected
    // 2-node def-use components — the LLM's causal_path strings them
    // together but graph_edges don't. Dagre then lays out the
    // disconnected components stacked above each other instead of as
    // a single linear taint flow, hence the "weird" sqli/pathtrav
    // rendering. We add synthetic edges (kind: 'causal-flow', taint:
    // true) only where the causal_path step isn't already present in
    // the def-use edge set.  Then we trim nodes that are NOT reachable
    // from any causal_path node so the graph shows the taint story
    // without unrelated def-use side-branches.
    //
    // Done across ALL findings (not just primary) so multi-finding
    // audits like the hardcoded fixture show both flows.
    const node_id_set = new Set(nodes.map(n => n.id));
    const edge_pair_set = new Set(edges.map(e => `${e.from}->${e.to}`));
    const synthetic_causal_edges = [];
    const all_causal_path_ids = new Set();
    for (const f of findings) {
      const cp = Array.isArray(f.causal_path) ? f.causal_path : [];
      for (const id of cp) all_causal_path_ids.add(id);
      for (let i = 0; i < cp.length - 1; i++) {
        const from = cp[i];
        const to = cp[i + 1];
        if (!node_id_set.has(from) || !node_id_set.has(to)) continue;
        const key = `${from}->${to}`;
        if (edge_pair_set.has(key)) continue;
        edge_pair_set.add(key);
        synthetic_causal_edges.push({
          id: `e_causal_${(from || '').slice(-8)}_${(to || '').slice(-8)}`,
          from,
          to,
          kind: 'causal-flow',
          taint: true,
        });
      }
    }
    const enriched_edges = edges.concat(synthetic_causal_edges);

    // Compute the set of nodes reachable (undirected) from any
    // causal_path node. Drop the rest. Backend def-use analysis
    // sometimes captures unrelated assignments in the same file
    // (sqli's nodes 43,80,86,183 are an example — not on the
    // causal flow but emitted by dag_constructor); they distract
    // from the taint story when rendered.  We always keep the
    // causal_path nodes themselves; only side-branches that aren't
    // connected to the path get filtered.
    let connected_nodes = new Set(all_causal_path_ids);
    if (connected_nodes.size > 0) {
      const adj = {};
      for (const e of enriched_edges) {
        (adj[e.from] = adj[e.from] || []).push(e.to);
        (adj[e.to]   = adj[e.to]   || []).push(e.from);
      }
      let frontier = Array.from(connected_nodes);
      while (frontier.length > 0) {
        const next = [];
        for (const nid of frontier) {
          for (const nb of (adj[nid] || [])) {
            if (!connected_nodes.has(nb)) {
              connected_nodes.add(nb);
              next.push(nb);
            }
          }
        }
        frontier = next;
      }
    } else {
      // No causal_path at all — fall back to keeping every node so the
      // "show me the whole graph backend produced" path still works.
      connected_nodes = new Set(nodes.map(n => n.id));
    }

    const filtered_nodes = nodes.filter(n => connected_nodes.has(n.id));
    const filtered_edges = enriched_edges.filter(
      e => connected_nodes.has(e.from) && connected_nodes.has(e.to)
    );

    // Slice 6.4.5.3 Phase 5: rebind to the filtered + enriched set so
    // downstream finding/graph construction uses the connected view.
    nodes.length = 0;
    nodes.push(...filtered_nodes);
    edges.length = 0;
    edges.push(...filtered_edges);

    // Build finding object in UI shape
    let ui_finding = null;
    if (primary) {
      const path = Array.isArray(primary.causal_path) ? primary.causal_path.slice() : [];
      // Find a representative line number from the path
      const path_nodes = nodes.filter(n => path.includes(n.id));
      const rep_line = path_nodes.length > 0 ? path_nodes[0].line : 0;
      // Slice 6.4.5.2 Phase 8 (BUG-79): propagate filepath from the first
      // graph node (or the path's representative node) for the FindingCard
      // line-N · filepath display. Falls back to the audit's repo_path.
      const filepath_source = (path_nodes[0] && path_nodes[0].filepath)
        || (sg.nodes && sg.nodes[0] && sg.nodes[0].filepath)
        || api_audit.repo_path
        || '';
      // Strip absolute-tempfile prefix; only the basename is useful in the UI.
      const filepath_clean = filepath_source.includes('/')
        ? filepath_source.split('/').pop()
        : filepath_source;
      ui_finding = {
        id: primary.finding_id || 'sec-001',
        title: primary.title || 'Security Finding',
        severity: (primary.severity || 'medium').toLowerCase(),
        category: primary.category || 'security',
        line: rep_line,
        hops: path.length,
        confidence: typeof primary.council_confidence === 'number' ? primary.council_confidence : 0,
        // Slice 6.4.5.2 Phase 8 (BUG-83): backend Phase 2 surfaces cwe_id;
        // UI inference is now a fallback (used only when the LLM skipped it
        // AND the title-regex fallback in _finding_to_dict didn't match).
        cwe: primary.cwe_id || inferCwe(primary.title),
        filepath: filepath_clean,
        explanation: primary.description || primary.intervention_recommendation || '',
        path: path,
        fix: {
          before: '',  // backend has no before/after; UI handles empty via SuggestionBox
          after: primary.intervention_recommendation || '',
        },
        intervention_recommendation: primary.intervention_recommendation || '',
        signals: extractSignals(report, fallback_id),
      };
    }

    // Slice 6.4.5.3 Phase 4 (Item 6, completes BUG-76): expose backend
    // metadata.repo_url / files_audited / commit_sha on the wired fixture so
    // surfaces like the LiveBuild meta strip can render "GitHub: <repo>"
    // instead of the generic finding-title fallback. report.metadata is
    // populated by Slice 6.4.5.2 Phase 1 + Phase 4 (cognitive_signals on
    // /audits/from-github). Field set kept minimal — UI reads what it needs.
    const reportMeta = (report && report.metadata) || {};
    const audit_metadata = {
      repo_url:      reportMeta.repo_url      || null,
      files_audited: reportMeta.files_audited || null,
      commit_sha:    reportMeta.commit_sha    || null,
      source:        reportMeta.source        || null,  // e.g., "github" vs "inline"
    };

    // Slice 6.4.7 Phase 3 (F-212 + HC-54..57): propagate council-mode
    // backend reality into the UI fixture shape so CouncilPanel +
    // AgentCard + LiveBuild can render real backend data instead of
    // the static window.DAEV_COUNCIL fixture. ``audit_mode`` is the
    // gate the council components branch on — null for non-council
    // audits (the fixture remains the renderer source).
    const reportMode = (report.regime && report.regime.mode) || null;
    const councilMetadataIn = report.council_metadata || null;
    let council_metadata = null;
    if (reportMode === 'council' && councilMetadataIn) {
      council_metadata = {
        rounds_completed: councilMetadataIn.rounds_completed != null
          ? councilMetadataIn.rounds_completed : 0,
        disagreement_flag: !!councilMetadataIn.disagreement_flag,
        round2_resolved: councilMetadataIn.round2_resolved != null
          ? !!councilMetadataIn.round2_resolved : null,
        council_confidence: typeof councilMetadataIn.council_confidence === 'number'
          ? councilMetadataIn.council_confidence
          : 0,
        findings_by_agent: councilMetadataIn.findings_by_agent || {},
        debate_transcript_path: report.debate_transcript_path || null,
      };
    }
    const agent_weights = (reportMode === 'council' && report.agent_weights) || null;

    // ALL findings (not just primary), needed for per-agent rendering
    // when mode=council. Each entry preserves the per-agent vote map so
    // AgentCard can render real "this agent contributed 2 findings"
    // counts and per-finding rationale.
    const all_findings = (Array.isArray(findings) ? findings : []).map(f => ({
      title: f.title,
      severity: (f.severity || '').toLowerCase(),
      cwe_id: f.cwe_id || null,
      council_confidence: typeof f.council_confidence === 'number' ? f.council_confidence : 0,
      agent_votes: f.agent_votes || {},
      causal_path: Array.isArray(f.causal_path) ? f.causal_path : [],
      finding_id: f.finding_id || null,
    }));

    return {
      id: fallback_id,
      lang: (report.regime && report.regime.language) || 'python',
      label: (primary && primary.title) ? primary.title.slice(0, 40) : 'Audit',
      audit_id: api_audit.audit_id,
      audit_mode: reportMode,
      audit_metadata,
      lines: source_lines || ['# (source not retained for this audit)'],
      finding: ui_finding,
      // Slice 6.4.7 Phase 3 council pass-through fields. Null when
      // audit_mode !== 'council' so existing fast/full panels don't
      // change behaviour.
      council_metadata,
      agent_weights,
      all_findings,
      graph: {
        nodes,
        edges,
        positions: {},  // positions: client-side Dagre handles
        // Slice 6.4.5.2 Phase 13 (BUG-86 / BUG-87): downsampling diagnostics.
        truncated: ds.truncated,
        original_node_count: rawNodes.length,
        original_edge_count: rawEdges.length,
        // Slice 6.4.x F-216 (2026-05-11 pre-LOI hotpatch): backend-honest
        // totals for the result-header chip on multi-scope github audits
        // where ``pickFirstScope`` renders only the first scope and the
        // causal_path filter can prune the rendered subgraph to 0 nodes.
        // Falls back to the rendered count for inline (single-scope) audits
        // so no behavioural regression on the working path.
        total_nodes:
          (report.metadata && report.metadata.graph_node_count) ??
          report.total_nodes_analyzed ??
          nodes.length,
        total_edges:
          (report.metadata && report.metadata.graph_edge_count) ??
          report.total_edges_analyzed ??
          edges.length,
      },
    };
  }

  // ─── Audit-list-row → UI dashboard-row shape ───────────────────
  function apiAuditListItemToRow(item) {
    return {
      id: item.audit_id,
      title: item.repo_path === '<inline>' ? 'inline · ad-hoc audit' : (item.repo_path || 'audit'),
      verdict: 'unknown',  // list endpoint emits "unknown"; UI re-derives via deriveVerdict in detail
      fix: 'wired',  // not used for routing in wired mode
      mode: 'fast',
      lang: 'python',
      ts: item.created_at || '',
      dur: '—',
      nodes: 0,
      hops: 0,
      llm: 0,
    };
  }

  // ─── Expose ────────────────────────────────────────────────────
  window.DAEV_TRANSFORM = {
    ENTITY_TYPE_MAP,
    synthLabel, synthRole,
    deriveVerdict, pickPrimaryFinding, deriveEdgeTaint,
    extractSignals, pickFirstScope,
    inferCwe,
    STATIC_SIGNALS_BY_ID,
    apiAuditToFixtureShape,
    apiAuditListItemToRow,
  };

  console.log('[daev-transform] module loaded');
})();
