package main

import "core:fmt"
import "core:os"
import "core:strconv"
import "core:strings"
import "../adapters"
import "../core"
import "../gui"
import "../shared"
import "../ui"

CLI_Command_Kind :: enum {
	Demo,
	Status,
	Save,
	Load,
	Tui,
	Gui,
	Help,
}

Parsed_CLI_Command :: struct {
	kind: CLI_Command_Kind,
	path: string,
}

parse_cli_command :: proc(args: []string) -> Parsed_CLI_Command {
	if len(args) == 0 {
		return Parsed_CLI_Command{kind = .Demo}
	}

	switch args[0] {
	case "status":
		return Parsed_CLI_Command{kind = .Status}
	case "save":
		if len(args) >= 2 {
			return Parsed_CLI_Command{kind = .Save, path = args[1]}
		}
		return Parsed_CLI_Command{kind = .Help}
	case "load":
		if len(args) >= 2 {
			return Parsed_CLI_Command{kind = .Load, path = args[1]}
		}
		return Parsed_CLI_Command{kind = .Help}
	case "tui":
		return Parsed_CLI_Command{kind = .Tui}
	case "gui":
		return Parsed_CLI_Command{kind = .Gui}
	case "help", "-h", "--help":
		return Parsed_CLI_Command{kind = .Help}
	}

	return Parsed_CLI_Command{kind = .Help}
}

usage_text :: proc() -> string {
	return "Usage: comic_odin [status|save <path>|load <path>|tui|gui|help]"
}

tui_help_text :: proc() -> string {
	return "TUI commands: help|h, status|s, doctor|?, ready|r, next|n, plan|p, auto|x, auto all <pdf|png|cbz> <path>, auto all local <pdf|png|cbz> <path> [pages], new, set idea <text>, set genre <text>, set audience <text>, generate script [pages], generate script local [pages], generate panels [page <n>], generate panels local [page <n>], layout auto, export <pdf|png|cbz> <path>, quick local <pdf|png|cbz> <path> [pages], quick local all <project_path> <pdf|png|cbz> <export_path> [pages], goto <screen> (or 1..8), step <workflow>, save|saveas <path>, load|open <path>, start <script|character|panel|export>, progress <0-100>, done|d, fail <message>, cancel|c, quit|q"
}

bool_text :: proc(v: bool) -> string {
	if v {
		return "yes"
	}
	return "no"
}

export_format_name :: proc(f: core.Export_Format) -> string {
	switch f {
	case .PDF: return "pdf"
	case .PNG: return "png"
	case .CBZ: return "cbz"
	}
	return "pdf"
}

command_available :: proc(name: string) -> bool {
	cmd := [2]string{"which", name}
	desc := os.Process_Desc{command = cmd[:]}
	state, _, _, err := os.process_exec(desc, context.temp_allocator)
	if err != nil {
		return false
	}
	return state.exited && state.exit_code == 0
}

build_doctor_report :: proc() -> string {
	cfg := shared.load_config()
	has_deepseek := len(cfg.deepseek_api_key) > 0
	has_fal := len(cfg.fal_api_key) > 0
	has_curl := command_available("curl")
	has_python := command_available("python3")
	return fmt.aprintf("Doctor\n- deepseek key: %s\n- fal key: %s\n- curl: %s\n- python3: %s", bool_text(has_deepseek), bool_text(has_fal), bool_text(has_curl), bool_text(has_python))
}

build_ready_report :: proc(c: ui.App_Controller) -> string {
	has_script := len(c.state.script.pages) > 0
	has_panels := len(c.state.panel_images) > 0
	has_layout := len(c.state.page_layouts) > 0
	can_export := has_layout && has_panels
	return fmt.aprintf("Ready\n- script generated: %s\n- panel images generated: %s\n- layout generated: %s\n- export ready: %s", bool_text(has_script), bool_text(has_panels), bool_text(has_layout), bool_text(can_export))
}

next_action_hint :: proc(c: ui.App_Controller) -> string {
	cfg := shared.load_config()
	if len(c.state.script.pages) == 0 {
		if len(cfg.deepseek_api_key) > 0 {
			return "next: generate script 4"
		}
		return "next: generate script local 2"
	}
	if len(c.state.panel_images) == 0 {
		if len(cfg.fal_api_key) > 0 {
			return "next: generate panels"
		}
		return "next: generate panels local"
	}
	if len(c.state.page_layouts) == 0 {
		return "next: layout auto"
	}
	return "next: export pdf ./comic.pdf"
}

plan_report :: proc(c: ui.App_Controller) -> string {
	script_done := len(c.state.script.pages) > 0
	panels_done := len(c.state.panel_images) > 0
	layout_done := len(c.state.page_layouts) > 0
	export_ready := panels_done && layout_done

	return fmt.aprintf(
		"Plan\n- [%-3s] 1) Script\n- [%-3s] 2) Panels\n- [%-3s] 3) Layout\n- [%-3s] 4) Export\n%s",
		bool_text(script_done),
		bool_text(panels_done),
		bool_text(layout_done),
		bool_text(export_ready),
		next_action_hint(c),
	)
}

collect_script_panels :: proc(script: core.Comic_Script) -> []core.Panel {
	out: [dynamic]core.Panel
	for p in script.pages {
		for pan in p.panels {
			append(&out, pan)
		}
	}
	return out[:]
}

