-- lx-bundle:true

--- Document lifecycle creation and open helpers
-- Keep universal document orchestration under the 'core.doc' module family
-- @module core.doc.lifecycle

local doc_options = require("core.doc.options")
local filetype = require("core.filetype")
local image_support = require("core.image.support")

local lifecycle = {}

--- Return the live core module when it is already loaded
-- @return table|nil core
local function get_core()
	return package.loaded["core"]
end

--- Emit one grouped core hook stage when hook bus is available
-- @param event_name string hook event name
-- @param stage string hook stage name
-- @param payload table|nil event payload
local function emit_core_hook(event_name, stage, payload)
	local core = get_core()

	if core and core.hook then
		core.hook.emit(event_name, stage, payload)
	end
end

--- Create a new text document and apply startup options
-- Supports optional synthetic filenames without opening from disk
-- @param filename string|nil synthetic filename
-- @param opts table|nil document options
-- @param source string hook source label
-- @return table doc
local function create_text_doc(filename, opts, source)
	local core = get_core()
	local TextDocument = require("core.text.document")
	local options = doc_options.build_new_doc_options(opts)
	local doc = TextDocument()

	if type(filename) == "string" and filename ~= "" then
		options.filename = filename
	end

	if type(options.filename) == "string" and options.filename ~= "" then
		doc.filename = options.filename
	end

	emit_core_hook("doc.new", "before", {
		doc = doc,
		options = doc_options.clone_doc_options(options),
		source = source,
	})

	local ok, err = pcall(function()
		doc_options.apply_doc_options_to_doc(doc, options, true)
		table.insert(core.docs, doc)
	end)

	if not ok then
		emit_core_hook("doc.new", "error", {
			doc = doc,
			options = doc_options.clone_doc_options(options),
			source = source,
			err = err,
		})
		error(err)
	end

	emit_core_hook("doc.new", "after", {
		doc = doc,
		options = doc_options.clone_doc_options(options),
		source = source,
	})

	core.log_quiet("Opened new doc with startup options")

	return doc
end

--- Return the final detected document kind for one filename
-- Honors force-binary requests and ANSI syntax hints
-- @param filename string|nil file path
-- @param opts table|nil detect options
-- @return table probe
-- @return string|nil err
function lifecycle.detect_type(filename, opts)
	opts = type(opts) == "table" and opts or {}

	if type(filename) ~= "string" or filename == "" then
		return nil, "missing_filename"
	end

	local absolute_filename = system.absolute_path(filename) or filename
	local probe, err = filetype.detect_file_type(absolute_filename)
	local is_image, image_extension = image_support.is_supported(absolute_filename)

	if not probe then
		return nil, err
	end

	local force_binary = opts.force_binary == true
	local is_binary = force_binary or ((probe and probe.is_binary and not probe.is_utf16) or false)
	local detect_meta = nil

	if not is_binary or not force_binary then
		local header = filetype.read_file_head(absolute_filename, 4096)

		if header then
			local syntax = require("core.syntax")
			local _, detected_meta = syntax.get(absolute_filename, header, false)
			detect_meta = detected_meta

			if is_binary and not force_binary and type(detect_meta) == "table" and detect_meta.force_text_open == true then
				is_binary = false
			end
		end
	end

	if is_image == true then
		is_binary = false
	end

	probe.is_binary = is_binary
	probe.is_binary_placeholder = is_binary
	probe.is_image = is_image == true
	probe.image_extension = image_extension
	probe.doc_kind = is_binary and "binary" or (probe.is_image and "image" or "text")
	probe.syntax_detect_meta = detect_meta
	probe.ansi_viewer = type(detect_meta) == "table" and detect_meta.ansi_viewer == true or false

	return probe
end

--- Create a new text document through the universal document lifecycle
-- @param filename string|nil synthetic filename
-- @param opts table|nil create options
-- @return table|nil doc
function lifecycle.create(filename, opts)
	return create_text_doc(filename, opts, "Doc.create")
end

