<script lang="ts">
	// Tool body for the `ask_user` tool. Unlike every other tool
	// body, this one is interactive: while the call is in flight
	// (`!hasResult`) it renders the agent's questions with clickable
	// options + a custom-answer textarea per question, and submits
	// the user's choices via `coder_respond_to_prompt`. Once the
	// tool settles (`hasResult`) it falls back to a read-only
	// summary of what was answered (or that the user skipped).
	//
	// The user can always skip the whole prompt by ignoring this
	// card and just sending a normal message in the composer — that
	// resolves the parked prompt with a `skipped` result on the
	// backend, so this card flips to its "skipped" summary on the
	// next `tool_result`.
	import type { AskUserQuestion, QuestionAnswer } from '../protocol';
	import { ipc } from '../ipc';
	import { fmtJson } from './toolBodyHelpers';

	interface Props {
		args: unknown;
		result: unknown;
		hasResult: boolean;
		/** Tool-call id — the key the backend parked the prompt
		 *  under. Passed through to `coder_respond_to_prompt`. */
		callId: string;
	}

	let { args, result, hasResult, callId }: Props = $props();

	/** Parse the `questions[]` array out of the tool args. Returns
	 *  `null` on any shape mismatch so the caller punts to the JSON
	 *  fallback rather than rendering a half-broken form. */
	function parseQuestions(value: unknown): AskUserQuestion[] | null {
		if (typeof value !== 'object' || value === null) {
			return null;
		}
		const raw = (value as { questions?: unknown }).questions;
		if (!Array.isArray(raw)) {
			return null;
		}
		const items: unknown[] = raw;
		const out: AskUserQuestion[] = [];
		for (const item of items) {
			if (typeof item !== 'object' || item === null) {
				return null;
			}
			const q = item as { id?: unknown; question?: unknown; options?: unknown; allow_multiple?: unknown };
			if (typeof q.id !== 'string' || typeof q.question !== 'string' || !Array.isArray(q.options)) {
				return null;
			}
			const opts: { id: string; label: string }[] = [];
			for (const o of q.options as unknown[]) {
				if (typeof o !== 'object' || o === null) {
					return null;
				}
				const opt = o as { id?: unknown; label?: unknown };
				if (typeof opt.id !== 'string' || typeof opt.label !== 'string') {
					return null;
				}
				opts.push({ id: opt.id, label: opt.label });
			}
			out.push({
				id: q.id,
				question: q.question,
				options: opts,
				allow_multiple: q.allow_multiple === true,
			});
		}
		return out;
	}

	const questions = $derived(parseQuestions(args));

	// Per-question working state, keyed by question id. `selected`
	// is the set of option ids the user has toggled; `text` is their
	// custom answer. Kept in plain `$state` maps so a re-render
	// (e.g. a sibling row updating) doesn't reset the user's
	// in-progress picks.
	let selectedByQuestion = $state<Record<string, string[]>>({});
	let textByQuestion = $state<Record<string, string>>({});
	let submitting = $state(false);
	// `true` once we've fired `respondToPrompt` from this card.
	// Guards against a double-submit while the `tool_result` round-
	// trip is still landing.
	let submitted = $state(false);

	function isSelected(qid: string, oid: string): boolean {
		return (selectedByQuestion[qid] ?? []).includes(oid);
	}

	function toggleOption(q: AskUserQuestion, oid: string): void {
		const current = selectedByQuestion[q.id] ?? [];
		if (q.allow_multiple) {
			selectedByQuestion[q.id] = current.includes(oid) ? current.filter((id) => id !== oid) : [...current, oid];
		} else {
			// Single-select: clicking the already-selected option
			// clears it (lets the user fall back to a custom answer).
			selectedByQuestion[q.id] = current.includes(oid) ? [] : [oid];
		}
	}

	/** A question counts as answered when the user picked at least
	 *  one option or typed a non-empty custom answer. */
	function isAnswered(q: AskUserQuestion): boolean {
		const sel = selectedByQuestion[q.id] ?? [];
		return sel.length > 0 || (textByQuestion[q.id] ?? '').trim().length > 0;
	}

	const anyAnswered = $derived((questions ?? []).some(isAnswered));

	async function submit(): Promise<void> {
		if (questions === null || submitting || submitted) {
			return;
		}
		const answers: QuestionAnswer[] = [];
		for (const q of questions) {
			const selected = selectedByQuestion[q.id] ?? [];
			const freeText = (textByQuestion[q.id] ?? '').trim();
			if (selected.length === 0 && freeText.length === 0) {
				// Unanswered question: omit it. The tool result
				// spells out which questions came back so the model
				// can decide whether the gap matters.
				continue;
			}
			answers.push({ question_id: q.id, selected, free_text: freeText });
		}
		submitting = true;
		try {
			await ipc.coder.respondToPrompt(callId, { answers });
			submitted = true;
		} finally {
			submitting = false;
		}
	}

	/** Submit a single-select question immediately on click (no
	 *  confirm button). Multi-select / custom-answer questions wait
	 *  for the shared confirm button. We only auto-submit when this
	 *  is the *only* question and it's single-select — multi-
	 *  question prompts always use the confirm button so the user
	 *  can answer them all before sending. */
	function onOptionClick(q: AskUserQuestion, oid: string): void {
		toggleOption(q, oid);
		if (questions !== null && questions.length === 1 && !q.allow_multiple && isSelected(q.id, oid)) {
			void submit();
		}
	}

	// --- Answered / skipped summary parsing (hasResult path) ---

	type ResultSummary = { status: 'skipped' } | { status: 'answered'; answers: QuestionAnswer[] } | { status: 'other' };

	function parseResult(value: unknown): ResultSummary {
		if (typeof value !== 'object' || value === null) {
			return { status: 'other' };
		}
		const o = value as { status?: unknown; answers?: unknown };
		if (o.status === 'skipped') {
			return { status: 'skipped' };
		}
		if (o.status === 'answered' && Array.isArray(o.answers)) {
			const answers: QuestionAnswer[] = [];
			for (const a of o.answers as unknown[]) {
				if (typeof a !== 'object' || a === null) {
					continue;
				}
				const ans = a as { question_id?: unknown; selected?: unknown; free_text?: unknown };
				answers.push({
					question_id: typeof ans.question_id === 'string' ? ans.question_id : '',
					selected: Array.isArray(ans.selected)
						? (ans.selected as unknown[]).filter((s): s is string => typeof s === 'string')
						: [],
					free_text: typeof ans.free_text === 'string' ? ans.free_text : '',
				});
			}
			return { status: 'answered', answers };
		}
		return { status: 'other' };
	}

	const summary = $derived(hasResult ? parseResult(result) : null);

	/** Map a question id + option id back to its human label for the
	 *  answered summary. Falls back to the raw id when the args
	 *  didn't parse (legacy traces). */
	function optionLabel(qid: string, oid: string): string {
		const q = (questions ?? []).find((x) => x.id === qid);
		const opt = q?.options.find((o) => o.id === oid);
		return opt?.label ?? oid;
	}

	function questionLabel(qid: string): string {
		return (questions ?? []).find((x) => x.id === qid)?.question ?? qid;
	}