collect_script_panels_for_page :: proc(script: core.Comic_Script, page_number: int) -> []core.Panel {
	out: [dynamic]core.Panel
	for p in script.pages {
		if p.page_number != page_number {
			continue
		}
		for pan in p.panels {
			append(&out, pan)
		}
	}
	return out[:]
}

parse_generate_script_pages :: proc(input: string) -> (int, bool, shared.App_Error) {
	trimmed := strings.trim_space(input)
	if trimmed == "generate script" {
		return 4, true, shared.ok()
	}
	if !strings.has_prefix(trimmed, "generate script ") || strings.has_prefix(trimmed, "generate script local") {
		return 0, false, shared.ok()
	}

	raw := strings.trim_space(trimmed[len("generate script "):])
	pages, ok := strconv.parse_int(raw)
	if !ok || pages <= 0 {
		return 0, true, shared.validation_error("generate script pages must be a positive integer")
	}
	return pages, true, shared.ok()
}

parse_generate_script_local_pages :: proc(input: string) -> (int, bool, shared.App_Error) {
	trimmed := strings.trim_space(input)
	if trimmed == "generate script local" {
		return 4, true, shared.ok()
	}
	if !strings.has_prefix(trimmed, "generate script local ") {
		return 0, false, shared.ok()
	}

	raw := strings.trim_space(trimmed[len("generate script local "):])
	pages, ok := strconv.parse_int(raw)
	if !ok || pages <= 0 {
		return 0, true, shared.validation_error("generate script local pages must be a positive integer")
	}
	return pages, true, shared.ok()
}

local_panel_id_by_index :: proc(i: int) -> string {
	switch i {
	case 0: return "panel_local_001"
	case 1: return "panel_local_002"
	case 2: return "panel_local_003"
	case 3: return "panel_local_004"
	case 4: return "panel_local_005"
	case 5: return "panel_local_006"
	case 6: return "panel_local_007"
	case 7: return "panel_local_008"
	case 8: return "panel_local_009"
	}
	return "panel_local_overflow"
}

build_local_script :: proc(story_idea: string, pages: int) -> core.Comic_Script {
	out_pages: [dynamic]core.Page
	for i in 0..<pages {
		chars_present := make([]string, 1)
		chars_present[0] = "char_001"
		dialogue := make([]core.Dialogue, 1)
		dialogue[0] = core.Dialogue{speaker_id = "char_001", text = "Let's do this.", bubble_type = .Normal, emotion = .Neutral}

		panel := core.Panel{
			panel_id = local_panel_id_by_index(i),
			panel_number = 1,
			shot_type = .Medium,
			description = story_idea,
			characters_present = chars_present,
			dialogue = dialogue,
			caption = "",
			sound_effects = nil,
			transition_from_previous = .None,
		}
		panels := make([]core.Panel, 1)
		panels[0] = panel
		append(&out_pages, core.Page{page_number = i + 1, layout_type = .Grid, panels = panels})
	}

	chars := make([]core.Character, 1)
	chars[0] = core.Character{id = "char_001", name = "Protagonist", role = .Protagonist, description = "Main character"}
	return core.Comic_Script{title = "Local Script", synopsis = story_idea, characters = chars, pages = out_pages[:]}
}

parse_generate_panels_page :: proc(input: string) -> (int, bool, shared.App_Error) {
	trimmed := strings.trim_space(input)
	if trimmed == "generate panels" {
		return 0, true, shared.ok()
	}
	if !strings.has_prefix(trimmed, "generate panels page ") || strings.has_prefix(trimmed, "generate panels local") {
		return 0, false, shared.ok()
	}

	raw := strings.trim_space(trimmed[len("generate panels page "):])
	page, ok := strconv.parse_int(raw)
	if !ok || page <= 0 {
		return 0, true, shared.validation_error("generate panels page must be a positive integer")
	}
	return page, true, shared.ok()
}

parse_generate_panels_local_page :: proc(input: string) -> (int, bool, shared.App_Error) {
	trimmed := strings.trim_space(input)
	if trimmed == "generate panels local" {
		return 0, true, shared.ok()
	}
	if !strings.has_prefix(trimmed, "generate panels local page ") {
		return 0, false, shared.ok()
	}

	raw := strings.trim_space(trimmed[len("generate panels local page "):])
	page, ok := strconv.parse_int(raw)
	if !ok || page <= 0 {
		return 0, true, shared.validation_error("generate panels local page must be a positive integer")
	}
	return page, true, shared.ok()
}

