--- passes/emission_model.lua: Per-atom emission projection.
---
--- The `emission-model` pass owns `atom.paths`, the canonical per-atom mutable surface for atoms and raw atoms with bodies in `ctx.shared.corpus.source_order`.
--- For each atom, the pass invokes `duffle.project_emission(body_text, component_index, word_counts, components)`.
--- It stores the ordered `items` stream plus the dense `word_events` / `markers` / `invocations` views on `atom.paths`.
---
--- Public boundary:
---   * `M.run(ctx)` is the only entry point.
---   * The pass returns `{outputs = {}, errors = ..., warnings = ...}`.
---     Pass kind = `validation` → `PASS_KIND_STOP_ON_ERROR.validation` preserves the existing build-stopping policy.
---
--- Source-order discipline:
---   * `corpus.source_order` sets the source-record order.
---   * Within each source, the pass visits `src.scan.atoms` and `src.scan.raw_atoms` in declaration order.
---
--- Per-atom projection fields on `atom.paths`:
---   `tokens`, `line_in_body`, `items`, `word_events`, `markers`, `invocations`, `errors`, `warnings`.
---   The construction walk appends `items` and derives each dense view from that ordered stream.
---
--- Component expansion and construction validation:
---   * known `mac_X(...)` calls recursively expand component bodies;
---   * invocation records retain monotonic IDs, parent IDs, immediate call text, and the immutable outermost root call text;
---   * invocation construction stamps `debug_skip` from `corpus.components[name].debug_skip` at the construction site (no second pass, no source parse, no parallel lookup);
---   * component cycles close balanced invocation boundaries and emit a `cycle` construction error at the recursive edge;
---   * declared-vs-measured component word counts emit `count_mismatch` construction errors; opaque uncounted macros emit warnings.
---
--- `passes.scan_source` strips its private `_code_macros` / `_code_macro_bodies` tables before this pass runs.

local M = {}

-- ─────────────────────────────────────────────────────────────────────────
-- Bootstrap: load `duffle_paths.lua` via debug.getinfo so the module works standalone (run as `luajit passes/emission_model.lua`) and when require'd from the orchestrator.
-- ─────────────────────────────────────────────────────────────────────────
local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./"
local duffle         = dofile(_bootstrap_dir .. "../duffle_paths.lua")

-- ─────────────────────────────────────────────────────────────────────────
-- Helpers
-- ─────────────────────────────────────────────────────────────────────────

