--- passes/report.lua — Per-MODULE annotation report renderer +
--- project-wide summary writer.
---
--- Two output files per build:
---   - `build/gen/<dir_basename>.annotations.txt` — one per source-directory
---     containing atoms; aggregates across all sources in the directory.
---   - `build/gen/annotation_validation.txt` — the project summary.
---
--- The annotation pass stashes per-MODULE summary entries in
--- `ctx.flags._annot_results` (set by `passes/annotation.lua`). This
--- pass re-validates each source via `annotation.validate()` to get
--- the detailed per-source results needed for the report. The cost is
--- acceptable: `validate()` is fast (~5ms per source) and runs once.
---
--- **Conventions**: tabs (1/level), EmmyLua annotations, no regex,
--- Lua 5.3 compatible. See
--- `C:\projects\Pikuma\ps1-ai\conductor\code_styleguides\lua.md`.

-- ════════════════════════════════════════════════════════════════════════════
-- Module-scope requires + package.path setup
-- ════════════════════════════════════════════════════════════════════════════

-- Resolve `arg[0]` to an absolute-ish script directory so that
-- `require("duffle")` resolves against `scripts/` regardless of CWD.
-- Note: this boilerplate is duplicated in 6 other entry scripts; a
-- Phase-6 extraction target (`duffle.setup_package_path()`).
-- Bootstrap: see `ps1_meta.lua` for the rationale.
dofile((arg[0]:match("(.*[/\\])") or "./") .. "duffle_paths.lua")
local duffle = require("duffle")

-- ════════════════════════════════════════════════════════════════════════════
-- Constants
-- ════════════════════════════════════════════════════════════════════════════

-- Section separators used in the rendered text reports. The thin rules
-- are hand-tuned to align with the per-section content width; do not
-- change without also checking the section renderers below.
local RULE_THICK = "========================================================"
local SECTION_HEADER_ATOMS     = "── Atoms ────────────────────────────────────────────────"
local SECTION_HEADER_ANNOTS    = "── Annotations ──────────────────────────────────────────"
local SECTION_HEADER_BINDS     = "── Binds_* structs ──────────────────────────────────────"
local SECTION_HEADER_MACROS    = "── Macro word-count declarations ─────────────────────────"
local SECTION_HEADER_ERRORS    = "── Errors ──────────────────────────────────────────────"
local SECTION_HEADER_WARNINGS  = "── Warnings ────────────────────────────────────────────"

-- Lua pattern that captures the basename (last path segment) of a
-- forward- or back-slash separated path.
local BASENAME_PATTERN = "([^/\\]+)$"

-- Debug flag name — set to truthy in `_G` to enable verbose logging.
local DEBUG_FLAG = "_DEBUG_REPORT"

-- Pass identifier for log messages.
local PASS_NAME = "report"

-- ════════════════════════════════════════════════════════════════════════════
-- Type declarations
-- ════════════════════════════════════════════════════════════════════════════

--- @class SourceFile
--- @field path     string  -- absolute path to the source file
--- @field text     string  -- the full source text
--- @field dir      string  -- the directory containing the source
--- @field basename string  -- filename without extension

--- @class PassCtx
--- @field sources            SourceFile[]            -- all source files in the build
--- @field metadata_path      string                  -- path to word_count.metadata.h
--- @field shared             table                   -- cross-pass shared state
--- @field out_root           string                  -- output root (e.g. "build/gen")
--- @field project_root       string                  -- project root (e.g. "code/")
--- @field upstream           table<string, table>    -- per-pass upstream outputs
--- @field flags              table                   -- CLI flags + per-pass stash
--- @field flags._annot_results ModuleEntry[]         -- stashed by annotation pass
--- @field dry_run            boolean                 -- if true, compute but don't write
--- @field verbose            boolean                 -- if true, log diagnostic info

--- @class PassResult
--- @field outputs  table[]  -- {kind=, path=} entries describing emit files
--- @field errors   table[]  -- {line=, msg=} entries; build-stops
--- @field warnings table[]  -- {line=, msg=} entries; build-succeeds

-- Shapes produced by `passes/annotation.lua`'s `M.validate()`.

--- @class AtomEntry
--- @field name string   -- atom name (e.g. "cube_g4_face")
--- @field line integer  -- source line of the atom declaration

--- @class AnnotEntry
--- @field line    integer  -- source line
--- @field macro   string   -- the macro name (e.g. "atom_reads")
--- @field name    string   -- the atom name (if a `name(...)` was given)
--- @field kind    string   -- "atom_info" | "atom_bind" | ...
--- @field binds   string|nil  -- Binds_X name if any
--- @field reads   string[]    -- R_* names (read targets)
--- @field writes  string[]    -- R_* names (write targets)
--- @field error   string|nil  -- error message if annotation was malformed

