package adapters

import json "core:encoding/json"
import "core:fmt"
import "core:os"
import "core:strings"
import "../core"
import "../shared"

Generate_Script_Options :: struct {
	story_idea: string,
	genre: string,
	art_style: string,
	num_pages: int,
	audience: string,
}

Deepseek_Request_Message :: struct {
	role: string,
	content: string,
}

Deepseek_Request_Response_Format :: struct {
	type: string,
}

Deepseek_Request_Body :: struct {
	model: string,
	messages: []Deepseek_Request_Message,
	response_format: Deepseek_Request_Response_Format,
	temperature: f32,
}

Deepseek_Transport :: #type proc(cfg: shared.Config, request_json: string) -> (response_json: string, status_code: int, err: shared.App_Error)

Deepseek_Client :: struct {
	transport: Deepseek_Transport,
	max_retries: int,
	initial_backoff_ms: int,
}

Deepseek_Response_Message :: struct {
	content: string,
}

Deepseek_Response_Choice :: struct {
	message: Deepseek_Response_Message,
}

Deepseek_Response :: struct {
	choices: []Deepseek_Response_Choice,
}

Raw_Dialogue :: struct {
	speakerId: string,
	text: string,
	bubbleType: string,
	emotion: string,
}

Raw_Panel :: struct {
	panelId: string,
	panelNumber: int,
	shotType: string,
	description: string,
	charactersPresent: []string,
	dialogue: []Raw_Dialogue,
	caption: string,
	soundEffects: []string,
	transitionFromPrevious: string,
}

Raw_Page :: struct {
	pageNumber: int,
	layoutType: string,
	panels: []Raw_Panel,
}

Raw_Character :: struct {
	id: string,
	name: string,
	role: string,
	description: string,
	firstAppearancePanel: string,
}

Raw_Script :: struct {
	title: string,
	synopsis: string,
	characters: []Raw_Character,
	pages: []Raw_Page,
}

Raw_Dialogue_Alt :: struct {
	speaker_id: string,
	text: string,
	bubble_type: string,
	emotion: string,
}

Raw_Panel_Alt :: struct {
	panel_id: string,
	panel_number: int,
	shot_type: string,
	description: string,
	characters_present: []string,
	dialogue: []Raw_Dialogue_Alt,
	caption: string,
	sound_effects: []string,
	transition_from_previous: string,
}

Raw_Page_Alt :: struct {
	page_number: int,
	layout_type: string,
	panels: []Raw_Panel_Alt,
}

Raw_Character_Alt :: struct {
	id: string,
	name: string,
	role: string,
	description: string,
	first_appearance_panel: string,
}

Raw_Script_Alt :: struct {
	title: string,
	synopsis: string,
	characters: []Raw_Character_Alt,
	pages: []Raw_Page_Alt,
}

Raw_Script_Wrapper :: struct {
	script: Raw_Script,
}

Raw_Script_Alt_Wrapper :: struct {
	script: Raw_Script_Alt,
}

deepseek_parse_curl_output :: proc(output: string) -> (body: string, status_code: int, ok: bool) {
	marker := "__STATUS__:"
	idx := strings.last_index(output, marker)
	if idx < 0 {
		return output, 0, false
	}

	body = output[:idx]
	status_str := output[idx+len(marker):]
	status_code = 0
	for c in status_str {
		if c < '0' || c > '9' {
			break
		}
		status_code = status_code*10 + int(c-'0')
	}
	return body, status_code, true
}