build_local_panel_images :: proc(panels: []core.Panel) -> (map[string]core.Panel_Image, shared.App_Error) {
	tmp_dir, terr := os.make_directory_temp("", "comic-local-panels-*", context.temp_allocator)
	if terr != nil {
		return nil, shared.new_error(.Generation, "failed to create local panel temp dir", true)
	}

	images := make(map[string]core.Panel_Image)
	for p, idx in panels {
		name := fmt.aprintf("panel_%03d_%s.png", idx+1, p.panel_id)
		out_path := fmt.aprintf("%s/%s", tmp_dir, name)
		delete(name)

		// Create a real PNG image using python3
		gen_w := 1024
		gen_h := 1024
		py_script := fmt.aprintf(
			"import struct,zlib,sys;w,h=%d,%d;rows=[]\nfor _ in range(h): rows.append(b'\\x00'+b'\\xff\\xff\\xff'*w)\nraw=b''.join(rows);comp=zlib.compress(raw)\ndef crc(d): return struct.pack('>I',zlib.crc32(d)&0xffffffff)\nf=open(sys.argv[1],'wb')\nf.write(b'\\x89PNG\\r\\n\\x1a\\n')\nihdr_data=struct.pack('>IIBBBBB',w,h,8,2,0,0,0)\nf.write(struct.pack('>I',13)+b'IHDR'+ihdr_data+crc(b'IHDR'+ihdr_data))\nf.write(struct.pack('>I',len(comp))+b'IDAT'+comp+crc(b'IDAT'+comp))\nf.write(struct.pack('>I',0)+b'IEND'+crc(b'IEND'))\nf.close()",
			gen_w, gen_h,
		)
		defer delete(py_script)

		py_cmd := [4]string{"python3", "-c", py_script, out_path}
		desc := os.Process_Desc{command = py_cmd[:]}
		state, _, stderr, cerr := os.process_exec(desc, context.temp_allocator)
		if cerr != nil || !state.exited || state.exit_code != 0 {
			delete(out_path)
			msg := fmt.aprintf("failed to create panel image: %s", string(stderr))
			defer delete(msg)
			return nil, shared.new_error(.Generation, msg, true)
		}

		url := fmt.aprintf("file://%s", out_path)
		prompt := fmt.aprintf("local panel %d", idx+1)
		images[p.panel_id] = core.Panel_Image{url = url, width = gen_w, height = gen_h, seed = i64(idx + 1), prompt = prompt}
		delete(out_path)
	}
	return images, shared.ok()
}

parse_export_command :: proc(input: string) -> (core.Export_Format, string, bool, shared.App_Error) {
	trimmed := strings.trim_space(input)
	if !strings.has_prefix(trimmed, "export ") {
		return .PDF, "", false, shared.ok()
	}

	rest := strings.trim_space(trimmed[len("export "):])
	sp := strings.index(rest, " ")
	if sp < 0 {
		return .PDF, "", true, shared.validation_error("usage: export <pdf|png|cbz> <path>")
	}

	fmt_name := strings.trim_space(rest[:sp])
	out_path := strings.trim_space(rest[sp+1:])
	if len(out_path) == 0 {
		return .PDF, "", true, shared.validation_error("export path is required")
	}

	switch fmt_name {
	case "pdf": return .PDF, out_path, true, shared.ok()
	case "png": return .PNG, out_path, true, shared.ok()
	case "cbz": return .CBZ, out_path, true, shared.ok()
	}

	return .PDF, "", true, shared.validation_error("unknown export format")
}

parse_quick_local_command :: proc(input: string) -> (core.Export_Format, string, int, bool, shared.App_Error) {
	trimmed := strings.trim_space(input)
	if !strings.has_prefix(trimmed, "quick local ") || strings.has_prefix(trimmed, "quick local all ") {
		return .PDF, "", 0, false, shared.ok()
	}

	rest := strings.trim_space(trimmed[len("quick local "):])
	parts, ferr := strings.fields(rest)
	if ferr != nil {
		return .PDF, "", 0, true, shared.new_error(.Validation, "quick local parse failed", true)
	}
	defer delete(parts)

	if len(parts) < 2 || len(parts) > 3 {
		return .PDF, "", 0, true, shared.validation_error("usage: quick local <pdf|png|cbz> <path> [pages]")
	}

	fmt_name := parts[0]
	out_path := parts[1]
	if len(out_path) == 0 {
		return .PDF, "", 0, true, shared.validation_error("quick local export path is required")
	}

	pages := 2
	if len(parts) == 3 {
		v, ok := strconv.parse_int(parts[2])
		if !ok || v <= 0 {
			return .PDF, "", 0, true, shared.validation_error("quick local pages must be a positive integer")
		}
		pages = v
	}

	switch fmt_name {
	case "pdf": return .PDF, out_path, pages, true, shared.ok()
	case "png": return .PNG, out_path, pages, true, shared.ok()
	case "cbz": return .CBZ, out_path, pages, true, shared.ok()
	}

	return .PDF, "", 0, true, shared.validation_error("unknown quick local export format")
}

parse_quick_local_all_command :: proc(input: string) -> (string, core.Export_Format, string, int, bool, shared.App_Error) {
	trimmed := strings.trim_space(input)
	if !strings.has_prefix(trimmed, "quick local all ") {
		return "", .PDF, "", 0, false, shared.ok()
	}

	rest := strings.trim_space(trimmed[len("quick local all "):])
	parts, ferr := strings.fields(rest)
	if ferr != nil {
		return "", .PDF, "", 0, true, shared.new_error(.Validation, "quick local all parse failed", true)
	}
	defer delete(parts)

	if len(parts) < 3 || len(parts) > 4 {
		return "", .PDF, "", 0, true, shared.validation_error("usage: quick local all <project_path> <pdf|png|cbz> <export_path> [pages]")
	}

	project_path := parts[0]
	fmt_name := parts[1]
	export_path := parts[2]
	if len(project_path) == 0 || len(export_path) == 0 {
		return "", .PDF, "", 0, true, shared.validation_error("quick local all paths are required")
	}

	pages := 2
	if len(parts) == 4 {
		v, ok := strconv.parse_int(parts[3])
		if !ok || v <= 0 {
			return "", .PDF, "", 0, true, shared.validation_error("quick local all pages must be a positive integer")
		}
		pages = v
	}

	switch fmt_name {
	case "pdf": return project_path, .PDF, export_path, pages, true, shared.ok()
	case "png": return project_path, .PNG, export_path, pages, true, shared.ok()
	case "cbz": return project_path, .CBZ, export_path, pages, true, shared.ok()
	}

	return "", .PDF, "", 0, true, shared.validation_error("unknown quick local all export format")
}