--- @class BindsField
--- @field name   string   -- field name
--- @field offset integer  -- byte offset within the Binds_X struct

--- @class BindsStruct
--- @field name   string         -- struct name (e.g. "Binds_Floor")
--- @field line   integer        -- source line of the typedef
--- @field bytes  integer        -- total byte size
--- @field fields BindsField[]   -- the field list

--- @class MacroEntry
--- @field name  string   -- macro name (e.g. "WORD_COUNT(my_macro, 4)")
--- @field line  integer  -- source line
--- @field words integer  -- declared word count

--- @class Finding
--- @field line integer  -- source line
--- @field msg  string   -- finding message

--- @class AnnotationResult
--- @field source   string         -- set by this pass; original source path
--- @field atoms    AtomEntry[]    -- atom declarations in this source
--- @field annots   AnnotEntry[]   -- annotation entries
--- @field macros   MacroEntry[]   -- macro word-count declarations
--- @field binds    BindsStruct[]  -- Binds_* struct declarations
--- @field errors   Finding[]      -- errors from validation
--- @field warnings Finding[]      -- warnings from validation
--- @field info     table          -- info summary (not rendered here)

--- @class ModuleEntry
--- @field dir          string   -- absolute directory path
--- @field dir_basename string   -- basename (e.g. "duffle", "gte_hello")
--- @field atoms_count  integer  -- pre-counted atoms for filtering

--- @class ModuleReport
--- @field dir       string             -- module directory
--- @field sources   SourceFile[]       -- sources in this module
--- @field results   AnnotationResult[] -- per-source validate() results

--- @class ProjectReport
--- @field results AnnotationResult[]  -- all per-source results

-- ════════════════════════════════════════════════════════════════════════════
-- Per-MODULE annotation report (aggregated across all sources in a dir)
-- ════════════════════════════════════════════════════════════════════════════

-- Extract the basename (last path segment) of a forward- or back-slash
-- separated path. Returns the input unchanged if no separator is found.
-- @param path string
-- @return string
local function source_basename(path)
	return path:match(BASENAME_PATTERN) or path
end

-- (internal) Format a single annotation entry as one rendered line.
-- @param a AnnotEntry
-- @param src_name string
-- @return string
local function format_annot_line(a, src_name)
	if a.error then
		return string.format("  ✗ line %d  %s  [ERROR: %s]  [%s]", a.line, a.macro or "?", a.error, src_name)
	end
	local line = string.format("  ●  line %d  %s  [%s]", a.line, a.name, src_name)
	if a.binds               then line = line .. "  binds=" .. a.binds end
	if #a.reads > 0          then line = line .. "  reads={" .. table.concat(a.reads, ",") .. "}" end
	if #a.writes > 0         then line = line .. "  writes={" .. table.concat(a.writes, ",") .. "}" end
	return line
end

-- (internal) Tally totals across all results in a module.
-- @param results AnnotationResult[]
-- @return integer, integer, integer, integer, integer, integer
local function tally_module_totals(results)
	local total_atoms, total_annots, total_binds, total_macros = 0, 0, 0, 0
	local total_errors, total_warnings = 0, 0
	for _, r in ipairs(results) do
		total_atoms    = total_atoms    + #r.atoms
		total_annots   = total_annots   + #r.annots
		total_binds    = total_binds    + #r.binds
		total_macros   = total_macros   + #r.macros
		total_errors   = total_errors   + #r.errors
		total_warnings = total_warnings + #r.warnings
	end
	return total_atoms, total_annots, total_binds, total_macros, total_errors, total_warnings
end

-- (internal) Section renderer: per-source atom declarations.
local function render_module_atoms_section(add, results)
	add(SECTION_HEADER_ATOMS)
	for _, r in ipairs(results) do
		local src_name = source_basename(r.source)
		for _, a in ipairs(r.atoms) do
			add(string.format("  MipsAtom_(%s)   line %d   [%s]", a.name, a.line, src_name))
		end
	end
	add("")
end

-- (internal) Section renderer: per-source annotation entries.
local function render_module_annots_section(add, results)
	add(SECTION_HEADER_ANNOTS)
	for _, r in ipairs(results) do
		local src_name = source_basename(r.source)
		for _, a in ipairs(r.annots) do
			add(format_annot_line(a, src_name))
		end
	end
	add("")
end