default_deepseek_transport :: proc(cfg: shared.Config, request_json: string) -> (string, int, shared.App_Error) {
	url := fmt.aprintf("%s/chat/completions", cfg.deepseek_base_url)
	auth := fmt.aprintf("Authorization: Bearer %s", cfg.deepseek_api_key)

	cmd := [13]string{
		"curl", "-sS", "-X", "POST", url,
		"-H", "Content-Type: application/json",
		"-H", auth,
		"-d", request_json,
		"-w", "\\n__STATUS__:%{http_code}",
	}
	desc := os.Process_Desc{command = cmd[:]}
	state, stdout, stderr, exec_err := os.process_exec(desc, context.temp_allocator)
	if exec_err != nil {
		return "", 0, shared.network_error(fmt.aprintf("curl execution failed: %v", exec_err))
	}
	if !state.exited || state.exit_code != 0 {
		return "", 0, shared.network_error(fmt.aprintf("curl failed: %s", string(stderr)))
	}

	body, status_code, ok := deepseek_parse_curl_output(string(stdout))
	if !ok {
		return string(stdout), 0, shared.network_error("unable to parse curl status output")
	}
	return body, status_code, shared.ok()
}

new_deepseek_client :: proc() -> Deepseek_Client {
	return Deepseek_Client{
		transport = default_deepseek_transport,
		max_retries = 3,
		initial_backoff_ms = 500,
	}
}

backoff_ms :: proc(initial_ms, attempt: int) -> int {
	if attempt <= 0 {
		return 0
	}
	mul := 1 << u32(attempt-1)
	return initial_ms * mul
}

extract_deepseek_error_message :: proc(body: string) -> string {
	trimmed := strings.trim_space(body)
	if len(trimmed) == 0 {
		return ""
	}
	marker := "\"message\":\""
	idx := strings.index(trimmed, marker)
	if idx < 0 {
		if len(trimmed) > 180 {
			return fmt.aprintf("%s…", trimmed[:179])
		}
		return trimmed
	}
	start := idx + len(marker)
	end := start
	for end < len(trimmed) {
		if trimmed[end] == '"' && (end == start || trimmed[end-1] != '\\') {
			break
		}
		end += 1
	}
	msg := trimmed[start:end]
	if len(msg) == 0 {
		if len(trimmed) > 180 {
			return fmt.aprintf("%s…", trimmed[:179])
		}
		return trimmed
	}
	return msg
}

map_http_error :: proc(status_code: int, response_body: string) -> shared.App_Error {
	detail := extract_deepseek_error_message(response_body)
	if status_code == 429 {
		if len(detail) > 0 {
			return shared.rate_limit_error(fmt.aprintf("deepseek rate-limited (429): %s", detail))
		}
		return shared.rate_limit_error("deepseek rate-limited (429)")
	}
	if status_code >= 500 {
		if len(detail) > 0 {
			return shared.network_error(fmt.aprintf("deepseek server error (%d): %s", status_code, detail))
		}
		return shared.network_error(fmt.aprintf("deepseek server error (%d)", status_code))
	}
	if status_code >= 400 {
		if len(detail) > 0 {
			return shared.validation_error(fmt.aprintf("deepseek request failed (%d): %s", status_code, detail))
		}
		return shared.validation_error(fmt.aprintf("deepseek request failed (%d)", status_code))
	}
	return shared.ok()
}

deepseek_json_escape :: proc(s: string) -> string {
	out: [dynamic]u8
	for c in s {
		switch c {
		case '"':
			append(&out, '\\')
			append(&out, '"')
		case '\\':
			append(&out, '\\')
			append(&out, '\\')
		case '\n':
			append(&out, '\\')
			append(&out, 'n')
		case '\r':
			append(&out, '\\')
			append(&out, 'r')
		case '\t':
			append(&out, '\\')
			append(&out, 't')
		case:
			append(&out, u8(c))
		}
	}
	return string(out[:])
}