parse_auto_all_command :: proc(input: string) -> (core.Export_Format, string, bool, shared.App_Error) {
	trimmed := strings.trim_space(input)
	if !strings.has_prefix(trimmed, "auto all ") || strings.has_prefix(trimmed, "auto all local ") {
		return .PDF, "", false, shared.ok()
	}

	rest := strings.trim_space(trimmed[len("auto all "):])
	parts, ferr := strings.fields(rest)
	if ferr != nil {
		return .PDF, "", true, shared.new_error(.Validation, "auto all parse failed", true)
	}
	defer delete(parts)

	if len(parts) != 2 {
		return .PDF, "", true, shared.validation_error("usage: auto all <pdf|png|cbz> <path>")
	}

	switch parts[0] {
	case "pdf": return .PDF, parts[1], true, shared.ok()
	case "png": return .PNG, parts[1], true, shared.ok()
	case "cbz": return .CBZ, parts[1], true, shared.ok()
	}
	return .PDF, "", true, shared.validation_error("unknown auto all export format")
}

parse_auto_all_local_command :: proc(input: string) -> (core.Export_Format, string, int, bool, shared.App_Error) {
	trimmed := strings.trim_space(input)
	if !strings.has_prefix(trimmed, "auto all local ") {
		return .PDF, "", 0, false, shared.ok()
	}

	rest := strings.trim_space(trimmed[len("auto all local "):])
	parts, ferr := strings.fields(rest)
	if ferr != nil {
		return .PDF, "", 0, true, shared.new_error(.Validation, "auto all local parse failed", true)
	}
	defer delete(parts)

	if len(parts) < 2 || len(parts) > 3 {
		return .PDF, "", 0, true, shared.validation_error("usage: auto all local <pdf|png|cbz> <path> [pages]")
	}

	pages := 2
	if len(parts) == 3 {
		v, ok := strconv.parse_int(parts[2])
		if !ok || v <= 0 {
			return .PDF, "", 0, true, shared.validation_error("auto all local pages must be a positive integer")
		}
		pages = v
	}

	switch parts[0] {
	case "pdf": return .PDF, parts[1], pages, true, shared.ok()
	case "png": return .PNG, parts[1], pages, true, shared.ok()
	case "cbz": return .CBZ, parts[1], pages, true, shared.ok()
	}
	return .PDF, "", 0, true, shared.validation_error("unknown auto all local export format")
}

run_quick_local_pipeline :: proc(controller: ^ui.App_Controller, export_format: core.Export_Format, export_path: string, pages: int) -> shared.App_Error {
	story := controller.state.story_idea
	if len(story) == 0 {
		story = "A local adventure"
	}

	script := build_local_script(story, pages)
	core.dispose_script(&controller.state.script)
	controller.state.script = script
	controller.state.characters = controller.state.script.characters

	panels := collect_script_panels(controller.state.script)
	defer delete(panels)
	if len(panels) == 0 {
		return shared.validation_error("quick local failed: no panels")
	}

	images, lerr := build_local_panel_images(panels)
	if !shared.is_ok(lerr) {
		return lerr
	}
	for _, img in controller.state.panel_images {
		delete(img.url)
		delete(img.prompt)
	}
	delete(controller.state.panel_images)
	controller.state.panel_images = images

	core.dispose_page_layouts(&controller.state.page_layouts)
	controller.state.page_layouts = core.auto_layout_pages(panels, controller.state.page_size, controller.state.story_genre, "")

	opts := adapters.Export_Options{format = export_format, page_size = controller.state.page_size, dpi = 300, quality = 90}
	err := adapters.export_comic_stub(export_path, controller.state.page_layouts, controller.state.panel_images, opts)
	if !shared.is_ok(err) {
		controller.state.workflow.error_message = err.message
		return err
	}
	controller.state.export_format = export_format
	controller.active_screen = .Export
	controller.state.workflow.current_step = .Complete
	controller.state.workflow.error_message = ""
	return shared.ok()
}

clear_screen :: proc() {
	fmt.print("\x1b[2J\x1b[H")
}

normalize_tui_command :: proc(input: string) -> string {
	cmd := strings.trim_space(input)
	switch cmd {
	case "h": return "help"
	case "s": return "status"
	case "?": return "doctor"
	case "r": return "ready"
	case "n": return "next"
	case "p": return "plan"
	case "x": return "auto"
	case "d": return "done"
	case "c": return "cancel"
	case "q": return "quit"
	case "1": return "goto story"
	case "2": return "goto script"
	case "3": return "goto characters"
	case "4": return "goto panels"
	case "5": return "goto layout"
	case "6": return "goto bubbles"
	case "7": return "goto export"
	case "8": return "goto community"
	}
	return cmd
}

screen_from_name :: proc(s: string) -> (ui.App_Screen, bool) {
	switch s {
	case "story": return .Story, true
	case "script": return .Script, true
	case "characters": return .Characters, true
	case "panels": return .Panels, true
	case "layout": return .Layout, true
	case "bubbles", "speech": return .Bubbles, true
	case "export": return .Export, true
	case "community": return .Community, true
	}
	return .Story, false
}