-- (internal) Section renderer: per-source Binds_* struct declarations.
local function render_module_binds_section(add, results)
	add(SECTION_HEADER_BINDS)
	for _, r in ipairs(results) do
		local src_name = source_basename(r.source)
		for _, b in ipairs(r.binds) do
			add(string.format("  %s   line %d   %d bytes   [%s]", b.name, b.line, b.bytes, src_name))
			for _, f in ipairs(b.fields) do
				add(string.format("    +%2d: %s", f.offset, f.name))
			end
		end
	end
	add("")
end

-- (internal) Section renderer: per-source macro word-count declarations.
local function render_module_macros_section(add, results)
	add(SECTION_HEADER_MACROS)
	for _, r in ipairs(results) do
		local src_name = source_basename(r.source)
		for _, m in ipairs(r.macros) do
			add(string.format("  %s  line %d  words=%d  [%s]", m.name, m.line, m.words, src_name))
		end
	end
	add("")
end

-- (internal) Section renderer: per-source errors (one-line + "(none)" if empty).
local function render_module_errors_section(add, results, total_errors)
	add(SECTION_HEADER_ERRORS)
	if total_errors == 0 then
		add("  (none)")
	else
		for _, r in ipairs(results) do
			local src_name = source_basename(r.source)
			for _, e in ipairs(r.errors) do
				add(string.format("  ✗ line %d  %s  [%s]", e.line, e.msg, src_name))
			end
		end
	end
	add("")
end

-- (internal) Section renderer: per-source warnings (one-line + "(none)" if empty).
local function render_module_warnings_section(add, results, total_warnings)
	add(SECTION_HEADER_WARNINGS)
	if total_warnings == 0 then
		add("  (none)")
	else
		for _, r in ipairs(results) do
			local src_name = source_basename(r.source)
			for _, w in ipairs(r.warnings) do
				add(string.format("  ⚠ line %d  %s  [%s]", w.line, w.msg, src_name))
			end
		end
	end
	add("")
end

