#!/usr/bin/env node
// Standalone Node 22+ client. No dependencies, repository checkout, or vendor SDK.
import { open, realpath } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';

const DEFAULT_ORIGIN = 'https://www.achieveaims.com';
const ID = /^[a-f\d]{24}$/i;
const UUID = /^[a-f\d]{8}-(?:[a-f\d]{4}-){3}[a-f\d]{12}$/i;
export class AimsError extends Error {}
async function readJson(path, limit, privateFile = false) {
  let file;
  try {
    file = await open(path, 'r');
    const stat = await file.stat();
    if (!stat.isFile() || stat.size > limit) throw new AimsError('Configuration or command file is too large or not a regular file.');
    if (privateFile && process.platform !== 'win32' && (stat.mode & 0o077)) {
      throw new AimsError('Connection file must be private. Run chmod 600 on that file.');
    }
    return JSON.parse(await file.readFile('utf8'));
  } catch (error) {
    if (error instanceof AimsError) throw error;
    throw new AimsError('Unable to read a valid JSON file. Check its path, permissions, and syntax.');
  } finally { await file?.close(); }
}
export async function readConnection(env = process.env) {
  const config = env.AIM_CONNECTION_FILE ? await readJson(env.AIM_CONNECTION_FILE, 16000, true) : {};
  if (!config || typeof config !== 'object' || Array.isArray(config)) throw new AimsError('Connection configuration must be an object.');
  // A file supplies its own origin and token as a pair; do not redirect its token via an environment override.
  const raw = env.AIM_CONNECTION_FILE ? config.apiUrl || DEFAULT_ORIGIN : env.AIM_API_URL || DEFAULT_ORIGIN;
  const token = env.AIM_CONNECTION_FILE ? config.token : env.AIM_AGENT_TOKEN;
  let base;
  try { base = new URL(raw); } catch { throw new AimsError('Configure a valid API origin.'); }
  if ((base.protocol !== 'https:' && !(base.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(base.hostname))) ||
      base.username || base.password || base.search || base.hash || base.pathname !== '/') {
    throw new AimsError('Use an HTTPS origin with no path, credentials, or query; HTTP is allowed only on loopback for tests.');
  }
  if (typeof token !== 'string' || !/^aag_[a-f\d]{24}_[A-Za-z0-9_-]{43}$/.test(token)) {
    throw new AimsError('Configure an owner-issued key in AIM_AGENT_TOKEN or a private AIM_CONNECTION_FILE. Never pass a key as an argument.');
  }
  return { apiUrl: base.origin, token };
}
export function validateCommand(command) {
  if (!command || typeof command !== 'object' || Array.isArray(command) ||
      !Number.isSafeInteger(command.version) || command.version < 0 || !UUID.test(command.operationId || '') ||
      !['create_item', 'update_item', 'add_note', 'set_criteria', 'handoff'].includes(command.operation?.type) ||
      Object.keys(command).some(key => !['version', 'operationId', 'operation'].includes(key))) {
    throw new AimsError('Command needs version, UUID operationId, and a supported operation. See openapi.json.');
  }
  if (Buffer.byteLength(JSON.stringify(command)) > 32000) throw new AimsError('Command exceeds 32 KB.');
  return command;
}
export function validateProposal(command) {
  if (!command || typeof command !== 'object' || !UUID.test(command.operationId || '') ||
      typeof command.summary !== 'string' || !Array.isArray(command.operations) || command.operations.length < 1 || command.operations.length > 20 ||
      Object.keys(command).some(key => !['operationId', 'summary', 'operations'].includes(key)) || Buffer.byteLength(JSON.stringify(command)) > 32000) {
    throw new AimsError('Proposal needs a UUID operationId, summary and 1–20 operations, within 32 KB. See openapi.json.');
  }
  return command;
}
export async function request(connection, action, goalId, command) {
  if (!['list', 'ideas', 'today', 'read', 'write', 'connection', 'propose', 'proposal'].includes(action)) throw new AimsError('Unknown action. Run help for supported actions.');
  if (['read', 'write', 'proposal'].includes(action) && !ID.test(goalId || '')) throw new AimsError('Provide a 24-character goal ID from list.');
  if (['list', 'ideas'].includes(action) && goalId !== undefined && !ID.test(goalId)) throw new AimsError('Provide a valid nextCursor, or omit it.');
  const path = action === 'connection' ? '/api/v1/connection' : action === 'propose' ? '/api/v1/goal-proposals' : action === 'proposal' ? `/api/v1/goal-proposals/${goalId}` : action === 'list' ? `/api/v1/goals${goalId ? `?after=${goalId}` : ''}`
    : action === 'ideas' ? `/api/v1/ideas${goalId ? `?after=${goalId}` : ''}`
    : action === 'today' ? '/api/v1/today' : `/api/v1/goals/${goalId}/workspace`;
  const body = action === 'write' ? JSON.stringify(validateCommand(command)) : action === 'propose' ? JSON.stringify(validateProposal(command)) : undefined;
  let response, data;
  try {
    response = await fetch(new URL(path, connection.apiUrl), {
      method: action === 'propose' ? 'POST' : body ? 'PATCH' : 'GET', redirect: 'error', signal: AbortSignal.timeout(20000),
      headers: { Authorization: `Bearer ${connection.token}`, ...(body ? { 'Content-Type': 'application/json' } : {}) },
      ...(body ? { body } : {}),
    });
    data = await response.json();
  } catch {
    throw new AimsError(body ? 'Response unavailable. The write may have committed. Retry the identical command file; do not generate another operation ID.' : 'Response unavailable. Check network access and the configured API origin.');
  }
  if (!response.ok) {
    const advice = {
      401: 'Key is invalid, expired, or revoked. Ask the owner to renew the connection.',
      403: data?.error?.code === 'ideas_not_shared'
        ? 'Idea access is not enabled. Ask the owner to create a new connection with idea sharing enabled and replace the key securely.'
        : 'This operation is not permitted by the connection. Do not retry it.',
      404: 'Goal or item is unavailable to this connection.',
      409: 'Read and reconcile the workspace before creating a new command. For an uncertain write, retry its identical file first.',
      429: 'Wait until the next minute before retrying.',
    };
    const code = /^[a-z_]{1,48}$/.test(data?.error?.code || '') ? data.error.code : 'request_failed';
    throw new AimsError(`${response.status} ${code}: ${advice[response.status] || 'Request rejected. Check the command against openapi.json.'}`);
  }
  return data;
}
export async function main(args = process.argv.slice(2), env = process.env) {
  if (!args.length || ['help', '--help'].includes(args[0])) {
    return 'Node 22+ · Achieve Aims\nlist [nextCursor]\nideas [nextCursor] (requires idea sharing)\nconnection\npropose proposal.json (owner approval required)\nproposal PROPOSAL_ID\ntoday\nread GOAL_ID\nwrite GOAL_ID command.json\nSet AIM_CONNECTION_FILE to a private downloaded connection file, or AIM_AGENT_TOKEN in the host secret environment. Output contains private goal and idea data; keep it local.';
  }
  const [action, id, path] = args;
  const valid = (['list', 'ideas'].includes(action) && args.length <= 2) || (['today', 'connection'].includes(action) && args.length === 1) ||
    (['read', 'proposal', 'propose'].includes(action) && args.length === 2) || (action === 'write' && args.length === 3);
  if (!valid) throw new AimsError('Invalid arguments. Run node aims.mjs help.');
  const connection = await readConnection(env);
  const commandPath = action === 'propose' ? id : path;
  const command = commandPath ? await readJson(commandPath, 32000) : undefined;
  const result = await request(connection, action, id, command);
  return JSON.stringify(result, null, 2).replaceAll(connection.token, '[REDACTED]');
}
// Resolve symlinked entry points as Node does for import.meta.url. Imports must stay quiet.
const isMain = process.argv[1] && await realpath(process.argv[1]).catch(() => null) === await realpath(fileURLToPath(import.meta.url));
if (isMain) {
  try { process.stdout.write(await main() + '\n'); }
  catch (error) {
    process.stderr.write((error instanceof AimsError ? error.message : 'Unable to complete the request.') + '\n');
    process.exitCode = 1;
  }
}
