export type V1BatchCommand = {
  command_id: string;
  capability: string;
  phase: 'execute' | 'validate';
  input: Record<string, unknown>;
  depends_on?: string[];
  continue_after_failure?: boolean;
};

export type V1BatchTerminalStatus =
  | 'succeeded'
  | 'failed'
  | 'denied'
  | 'rejected'
  | 'skipped'
  | 'unsupported'
  | 'unavailable'
  | 'expired';

export type V1BatchResult = {
  command_id: string;
  capability?: string;
  status: V1BatchTerminalStatus;
  data?: unknown;
  meta?: Record<string, unknown>;
  artifacts?: unknown[];
  error?: {
    code: string;
    message: string;
  };
};

export interface V1CommandBatchRuntimeOptions {
  initialResults?: Iterable<V1BatchResult>;
  resultMeta?: () => Record<string, unknown> | undefined;
  executeOne: (command: V1BatchCommand) => Promise<V1BatchResult>;
}

type BatchItem = {
  command: V1BatchCommand;
  base: {
    command_id: string;
    capability: string;
  };
};

export async function runV1CommandBatch(
  commands: V1BatchCommand[],
  options: V1CommandBatchRuntimeOptions,
): Promise<V1BatchResult[]> {
  const items = new Map<number, BatchItem>();
  const commandIndexes = new Map<string, number>();
  const commandIdCounts = new Map<string, number>();
  const initialResults = new Map<string, V1BatchResult>();

  for (const result of options.initialResults || []) {
    const commandId = scalarString(result.command_id);
    if (commandId) {
      initialResults.set(commandId, result);
    }
  }

  commands.forEach((command, index) => {
    const base = commandResultBase(command);
    items.set(index, { command, base });
    commandIdCounts.set(base.command_id, (commandIdCounts.get(base.command_id) || 0) + 1);
  });

  const duplicateCommandIds = new Set(
    Array.from(commandIdCounts.entries())
      .filter(([, count]) => count > 1)
      .map(([commandId]) => commandId)
  );

  items.forEach((item, index) => {
    if (!duplicateCommandIds.has(item.base.command_id)) {
      commandIndexes.set(item.base.command_id, index);
    }
  });

  const remaining = Array.from(items.keys());
  const resultsByIndex = new Map<number, V1BatchResult>();
  const resultsByCommandId = new Map<string, V1BatchResult>();
  let hasFailedCommand = false;

  initialResults.forEach((result, commandId) => {
    if (duplicateCommandIds.has(commandId)) return;
    resultsByCommandId.set(commandId, result);
    if (!commandIndexes.has(commandId) && result.status !== 'succeeded') {
      hasFailedCommand = true;
    }
  });

  while (remaining.length > 0) {
    let madeProgress = false;

    for (let offset = 0; offset < remaining.length; offset += 1) {
      const index = remaining[offset];
      const item = items.get(index);
      if (!item) continue;

      let result: V1BatchResult | undefined;
      const initialResult = initialResults.get(item.base.command_id);
      if (duplicateCommandIds.has(item.base.command_id)) {
        result = invalidCommandResult(
          item.base,
          'command_id values must be unique within a command batch.',
          options.resultMeta,
        );
      } else if (initialResult) {
        result = initialResult;
      } else if (hasPendingDependencies(item.command, commandIndexes, resultsByIndex, resultsByCommandId)) {
        continue;
      } else if (shouldSkipCommand(item.command, resultsByCommandId, hasFailedCommand)) {
        result = skippedCommandResult(item.base, options.resultMeta);
      } else {
        const resolved = resolveCommandReferences(item.command, resultsByCommandId);
        result = resolved.error
          ? invalidReferenceCommandResult(item.base, resolved.error, options.resultMeta)
          : await options.executeOne(resolved.command);
      }

      resultsByIndex.set(index, result);
      resultsByCommandId.set(result.command_id, result);
      hasFailedCommand = hasFailedCommand || result.status !== 'succeeded';
      remaining.splice(offset, 1);
      offset -= 1;
      madeProgress = true;
    }

    if (!madeProgress) {
      for (const index of remaining) {
        const item = items.get(index);
        if (!item) continue;
        const result = skippedCommandResult(item.base, options.resultMeta);
        resultsByIndex.set(index, result);
        resultsByCommandId.set(result.command_id, result);
      }
      remaining.splice(0, remaining.length);
    }
  }

  return Array.from(resultsByIndex.entries())
    .sort(([left], [right]) => left - right)
    .map(([, result]) => result);
}

function hasPendingDependencies(
  command: V1BatchCommand,
  commandIndexes: Map<string, number>,
  resultsByIndex: Map<number, V1BatchResult>,
  resultsByCommandId: Map<string, V1BatchResult>,
): boolean {
  if (command.phase === 'validate') return false;
  for (const dependencyId of commandDependencies(command)) {
    if (resultsByCommandId.has(dependencyId)) continue;
    const dependencyIndex = commandIndexes.get(dependencyId);
    if (dependencyIndex !== undefined && !resultsByIndex.has(dependencyIndex)) {
      return true;
    }
  }
  return false;
}