build_deepseek_request_json :: proc(opts: Generate_Script_Options) -> string {
	user_content := fmt.aprintf(
		"Create a %d-page comic script. Idea: %s. Genre: %s. Art Style: %s. Audience: %s. Return valid JSON.",
		opts.num_pages,
		opts.story_idea,
		opts.genre,
		opts.art_style,
		opts.audience,
	)
	defer delete(user_content)

	messages := [2]Deepseek_Request_Message{
		{role = "system", content = "You are an expert comic writer. Return JSON only."},
		{role = "user", content = user_content},
	}
	body := Deepseek_Request_Body{
		model = "deepseek-chat",
		messages = messages[:],
		response_format = Deepseek_Request_Response_Format{type = "json_object"},
		temperature = 0.8,
	}
	request_json, merr := json.marshal(body, {}, context.allocator)
	if merr == nil {
		return string(request_json)
	}

	escaped := deepseek_json_escape(user_content)
	defer delete(escaped)
	return fmt.aprintf(
		"{\"model\":\"deepseek-chat\",\"messages\":[{\"role\":\"system\",\"content\":\"You are an expert comic writer. Return JSON only.\"},{\"role\":\"user\",\"content\":\"%s\"}],\"response_format\":{\"type\":\"json_object\"},\"temperature\":0.8}",
		escaped,
	)
}

validate_generate_script_options :: proc(opts: Generate_Script_Options) -> shared.App_Error {
	if len(opts.story_idea) == 0 {
		return shared.validation_error("story_idea is required")
	}
	if opts.num_pages <= 0 {
		return shared.validation_error("num_pages must be > 0")
	}
	return shared.ok()
}

build_fallback_script :: proc(opts: Generate_Script_Options) -> core.Comic_Script {
	dialogues: [1]core.Dialogue = [1]core.Dialogue{
		{speaker_id = "char_001", text = "Let's begin.", bubble_type = .Normal, emotion = .Neutral},
	}
	chars_present: [1]string = [1]string{"char_001"}
	panels: [1]core.Panel = [1]core.Panel{
		{
			panel_id = "panel_001_001",
			panel_number = 1,
			shot_type = .Medium,
			description = opts.story_idea,
			characters_present = chars_present[:],
			dialogue = dialogues[:],
			caption = "",
			sound_effects = nil,
			transition_from_previous = .None,
		},
	}
	pages: [1]core.Page = [1]core.Page{
		{page_number = 1, layout_type = .Grid, panels = panels[:]},
	}
	chars: [1]core.Character = [1]core.Character{
		{name = "Protagonist", description = "A determined lead character"},
	}

	raw := core.Comic_Script{
		title = "Generated Comic",
		synopsis = opts.story_idea,
		characters = chars[:],
		pages = pages[:],
	}
	return core.normalize_script(raw)
}

extract_json_block :: proc(s: string) -> string {
	trimmed := strings.trim(s, " \n\r\t")
	start := strings.index(trimmed, "{")
	finish := strings.last_index(trimmed, "}")
	if start >= 0 && finish >= start {
		return trimmed[start : finish+1]
	}
	return trimmed
}

character_role_from_string :: proc(role: string) -> core.Character_Role {
	switch role {
	case "protagonist":
		return .Protagonist
	case "antagonist":
		return .Antagonist
	case "supporting":
		return .Supporting
	case "extra":
		return .Extra
	}
	return .Supporting
}

layout_type_from_string :: proc(layout: string) -> core.Layout_Type {
	switch layout {
	case "grid":
		return .Grid
	case "manga":
		return .Manga
	case "western":
		return .Western
	case "action":
		return .Action
	case "dialogue":
		return .Dialogue
	case "splash":
		return .Splash
	}
	return .Grid
}

shot_type_from_string :: proc(shot: string) -> core.Shot_Type {
	switch shot {
	case "establishing":
		return .Establishing
	case "wide":
		return .Wide
	case "medium":
		return .Medium
	case "close-up":
		return .Close_Up
	case "extreme-close-up":
		return .Extreme_Close_Up
	case "over-shoulder":
		return .Over_Shoulder
	case "aerial":
		return .Aerial
	}
	return .Medium
}