workflow_from_name :: proc(s: string) -> (core.Workflow_Step, bool) {
	switch s {
	case "story": return .Story_Input, true
	case "generating-script": return .Generating_Script, true
	case "script-review": return .Script_Review, true
	case "character-setup": return .Character_Setup, true
	case "generating-panels": return .Generating_Panels, true
	case "layout": return .Layout, true
	case "speech-bubbles": return .Speech_Bubbles, true
	case "complete": return .Complete, true
	}
	return .Story_Input, false
}

job_type_from_name :: proc(s: string) -> (ui.Job_Type, bool) {
	switch s {
	case "script": return .Generate_Script, true
	case "character": return .Generate_Character, true
	case "panel": return .Generate_Panel, true
	case "export": return .Export, true
	}
	return .Generate_Script, false
}

read_stdin_line :: proc() -> (line: string, ok: bool, err: shared.App_Error) {
	buf: [dynamic]u8
	one: [1]u8

	for {
		n, rerr := os.read(os.stdin, one[:])
		if rerr != nil {
			if rerr == .EOF {
				if len(buf) == 0 {
					return "", false, shared.ok()
				}
				break
			}
			delete(buf)
			return "", false, shared.new_error(.Config, "stdin read error", true)
		}
		if n == 0 {
			continue
		}
		if one[0] == '\n' {
			break
		}
		if one[0] != '\r' {
			append(&buf, one[0])
		}
	}

	return string(buf[:]), true, shared.ok()
}

