using System;
using System.Collections.Generic;
using Codely.Newtonsoft.Json;
using Codely.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityTcp.Editor.Helpers;
namespace UnityTcp.Editor.Tools
{
///
/// High-level Unity workflows (init_session, compile_and_validate, checkpoint).
///
/// Important:
/// - Workflows are advanced incrementally on each call to avoid blocking the editor thread.
/// - Minimal state is persisted via SessionState so that the workflow can survive domain reloads.
/// - Clients can poll by calling the same action again with op_id.
///
public static class ManageWorkflow
{
private static readonly List ValidActions = new List
{
"init_session",
"compile_and_validate",
"checkpoint",
};
private const string KeyPrefix = "ManageWorkflow_";
private static string CtxKey(string opId) => $"{KeyPrefix}Ctx_{opId}";
public static object HandleCommand(JObject @params)
{
if (@params == null)
{
return Response.Error("Parameters cannot be null.");
}
string action = @params["action"]?.ToString()?.ToLowerInvariant();
if (string.IsNullOrEmpty(action))
{
return Response.Error("Action parameter is required.");
}
if (!ValidActions.Contains(action))
{
string valid = string.Join(", ", ValidActions);
return Response.Error($"Unknown action: '{action}'. Valid actions are: {valid}");
}
// Optional explicit op_id for polling
string requestedOpId = @params["op_id"]?.ToString();
string opId = null;
JObject ctx = null;
if (!string.IsNullOrEmpty(requestedOpId))
{
opId = requestedOpId;
ctx = LoadContext(opId);
if (ctx == null)
{
return BuildErrorWithOpId(opId, "unknown_op_id", $"Unknown workflow op_id: {opId}");
}
// Guard: prevent action/op_id mismatches from accidentally advancing the wrong workflow.
string ctxAction = ctx["action"]?.ToString()?.ToLowerInvariant();
if (!string.IsNullOrEmpty(ctxAction) && !string.Equals(ctxAction, action, StringComparison.OrdinalIgnoreCase))
{
return BuildErrorWithOpId(
opId,
"action_mismatch",
$"Workflow op_id '{opId}' belongs to action '{ctxAction}', not '{action}'."
);
}
}
if (ctx == null)
{
// Start a new workflow
opId = Guid.NewGuid().ToString("N");
ctx = new JObject
{
["op_id"] = opId,
["action"] = action,
["stage"] = "start",
["createdAtUtcTicks"] = DateTime.UtcNow.Ticks,
};
// Per-action defaults (match TypeScript tool behavior)
int timeoutSeconds =
@params["timeoutSeconds"]?.ToObject()
?? (action == "init_session" ? 600 : 180);
if (timeoutSeconds < 1) timeoutSeconds = 1;
ctx["timeoutSeconds"] = timeoutSeconds;
// Persist checkpoint parameters so polling doesn't require resending options
if (action == "checkpoint")
{
bool screenshot = @params["screenshot"]?.ToObject() ?? true;
ctx["screenshot"] = screenshot;
string screenshotAction = @params["screenshotAction"]?.ToString();
if (string.IsNullOrEmpty(screenshotAction)) screenshotAction = "capture";
ctx["screenshotAction"] = screenshotAction;
ctx["screenshotPath"] = @params["screenshotPath"]?.ToString();
ctx["screenshotFilename"] = @params["screenshotFilename"]?.ToString();
}
SaveContext(opId, ctx);
}
// Overall workflow timeout guard
if (IsTimedOut(ctx, out double elapsedSec))
{
DeleteContext(opId);
return BuildErrorWithOpId(
opId,
"timeout",
$"unity_workflow '{action}' timed out after {Math.Round(elapsedSec)}s"
);
}
try
{
switch (action)
{
case "init_session":
return AdvanceInitSession(opId, ctx);
case "compile_and_validate":
return AdvanceCompileAndValidate(opId, ctx);
case "checkpoint":
return ExecuteCheckpoint(opId, ctx);
default:
return Response.Error($"Unknown action: '{action}'");
}
}
catch (Exception e)
{
Debug.LogError($"[ManageWorkflow] Action '{action}' failed: {e}");
DeleteContext(opId);
return BuildErrorWithOpId(opId, "exception", e.Message);
}
}
private static object AdvanceInitSession(string opId, JObject ctx)
{
string stage = ctx["stage"]?.ToString() ?? "start";
var deltas = new List