--- Open or reuse a document through the universal document lifecycle
-- @param filename string file path
-- @param opts table|nil open options
-- @return table|nil doc
function lifecycle.open_file(filename, opts)
	local core = get_core()
	local Doc = require("core.doc")
	local TextDocument = require("core.text.document")
	local cli = require("core.cli")

	opts = type(opts) == "table" and opts or {}

	if type(filename) ~= "string" or filename == "" then
		return nil, "missing_filename"
	end

	local start_time = system.get_time()
	local absolute_filename = system.absolute_path(filename)
	local save_disabled_reason = nil
	local force_binary = opts.force_binary
	local requested_doc_options = doc_options.clone_doc_options(opts.doc_options)
	local open_doc_options = doc_options.build_open_doc_options(requested_doc_options)
	local force_load_options = doc_options.build_doc_force_load_options(open_doc_options)

	core.log_quiet(
		"[open-doc] request file=%s opts={%s} effective={%s} force={%s}",
		tostring(filename),
		cli.format_doc_options(requested_doc_options),
		cli.format_doc_options(open_doc_options),
		cli.format_force_load_options(force_load_options)
	)

	emit_core_hook("doc.open", "before", {
		filename = filename,
		options = doc_options.clone_doc_options(open_doc_options),
		force_options = force_load_options,
		source = "Doc.open_file",
	})

	if force_binary == nil then
		force_binary = core.pending_binary_open_mode ~= nil
	else
		force_binary = force_binary == true
	end

	if absolute_filename then
		local missing_target = system.is_file(absolute_filename or filename) ~= true

		for _, doc in ipairs(core.docs) do
			if doc.filename and absolute_filename == system.absolute_path(doc.filename) then
				if force_binary and not doc.is_binary_placeholder then
					doc.is_binary = true
					doc.is_binary_placeholder = true
					doc.save_disabled = true
					doc.save_disabled_reason = "binary mode forced by CLI"
					doc.encoding_label = "Binary"
					doc.codepage_label = nil
					doc:reset_syntax()
					core.log_quiet('Forced binary placeholder for "%s" via CLI mode', filename)
				end

				if force_load_options and type(doc.reopen) == "function" and type(doc.is_dirty) == "function" and not doc:is_dirty() then
					if system.is_file(doc.filename) ~= true then
						return doc_options.handle_missing_file_open(doc.filename, "file not found", open_doc_options)
					end

					local ok, load_err = pcall(function()
						doc:reopen(force_load_options)
					end)

					if not ok then
						core.error('%s', tostring(load_err))
						emit_core_hook("doc.open", "error", {
							doc = doc,
							filename = doc.filename,
							options = doc_options.clone_doc_options(open_doc_options),
							force_options = force_load_options,
							source = "Doc.open_file",
							err = load_err,
						})
					end
				end

				doc_options.apply_doc_options_to_doc(doc, open_doc_options, false)
				core.log_quiet(
					"[open-doc] reuse applied file=%s encoding=%s bom=%s codepage=%s labels={enc:%s cp:%s}",
					tostring(doc.filename),
					tostring(doc.file_encoding),
					tostring(doc.file_bom),
					tostring(doc.file_codepage),
					tostring(doc.encoding_label),
					tostring(doc.codepage_label)
				)
				emit_core_hook("doc.open", "after", {
					doc = doc,
					filename = doc.filename,
					options = doc_options.clone_doc_options(open_doc_options),
					reused = true,
					source = "Doc.open_file",
				})

				return doc
			end
		end

		if missing_target then
			local opened = doc_options.handle_missing_file_open(absolute_filename or filename, "file not found", open_doc_options)

			if opened then
				emit_core_hook("doc.open", "after", {
					doc = opened,
					filename = opened.filename or absolute_filename or filename,
					options = doc_options.clone_doc_options(open_doc_options),
					reused = false,
					source = "Doc.open_file",
				})
			else
				emit_core_hook("doc.open", "error", {
					filename = absolute_filename or filename,
					options = doc_options.clone_doc_options(open_doc_options),
					force_options = force_load_options,
					source = "Doc.open_file",
					err = "file not found",
				})
			end

			return opened
		end

		local probe = lifecycle.detect_type(absolute_filename, { force_binary = force_binary })
		local is_binary = probe and probe.is_binary == true
		local is_image = probe and probe.is_image == true

		if is_binary then
			save_disabled_reason = force_binary and "binary mode forced by CLI" or "binary file"
		elseif is_image then
			save_disabled_reason = "image file"
		end

		local doc

		if is_binary then
			doc = Doc()
			doc.filename = absolute_filename or filename
			doc.is_binary = true
			doc.is_binary_placeholder = true
			doc.save_disabled = true
			doc.save_disabled_reason = save_disabled_reason or "binary file"
			doc.encoding_label = "Binary"
			doc.codepage_label = nil
			doc:reset_syntax()
		elseif is_image then
			doc = Doc()
			doc.filename = absolute_filename or filename
			doc.is_image = true
			doc.save_disabled = true
			doc.save_disabled_reason = save_disabled_reason or "image file"
			doc.syntax = nil
			doc.syntax_detect_meta = nil
			doc:reset_syntax()
		else
			doc = TextDocument()

			local ok, load_err = pcall(function()
				doc:open(filename, force_load_options)
			end)

			if not ok then
				emit_core_hook("doc.open", "error", {
					doc = doc,
					filename = filename,
					options = doc_options.clone_doc_options(open_doc_options),
					force_options = force_load_options,
					source = "Doc.open_file",
					err = load_err,
				})
				error(load_err)
			end

			doc.is_binary = false
			core.log_quiet(
				"[open-doc] loaded file=%s encoding=%s bom=%s codepage=%s labels={enc:%s cp:%s}",
				tostring(doc.filename),
				tostring(doc.file_encoding),
				tostring(doc.file_bom),
				tostring(doc.file_codepage),
				tostring(doc.encoding_label),
				tostring(doc.codepage_label)
			)
		end

		doc:reset_syntax()

		if save_disabled_reason and not doc.save_disabled then
			doc.save_disabled = true
			doc.save_disabled_reason = save_disabled_reason
			core.log('Opened "%s" with saving disabled (%s)', filename, save_disabled_reason)
		end

		doc_options.apply_doc_options_to_doc(doc, open_doc_options, false)

		core.log_quiet(
			"[open-doc] final file=%s encoding=%s bom=%s codepage=%s labels={enc:%s cp:%s}",
			tostring(doc.filename),
			tostring(doc.file_encoding),
			tostring(doc.file_bom),
			tostring(doc.file_codepage),
			tostring(doc.encoding_label),
			tostring(doc.codepage_label)
		)

		table.insert(core.docs, doc)

		emit_core_hook("doc.open", "after", {
			doc = doc,
			filename = doc.filename,
			options = doc_options.clone_doc_options(open_doc_options),
			reused = false,
			source = "Doc.open_file",
		})

		local duration_ms = (system.get_time() - start_time) * 1000

		if filename then
			core.log_quiet('Opened doc "%s" in %.1fms', filename, duration_ms)
		else
			core.log_quiet("Opened new doc in %.1fms", duration_ms)
		end

		return doc
	end

	return nil
end

return lifecycle
