package adapters

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

Fal_Transport :: #type proc(cfg: shared.Config, endpoint: string, prompt: string, negative_prompt: string, seed: i64, image_size: string, reference_images: []string, reference_strength: f32) -> (image_url: string, status_code: int, err: shared.App_Error)

Fal_Generation_Queue :: struct {
	max_concurrency: int,
	in_flight: int,
}

new_fal_queue :: proc(max_concurrency: int) -> Fal_Generation_Queue {
	cap := max_concurrency
	if cap < 1 {
		cap = 1
	}
	return Fal_Generation_Queue{max_concurrency = cap, in_flight = 0}
}

try_acquire_slot :: proc(q: ^Fal_Generation_Queue) -> bool {
	if q.in_flight >= q.max_concurrency {
		return false
	}
	q.in_flight += 1
	return true
}

release_slot :: proc(q: ^Fal_Generation_Queue) {
	if q.in_flight > 0 {
		q.in_flight -= 1
	}
}

Fal_Client :: struct {
	transport: Fal_Transport,
	max_retries: int,
	initial_backoff_ms: int,
	queue: ^Fal_Generation_Queue,
}

fal_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
}

Fal_Image :: struct {
	url: string,
	width: int,
	height: int,
}

Fal_Response :: struct {
	images: []Fal_Image,
}

dispose_fal_response :: proc(resp: ^Fal_Response) {
	for img in resp.images {
		delete(img.url)
	}
	delete(resp.images)
}

fal_parse_response_body :: proc(body: string) -> (Fal_Response, shared.App_Error) {
	resp: Fal_Response
	if err := json.unmarshal_string(body, &resp); err != nil {
		return Fal_Response{}, shared.generation_error(fmt.aprintf("failed to parse fal response JSON: %v", err))
	}
	if len(resp.images) == 0 {
		return Fal_Response{}, shared.generation_error("fal response missing images array")
	}
	if len(resp.images[0].url) == 0 {
		return Fal_Response{}, shared.generation_error("fal response image url is empty")
	}
	return resp, shared.ok()
}

fal_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[:])
}

