export type FacadeCommand = 'project.list' | 'project.open' | 'project.save' | 'geometry.createBox' | 'geometry.createCylinder' | 'geometry.boolean' | 'sketch.solve' | 'spreadsheet.evaluate'
export type ScriptArgument = string | number | boolean | null | ScriptArgument[] | { [key: string]: ScriptArgument }
export type ScriptCommand = { sequence: number; command: FacadeCommand; arguments: ScriptArgument }
export type ScriptReplay = { status: 'replayed' | 'rejected'; executed: number; rejected: Array<{ sequence: number; reason: string }> }
export type ScriptCapability = 'file' | 'network' | 'time' | 'resource'
export type ScriptSandboxSnapshot = { commands: ScriptCommand[]; version: number; policy: { allowedCommands: FacadeCommand[]; allowedCapabilities: ScriptCapability[]; maxCommands: number; maxArgumentBytes: number } }
export type ScriptSandboxApi = { record(command: FacadeCommand, args?: ScriptArgument): ScriptCommand; replay(commands?: ScriptCommand[]): ScriptReplay; requestCapability(capability: ScriptCapability): { capability: ScriptCapability; granted: boolean; reason?: string }; clear(): void; snapshot(): ScriptSandboxSnapshot; exportMacro(): string; importMacro(serialized: string): ScriptCommand[] }
export type ScriptSandboxOptions = { allowedCommands?: FacadeCommand[]; allowedCapabilities?: ScriptCapability[]; maxCommands?: number; maxArgumentBytes?: number }

const allFacadeCommands: FacadeCommand[] = ['project.list', 'project.open', 'project.save', 'geometry.createBox', 'geometry.createCylinder', 'geometry.boolean', 'sketch.solve', 'spreadsheet.evaluate']
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T
const argumentBytes = (value: ScriptArgument) => new TextEncoder().encode(JSON.stringify(value ?? null)).byteLength
const isCommand = (value: unknown): value is FacadeCommand => typeof value === 'string' && allFacadeCommands.includes(value as FacadeCommand)

export const createScriptSandbox = (options: ScriptSandboxOptions = {}): ScriptSandboxApi => {
  const allowedCommands = [...new Set(options.allowedCommands ?? allFacadeCommands)]; const allowedCapabilities = [...new Set(options.allowedCapabilities ?? [])]; const maxCommands = options.maxCommands ?? 1000; const maxArgumentBytes = options.maxArgumentBytes ?? 16 * 1024; const commands: ScriptCommand[] = []; let version = 0
  const validateCommand = (command: FacadeCommand, args: ScriptArgument) => { if (!isCommand(command) || !allowedCommands.includes(command)) throw new Error(`Script command is not allowed: ${command}`); if (argumentBytes(args) > maxArgumentBytes) throw new RangeError('Script argument quota exceeded.'); if (commands.length >= maxCommands) throw new RangeError('Script command quota exceeded.') }
  const record = (command: FacadeCommand, args: ScriptArgument = null) => { validateCommand(command, args); const entry = { sequence: commands.length + 1, command, arguments: clone(args) }; commands.push(entry); version += 1; return clone(entry) }
  const normalize = (input: ScriptCommand[]) => input.map((entry, index) => { if (!entry || !Number.isSafeInteger(entry.sequence) || entry.sequence !== index + 1 || !isCommand(entry.command)) throw new Error(`Invalid macro command at sequence ${index + 1}.`); if (!allowedCommands.includes(entry.command)) throw new Error(`Script command is not allowed: ${entry.command}`); if (argumentBytes(entry.arguments) > maxArgumentBytes) throw new RangeError('Script argument quota exceeded.'); return clone(entry) })
  const replay = (input = commands): ScriptReplay => { const rejected: Array<{ sequence: number; reason: string }> = []; let executed = 0; for (const entry of input) { if (!isCommand(entry.command) || !allowedCommands.includes(entry.command)) rejected.push({ sequence: entry.sequence, reason: 'command not allowed' }); else if (argumentBytes(entry.arguments) > maxArgumentBytes) rejected.push({ sequence: entry.sequence, reason: 'argument quota exceeded' }); else if (executed >= maxCommands) rejected.push({ sequence: entry.sequence, reason: 'command quota exceeded' }); else executed += 1 }; return { status: rejected.length === 0 ? 'replayed' : 'rejected', executed, rejected } }
  const requestCapability = (capability: ScriptCapability) => allowedCapabilities.includes(capability) ? { capability, granted: true } : { capability, granted: false, reason: `${capability} capability is denied by policy` }
  const clear = () => { commands.length = 0; version += 1 }
  const snapshot = (): ScriptSandboxSnapshot => ({ commands: clone(commands), version, policy: { allowedCommands: [...allowedCommands], allowedCapabilities: [...allowedCapabilities], maxCommands, maxArgumentBytes } })
  const importMacro = (serialized: string) => { let parsed: unknown; try { parsed = JSON.parse(serialized) } catch { throw new Error('Macro is not valid JSON.') }; if (!Array.isArray(parsed)) throw new Error('Macro must contain a command array.'); return normalize(parsed as ScriptCommand[]) }
  return { record, replay, requestCapability, clear, snapshot, exportMacro: () => `${JSON.stringify(commands, null, 2)}\n`, importMacro }
}