bubble_type_from_string :: proc(bt: string) -> core.Bubble_Type {
	switch bt {
	case "normal":
		return .Normal
	case "thought":
		return .Thought
	case "shout":
		return .Shout
	case "whisper":
		return .Whisper
	case "narration":
		return .Narration
	case "sound-effect":
		return .Sound_Effect
	}
	return .Normal
}

transition_from_string :: proc(t: string) -> core.Transition_Type {
	switch t {
	case "none":
		return .None
	case "fade":
		return .Fade
	case "wipe":
		return .Wipe
	case "dissolve":
		return .Dissolve
	case "action-lines":
		return .Action_Lines
	}
	return .None
}

clone_string_array :: proc(items: []string) -> []string {
	out: [dynamic]string
	for s in items {
		append(&out, strings.clone(s, context.allocator))
	}
	return out[:]
}

dispose_deepseek_response :: proc(resp: ^Deepseek_Response) {
	for c in resp.choices {
		delete(c.message.content)
	}
	delete(resp.choices)
}

dispose_raw_script :: proc(raw: ^Raw_Script) {
	delete(raw.title)
	delete(raw.synopsis)

	for c in raw.characters {
		delete(c.id)
		delete(c.name)
		delete(c.role)
		delete(c.description)
		delete(c.firstAppearancePanel)
	}
	delete(raw.characters)

	for p in raw.pages {
		delete(p.layoutType)
		for pan in p.panels {
			delete(pan.panelId)
			delete(pan.shotType)
			delete(pan.description)
			delete(pan.caption)
			delete(pan.transitionFromPrevious)
			for d in pan.dialogue {
				delete(d.speakerId)
				delete(d.text)
				delete(d.bubbleType)
				delete(d.emotion)
			}
			delete(pan.dialogue)
			for s in pan.charactersPresent {
				delete(s)
			}
			delete(pan.charactersPresent)
			for s in pan.soundEffects {
				delete(s)
			}
			delete(pan.soundEffects)
		}
		delete(p.panels)
	}
	delete(raw.pages)
}

dispose_raw_script_alt :: proc(raw: ^Raw_Script_Alt) {
	delete(raw.title)
	delete(raw.synopsis)

	for c in raw.characters {
		delete(c.id)
		delete(c.name)
		delete(c.role)
		delete(c.description)
		delete(c.first_appearance_panel)
	}
	delete(raw.characters)

	for p in raw.pages {
		delete(p.layout_type)
		for pan in p.panels {
			delete(pan.panel_id)
			delete(pan.shot_type)
			delete(pan.description)
			delete(pan.caption)
			delete(pan.transition_from_previous)
			for d in pan.dialogue {
				delete(d.speaker_id)
				delete(d.text)
				delete(d.bubble_type)
				delete(d.emotion)
			}
			delete(pan.dialogue)
			for s in pan.characters_present {
				delete(s)
			}
			delete(pan.characters_present)
			for s in pan.sound_effects {
				delete(s)
			}
			delete(pan.sound_effects)
		}
		delete(p.panels)
	}
	delete(raw.pages)
}

convert_raw_script :: proc(raw: Raw_Script) -> core.Comic_Script {
	chars: [dynamic]core.Character
	for c in raw.characters {
		append(&chars, core.Character{
			id = strings.clone(c.id, context.allocator),
			name = strings.clone(c.name, context.allocator),
			role = character_role_from_string(c.role),
			description = strings.clone(c.description, context.allocator),
			first_appearance_panel = strings.clone(c.firstAppearancePanel, context.allocator),
		})
	}

	pages: [dynamic]core.Page
	for p in raw.pages {
		panels: [dynamic]core.Panel
		for pan in p.panels {
			dialogues: [dynamic]core.Dialogue
			for d in pan.dialogue {
				append(&dialogues, core.Dialogue{
					speaker_id = strings.clone(d.speakerId, context.allocator),
					text = strings.clone(d.text, context.allocator),
					bubble_type = bubble_type_from_string(d.bubbleType),
					emotion = core.parse_emotion(d.emotion),
				})
			}

			append(&panels, core.Panel{
				panel_id = strings.clone(pan.panelId, context.allocator),
				panel_number = pan.panelNumber,
				shot_type = shot_type_from_string(pan.shotType),
				description = strings.clone(pan.description, context.allocator),
				characters_present = clone_string_array(pan.charactersPresent),
				dialogue = dialogues[:],
				caption = strings.clone(pan.caption, context.allocator),
				sound_effects = clone_string_array(pan.soundEffects),
				transition_from_previous = transition_from_string(pan.transitionFromPrevious),
			})
		}

		append(&pages, core.Page{
			page_number = p.pageNumber,
			layout_type = layout_type_from_string(p.layoutType),
			panels = panels[:],
		})
	}

	return core.Comic_Script{
		title = strings.clone(raw.title, context.allocator),
		synopsis = strings.clone(raw.synopsis, context.allocator),
		characters = chars[:],
		pages = pages[:],
	}
}