default_fal_transport :: proc(cfg: shared.Config, endpoint, prompt, negative_prompt: string, seed: i64, image_size: string, reference_images: []string, reference_strength: f32) -> (string, int, shared.App_Error) {
	url := fmt.aprintf("https://fal.run/fal-ai/%s", endpoint)
	auth := fmt.aprintf("Authorization: Key %s", cfg.fal_api_key)

	// Build reference_images JSON array
	ref_json := ""
	if len(reference_images) > 0 && reference_strength > 0 {
		ref_items: [dynamic]u8
		append(&ref_items, '[')
		for i in 0..<len(reference_images) {
			if i > 0 {
				append(&ref_items, ',')
			}
			escaped := fal_json_escape(reference_images[i])
			item_str := fmt.aprintf("{\"url\":\"%s\"}", escaped)
			for b in item_str {
				append(&ref_items, u8(b))
			}
			delete(item_str)
		}
		append(&ref_items, ']')
		ref_json = fmt.aprintf(",\"reference_images\":%s,\"reference_image_strength\":%.2f", string(ref_items[:]), reference_strength)
		defer delete(ref_items)
	}

	neg_json := ""
	if len(negative_prompt) > 0 {
		neg_json = fmt.aprintf(",\"negative_prompt\":\"%s\"", fal_json_escape(negative_prompt))
	}

	size_json := ""
	if len(image_size) > 0 {
		size_json = fmt.aprintf(",\"image_size\":\"%s\"", fal_json_escape(image_size))
	}

	payload := fmt.aprintf("{\"prompt\":\"%s\"%s%s%s,\"seed\":%d}", fal_json_escape(prompt), neg_json, size_json, ref_json, seed)

	cmd := [13]string{
		"curl", "-sS", "-X", "POST", url,
		"-H", "Content-Type: application/json",
		"-H", auth,
		"-d", payload,
		"-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 := fal_parse_curl_output(string(stdout))
	if !ok {
		return "", 0, shared.network_error("unable to parse curl status output")
	}

	if status_code >= 400 {
		return "", status_code, shared.ok()
	}

	resp, parse_err := fal_parse_response_body(body)
	if !shared.is_ok(parse_err) {
		return "", status_code, parse_err
	}
	return resp.images[0].url, status_code, shared.ok()
}

new_fal_client :: proc(queue: ^Fal_Generation_Queue) -> Fal_Client {
	return Fal_Client{
		transport = default_fal_transport,
		max_retries = 3,
		initial_backoff_ms = 500,
		queue = queue,
	}
}

fal_http_error :: proc(status_code: int) -> shared.App_Error {
	if status_code == 429 {
		return shared.rate_limit_error("fal rate-limited (429)")
	}
	if status_code >= 500 {
		return shared.network_error(fmt.aprintf("fal server error (%d)", status_code))
	}
	if status_code >= 400 {
		return shared.validation_error(fmt.aprintf("fal request failed (%d)", status_code))
	}
	return shared.ok()
}

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

generate_character_reference :: proc(client: Fal_Client, cfg: shared.Config, c: core.Character, art_style: string) -> (string, shared.App_Error) {
	if len(cfg.fal_api_key) == 0 {
		return "", shared.config_error("FAL_API_KEY is missing")
	}
	if client.queue == nil {
		return "", shared.config_error("fal queue is not configured")
	}
	if !try_acquire_slot(client.queue) {
		return "", shared.generation_error("fal queue saturated")
	}
	defer release_slot(client.queue)

	style_key := core.parse_art_style_key(art_style)
	style_keywords := core.get_style_keywords(style_key)
	prompt := fmt.aprintf("%s. %s, standing in neutral pose facing camera, character portrait, clean background, studio lighting, white background, front-facing portrait, centered composition. %s", style_keywords, core.build_character_prompt(c, "", "", "", ""), core.QUALITY_MODIFIER)

	attempts := client.max_retries
	if attempts < 1 {
		attempts = 1
	}

	last_err := shared.generation_error("unknown fal character generation error")
	for attempt in 1..=attempts {
		url, status_code, transport_err := client.transport(cfg, "fast-sdxl", prompt, core.NEGATIVE_PROMPT_CHARACTER, c.seed, "square_hd", nil, 0)
		if !shared.is_ok(transport_err) {
			last_err = transport_err
		} else if status_code >= 400 {
			last_err = fal_http_error(status_code)
		} else if len(url) == 0 {
			last_err = shared.generation_error("fal returned empty image url")
		} else {
			return url, shared.ok()
		}

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

	return "", last_err
}

generate_panel_image :: proc(client: Fal_Client, cfg: shared.Config, panel: core.Panel, characters: []core.Character, art_style, project_id: string) -> (core.Panel_Image, shared.App_Error) {
	if len(cfg.fal_api_key) == 0 {
		return core.Panel_Image{}, shared.config_error("FAL_API_KEY is missing")
	}
	if client.queue == nil {
		return core.Panel_Image{}, shared.config_error("fal queue is not configured")
	}
	if !try_acquire_slot(client.queue) {
		return core.Panel_Image{}, shared.generation_error("fal queue saturated")
	}
	defer release_slot(client.queue)

	seed := core.generate_panel_seed(project_id, 1, panel.panel_number, panel.panel_id)

	// Build prompt with art style keywords
	style_key := core.parse_art_style_key(art_style)
	style_keywords := core.get_style_keywords(style_key)

	// Build character descriptions
	char_desc: [dynamic]u8
	for char_id in panel.characters_present {
		for c in characters {
			if c.id == char_id {
				if len(char_desc) > 0 {
					append(&char_desc, ' ')
				}
				tmpl := c.prompt_template
				desc := fmt.aprintf("%s %s with %s %s hair, %s eyes, %s skin, %s build, wearing %s", tmpl.age, tmpl.gender, tmpl.hair_color, tmpl.hair_style, tmpl.eye_color, tmpl.skin_tone, tmpl.body_type, tmpl.outfit)
				for b in desc {
					append(&char_desc, u8(b))
				}
				delete(desc)
				break
			}
		}
	}
	char_str := string(char_desc[:])
	defer delete(char_desc)

	// Shot type for sizing
	image_size := core.sdxl_size_name(core.get_image_size_for_shot_type(panel.shot_type))

	// Collect reference images from characters
	ref_images: [dynamic]string
	for char_id in panel.characters_present {
		for c in characters {
			if c.id == char_id && len(c.reference_image_url) > 0 {
				append(&ref_images, c.reference_image_url)
				break
			}
		}
	}
	defer delete(ref_images)

	prompt := fmt.aprintf("%s comic panel. %s. Characters: %s. %s shot. %s", style_keywords, panel.description, char_str, panel.shot_type, core.QUALITY_MODIFIER)

	attempts := client.max_retries
	if attempts < 1 {
		attempts = 1
	}

	last_err := shared.generation_error("unknown fal panel generation error")
	for attempt in 1..=attempts {
		url, status_code, transport_err := client.transport(cfg, "fast-sdxl", prompt, core.NEGATIVE_PROMPT_PANEL, seed, image_size, ref_images[:], 0.65)
		if !shared.is_ok(transport_err) {
			last_err = transport_err
		} else if status_code >= 400 {
			last_err = fal_http_error(status_code)
		} else if len(url) == 0 {
			last_err = shared.generation_error("fal returned empty image url")
		} else {
			// Return dimensions from response (we'll use the image_size preset to estimate)
			w, h := dimensions_from_sdxl_size(image_size)
			return core.Panel_Image{url = url, width = w, height = h, seed = seed, prompt = prompt}, shared.ok()
		}

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

	return core.Panel_Image{}, last_err
}

dimensions_from_sdxl_size :: proc(image_size: string) -> (int, int) {
	switch image_size {
	case "landscape_16_9": return 1344, 768
	case "landscape_4_3": return 1152, 896
	case "portrait_4_3": return 896, 1152
	case "square_hd": return 1024, 1024
	case "portrait_16_9": return 768, 1344
	}
	return 1024, 1024
}

generate_all_panels_batched :: proc(client: Fal_Client, cfg: shared.Config, panels: []core.Panel, characters: []core.Character, art_style, project_id: string, progress_callback: #type proc(current, total: int) -> ()) -> (map[string]core.Panel_Image, shared.App_Error) {
	results := make(map[string]core.Panel_Image)
	total := len(panels)

	for p, idx in panels {
		img, err := generate_panel_image(client, cfg, p, characters, art_style, project_id)
		if !shared.is_ok(err) {
			return results, err
		}
		results[p.panel_id] = img
		if progress_callback != nil {
			progress_callback(idx+1, total)
		}
	}

	return results, shared.ok()
}

generate_character_reference_stub :: proc(cfg: shared.Config, c: core.Character, art_style: string) -> (string, shared.App_Error) {
	q := new_fal_queue(2)
	client := new_fal_client(&q)
	return generate_character_reference(client, cfg, c, art_style)
}

Character_Sheet_Pose :: struct {
	name: string,
	prompt_suffix: string,
}

CHARACTER_SHEET_POSES :: [4]Character_Sheet_Pose{
	Character_Sheet_Pose{name = "front", prompt_suffix = "front-facing portrait, looking at camera"},
	Character_Sheet_Pose{name = "three-quarter", prompt_suffix = "three-quarter view portrait, slightly turned"},
	Character_Sheet_Pose{name = "profile", prompt_suffix = "side profile view, facing left"},
	Character_Sheet_Pose{name = "back", prompt_suffix = "back view showing hair and outfit from behind"},
}

generate_character_sheet :: proc(client: Fal_Client, cfg: shared.Config, c: core.Character, art_style: string) -> ([]string, shared.App_Error) {
	if len(cfg.fal_api_key) == 0 {
		return nil, shared.config_error("FAL_API_KEY is missing")
	}
	if client.queue == nil {
		return nil, shared.config_error("fal queue is not configured")
	}

	style_key := core.parse_art_style_key(art_style)
	style_keywords := core.get_style_keywords(style_key)

	sheet_urls: [dynamic]string
	all_failed := true

	for pose, i in CHARACTER_SHEET_POSES {
		if !try_acquire_slot(client.queue) {
			return nil, shared.generation_error("fal queue saturated")
		}

		pose_seed := c.seed + i64(i)

		// Build prompt for this pose
		base_desc := core.build_character_prompt(c, "", "", "", "")
		prompt := fmt.aprintf("%s. %s, %s, character sheet, clean background, studio lighting, white background, centered composition. %s", style_keywords, base_desc, pose.prompt_suffix, core.QUALITY_MODIFIER)

		// Use first image as reference for subsequent poses
		ref_images: [dynamic]string
		if len(sheet_urls) > 0 {
			append(&ref_images, sheet_urls[0])
		}
		defer delete(ref_images)

		url, status_code, transport_err := client.transport(cfg, "fast-sdxl", prompt, core.NEGATIVE_PROMPT_CHARACTER, pose_seed, "square_hd", ref_images[:], 0.65)
		release_slot(client.queue)

		if shared.is_ok(transport_err) && status_code < 400 && len(url) > 0 {
			append(&sheet_urls, url)
			all_failed = false
		}
	}

	if all_failed {
		for u in sheet_urls {
			delete(u)
		}
		delete(sheet_urls)
		return nil, shared.generation_error("all character sheet poses failed")
	}

	return sheet_urls[:], shared.ok()
}

generate_character_sheet_stub :: proc(cfg: shared.Config, c: core.Character, art_style: string) -> ([]string, shared.App_Error) {
	q := new_fal_queue(2)
	client := new_fal_client(&q)
	return generate_character_sheet(client, cfg, c, art_style)
}

generate_panel_image_stub :: proc(cfg: shared.Config, panel: core.Panel, characters: []core.Character, art_style, project_id: string) -> (core.Panel_Image, shared.App_Error) {
	q := new_fal_queue(2)
	client := new_fal_client(&q)
	return generate_panel_image(client, cfg, panel, characters, art_style, project_id)
}