run_tui_command :: proc(controller: ^ui.App_Controller, input: string, last_job_id: ^int) -> (quit: bool, out: string, err: shared.App_Error) {
	if input == "help" {
		return false, fmt.aprintf("%s", tui_help_text()), shared.ok()
	}
	if input == "status" {
		return false, ui.render_app_to_string(controller^), shared.ok()
	}
	if input == "doctor" {
		return false, build_doctor_report(), shared.ok()
	}
	if input == "ready" {
		return false, build_ready_report(controller^), shared.ok()
	}
	if input == "next" {
		return false, fmt.aprintf("%s", next_action_hint(controller^)), shared.ok()
	}
	if input == "plan" {
		return false, plan_report(controller^), shared.ok()
	}
	auto_all_local_format, auto_all_local_path, auto_all_local_pages, is_auto_all_local, aalerr := parse_auto_all_local_command(input)
	if !shared.is_ok(aalerr) {
		return false, "", aalerr
	}
	if is_auto_all_local {
		if err := run_quick_local_pipeline(controller, auto_all_local_format, auto_all_local_path, auto_all_local_pages); !shared.is_ok(err) {
			return false, "", err
		}
		return false, fmt.aprintf("auto all local exported %s (%d pages)", auto_all_local_path, auto_all_local_pages), shared.ok()
	}

	auto_all_format, auto_all_path, is_auto_all, aaerr := parse_auto_all_command(input)
	if !shared.is_ok(aaerr) {
		return false, "", aaerr
	}
	if is_auto_all {
		for i in 0..<6 {
			hint := next_action_hint(controller^)
			if strings.has_prefix(hint, "next: export ") {
				export_cmd := fmt.aprintf("export %s %s", export_format_name(auto_all_format), auto_all_path)
				_, out, eerr := run_tui_command(controller, export_cmd, last_job_id)
				delete(export_cmd)
				if !shared.is_ok(eerr) {
					return false, "", eerr
				}
				if len(out) > 0 { delete(out) }
				return false, fmt.aprintf("auto all exported %s", auto_all_path), shared.ok()
			}
			if !strings.has_prefix(hint, "next: ") {
				return false, "", shared.validation_error("auto all failed: invalid next hint")
			}
			next_cmd := strings.trim_space(hint[len("next: "):])
			_, out, aerr := run_tui_command(controller, next_cmd, last_job_id)
			if len(out) > 0 { delete(out) }
			if !shared.is_ok(aerr) {
				return false, "", aerr
			}
		}
		return false, "", shared.validation_error("auto all exceeded step limit")
	}

	if input == "auto" {
		hint := next_action_hint(controller^)
		if !strings.has_prefix(hint, "next: ") {
			return false, "", shared.validation_error("auto failed: invalid next hint")
		}
		next_cmd := strings.trim_space(hint[len("next: "):])
		_, out, aerr := run_tui_command(controller, next_cmd, last_job_id)
		if !shared.is_ok(aerr) {
			return false, "", aerr
		}
		if len(out) > 0 {
			msg := fmt.aprintf("auto ran: %s\n%s", next_cmd, out)
			delete(out)
			return false, msg, shared.ok()
		}
		return false, fmt.aprintf("auto ran: %s", next_cmd), shared.ok()
	}
	if input == "done" {
		if last_job_id^ <= 0 {
			return false, "", shared.validation_error("no active job")
		}
		err = ui.finish_background_job(controller, last_job_id^, "")
		if !shared.is_ok(err) {
			return false, "", err
		}
		return false, fmt.aprintf("job marked done"), shared.ok()
	}
	if input == "cancel" {
		if last_job_id^ <= 0 {
			return false, "", shared.validation_error("no active job")
		}
		err = ui.cancel_background_job(controller, last_job_id^)
		if !shared.is_ok(err) {
			return false, "", err
		}
		return false, fmt.aprintf("job cancelled"), shared.ok()
	}
	if input == "quit" || input == "exit" {
		return true, fmt.aprintf("exiting tui"), shared.ok()
	}

	if strings.has_prefix(input, "goto ") {
		target_name := strings.trim_space(input[len("goto "):])
		target, ok := screen_from_name(target_name)
		if !ok {
			return false, "", shared.validation_error("unknown screen")
		}
		err = ui.navigate_to_screen(controller, target)
		if !shared.is_ok(err) {
			return false, "", err
		}
		return false, fmt.aprintf("screen: %s", ui.screen_name(target)), shared.ok()
	}

	if strings.has_prefix(input, "step ") {
		step_name := strings.trim_space(input[len("step "):])
		next, ok := workflow_from_name(step_name)
		if !ok {
			return false, "", shared.validation_error("unknown workflow step")
		}
		err = ui.set_workflow_step(controller, next)
		if !shared.is_ok(err) {
			return false, "", err
		}
		return false, fmt.aprintf("step: %v", next), shared.ok()
	}

	if strings.trim_space(input) == "new" {
		core.dispose_state(&controller.state)
		controller.state = core.new_initial_state()
		controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
		last_job_id^ = 0
		return false, fmt.aprintf("new project initialized"), shared.ok()
	}

	local_pages, is_generate_local_script, lserr := parse_generate_script_local_pages(input)
	if !shared.is_ok(lserr) {
		return false, "", lserr
	}
	if is_generate_local_script {
		core.set_workflow_step(&controller.state, .Generating_Script)
		controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
		controller.state.workflow.is_generating = true
		controller.state.workflow.generation_progress = 25

		script := build_local_script(controller.state.story_idea, local_pages)
		core.dispose_script(&controller.state.script)
		controller.state.script = script
		controller.state.characters = controller.state.script.characters
		core.set_workflow_step(&controller.state, .Script_Review)
		controller.active_screen = .Script
		controller.state.workflow.is_generating = false
		controller.state.workflow.generation_progress = 100
		controller.state.workflow.error_message = ""
		return false, fmt.aprintf("local script generated (%d pages)", local_pages), shared.ok()
	}

	script_pages, is_generate_script, pserr := parse_generate_script_pages(input)
	if !shared.is_ok(pserr) {
		return false, "", pserr
	}
	if is_generate_script {
		cfg := shared.load_config()
		core.set_workflow_step(&controller.state, .Generating_Script)
		controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
		controller.state.workflow.is_generating = true
		controller.state.workflow.generation_progress = 25

		opts := adapters.Generate_Script_Options{
			story_idea = controller.state.story_idea,
			genre = controller.state.story_genre,
			art_style = controller.state.art_style,
			num_pages = script_pages,
			audience = controller.state.target_audience,
		}
		script, gerr := adapters.generate_comic_script_stub(cfg, opts)
		if !shared.is_ok(gerr) {
			controller.state.workflow.is_generating = false
			controller.state.workflow.error_message = gerr.message
			controller.state.workflow.current_step = .Story_Input
			controller.active_screen = .Story
			return false, "", gerr
		}

		core.dispose_script(&controller.state.script)
		controller.state.script = script
		controller.state.characters = controller.state.script.characters
		core.set_workflow_step(&controller.state, .Script_Review)
		controller.active_screen = .Script
		controller.state.workflow.is_generating = false
		controller.state.workflow.generation_progress = 100
		controller.state.workflow.error_message = ""
		return false, fmt.aprintf("script generated (%d pages)", script_pages), shared.ok()
	}

	panel_local_page, is_generate_panels_local, plerr := parse_generate_panels_local_page(input)
	if !shared.is_ok(plerr) {
		return false, "", plerr
	}
	if is_generate_panels_local {
		panels: []core.Panel
		if panel_local_page > 0 {
			panels = collect_script_panels_for_page(controller.state.script, panel_local_page)
		} else {
			panels = collect_script_panels(controller.state.script)
		}
		defer delete(panels)
		if len(panels) == 0 {
			return false, "", shared.validation_error("no script panels available")
		}

		core.set_workflow_step(&controller.state, .Generating_Panels)
		controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
		controller.state.workflow.is_generating = true
		controller.state.workflow.generation_progress = 35

		images, gerr := build_local_panel_images(panels)
		if !shared.is_ok(gerr) {
			controller.state.workflow.is_generating = false
			controller.state.workflow.error_message = gerr.message
			return false, "", gerr
		}

		for _, img in controller.state.panel_images {
			delete(img.url)
			delete(img.prompt)
		}
		delete(controller.state.panel_images)
		controller.state.panel_images = images
		core.set_workflow_step(&controller.state, .Layout)
		controller.active_screen = .Layout
		controller.state.workflow.is_generating = false
		controller.state.workflow.generation_progress = 100
		controller.state.workflow.error_message = ""
		if panel_local_page > 0 {
			return false, fmt.aprintf("local panel images generated for page %d (%d)", panel_local_page, len(images)), shared.ok()
		}
		return false, fmt.aprintf("local panel images generated (%d)", len(images)), shared.ok()
	}

	panel_page, is_generate_panels, pperr := parse_generate_panels_page(input)
	if !shared.is_ok(pperr) {
		return false, "", pperr
	}
	if is_generate_panels {
		cfg := shared.load_config()
		panels: []core.Panel
		if panel_page > 0 {
			panels = collect_script_panels_for_page(controller.state.script, panel_page)
		} else {
			panels = collect_script_panels(controller.state.script)
		}
		defer delete(panels)
		if len(panels) == 0 {
			return false, "", shared.validation_error("no script panels available")
		}

		core.set_workflow_step(&controller.state, .Generating_Panels)
		controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
		controller.state.workflow.is_generating = true
		controller.state.workflow.generation_progress = 35

		q := adapters.new_fal_queue(2)
		client := adapters.new_fal_client(&q)
		images, gerr := adapters.generate_all_panels_batched(client, cfg, panels, controller.state.characters, controller.state.art_style, controller.state.project.project_id, nil)
		if !shared.is_ok(gerr) {
			controller.state.workflow.is_generating = false
			controller.state.workflow.error_message = gerr.message
			return false, "", gerr
		}

		for _, img in controller.state.panel_images {
			delete(img.url)
			delete(img.prompt)
		}
		delete(controller.state.panel_images)
		controller.state.panel_images = images
		core.set_workflow_step(&controller.state, .Layout)
		controller.active_screen = .Layout
		controller.state.workflow.is_generating = false
		controller.state.workflow.generation_progress = 100
		controller.state.workflow.error_message = ""
		if panel_page > 0 {
			return false, fmt.aprintf("panel images generated for page %d (%d)", panel_page, len(images)), shared.ok()
		}
		return false, fmt.aprintf("panel images generated (%d)", len(images)), shared.ok()
	}

	if strings.trim_space(input) == "layout auto" {
		panels := collect_script_panels(controller.state.script)
		defer delete(panels)
		if len(panels) == 0 {
			return false, "", shared.validation_error("no script panels available")
		}
		core.dispose_page_layouts(&controller.state.page_layouts)
		controller.state.page_layouts = core.auto_layout_pages(panels, controller.state.page_size, controller.state.story_genre, "")
		controller.state.workflow.current_step = .Layout
		controller.active_screen = .Layout
		return false, fmt.aprintf("layout generated (%d pages)", len(controller.state.page_layouts)), shared.ok()
	}

	project_path, quick_all_format, quick_all_export_path, quick_all_pages, is_quick_local_all, qaerr := parse_quick_local_all_command(input)
	if !shared.is_ok(qaerr) {
		return false, "", qaerr
	}
	if is_quick_local_all {
		if err := run_quick_local_pipeline(controller, quick_all_format, quick_all_export_path, quick_all_pages); !shared.is_ok(err) {
			return false, "", err
		}
		err = adapters.save_project(project_path, controller.state)
		if !shared.is_ok(err) {
			return false, "", err
		}
		return false, fmt.aprintf("quick local all saved %s and exported %s (%d pages)", project_path, quick_all_export_path, quick_all_pages), shared.ok()
	}

	quick_format, quick_path, quick_pages, is_quick_local, qerr := parse_quick_local_command(input)
	if !shared.is_ok(qerr) {
		return false, "", qerr
	}
	if is_quick_local {
		if err := run_quick_local_pipeline(controller, quick_format, quick_path, quick_pages); !shared.is_ok(err) {
			return false, "", err
		}
		return false, fmt.aprintf("quick local exported %s (%d pages)", quick_path, quick_pages), shared.ok()
	}

	export_format, export_path, is_export, exerr := parse_export_command(input)
	if !shared.is_ok(exerr) {
		return false, "", exerr
	}
	if is_export {
		opts := adapters.Export_Options{format = export_format, page_size = controller.state.page_size, dpi = 300, quality = 90}
		err = adapters.export_comic_stub(export_path, controller.state.page_layouts, controller.state.panel_images, opts)
		if !shared.is_ok(err) {
			controller.state.workflow.error_message = err.message
			return false, "", err
		}
		controller.state.export_format = export_format
		controller.active_screen = .Export
		controller.state.workflow.current_step = .Complete
		controller.state.workflow.error_message = ""
		return false, fmt.aprintf("exported %s", export_path), shared.ok()
	}

	if strings.has_prefix(input, "set idea ") {
		v := strings.trim_space(input[len("set idea "):])
		controller.state.story_idea = fmt.aprintf("%s", v)
		return false, fmt.aprintf("story idea updated"), shared.ok()
	}

	if strings.has_prefix(input, "set genre ") {
		v := strings.trim_space(input[len("set genre "):])
		controller.state.story_genre = fmt.aprintf("%s", v)
		return false, fmt.aprintf("story genre updated"), shared.ok()
	}

	if strings.has_prefix(input, "set audience ") {
		v := strings.trim_space(input[len("set audience "):])
		controller.state.target_audience = fmt.aprintf("%s", v)
		return false, fmt.aprintf("target audience updated"), shared.ok()
	}

	if strings.has_prefix(input, "saveas ") {
		path := strings.trim_space(input[len("saveas "):])
		err = adapters.save_project(path, controller.state)
		if !shared.is_ok(err) {
			return false, "", err
		}
		return false, fmt.aprintf("saved to %s", path), shared.ok()
	}

	if strings.has_prefix(input, "save ") {
		path := strings.trim_space(input[len("save "):])
		err = adapters.save_project(path, controller.state)
		if !shared.is_ok(err) {
			return false, "", err
		}
		return false, fmt.aprintf("saved to %s", path), shared.ok()
	}

	if strings.has_prefix(input, "open ") {
		path := strings.trim_space(input[len("open "):])
		loaded, lerr := adapters.load_project(path)
		if !shared.is_ok(lerr) {
			return false, "", lerr
		}
		core.dispose_state(&controller.state)
		controller.state = loaded
		controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
		return false, fmt.aprintf("loaded from %s", path), shared.ok()
	}

	if strings.has_prefix(input, "load ") {
		path := strings.trim_space(input[len("load "):])
		loaded, lerr := adapters.load_project(path)
		if !shared.is_ok(lerr) {
			return false, "", lerr
		}
		core.dispose_state(&controller.state)
		controller.state = loaded
		controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
		return false, fmt.aprintf("loaded from %s", path), shared.ok()
	}

	if strings.has_prefix(input, "start ") {
		name := strings.trim_space(input[len("start "):])
		job_type, ok := job_type_from_name(name)
		if !ok {
			return false, "", shared.validation_error("unknown job type")
		}
		id := ui.start_background_job(controller, job_type, "job")
		_ = ui.mark_job_running(&controller.jobs, id)
		last_job_id^ = id
		return false, fmt.aprintf("started job %d", id), shared.ok()
	}

	if strings.has_prefix(input, "progress ") {
		v := strings.trim_space(input[len("progress "):])
		n, ok := strconv.parse_int(v)
		if !ok {
			return false, "", shared.validation_error("invalid progress")
		}
		if n < 0 || n > 100 {
			return false, "", shared.validation_error("progress must be 0..100")
		}
		ui.set_generation_progress(controller, f32(n))
		controller.state.workflow.is_generating = true
		return false, fmt.aprintf("progress set to %d", n), shared.ok()
	}

	if strings.has_prefix(input, "fail ") {
		if last_job_id^ <= 0 {
			return false, "", shared.validation_error("no active job")
		}
		msg := strings.trim_space(input[len("fail "):])
		if len(msg) == 0 {
			msg = "job failed"
		}
		err = ui.finish_background_job(controller, last_job_id^, msg)
		if !shared.is_ok(err) {
			return false, "", err
		}
		return false, fmt.aprintf("job marked failed"), shared.ok()
	}

	return false, "", shared.validation_error("unknown command")
}