convert_raw_script_alt :: proc(raw: Raw_Script_Alt) -> core.Comic_Script {
	chars: [dynamic]core.Character
	for c in raw.characters {
		append(&chars, core.Character{
			id = strings.clone(c.id, context.allocator),
			name = strings.clone(c.name, context.allocator),
			role = character_role_from_string(c.role),
			description = strings.clone(c.description, context.allocator),
			first_appearance_panel = strings.clone(c.first_appearance_panel, context.allocator),
		})
	}

	pages: [dynamic]core.Page
	for p in raw.pages {
		panels: [dynamic]core.Panel
		for pan in p.panels {
			dialogues: [dynamic]core.Dialogue
			for d in pan.dialogue {
				append(&dialogues, core.Dialogue{
					speaker_id = strings.clone(d.speaker_id, context.allocator),
					text = strings.clone(d.text, context.allocator),
					bubble_type = bubble_type_from_string(d.bubble_type),
					emotion = core.parse_emotion(d.emotion),
				})
			}

			append(&panels, core.Panel{
				panel_id = strings.clone(pan.panel_id, context.allocator),
				panel_number = pan.panel_number,
				shot_type = shot_type_from_string(pan.shot_type),
				description = strings.clone(pan.description, context.allocator),
				characters_present = clone_string_array(pan.characters_present),
				dialogue = dialogues[:],
				caption = strings.clone(pan.caption, context.allocator),
				sound_effects = clone_string_array(pan.sound_effects),
				transition_from_previous = transition_from_string(pan.transition_from_previous),
			})
		}

		append(&pages, core.Page{
			page_number = p.page_number,
			layout_type = layout_type_from_string(p.layout_type),
			panels = panels[:],
		})
	}

	return core.Comic_Script{
		title = strings.clone(raw.title, context.allocator),
		synopsis = strings.clone(raw.synopsis, context.allocator),
		characters = chars[:],
		pages = pages[:],
	}
}

invalid_normalized_script_err :: proc(script: core.Comic_Script) -> shared.App_Error {
	return shared.generation_error(fmt.aprintf("normalized script failed minimal validation (title:%d synopsis:%d pages:%d)", len(script.title), len(script.synopsis), len(script.pages)))
}