--- Render the per-MODULE annotation report (one `<dir_basename>.annotations.txt`).
--- @param dir string                  -- module directory path
--- @param sources SourceFile[]        -- sources in this module
--- @param results AnnotationResult[]  -- per-source validate() results
--- @return string                     -- the rendered report text
local function render_module_report(dir, sources, results)
	local lines = {}
	local function add(s) lines[#lines + 1] = s end

	add(RULE_THICK)
	add("ANNOTATION PASS — module " .. source_basename(dir))
	add(RULE_THICK)
	add(string.format("Sources: %d", #sources))
	for _, s in ipairs(sources) do add("  " .. s.path) end
	add("")

	local total_atoms, total_annots, total_binds, total_macros, total_errors, total_warnings = tally_module_totals(results)
	add(string.format("Atoms: %d   Annotations: %d   Binds structs: %d   Macro decls: %d",
		total_atoms, total_annots, total_binds, total_macros))
	add("")

	render_module_atoms_section(add, results)
	render_module_annots_section(add, results)
	render_module_binds_section(add, results)
	render_module_macros_section(add, results)
	render_module_errors_section(add, results, total_errors)
	render_module_warnings_section(add, results, total_warnings)

	return table.concat(lines, "\n") .. "\n"
end

-- ════════════════════════════════════════════════════════════════════════════
-- Per-project summary (ported from tape_atom_annotation_pass.lua:1488-1528)
-- ════════════════════════════════════════════════════════════════════════════

--- Render the per-project summary (`build/gen/annotation_validation.txt`).
--- Aggregates totals across all sources; lists per-source error counts
--- if any source has errors.
--- @param all_results AnnotationResult[]
--- @return string
local function render_project_report(all_results)
	local lines = {}
	local function add(s) lines[#lines + 1] = s end

	local total_atoms, total_annots, total_macros, total_binds = 0, 0, 0, 0
	local total_errors, total_warnings = 0, 0
	for _, r in ipairs(all_results) do
		total_atoms    = total_atoms    + #r.atoms
		total_annots   = total_annots   + #r.annots
		total_macros   = total_macros   + #r.macros
		total_binds    = total_binds    + #r.binds
		total_errors   = total_errors   + #r.errors
		total_warnings = total_warnings + #r.warnings
	end

	add(RULE_THICK)
	add("ANNOTATION VALIDATION — project summary")
	add(RULE_THICK)
	add("")
	add(string.format("Atoms:        %d", total_atoms))
	add(string.format("Annotations:  %d", total_annots))
	add(string.format("Macros:       %d", total_macros))
	add(string.format("Binds:        %d", total_binds))
	add("")
	add(string.format("Errors:       %d", total_errors))
	add(string.format("Warnings:     %d", total_warnings))
	add("")

	if total_errors > 0 then
		add("Per-source error counts:")
		for _, r in ipairs(all_results) do
			if #r.errors > 0 then
				local src_name = source_basename(r.source)
				add(string.format("  %s : %d error(s)", src_name, #r.errors))
			end
		end
		add("")
	end

	return table.concat(lines, "\n") .. "\n"
end

-- ════════════════════════════════════════════════════════════════════════════
-- Orchestration helpers
-- ════════════════════════════════════════════════════════════════════════════

-- Group source files by their `dir` field. Used to mirror the
-- per-DIRECTORY partitioning the annotation pass uses.
-- @param sources SourceFile[]
-- @return table<string, SourceFile[]>  -- map of dir -> sources in that dir
local function group_sources_by_dir(sources)
	local by_dir = {}
	for _, src in ipairs(sources) do
		by_dir[src.dir] = by_dir[src.dir] or {}
		table.insert(by_dir[src.dir], src)
	end
	return by_dir
end

-- (internal) Validate each source in `dir_sources` via the annotation pass,
-- tagging each result with `result.source = src.path` for downstream rendering.
-- Returns the list of module results + the flat list of all results (for the
-- project-wide summary).
-- @param ctx PassCtx
-- @param dir_sources SourceFile[]
-- @return AnnotationResult[], AnnotationResult[]
local function validate_module_sources(ctx, dir_sources)
	local annotation = require("passes.annotation")
	local module_results = {}
	local all_results   = {}
	for _, src in ipairs(dir_sources) do
		local result = annotation.validate(ctx, src)
		result.source = src.path
		module_results[#module_results + 1] = result
		all_results[#all_results + 1] = result
	end
	return module_results, all_results
end

-- (internal) Does this module's results contain anything worth emitting?
-- @param module_results AnnotationResult[]
-- @return boolean
local function module_has_content(module_results)
	for _, r in ipairs(module_results) do
		if #r.atoms > 0 or #r.annots > 0 or #r.binds > 0
		or #r.macros > 0 or #r.errors > 0 or #r.warnings > 0 then
			return true
		end
	end
	return false
end

-- (internal) Log a debug message if `_G[DEBUG_FLAG]` is truthy.
-- @param fmt string
local function debug_log(fmt, ...)
	if _G[DEBUG_FLAG] then
		io.stderr:write(string.format("[%s] " .. fmt, PASS_NAME, ...))
	end
end

-- ════════════════════════════════════════════════════════════════════════════
-- M — module exports
-- ════════════════════════════════════════════════════════════════════════════

local M = {}

--- Run the report pass. Renders one `<dir_basename>.annotations.txt`
--- per source-directory that has content, plus the project-wide
--- `annotation_validation.txt` summary.
--- @param ctx PassCtx
--- @return PassResult
function M.run(ctx)
	local outputs  = {}
	local errors   = {}
	local warnings = {}

	local module_entries = (ctx.flags and ctx.flags._annot_results) or {}
	local by_dir         = group_sources_by_dir(ctx.sources)

	if not ctx.dry_run then duffle.ensure_dir(ctx.out_root) end

	local all_results_for_summary = {}
	for _, entry in ipairs(module_entries) do
		debug_log("entry: dir=%s basename=%s atoms_count=%d dir_sources=%d\n",
			entry.dir, entry.dir_basename, entry.atoms_count, #(by_dir[entry.dir] or {}))

		if entry.atoms_count > 0 or #(by_dir[entry.dir] or {}) > 0 then
			local dir_sources   = by_dir[entry.dir] or {}
			local module_results, all_results = validate_module_sources(ctx, dir_sources)
			for _, r in ipairs(all_results) do
				all_results_for_summary[#all_results_for_summary + 1] = r
			end

			if module_has_content(module_results) then
				local out_path = ctx.out_root .. "/" .. entry.dir_basename .. ".annotations.txt"
				if not ctx.dry_run then
					duffle.write_file(out_path, render_module_report(entry.dir, dir_sources, module_results))
				end
				outputs[#outputs + 1] = { annotations_txt = out_path }
			else
				debug_log("  -> no content; skipping\n")
			end
		end
	end

	if not ctx.dry_run and #all_results_for_summary > 0 then
		local summary_path = ctx.out_root .. "/annotation_validation.txt"
		duffle.write_file(summary_path, render_project_report(all_results_for_summary))
		outputs[#outputs + 1] = { summary_txt = summary_path }
	end

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

return M