run_tui_loop :: proc(state: ^core.Comic_State) -> (string, shared.App_Error) {
	controller := ui.new_controller(state^)
	defer ui.dispose_job_manager(&controller.jobs)

	last_job_id := 0

	for {
		clear_screen()
		fmt.println("comic-odin interactive tui")
		fmt.println("screens: [1]Story [2]Script [3]Characters [4]Panels [5]Layout [6]Bubbles [7]Export [8]Community")
		fmt.println("shortcuts: h help | s status | ? doctor | r ready | n next | p plan | x auto | d done | c cancel | q quit")
		fmt.println()
		view := ui.render_app_to_string(controller)
		fmt.println(view)
		delete(view)
		fmt.print("\ntui> ")

		line, ok, rerr := read_stdin_line()
		if !shared.is_ok(rerr) {
			return "", rerr
		}
		if !ok {
			break
		}

		cmd := normalize_tui_command(line)
		if len(cmd) == 0 {
			delete(line)
			continue
		}

		quit, out, cerr := run_tui_command(&controller, cmd, &last_job_id)
		delete(line)
		if !shared.is_ok(cerr) {
			fmt.printf("error: %s\n", cerr.message)
		} else if len(out) > 0 {
			fmt.println(out)
			delete(out)
		}
		if quit {
			break
		}
	}

	core.dispose_state(state)
	state^ = controller.state
	controller.state = core.Comic_State{}
	return "", shared.ok()
}