parse_deepseek_script_response :: proc(response_json: string) -> (core.Comic_Script, shared.App_Error) {
	outer: Deepseek_Response
	if err := json.unmarshal_string(response_json, &outer); err != nil {
		return core.Comic_Script{}, shared.generation_error(fmt.aprintf("failed to parse deepseek envelope: %v", err))
	}
	defer dispose_deepseek_response(&outer)
	if len(outer.choices) == 0 {
		return core.Comic_Script{}, shared.generation_error("deepseek response has no choices")
	}

	content := outer.choices[0].message.content
	if len(content) == 0 {
		return core.Comic_Script{}, shared.generation_error("deepseek content is empty")
	}

	raw_json := extract_json_block(content)

	raw_script: Raw_Script
	if err := json.unmarshal_string(raw_json, &raw_script); err == nil {
		defer dispose_raw_script(&raw_script)
		script := convert_raw_script(raw_script)
		norm := core.normalize_script(script)
		if !core.script_is_valid_minimal(norm) {
			core.dispose_script(&norm)
			return core.Comic_Script{}, invalid_normalized_script_err(script)
		}
		return norm, shared.ok()
	}

	wrapped: Raw_Script_Wrapper
	if err := json.unmarshal_string(raw_json, &wrapped); err == nil {
		defer dispose_raw_script(&wrapped.script)
		script := convert_raw_script(wrapped.script)
		norm := core.normalize_script(script)
		if !core.script_is_valid_minimal(norm) {
			core.dispose_script(&norm)
			return core.Comic_Script{}, invalid_normalized_script_err(script)
		}
		return norm, shared.ok()
	}

	raw_alt: Raw_Script_Alt
	if err := json.unmarshal_string(raw_json, &raw_alt); err == nil {
		defer dispose_raw_script_alt(&raw_alt)
		script := convert_raw_script_alt(raw_alt)
		norm := core.normalize_script(script)
		if !core.script_is_valid_minimal(norm) {
			core.dispose_script(&norm)
			return core.Comic_Script{}, invalid_normalized_script_err(script)
		}
		return norm, shared.ok()
	}

	wrapped_alt: Raw_Script_Alt_Wrapper
	if err := json.unmarshal_string(raw_json, &wrapped_alt); err == nil {
		defer dispose_raw_script_alt(&wrapped_alt.script)
		script := convert_raw_script_alt(wrapped_alt.script)
		norm := core.normalize_script(script)
		if !core.script_is_valid_minimal(norm) {
			core.dispose_script(&norm)
			return core.Comic_Script{}, invalid_normalized_script_err(script)
		}
		return norm, shared.ok()
	}

	return core.Comic_Script{}, shared.generation_error("failed to parse deepseek script content (unsupported JSON shape)")
}

generate_comic_script :: proc(client: Deepseek_Client, cfg: shared.Config, opts: Generate_Script_Options) -> (core.Comic_Script, shared.App_Error) {
	if len(cfg.deepseek_api_key) == 0 {
		return core.Comic_Script{}, shared.config_error("DEEPSEEK_API_KEY is missing")
	}

	if verr := validate_generate_script_options(opts); !shared.is_ok(verr) {
		return core.Comic_Script{}, verr
	}

	request_json := build_deepseek_request_json(opts)
	defer delete(request_json)
	attempts := client.max_retries
	if attempts < 1 {
		attempts = 1
	}

	last_err := shared.generation_error("unknown deepseek error")
	for attempt in 1..=attempts {
		response_json, status_code, transport_err := client.transport(cfg, request_json)
		if !shared.is_ok(transport_err) {
			last_err = transport_err
		} else if status_code >= 400 {
			last_err = map_http_error(status_code, response_json)
		} else {
			if len(response_json) == 0 {
				last_err = shared.generation_error("deepseek returned empty response")
			} else {
				script, parse_err := parse_deepseek_script_response(response_json)
				if shared.is_ok(parse_err) {
					return script, shared.ok()
				}
				if strings.has_prefix(parse_err.message, "normalized script failed minimal validation") {
					fallback := build_fallback_script(opts)
					return fallback, shared.ok()
				}
				last_err = parse_err
			}
		}

		if attempt < attempts && shared.should_retry(last_err) {
			_ = backoff_ms(client.initial_backoff_ms, attempt)
			continue
		}
		break
	}

	return core.Comic_Script{}, last_err
}

generate_comic_script_stub :: proc(cfg: shared.Config, opts: Generate_Script_Options) -> (core.Comic_Script, shared.App_Error) {
	client := new_deepseek_client()
	return generate_comic_script(client, cfg, opts)
}

Stream_Callback :: #type proc(chunk: string, is_complete: bool) -> ()

