// ASCT Recipe Definition

// @ts-check

const ASCT_VERSION = '0.5.1';
let tool_asct_name = 'asct';
let tool_asct_version = ASCT_VERSION;

// Keep benchmark metadata centralized for parameters and execution.
let ASCT_BENCHMARKS = [
  {
    id: 'benchmark_idle_latency',
    name: 'idle-latency',
    label: 'Idle latency',
    description: 'Report a matrix of idle memory latency across NUMA nodes.',
  },
  {
    id: 'benchmark_peak_bandwidth',
    name: 'peak-bandwidth',
    label: 'Peak bandwidth',
    description: 'Report peak memory bandwidth.',
  },
  {
    id: 'benchmark_cross_numa_bandwidth',
    name: 'cross-numa-bandwidth',
    label: 'Cross-NUMA bandwidth',
    description: 'Report cross-NUMA node memory bandwidth.',
  },
  {
    id: 'benchmark_latency_sweep',
    name: 'latency-sweep',
    label: 'Latency sweep',
    description:
      'Sweep latency by datasize to map cache hierarchy and find optimal datasize for other benchmarks.',
  },
  {
    id: 'benchmark_bandwidth_sweep',
    name: 'bandwidth-sweep',
    label: 'Bandwidth sweep',
    description: 'Sweep bandwidth by datasize to map cache hierarchy.',
  },
  {
    id: 'benchmark_c2c_latency',
    name: 'c2c-latency',
    label: 'Core-to-core latency',
    description: 'Report core to core latency.',
  },
];

/**
 * @type {import("./docs/jsdocs").Recipe}
 */
var recipe = {
  name: 'asct',
  title: 'System Characterization',
  version: '1.0',
  api_version: '1.0.0',
  description:
    'This *preview* recipe runs the Arm System Characterization Tool (ASCT) to collect system information and microbenchmark results. It measures memory latency/bandwidth behavior to help with platform bring-up, tuning, and architectural comparisons.',
  parameters: [
    {
      id: 'system_info_only',
      required: false,
      label: 'System info only',
      description: 'Run only system-info (no benchmarks).',
      config: /** @type {any} */ ({
        type: 'checkbox',
        defaultValue: false,
      }),
    },
    ...ASCT_BENCHMARKS.map((benchmark) => ({
      id: benchmark.id,
      required: false,
      label: benchmark.label,
      description: benchmark.description,
      config: /** @type {any} */ ({
        type: 'checkbox',
        defaultValue: false,
      }),
    })),
  ],
  deployments: [
    {
      appliesTo: [
        { architecture: 'aarch64', os: 'Linux' },
        { architecture: 'x86_64', os: 'Linux' },
      ],
      dependencies: [
        {
          type: 'tool',
          name: tool_asct_name,
          version: tool_asct_version,
          requiredWhen: { type: 'always' },
        },
      ],
    },
  ],
  readyStages: [
    {
      name: 'Checking ASCT is ready',
      description:
        'Check that the target has the necessary dependencies to run the ASCT tool (in agent mode)',
      exec: readyAsct,
    },
  ],
  runStages: [
    {
      name: 'Validating recipe parameters',
      description:
        'This stage validates the user has specified appropriate parameter values',
      exec: validateRecipeParameters,
    },
    {
      name: 'Running ASCT',
      description: 'This stage runs ASCT and retrieves the files',
      exec: runAsct,
    },
  ],
  renderStages: [
    {
      name: 'Creating render',
      description:
        'Create the renderer specs that are used to produce visualizations',
      exec: renderAsct,
    },
  ],
};

/**
 * toolStatusToRecipeStatus converts recipe ready advice to a single
 * recipe ready status. The most severe is returned.
 * @param {import("./docs/jsdocs").RecipeReadyAdvice[]} advice
 * @returns {string} readyStatus
 */
function toolStatusToRecipeStatus(advice) {
  /** @type {Record<string, number>} */
  const rank = { ready: 0, unknown: 1, warning: 2, error: 3 };
  return advice.reduce(
    (worst, { AdviceSeverity }) =>
      (rank[AdviceSeverity] ?? 0) > (rank[worst] ?? 0) ? AdviceSeverity : worst,
    'ready',
  );
}

/**
 * generateAsctToolConfig generates a ToolConfiguration for the
 * ASCT tool integration.
 * @param {import("./docs/jsdocs").Workload} workload
 * @param {Object.<string, any>} params
 * @return {import("./docs/jsdocs").ToolConfiguration}
 */
function generateAsctToolConfig(workload, params) {
  return /** @type {any} */ ({
    name: tool_asct_name,
    params: params,
    workload: workload,
    env: {},
  });
}

/** @param {unknown} value */
function parseBool(value) {
  return String(value).toLowerCase() === 'true';
}

