"use client";

import { useState, useRef, useCallback, useEffect } from "react";
import { Button } from "@/components/ui/Button";
import { Dropdown } from "@/components/ui/Dropdown";
import { Slider } from "@/components/ui/Slider";
import { SidePanel } from "@/components/ui/SidePanel";
import { AgentSelector } from "@/components/ui/AgentSelector";
import { Editor } from "@/components/Editor";
import { Terminal, TerminalRef } from "@/components/Terminal";
import {
	sendDataToScript,
	startScript,
	startScriptBulk,
	stopScript,
} from "@/lib/runner";
import styles from "@/styles/Playground.module.css";

const initialCode = `fn random_action(board) {
	let symb = rand_symb();
	let pos = rand_int(0, 10);
	let action = Action(symb, pos);

	// If this action is invalid, randomly select a new one.
	while !board.can_play(action) {
		let symb = rand_symb();
		let pos = rand_int(0, 10);
		action = Action(symb, pos);
	}

	return action;
}

fn step_min(board) {
	return random_action(board);
}

fn step_max(board) {
	return random_action(board);
}`;

const AGENTS = {
	// special-cased below
	Self: undefined,
};

export default function Playground() {
	const [isScriptRunning, setIsScriptRunning] = useState(false);
	const [isEditorReady, setIsEditorReady] = useState(false);
	const [fontSize, setFontSize] = useState(14);
	const [bulkRounds, setBulkRounds] = useState(1000);
	const [selectedAgent, setSelectedAgent] = useState("Self");
	const [isHelpOpen, setIsHelpOpen] = useState(false);
	const [scriptName, setScriptName] = useState("");
	const [saveSecret, setSaveSecret] = useState("");
	const [isSaving, setIsSaving] = useState(false);
	const [availableAgents, setAvailableAgents] = useState<string[]>([]);
	const [savedScripts, setSavedScripts] = useState<Record<string, string>>(
		{}
	);

	const editorRef = useRef<any>(null);
	const resultRef = useRef<HTMLTextAreaElement>(null);
	const terminalRef = useRef<TerminalRef>(null);

	const runDisabled = isScriptRunning || !isEditorReady;
	const stopDisabled = !isScriptRunning;

	// Fetch saved scripts and update available agents
	const loadSavedScripts = useCallback(async () => {
		try {
			const response = await fetch("/api/list-scripts");
			const data = await response.json();

			if (response.ok) {
				const scripts = data.scripts || [];
				const scriptContents: Record<string, string> = {};

				// Fetch content for each saved script
				await Promise.all(
					scripts.map(async (scriptName: string) => {
						try {
							const scriptResponse = await fetch(
								`/api/get-script?name=${encodeURIComponent(
									scriptName
								)}`
							);
							const scriptData = await scriptResponse.json();

							if (scriptResponse.ok) {
								scriptContents[scriptName] = scriptData.content;
							}
						} catch (error) {
							console.error(
								`Failed to load script ${scriptName}:`,
								error
							);
						}
					})
				);

				setSavedScripts(scriptContents);

				// Combine hardcoded agents with saved scripts, ensuring Self and Random are first
				const combinedAgents = [
					"Self",
					...Object.keys(AGENTS).filter((key) => key !== "Self"),
					...scripts,
				];

				setAvailableAgents(combinedAgents);
			}
		} catch (error) {
			console.error("Failed to load saved scripts:", error);
			// Fallback to hardcoded agents only
			setAvailableAgents(Object.keys(AGENTS));
		}
	}, []);

	// Load saved scripts on component mount
	useEffect(() => {
		loadSavedScripts();
	}, [loadSavedScripts]);

	const runHuman = useCallback(async () => {
		if (resultRef.current) {
			resultRef.current.value = "";
		}

		if (runDisabled || !editorRef.current) return;

		setIsScriptRunning(true);

		try {
			terminalRef.current?.clear();
			terminalRef.current?.focus();

			await startScript(
				editorRef.current.getValue(),
				(line: string) => {
					if (resultRef.current) {
						let v = resultRef.current.value + line + "\n";
						if (v.length > 10000) {
							v = v.substring(v.length - 10000);
						}
						resultRef.current.value = v;
						resultRef.current.scrollTop =
							resultRef.current.scrollHeight -
							resultRef.current.clientHeight;
					}
				},

				(line: string) => {
					terminalRef.current?.write(line);
				}
			);
		} catch (ex) {
			console.error(ex);
			if (resultRef.current) {
				resultRef.current.value += `\nScript exited with error:\n${ex}\n`;
			}
			terminalRef.current?.write(
				"\r\n\x1B[1;31mScript exited with error:\x1B[0m\n\r" +
					String(ex).replace("\n", "\n\r") +
					"\r\n"
			);
		}

		setIsScriptRunning(false);
	}, [runDisabled]);

	const runBulk = useCallback(async () => {
		if (resultRef.current) {
			resultRef.current.value = "";
		}

		if (runDisabled) return;

		setIsScriptRunning(true);

		try {
			terminalRef.current?.clear();
			terminalRef.current?.focus();

			// Get script content from either hardcoded agents or saved scripts
			const hardcodedAgent = AGENTS[selectedAgent as keyof typeof AGENTS];
			const savedScript = savedScripts[selectedAgent];

			const agentScript = hardcodedAgent || savedScript;
			const blueScript =
				agentScript || (editorRef.current?.getValue() ?? "");
			const redScript = editorRef.current?.getValue() ?? "";
			const opponentName = agentScript ? selectedAgent : "script";

			await startScriptBulk(
				redScript,
				blueScript,
				opponentName,
				(line: string) => {
					if (resultRef.current) {
						let v = resultRef.current.value + line + "\n";
						if (v.length > 10000) {
							v = v.substring(v.length - 10000);
						}
						resultRef.current.value = v;
						resultRef.current.scrollTop =
							resultRef.current.scrollHeight -
							resultRef.current.clientHeight;
					}
				},

				(line: string) => {
					terminalRef.current?.write(line);
				},
				bulkRounds
			);
		} catch (ex) {
			console.error(ex);
			if (resultRef.current) {
				resultRef.current.value += `\nScript exited with error:\n${ex}\n`;
			}
			terminalRef.current?.write(
				"\r\n\x1B[1;31mScript exited with error:\x1B[0m\n\r" +
					String(ex).replace("\n", "\n\r") +
					"\r\n"
			);
		}

		setIsScriptRunning(false);
	}, [runDisabled, bulkRounds, selectedAgent, savedScripts]);

	const stopScriptHandler = useCallback(() => {
		stopScript();
	}, []);

	const saveScript = useCallback(async () => {
		if (!scriptName.trim()) {
			alert("Please enter a script name");
			return;
		}

		if (!saveSecret.trim()) {
			alert("Please enter a secret");
			return;
		}

		if (!editorRef.current) {
			alert("No script content to save");
			return;
		}

		setIsSaving(true);

		try {
			const response = await fetch("/api/save-script", {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					name: scriptName.trim(),
					content: editorRef.current.getValue(),
					secret: saveSecret.trim(),
				}),
			});

			const result = await response.json();

			if (response.ok) {
				alert(result.message);
				setScriptName("");
				// Reload saved scripts to include the new one
				loadSavedScripts();
			} else {
				alert(result.error || "Failed to save script");
			}
		} catch (error) {
			console.error("Save error:", error);
			alert("Network error: Failed to save script");
		}

		setIsSaving(false);
	}, [scriptName, saveSecret, loadSavedScripts]);

	const copyScriptToEditor = useCallback(() => {
		if (!selectedAgent || selectedAgent === "Self") {
			alert("Please select a script to copy to the editor");
			return;
		}

		// Get the script content
		const hardcodedAgent = AGENTS[selectedAgent as keyof typeof AGENTS];
		const savedScript = savedScripts[selectedAgent];
		const scriptContent = hardcodedAgent || savedScript;

		if (!scriptContent) {
			alert("No script content available for the selected agent");
			return;
		}

		if (scriptContent.trim().startsWith("// SECRET")) {
			alert("This script is hidden :)");
			return;
		}

		// Warn user about losing current content
		const confirmed = confirm(
			`This will replace your current script with "${selectedAgent}". Your current work will be lost. Continue?`
		);

		if (confirmed && editorRef.current) {
			editorRef.current.setValue(scriptContent);
		}
	}, [selectedAgent, savedScripts]);

	return (
		<div className={styles.playgroundRoot}>
			<header className={styles.header}>
				<div className={styles.headerField}>
					<div className={styles.buttonGroup}>
						<Button
							variant="success"
							iconLeft="play"
							onClick={runHuman}
							loading={isScriptRunning}
							disabled={runDisabled}
						>
							Run
						</Button>

						<Button
							variant="success"
							iconLeft="play"
							onClick={runBulk}
							loading={isScriptRunning}
							disabled={runDisabled}
						>
							Bulk Run
						</Button>

						<Button
							variant="danger"
							iconLeft="stop"
							onClick={stopScriptHandler}
							disabled={stopDisabled}
						>
							Stop
						</Button>
					</div>

					<div className={styles.buttonGroup}>
						<Dropdown
							trigger="Config"
							align="right"
							customContent={
								<div className={styles.configPanel}>
									<Slider
										label="Font Size"
										value={fontSize}
										min={10}
										max={24}
										step={1}
										onChange={setFontSize}
										unit="px"
									/>
									<Slider
										label="Bulk Rounds"
										value={bulkRounds}
										min={100}
										max={10000}
										step={100}
										onChange={setBulkRounds}
										unit=""
									/>
									<div className={styles.configField}>
										<label>Bulk opponent</label>
										<AgentSelector
											agents={availableAgents}
											selectedAgent={selectedAgent}
											onSelect={setSelectedAgent}
											placeholder="Select an agent..."
										/>
									</div>

									<div className={styles.configField}>
										<Button
											variant="info"
											onClick={copyScriptToEditor}
											disabled={
												!selectedAgent ||
												selectedAgent === "Self"
											}
										>
											Copy to Editor
										</Button>
									</div>

									<div className={styles.saveSection}>
										<div className={styles.configField}>
											<label>Script name</label>
											<input
												type="text"
												className={styles.saveInput}
												placeholder="Enter script name..."
												value={scriptName}
												onChange={(e) =>
													setScriptName(
														e.target.value
													)
												}
												maxLength={32}
											/>
										</div>

										<div className={styles.configField}>
											<label>Secret</label>
											<input
												type="password"
												className={styles.saveInput}
												placeholder="Enter secret..."
												value={saveSecret}
												onChange={(e) =>
													setSaveSecret(
														e.target.value
													)
												}
											/>
										</div>

										<div className={styles.configField}>
											<Button
												variant="primary"
												onClick={saveScript}
												loading={isSaving}
												disabled={isSaving}
											>
												Save Script
											</Button>
										</div>
									</div>
								</div>
							}
						/>

						<Button
							iconLeft="help-circle"
							onClick={() => setIsHelpOpen(true)}
						>
							Help
						</Button>
					</div>
				</div>
			</header>

			<div className={styles.mainContent}>
				<div className={styles.leftPanel}>
					<Editor
						ref={editorRef}
						initialValue={initialCode}
						onChange={() => {}}
						onReady={() => setIsEditorReady(true)}
						fontSize={fontSize}
					/>
				</div>
				<div className={styles.rightPanel}>
					<div className={styles.terminalPanel}>
						<div className={styles.panelHeader}>Terminal</div>
						<div className={styles.terminalContainer}>
							<Terminal
								ref={terminalRef}
								onData={sendDataToScript}
								fontSize={fontSize}
							/>
						</div>
					</div>
					<div className={styles.outputPanel}>
						<div className={styles.panelHeader}>Output</div>
						<textarea
							ref={resultRef}
							className={styles.result}
							readOnly
							autoComplete="off"
							placeholder="Use print() to produce output"
							style={{ fontSize: `${fontSize}px` }}
						/>
					</div>
				</div>
			</div>

			<SidePanel isOpen={isHelpOpen} onClose={() => setIsHelpOpen(false)}>
				<h2>Game Rules</h2>

				<p>
					This game is played in two rounds, on an empty eleven-space
					board. The first round is played on {"Red's"} board, the
					second is played on {"Blue's"}.
				</p>

				<p>
					On {"Red's"} board, {"Red's"} goal is to maximize the value
					of the expression.
					{" Blue's"} goal is to minimize it. Players take turns
					placing the fourteen symbols <code>0123456789+-×÷</code>
					on the board, with the maximizing player taking the first
					move.
				</p>

				<p>
					A {"board's"} syntax must always be valid, and the following
					rules are enforced:
				</p>

				<ol>
					<li>Each symbol may only be used once</li>
					<li>
						The binary operators <code>+-×÷</code> may not be next
						to one another, and may not be at the end slots.
					</li>
					<li>
						The unary operator <code>-</code> (negative) must have a
						number as an argument. Therefore, it cannot be left of
						an operator (like <code>-×</code>), and it may not be in
						the rightmost slot.
					</li>
					<li>
						Unary <code>+</code> may not be used.
					</li>
					<li>
						{" "}
						<code>0</code> may not follow <code>÷</code>. This
						prevents most cases of zero-division, but{" "}
						{"isn't perfect"}.<code>÷-0</code> will result in an
						invalid board (causing a draw), and <code>÷0_+</code> is
						forbidden despite being valid syntax once the empty slot
						is filled. This is done to simplyify game logic, and
						might be improved later.
					</li>
					<li>Division by zero results in a draw.</li>
					<li>
						An incomplete board with no valid moves results in a
						draw.
					</li>
				</ol>

				<h2>How to Play</h2>
				<ol>
					<li>
						Click <strong>Run</strong> to start a single game. Play
						against your agent in the terminal. Use your arrow keys
						(up, down, left, right) to select a symbol. Use enter or
						space to make a move.
					</li>
					<li>
						Click <strong>Bulk Run</strong> to collect statistics
						from a many games.
					</li>
				</ol>

				<h2>Overview</h2>
				<ul>
					<li>
						<code>step_min()</code> is called once per turn with the{" "}
						{"board's"} current state. This function must return an{" "}
						<code>Action</code> that aims to minimize the total
						value of the board.
					</li>
					<li>
						<code>step_max()</code> is just like{" "}
						<code>step_min</code>, but should aim to maximize the
						value of the board.{" "}
					</li>
					<li>
						Agent code may not be edited between games. Start a new
						game to use new code.
					</li>
					<li>
						If your agent takes more than 5 seconds to compute a
						move, the script will exit with an error.
					</li>
				</ul>

				<h2>Rhai basics</h2>
				<p>
					Agents are written in <a href="https://rhai.rs">Rhai</a>, a
					wonderful embedded scripting language powered by Rust. Basic
					language features are outlined below.
				</p>
				<ul>
					<li>
						All statements must be followed by a <code>;</code>
					</li>
					<li>
						Use <code>return</code> to return a value from a
						function.
					</li>
					<li>
						<code>print(anything)</code> - Prints to the output
						panel. Prefer this over <code>debug</code>.
					</li>
					<li>
						<code>debug(anything)</code> - Prints to the output
						panel. Includes extra debug info.
					</li>
					<li>
						<code>()</code> is the {'"none"'} type, returned by some
						methods above.
					</li>
					<li>
						<code>for i in 0..5 {"{}"}</code> will iterate five
						times, with <code>i = 0, 1, 2, 3, 4</code>
					</li>
					<li>
						<code>for i in 0..=5 {"{}"}</code> will iterate six
						times, with <code>i = 0, 1, 2, 3, 4, 5</code>
					</li>
					<li>
						<code>let a = [];</code> initializes an empty array.
					</li>
					<li>
						<code>a.push(value)</code> adds a value to the end of an
						array
					</li>
					<li>
						<code>a.pop()</code> removes a value from the end of an
						array and returns it
					</li>
					<li>
						<code>a[0]</code> returns the first item of an array
					</li>
					<li>
						<code>a[1]</code> returns the second item of an array
					</li>
					<li>
						Refer to{" "}
						<a href="https://rhai.rs/book/language/values-and-types.html">
							the Rhai book
						</a>{" "}
						for more details.
					</li>
				</ul>

				<h2>Notable Functions</h2>
				<ul>
					<li>
						<code>Action(symbol, position)</code> - Creates a new
						action that places <code>symbol</code> at{" "}
						<code>position</code>. Valid symbols are{" "}
						<code>01234567890+-/*</code>. Both <code>0</code> and{" "}
						<code>{'"0"'}</code> are valid symbols.
					</li>
					<li>
						<code>board.can_play(action)</code> - Checks if an
						action is valid. Returns a boolean.
					</li>
					<li>
						<code>board.size()</code> - Return the total number of
						spots on this board.
					</li>
					<li>
						<code>board.free_spots()</code> - Count the number of
						free spots on the board.
					</li>
					<li>
						<code>board.play(action)</code> - Apply the given action
						on this board. This mutates the <code>board</code>, but
						does NOT make the move in the game. The only way to
						commit to an action is to return it from{" "}
						<code>step_min</code> or <code>step_max</code>. This
						method lets you compute potential values of a board when
						used with <code>board.evaluate()</code>.
					</li>
					<li>
						<code>board.ith_free_slot(idx)</code> - Returns the
						index of the <code>n</code>th free slot on this board.
						Returns <code>-1</code> if no such slot exists.
					</li>
					<li>
						<code>board.contains(symbol)</code> - Checks if this
						board contains the given symbol. Returns a boolean.
					</li>
					<li>
						<code>board.evaluate()</code> - Return the value of a
						board if it can be computed. Returns <code>()</code>{" "}
						otherwise.
					</li>
					<li>
						<code>board.free_spots_idx(action)</code> - Checks if an
						action is valid. Returns a boolean.
					</li>
					<li>
						<code>for i in board {"{ ... }"}</code> - Iterate over
						all slots on this board. Items are returned as strings,
						empty slots are the empty string (<code>{'""'}</code>)
					</li>
					<li>
						<code>is_op(symbol)</code> - Returns <code>true</code>{" "}
						if <code>symbol</code> is one of <code>+-*/</code>
					</li>

					<li>
						<code>rand_symb()</code> - Returns a random symbol
						(number or operation)
					</li>
					<li>
						<code>rand_op()</code> - Returns a random operator
						symbol (one of <code>+-*/</code>)
					</li>
					<li>
						<code>rand_action()</code> - Returns a random{" "}
						<code>Action</code>
					</li>
					<li>
						<code>rand_int(min, max)</code> - Returns a random
						integer between min and max, including both endpoints.
					</li>
					<li>
						<code>rand_bool(probability)</code> - Return{" "}
						<code>true</code> with the given probability. Otherwise
						return <code>false</code>.
					</li>
					<li>
						<code>rand_shuffle(array)</code> - Shuffle the given
						array
					</li>
					<li>
						<code>for p in permutations(array, 5) {"{}"}</code> -
						Iterate over all permutations of 5 elements of the given
						array.
					</li>
				</ul>
			</SidePanel>
		</div>
	);
}