run_cli_command :: proc(cmd: Parsed_CLI_Command, state: ^core.Comic_State) -> (string, shared.App_Error) {
	switch cmd.kind {
	case .Demo:
		controller := ui.new_controller(state^)
		defer ui.dispose_controller(&controller)
		job_id := ui.start_background_job(&controller, .Generate_Script, "Generating script")
		_ = ui.mark_job_running(&controller.jobs, job_id)
		ui.set_generation_progress(&controller, 35)
		_ = ui.finish_background_job(&controller, job_id, "")
		return ui.render_app_to_string(controller), shared.ok()
	case .Status:
		controller := ui.new_controller(state^)
		defer ui.dispose_controller(&controller)
		return ui.render_app_to_string(controller), shared.ok()
	case .Save:
		if len(cmd.path) == 0 {
			return usage_text(), shared.validation_error("missing save path")
		}
		err := adapters.save_project(cmd.path, state^)
		if !shared.is_ok(err) {
			return "", err
		}
		return fmt.aprintf("Saved project to %s", cmd.path), shared.ok()
	case .Load:
		if len(cmd.path) == 0 {
			return usage_text(), shared.validation_error("missing load path")
		}
		loaded, err := adapters.load_project(cmd.path)
		if !shared.is_ok(err) {
			return "", err
		}
		core.dispose_state(state)
		state^ = loaded
		return fmt.aprintf("Loaded project from %s", cmd.path), shared.ok()
	case .Tui:
		return run_tui_loop(state)
	case .Gui:
		err := gui.run_gui_app(state)
		if !shared.is_ok(err) {
			return "", err
		}
		return fmt.aprintf("GUI session ended"), shared.ok()
	case .Help:
		fallthrough
	case:
		return usage_text(), shared.ok()
	}
}

run_cli_from_process_args :: proc(state: ^core.Comic_State) -> (string, shared.App_Error) {
	if len(os.args) <= 1 {
		return run_cli_command(Parsed_CLI_Command{kind = .Demo}, state)
	}
	cmd := parse_cli_command(os.args[1:])
	return run_cli_command(cmd, state)
}