// Recipe-defined benchmark defaults when no benchmark checkboxes are selected.
// These explicit defaults are applied by the recipe (not inferred from ASCT).
const ASCT_DEFAULT_BENCHMARK_IDS = new Set([
  'benchmark_idle_latency',
  'benchmark_peak_bandwidth',
  'benchmark_cross_numa_bandwidth',
  'benchmark_latency_sweep',
  'benchmark_bandwidth_sweep',
  'benchmark_c2c_latency',
]);

/**
 * Resolve benchmark selection rules:
 * 1) No benchmark params selected => recipe default benchmarks selected
 *    from ASCT_DEFAULT_BENCHMARK_IDS.
 * 2) system_info_only=true => no benchmarks selected.
 * 3) Some benchmarks selected => only selected benchmarks.
 * @param {(id: string) => unknown} getParam
 */
function resolveBenchmarkSelection(getParam) {
  const systemInfoOnly = parseBool(getParam('system_info_only'));
  const explicitlySelectedBenchmarks = ASCT_BENCHMARKS.filter((benchmark) =>
    parseBool(getParam(benchmark.id)),
  );

  if (systemInfoOnly) {
    return { systemInfoOnly: true, selectedBenchmarks: [] };
  }

  if (explicitlySelectedBenchmarks.length > 0) {
    return {
      systemInfoOnly: false,
      selectedBenchmarks: explicitlySelectedBenchmarks,
    };
  }

  return {
    systemInfoOnly: false,
    selectedBenchmarks: ASCT_BENCHMARKS.filter((benchmark) =>
      ASCT_DEFAULT_BENCHMARK_IDS.has(benchmark.id),
    ),
  };
}

/**
 * Build ASCT tool params from recipe checkboxes.
 * - If system_info_only=true, benchmark selections are ignored.
 * - If no benchmark checkboxes are selected, apply recipe-defined defaults
 *   from ASCT_DEFAULT_BENCHMARK_IDS.
 * @param {any} context
 * @returns {Object.<string, any>}
 */
function buildAsctParams(context) {
  const { systemInfoOnly, selectedBenchmarks } = resolveBenchmarkSelection(
    (id) => context.getParameter(id),
  );

  /** @type {Object.<string, any>} */
  const params = {};

  params.systemInfoOnly = systemInfoOnly;
  params.benchmarks = selectedBenchmarks.map((benchmark) => benchmark.name);

  return params;
}

/**
 * @param {import("./docs/jsdocs").ReadyExecutionContext} context
 */
function readyAsct(context) {
  let workload = context.getWorkload();

  /** @type {import("./docs/jsdocs").ToolConfigurationsArg} */
  let tools = { toolConfigs: [] };

  const { systemInfoOnly, selectedBenchmarks } = resolveBenchmarkSelection(
    (id) => context.getParameter(id),
  );

  let params = {
    benchmarks: systemInfoOnly
      ? []
      : selectedBenchmarks.map((benchmark) => benchmark.name),
    systemInfoOnly: systemInfoOnly,
  };

  tools.toolConfigs.push(generateAsctToolConfig(workload, params));

  const toolResponses = context.probeTools(tools);
  const responseList = Array.isArray(toolResponses) ? toolResponses : [];

  const allAdvice = responseList.flatMap((tr, i) =>
    (tr.advice ?? []).map((a) => ({
      ToolName: tools.toolConfigs[i].name,
      AdviceSeverity: a.level,
      MessageCode: a.messageCode ?? '',
      Metadata: a.metadata ?? {},
      Cause: '',
    })),
  );

  return {
    Status: toolStatusToRecipeStatus(allAdvice),
    Advice: allAdvice,
  };
}

/**
 * @param {import("./docs/jsdocs").RunExecutionContext} context
 */
function validateRecipeParameters(context) {
  const { systemInfoOnly, selectedBenchmarks } = resolveBenchmarkSelection(
    (id) => context.getParameter(id),
  );

  if (systemInfoOnly) {
    const explicitlySelectedLabels = ASCT_BENCHMARKS.filter((benchmark) =>
      parseBool(context.getParameter(benchmark.id)),
    ).map((benchmark) => benchmark.label);
    if (explicitlySelectedLabels.length > 0) {
      context.writeUserMessage(
        'warn',
        `System info only is enabled, so benchmark selections are ignored: ${explicitlySelectedLabels.join(', ')}`,
      );
    }
  } else {
    const explicitlySelectedBenchmarks = ASCT_BENCHMARKS.filter((benchmark) =>
      parseBool(context.getParameter(benchmark.id)),
    );
    if (explicitlySelectedBenchmarks.length === 0) {
      context.writeUserMessage(
        'info',
        `No benchmarks explicitly selected; defaulting to ${selectedBenchmarks.map((benchmark) => benchmark.label).join(', ')}.`,
      );
    }
  }
}