</script>

{#if questions === null}
	<div class="block-label">args</div>
	<pre class="block">{fmtJson(args)}</pre>
	{#if hasResult}
		<div class="block-label">result</div>
		<pre class="block">{fmtJson(result)}</pre>
	{/if}
{:else if summary !== null}
	<!-- Settled: read-only summary of the outcome. -->
	{#if summary.status === 'skipped'}
		<div class="ask-settled skipped">Skipped — the user continued with their own message.</div>
	{:else if summary.status === 'answered'}
		<ul class="ask-summary">
			{#each summary.answers as ans (ans.question_id)}
				<li>
					<span class="ask-q">{questionLabel(ans.question_id)}</span>
					<span class="ask-a">
						{#if ans.selected.length > 0}{ans.selected.map((s) => optionLabel(ans.question_id, s)).join(', ')}{/if}
						{#if ans.free_text.length > 0}<span class="ask-free"
								>{ans.selected.length > 0 ? ' · ' : ''}{ans.free_text}</span
							>{/if}
					</span>
				</li>
			{/each}
		</ul>
	{:else}
		<div class="block-label">result</div>
		<pre class="block">{fmtJson(result)}</pre>
	{/if}
{:else}
	<!-- In flight: the interactive prompt. Free-flowing — no inner
	     boxes, full-width option rows the width of the message
	     column, a radio (single-select) or checkbox (multi-select)
	     glyph per row. -->
	<div class="ask-form" class:submitting={submitting || submitted}>
		{#each questions as q (q.id)}
			<div class="ask-question">
				<div class="ask-question-text">{q.question}</div>
				<div class="ask-options" role={q.allow_multiple ? 'group' : 'radiogroup'}>
					{#each q.options as opt (opt.id)}
						<button
							type="button"
							class="ask-option"
							class:selected={isSelected(q.id, opt.id)}
							role={q.allow_multiple ? 'checkbox' : 'radio'}
							aria-checked={isSelected(q.id, opt.id)}
							disabled={submitting || submitted}
							onclick={() => onOptionClick(q, opt.id)}
						>
							<span class="ask-marker" class:multi={q.allow_multiple} aria-hidden="true"></span>
							<span class="ask-option-label">{opt.label}</span>
						</button>
					{/each}
				</div>
				<!-- Multi-line so a custom answer can hold a paragraph.
				     Keymap matches the composer: Enter submits, Shift+Enter
				     inserts a newline (the default textarea behaviour we
				     let through by only handling the unmodified Enter). -->
				<textarea
					class="ask-custom"
					rows="1"
					placeholder="Or type a custom answer… (Shift+Enter for a new line)"
					bind:value={textByQuestion[q.id]}
					disabled={submitting || submitted}
					onkeydown={(e) => {
						if (e.key === 'Enter' && !e.shiftKey) {
							e.preventDefault();
							void submit();
						}
					}}
				></textarea>
			</div>
		{/each}
		<!-- Multi-question / multi-select prompts can't auto-submit
		     on a single click, so they keep the confirm button. A
		     lone question (single-select or custom answer) submits
		     on option-click or Enter in the textarea, so its button
		     would be pure redundancy — drop it and just show the
		     skip hint. -->
		{#if questions.length > 1 || questions.some((q) => q.allow_multiple)}
			<div class="ask-actions">
				<button
					type="button"
					class="ask-submit"
					disabled={!anyAnswered || submitting || submitted}
					onclick={() => void submit()}
				>
					{submitted ? 'Sent' : submitting ? 'Sending…' : 'Send answer'}
				</button>
				<span class="ask-skip-hint">or just type in the composer to skip</span>
			</div>
		{/if}
	</div>
{/if}

<style>
	/* Free-flowing layout: no card background of its own (the row
	   already provides one), full message-column width, generous
	   vertical rhythm so a multi-question prompt reads like a form
	   rather than a cramped popover. */
	.ask-form {
		display: flex;
		flex-direction: column;
		gap: 14px;
		margin-top: 6px;
	}
	.ask-form.submitting {
		opacity: 0.6;
		pointer-events: none;
	}
	.ask-question {
		display: flex;
		flex-direction: column;
		gap: 6px;
	}
	.ask-question-text {
		font-size: 12px;
		font-weight: 500;
		color: var(--m-fg);
		line-height: 1.4;
	}
	/* Options stack vertically, each row spanning the full width —
	   long labels wrap inside the row instead of being squeezed into
	   a pill. */
	.ask-options {
		display: flex;
		flex-direction: column;
		gap: 3px;
	}
	.ask-option {
		display: flex;
		align-items: flex-start;
		gap: 8px;
		width: 100%;
		box-sizing: border-box;
		padding: 6px 8px;
		font-size: 12px;
		line-height: 1.4;
		text-align: left;
		color: var(--m-fg);
		background: transparent;
		border: none;
		border-radius: 5px;
		cursor: pointer;
	}
	.ask-option:hover:not(:disabled) {
		background: var(--m-bg-hover, color-mix(in srgb, var(--m-fg) 6%, transparent));
	}
	.ask-option:disabled {
		cursor: default;
	}
	/* Radio (single-select) / checkbox (multi-select) marker drawn
	   as a small box so the row reads as a real choice control. */
	.ask-marker {
		flex: 0 0 auto;
		width: 13px;
		height: 13px;
		margin-top: 1px;
		border: 1.5px solid var(--m-border-strong, var(--m-border));
		border-radius: 50%;
		box-sizing: border-box;
	}
	.ask-marker.multi {
		border-radius: 3px;
	}
	.ask-option.selected .ask-marker {
		border-color: var(--m-accent);
		background: radial-gradient(circle at center, var(--m-accent) 0 4px, transparent 4px);
	}
	.ask-option.selected .ask-marker.multi {
		background: var(--m-accent);
	}
	.ask-option.selected .ask-option-label {
		color: var(--m-accent);
		font-weight: 500;
	}
	.ask-option-label {
		flex: 1 1 auto;
		min-width: 0;
	}
	.ask-custom {
		width: 100%;
		box-sizing: border-box;
		margin-top: 2px;
		padding: 5px 8px;
		font-size: 12px;
		font-family: inherit;
		line-height: 1.4;
		color: var(--m-fg);
		background: var(--m-bg);
		border: 1px solid var(--m-border);
		border-radius: 5px;
		resize: vertical;
		min-height: 28px;
		field-sizing: content;
	}
	.ask-custom::placeholder {
		color: var(--m-fg-subtle);
	}
	.ask-custom:focus {
		outline: none;
		border-color: var(--m-accent);
	}
	.ask-actions {
		display: flex;
		align-items: center;
		gap: 10px;
		margin-top: 2px;
	}
	.ask-submit {
		padding: 5px 14px;
		font-size: 12px;
		font-weight: 500;
		color: var(--m-accent-fg, #fff);
		background: var(--m-accent);
		border: none;
		border-radius: 5px;
		cursor: pointer;
	}
	.ask-submit:disabled {
		opacity: 0.5;
		cursor: default;
	}
	.ask-skip-hint {
		font-size: 11px;
		color: var(--m-fg-subtle);
		font-style: italic;
	}
	/* Settled summary: also free-flowing, no box. */
	.ask-settled {
		font-size: 12px;
		color: var(--m-fg-subtle);
		margin-top: 4px;
	}
	.ask-summary {
		list-style: none;
		margin: 6px 0 0;
		padding: 0;
		display: flex;
		flex-direction: column;
		gap: 6px;
	}
	.ask-summary li {
		display: flex;
		flex-direction: column;
		gap: 1px;
		font-size: 12px;
	}
	.ask-q {
		color: var(--m-fg-subtle);
	}
	.ask-a {
		color: var(--m-fg);
		font-weight: 500;
	}
	.ask-free {
		font-weight: 400;
		font-style: italic;
	}
</style>