function shouldSkipCommand(
  command: V1BatchCommand,
  resultsByCommandId: Map<string, V1BatchResult>,
  hasFailedCommand: boolean,
): boolean {
  if (command.phase === 'validate') return false;
  for (const dependencyId of commandDependencies(command)) {
    const result = resultsByCommandId.get(dependencyId);
    if (!result || result.status !== 'succeeded') {
      return true;
    }
  }
  return hasFailedCommand && command.continue_after_failure !== true;
}

function commandDependencies(command: V1BatchCommand): string[] {
  return Array.isArray(command.depends_on)
    ? command.depends_on.filter(
      (dependencyId): dependencyId is string => typeof dependencyId === 'string' && dependencyId.length > 0
    )
    : [];
}

function resolveCommandReferences(
  command: V1BatchCommand,
  resultsByCommandId: Map<string, V1BatchResult>,
): { command: V1BatchCommand; error?: undefined } | { command: V1BatchCommand; error: string } {
  if (command.phase === 'validate') {
    return { command };
  }

  const resolved = resolveValueReferences(command.input, resultsByCommandId);
  if (resolved.error) {
    return { command, error: resolved.error };
  }

  return {
    command: {
      ...command,
      input: resolved.value as Record<string, unknown>,
    },
  };
}

function resolveValueReferences(
  value: unknown,
  resultsByCommandId: Map<string, V1BatchResult>,
): { value: unknown; error?: undefined } | { value: unknown; error: string } {
  if (!value || typeof value !== 'object') {
    return { value };
  }

  if (Array.isArray(value)) {
    const resolvedItems: unknown[] = [];
    for (const item of value) {
      const resolved = resolveValueReferences(item, resultsByCommandId);
      if (resolved.error) return { value, error: resolved.error };
      resolvedItems.push(resolved.value);
    }
    return { value: resolvedItems };
  }

  const objectValue = value as Record<string, unknown>;
  if (Object.prototype.hasOwnProperty.call(objectValue, '$from')) {
    return resolveReferenceObject(objectValue, resultsByCommandId);
  }

  const resolvedObject: Record<string, unknown> = {};
  for (const [key, child] of Object.entries(objectValue)) {
    const resolved = resolveValueReferences(child, resultsByCommandId);
    if (resolved.error) return { value, error: resolved.error };
    resolvedObject[key] = resolved.value;
  }
  return { value: resolvedObject };
}

function resolveReferenceObject(
  reference: Record<string, unknown>,
  resultsByCommandId: Map<string, V1BatchResult>,
): { value: unknown; error?: undefined } | { value: unknown; error: string } {
  const keys = Object.keys(reference);
  const sourceId = reference.$from;
  const path = reference.path;
  if (
    keys.length !== 2
    || typeof sourceId !== 'string'
    || sourceId.length < 1
    || !Array.isArray(path)
    || !path.every(segment => (
      typeof segment === 'string'
      || (typeof segment === 'number' && Number.isInteger(segment))
    ))
  ) {
    return { value: reference, error: 'Reference objects must contain a string $from and a string/integer path.' };
  }

  const source = resultsByCommandId.get(sourceId);
  if (!source || source.status !== 'succeeded') {
    return { value: reference, error: 'Reference source did not complete successfully before this command.' };
  }

  let node: unknown = source;
  for (const segment of path) {
    if (!node || typeof node !== 'object') {
      return { value: reference, error: 'Reference path did not resolve in the source command result.' };
    }
    const key = typeof segment === 'number' ? segment : String(segment);
    if (!Object.prototype.hasOwnProperty.call(node, key)) {
      return { value: reference, error: 'Reference path did not resolve in the source command result.' };
    }
    node = (node as Record<string | number, unknown>)[key];
  }

  return { value: node };
}

function commandResultBase(command: V1BatchCommand): { command_id: string; capability: string } {
  const commandId = scalarString(command.command_id);
  const capability = scalarString(command.capability);
  return {
    command_id: commandId || 'cmd_missing_command_id',
    capability,
  };
}

function scalarString(value: unknown): string {
  return ['string', 'number', 'boolean'].includes(typeof value) ? String(value) : '';
}

function invalidCommandResult(
  base: { command_id: string; capability: string },
  message: string,
  resultMeta?: () => Record<string, unknown> | undefined,
): V1BatchResult {
  return withMeta({
    command_id: base.command_id,
    capability: base.capability,
    status: 'failed',
    artifacts: [],
    error: {
      code: 'invalid_command',
      message,
    },
  }, resultMeta);
}

function invalidReferenceCommandResult(
  base: { command_id: string; capability: string },
  message: string,
  resultMeta?: () => Record<string, unknown> | undefined,
): V1BatchResult {
  return withMeta({
    command_id: base.command_id,
    capability: base.capability,
    status: 'failed',
    artifacts: [],
    error: {
      code: 'invalid_reference',
      message,
    },
  }, resultMeta);
}

function skippedCommandResult(
  base: { command_id: string; capability: string },
  resultMeta?: () => Record<string, unknown> | undefined,
): V1BatchResult {
  return withMeta({
    command_id: base.command_id,
    capability: base.capability,
    status: 'skipped',
    data: { skipped: true },
    artifacts: [],
  }, resultMeta);
}

function withMeta(
  result: V1BatchResult,
  resultMeta?: () => Record<string, unknown> | undefined,
): V1BatchResult {
  const meta = resultMeta?.();
  return meta && Object.keys(meta).length > 0
    ? { ...result, meta }
    : result;
}