/**
 * @param {import("./docs/jsdocs").RunExecutionContext} context
 */
function runAsct(context) {
  let workload = context.getWorkload();

  /** @type {import("./docs/jsdocs").ToolConfigurationsArg} */
  let tools = { toolConfigs: [] };

  let params = buildAsctParams(context);

  tools.toolConfigs.push(generateAsctToolConfig(workload, params));

  context.runTools(tools);
}

/**
 * @param {import("./docs/jsdocs").RenderExecutionContext} context
 */
function renderAsct(context) {
  const runDescriptions = context.getRunDescriptions();
  const renderers = /** @type {any[]} */ ([]);
  const visualizations = [];

  const entity = 'tool/asct/0/output';

  // Always include the summary/open-directory visualization.
  const openDirectoryRendererId = 'asct_open_directory_renderer';
  renderers.push({
    type: 'CSV',
    id: openDirectoryRendererId,
    config: {
      component: `${entity}/system_info.csv`,
    },
  });

  visualizations.push(
    /** @type {any} */ ({
      type: 'open_directory',
      rendererId: openDirectoryRendererId,
      id: 'asct_open_directory',
      title: 'Summary',
      description: '',
      config: {
        tool_name: tool_asct_name,
        title: 'System Characterization report generated',
        description:
          'This run generated system information and microbenchmark results using the Arm System Characterization Tool (ASCT). The full report and raw output files are available in the run directory.',
        caption:
          'Opens the folder on your machine containing the ASCT output files for this run.',
      },
    }),
  );

  const params =
    runDescriptions.length > 0 ? runDescriptions[0].Parameters : {};

  // Apply the shared selection rules so rendering matches run/ready behavior.
  const { selectedBenchmarks } = resolveBenchmarkSelection((id) => params[id]);

  // CSVs with tabular benchmark data rendered as generic grids.
  const dataCsvList = new Set([
    'idle-latency.csv',
    'peak-bandwidth.csv',
    'cross-numa-bandwidth.csv',
    'latency-sweep.csv',
    'bandwidth-sweep.csv',
    'c2c-latency.csv',
  ]);

  const entries = selectedBenchmarks
    .map((benchmark) => ({
      id: benchmark.id,
      title: benchmark.label,
      description: benchmark.description,
      file: `${benchmark.name}.csv`,
    }))
    .filter((entry) => dataCsvList.has(entry.file));

  for (const entry of entries) {
    const rendererId = `asct_csv_${entry.id}`;
    renderers.push({
      type: 'CSV',
      id: rendererId,
      config: {
        component: `${entity}/${entry.file}`,
      },
    });

    visualizations.push({
      type: 'generic_grid',
      id: `asct_${entry.id}_table`,
      rendererId: rendererId,
      title: entry.title,
      description: entry.description,
      config: {
        autoSizeColumns: true,
        data_source: {
          tables: {
            table: [{ renderer_id: rendererId, output: 'csv' }],
          },
        },
        customQuery: {
          tableNamePlaceholder: '__table__',
          query:
            "SELECT COLUMNS(c -> c = 'index' OR c = 'column0' OR c = '') AS \"-\", " +
            "COLUMNS(c -> c != 'index' AND c != 'column0' AND c != '') FROM __table__",
        },
      },
    });
  }

  // CSVs with key-value style content rendered as generic grids.
  const keyValueCsvList = new Set(['system_info.csv']);

  const keyValueEntries = [
    {
      id: 'system_info',
      title: 'System Information',
      description:
        'System information collected by ASCT, including CPU, memory, and storage details.',
      file: 'system_info.csv',
    },
    ...selectedBenchmarks.map((benchmark) => ({
      id: benchmark.id,
      title: benchmark.label,
      description: benchmark.description,
      file: `${benchmark.name}.csv`,
    })),
  ].filter((entry) => keyValueCsvList.has(entry.file));

  for (const entry of keyValueEntries) {
    const rendererId = `asct_key_value_${entry.id}`;

    renderers.push({
      type: 'CSV',
      id: rendererId,
      config: {
        component: `${entity}/${entry.file}`,
      },
    });

    visualizations.push({
      type: 'generic_grid',
      id: `asct_${entry.id}_table`,
      rendererId: rendererId,
      title: entry.title,
      description: entry.description,
      config: {
        autoSizeColumns: true,
        data_source: {
          tables: {
            table: [{ renderer_id: rendererId, output: 'csv' }],
          },
        },
        customQuery: {
          tableNamePlaceholder: '__table__',
          query:
            'SELECT * FROM (' +
            'SELECT \'\' AS "Name", \'\' AS "Value" WHERE FALSE ' +
            'UNION ALL ' +
            'SELECT * FROM __table__' +
            ') t',
        },
      },
    });
  }

  return { renderers, visualizations };
}