-- Convert the recursive walk's body-relative line numbers into physical source lines once.
-- The walker builds `line_of` from `body_text` and stamps body-relative line numbers (1..N) into `item.line` and `invocation.call_line`.
-- This function converts those values to physical source lines at the close site with the forwarded source `line_of` closure.
-- `call_line` discipline:
--   * ROOT invocations (`inv.parent_id == 0`) receive body-relative `call_line` values directly from `M.LineIndex(body_text)` in the walker.
--     The source `line_of` closure supplies physical lines at the close site, so this function converts each root value exactly once.
--   * INNER invocations (`inv.parent_id ~= 0`) receive physical `call_line` values directly from the COMPONENT's `line_of` in the walker.
--     Recursive descent forwards that closure through `corpus.component_body_index[name].line_of`; those values arrive physical and remain unchanged.
--
-- After this function, every `inv.call_line` is physical. DWARF and provenance output read it directly.
-- The word-event loop forwards the already-physical `outer_inv.call_line` into `we.call_line` for words inside an invocation.
local function stamp_root_provenance(projection, atom_record, src, corpus)
	local root_line_of   = src.scan and src.scan.line_of
	assert(type(root_line_of) == "function"
		, "emission_model: src.scan.line_of is required (canonical LineIndex closure over the source text) to stamp physical provenance")
	assert(type(atom_record.body_off) == "number"
		, "emission_model: atom_record.body_off (byte offset of the body's first byte in source) is required to derive `root_body_line`. The scanner must populate body_off for every atom record.")
	-- `root_body_line` is the physical source line of the ATOM HEADER byte containing the opening `{`; that byte is one byte BEFORE `atom_record.body_off`.
	-- The walker assigns line 2 to the body's first content line because line 1 is the trailing `\n` after `{`. Body-text line k therefore maps to `root_body_line + (k - 1)`.
	-- `body_off - 1` points at the opening `{`, whose line index identifies the header line. `body_off` points after `{` and would shift every word row forward by one line.
	local root_body_line  = root_line_of(atom_record.body_off - 1) or atom_record.line or 0
	local component_index = corpus.component_body_index or {}
	local word_items      = {}

	for _, item in ipairs(projection.items) do
		if item.kind == "word" then word_items[#word_items + 1] = item end
	end

	-- Resolve one word's physical body line, where the byte containing that word appears in source.
	--   * Component expansions carry `invocation_ids`; the component's full-file `line_of` leaves `item.line` physical.
	--   * Raw tokens in the root atom body carry an empty `invocation_ids` list and a body-relative `item.line`; convert them here.
	local function body_line_for(event, item)
		local ids = event.invocation_ids or {}
		-- The innermost open invocation identifies which line index the walker used.
		-- A component `line_of` makes `item.line` physical; the atom's `body_text` line index makes it body-relative.
		if ids and #ids > 0 then
			local inner_id  = ids[#ids]
			local inner_inv = inner_id and projection.invocations[inner_id]
			if inner_inv then
				local component = component_index[inner_inv.component_name]
				if component and component.line_of then
					-- Walker used `comp.line_of`, which is the source's physical LineIndex. item.line is already physical.
					return item.line or 0
				end
			end
		end
		-- RAW root-body word: item.line is body-text's 1-based line number (the first content line is line 2 because line 1 is the trailing `\n` after `{`).
		-- Convert body-text-relative → physical using `root_body_line + (item.line - 1)`.
		return (root_body_line or 0) + (item.line or 1) - 1
	end

	-- Stamp the root source path onto invocation records whose `call_path` the walker left empty.
	-- The walker passes `body_entry.source` to `emit_invoke_begin`; `M.project_emission` creates the root `body_entry` with source `""`, leaving its `call_path` empty.
	-- This stamp gives every invocation a physical `call_path` matching `passes/atoms_source_map.lua`'s in-memory provenance projection.
	local root_path = src.path or ""
	for _, inv in ipairs(projection.invocations) do
		if inv.call_path == nil or inv.call_path == "" then
			inv.call_path = root_path
		end
	end

	-- Normalize `inv.call_line` to a physical source line.
	--   * ROOT  invocations (`parent_id == 0`) carry body-relative `call_line` values from `M.LineIndex(body_text)`; convert them once with `root_body_line`.
	--   * INNER invocations (`parent_id ~= 0`) carry physical `call_line` values from the component's `line_of`; retain them unchanged.
	for _, inv in ipairs(projection.invocations) do
		if inv.parent_id == 0 then
			inv.call_line = (root_body_line or 0) + (inv.call_line or 1) - 1
		end
	end

	-- Build `body_lines` for each invocation.
	-- `atoms_source_map` and `dwarf_injection` read `inv.body_lines[k]` directly from the invocation record created here.
	-- Component words already carry physical `item.line` values from the walker's COMPONENT line index, so `body_line_for` returns them unchanged.
	for _, inv in ipairs(projection.invocations) do
		local sw  = inv.start_word
		local ew  = inv.end_word
		local bls = {}
		for i = sw, ew do
			local it = projection.items and projection.items[i]
			if    it and it.kind == "word" then
				local fake_event = { invocation_ids = { inv.id } }
				bls[#bls + 1]    = body_line_for(fake_event, it) or 0
			end
		end
		inv.body_lines = bls
	end

	-- Resolve each `word_event`'s physical `body_line` and `call_line`.
	-- For words inside an invocation, `we.call_line` identifies the OUTER atom source line containing the `mac_X(...)` token that triggered expansion.
	-- The root-invocation conversion above makes every `inv.call_line` physical; forward it directly and use each raw word's `body_line` as the fallback.
	for index, we in ipairs(projection.word_events) do
		local item      = word_items[index] or {}
		local body_line = body_line_for(we, item)
		item.line    = body_line
		we.body_line = body_line

		local call_line = body_line
		local outer_id  = we.outermost_invocation_id or 0
		local outer_inv = projection.invocations[outer_id]
		if outer_inv then
			-- `outer_inv.call_line` is physical after the conversion loop above, so use it directly.
			call_line = outer_inv.call_line
		end
		we.call_line = call_line

		if we.def_path  == nil or we.def_path  == "" then we.def_path  = src.path         or "" end
		if we.def_line  == nil or we.def_line  == 0  then we.def_line  = atom_record.line or 0  end
		if we.call_path == nil or we.call_path == "" then we.call_path = src.path         or "" end
	end
end

-- Project one atom record into `atom.paths`.
-- Mutates the atom record in-place and returns the projection (for pass-level error/warning accumulation).
local function project_atom(atom_record, src, corpus)
	local body  = atom_record.body or ""
	local wc    = corpus.word_counts or {}
	local cbi   = corpus.component_body_index or {}
	-- That construction site stamps `invocation.debug_skip` while appending each record to `proj.invocations`.
	local proj  = duffle.project_emission(body, cbi, wc, corpus.components)
	local paths = {
		tokens       = atom_record.body_tokens or {},
		line_in_body = duffle.build_body_line_index(body),
		items        = proj.items,
		word_events  = proj.word_events,
		markers      = proj.markers,
		invocations  = proj.invocations,
		errors       = proj.errors,
		warnings     = proj.warnings,
	}
	stamp_root_provenance(proj, atom_record, src, corpus)
	atom_record.paths = paths
	return proj
end

-- ─────────────────────────────────────────────────────────────────────────
-- Run the emission-model pass.
-- ─────────────────────────────────────────────────────────────────────────

--- @param ctx PassCtx  -- { shared = { corpus = ... }, out_root, ... }
--- @return PassResult
function M.run(ctx)
	local outputs  = {}
	local errors   = {}
	local warnings = {}

	local corpus = ctx and ctx.shared and ctx.shared.corpus
	if type(corpus)              ~= "table" then error("emission_model: ctx.shared.corpus is required (canonical projection)", 0) end
	if type(corpus.source_order) ~= "table" then error("emission_model: ctx.shared.corpus.source_order is required", 0) end

	-- Project once, collect errors + warnings for one atom.
	-- Kind must be one of: atom | atom_proc | raw_atom | comp_bare | comp_proc.
	local function process_atom(atom, src)
		if not (atom and atom.body) then return end
		local kind = atom.kind
		if kind ~= "atom" and kind ~= "atom_proc" and kind ~= "raw_atom" and kind ~= "comp_bare" and kind ~= "comp_proc" then
			return
		end
		local proj = project_atom(atom, src, corpus)
		for _, e in ipairs(proj.errors) do
			-- Preserve `kind` (cycle / count_mismatch / unbalanced) so readers dispatch on the diagnostic class and leave the message string as display text.
			errors[#errors + 1] = {
				kind   = e.kind,
				line   = e.line,
				msg    = e.msg,
				source = e.source or src.path,
			}
		end
		for _, w in ipairs(proj.warnings) do
			warnings[#warnings + 1] = {
				kind = w.kind,
				line = w.line,
				msg  = w.msg,
			}
		end
	end

	-- Walk `corpus.source_order`; within each source, visit atoms followed by raw_atoms.
	-- Recognized kinds (atom | atom_proc | raw_atom | comp_bare | comp_proc) each receive the atom.paths projection via duffle.project_emission.
	-- Components are macros inlined into atom bodies; focused tests and isolated component analyses consume atom.paths directly.
	for _, src in ipairs(corpus.source_order) do
		local scan = src.scan or {}
		for _, atom in ipairs(scan.atoms or {}) do
			process_atom(atom, src)
		end
		for _, atom in ipairs(scan.raw_atoms or {}) do
			process_atom(atom, src)
		end
	end

	return {
		outputs  = outputs,
		errors   = errors,
		warnings = warnings,
	}
end

return M