stream_comic_script :: proc(client: Deepseek_Client, cfg: shared.Config, opts: Generate_Script_Options, callback: Stream_Callback) -> (core.Comic_Script, shared.App_Error) {
	if len(cfg.deepseek_api_key) == 0 {
		return core.Comic_Script{}, shared.config_error("DEEPSEEK_API_KEY is missing")
	}

	if verr := validate_generate_script_options(opts); !shared.is_ok(verr) {
		return core.Comic_Script{}, verr
	}

	url := fmt.aprintf("%s/chat/completions", cfg.deepseek_base_url)
	auth := fmt.aprintf("Authorization: Bearer %s", cfg.deepseek_api_key)

	user_content := fmt.aprintf(
		"Create a %d-page comic script. Idea: %s. Genre: %s. Art Style: %s. Audience: %s. Return valid JSON.",
		opts.num_pages,
		opts.story_idea,
		opts.genre,
		opts.art_style,
		opts.audience,
	)
	defer delete(user_content)

	body_json := fmt.aprintf(
		"{\"model\":\"deepseek-chat\",\"messages\":[{\"role\":\"system\",\"content\":\"You are an expert comic writer. Return JSON only.\"},{\"role\":\"user\",\"content\":\"%s\"}],\"response_format\":{\"type\":\"json_object\"},\"temperature\":0.8,\"stream\":true}",
		deepseek_json_escape(user_content),
	)
	defer delete(body_json)

	cmd := [12]string{
		"curl", "-sS", "-X", "POST", url,
		"-H", "Content-Type: application/json",
		"-H", auth,
		"-d", body_json,
		"--no-buffer",
	}
	desc := os.Process_Desc{command = cmd[:]}
	state, stdout, stderr, exec_err := os.process_exec(desc, context.temp_allocator)
	if exec_err != nil {
		return core.Comic_Script{}, shared.network_error(fmt.aprintf("curl streaming failed: %v", exec_err))
	}
	if !state.exited || state.exit_code != 0 {
		return core.Comic_Script{}, shared.network_error(fmt.aprintf("curl streaming exited with error: %s", string(stderr)))
	}

	output := string(stdout)
	defer delete(output)

	// Parse SSE stream chunks
	accumulated: [dynamic]u8
	lines: [dynamic]string
	current_line: [dynamic]u8
	for c in output {
		if c == '\n' {
			append(&lines, string(current_line[:]))
			delete(current_line)
		} else {
			append(&accumulated, u8(c))
		}
	}
	if len(current_line) > 0 {
		append(&lines, string(current_line[:]))
	}
	defer delete(lines)

	for line in lines {
		trimmed := strings.trim_space(line)
		if len(trimmed) == 0 || !strings.has_prefix(trimmed, "data:") {
			continue
		}
		data := strings.trim_space(trimmed[5:])
		if data == "[DONE]" {
			if callback != nil {
				callback("", true)
			}
			break
		}
		for b in data {
			append(&accumulated, u8(b))
		}
		if callback != nil {
			callback(data, false)
		}
	}

	accumulated_str := string(accumulated[:])
	defer delete(accumulated_str)

	// Parse the accumulated response
	if len(accumulated_str) == 0 {
		return core.Comic_Script{}, shared.generation_error("streaming produced no content")
	}

	script, perr := parse_deepseek_script_response(accumulated_str)
	if shared.is_ok(perr) {
		return script, shared.ok()
	}
	if strings.has_prefix(perr.message, "normalized script failed minimal validation") {
		fallback := build_fallback_script(opts)
		return fallback, shared.ok()
	}
	return core.Comic_Script{}, perr
}

stream_comic_script_stub :: proc(cfg: shared.Config, opts: Generate_Script_Options, callback: Stream_Callback) -> (core.Comic_Script, shared.App_Error) {
	client := new_deepseek_client()
	return stream_comic_script(client, cfg, opts, callback)
}
