import type { Attributes } from "@opentelemetry/api";
import {
  ATTR_GEN_AI_AGENT_DESCRIPTION,
  ATTR_GEN_AI_AGENT_ID,
  ATTR_GEN_AI_AGENT_NAME,
  ATTR_GEN_AI_COMPLETION,
  ATTR_GEN_AI_INPUT_MESSAGES,
  ATTR_GEN_AI_OPERATION_NAME,
  ATTR_GEN_AI_OUTPUT_MESSAGES,
  ATTR_GEN_AI_PROMPT,
  ATTR_GEN_AI_PROVIDER_NAME,
  ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY,
  ATTR_GEN_AI_REQUEST_MAX_TOKENS,
  ATTR_GEN_AI_REQUEST_MODEL,
  ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY,
  ATTR_GEN_AI_REQUEST_SEED,
  ATTR_GEN_AI_REQUEST_STOP_SEQUENCES,
  ATTR_GEN_AI_REQUEST_TEMPERATURE,
  ATTR_GEN_AI_REQUEST_TOP_K,
  ATTR_GEN_AI_REQUEST_TOP_P,
  ATTR_GEN_AI_RESPONSE_MODEL,
  ATTR_GEN_AI_TOOL_CALL_ID,
  ATTR_GEN_AI_TOOL_DESCRIPTION,
  ATTR_GEN_AI_TOOL_NAME,
  ATTR_GEN_AI_TOOL_TYPE,
  ATTR_GEN_AI_USAGE_INPUT_TOKENS,
  ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
  GEN_AI_OPERATION_NAME_VALUE_CHAT,
  GEN_AI_OPERATION_NAME_VALUE_CREATE_AGENT,
  GEN_AI_OPERATION_NAME_VALUE_EMBEDDINGS,
  GEN_AI_OPERATION_NAME_VALUE_EXECUTE_TOOL,
  GEN_AI_OPERATION_NAME_VALUE_GENERATE_CONTENT,
  GEN_AI_OPERATION_NAME_VALUE_INVOKE_AGENT,
  GEN_AI_OPERATION_NAME_VALUE_INVOKE_WORKFLOW,
  GEN_AI_OPERATION_NAME_VALUE_RETRIEVAL,
  GEN_AI_OPERATION_NAME_VALUE_TEXT_COMPLETION,
} from "@opentelemetry/semantic-conventions/incubating";

import {
  OpenInferenceSpanKind,
  SemanticConventions,
} from "@arizeai/openinference-semantic-conventions";

import type { ChatMessage, GenericPart } from "./__generated__/opentelemetryInputMessages.js";
import type { OutputMessage } from "./__generated__/opentelemetryOutputMessages.js";
import {
  getMimeType,
  getNumber,
  getString,
  getStringArray,
  merge,
  safelyJSONStringify,
  safelyParseJSON,
  set,
  toStringContent,
} from "./utils.js";

export type GenAIInputMessage = ChatMessage;
export type GenAIInputMessagePart = ChatMessage["parts"][number];
export type GenAIOutputMessage = OutputMessage & {
  /** @deprecated use parts instead */
  text?: string;
};
export type GenAIOutputMessagePart = OutputMessage["parts"][number];

const AGENT_KIND_PREFIXES = [
  ATTR_GEN_AI_AGENT_ID,
  ATTR_GEN_AI_AGENT_NAME,
  ATTR_GEN_AI_AGENT_DESCRIPTION,
] as const;

// "plan" was added to the OTel GenAI semantic conventions
// (open-telemetry/semantic-conventions-genai#97, merged 2026-05-11) but is not yet released,
// so @opentelemetry/semantic-conventions exports no constant for it — hence the literal.
const GEN_AI_OPERATION_NAME_VALUE_PLAN = "plan";

// gen_ai.operation.name values that classify a span by OpenInference span kind. Mirrors
// AGENT_KIND_PREFIXES / TOOL_EXECUTION_PREFIXES. These operations are recognized by value
// because the conversion target — OpenInferenceSpanKind — is a closed enum with no per-
// operation member, so each operation must be mapped into our vocabulary; the original
// operation name is not preserved on the OI span.
const AGENT_OPERATIONS: ReadonlySet<string> = new Set([
  GEN_AI_OPERATION_NAME_VALUE_CREATE_AGENT,
  GEN_AI_OPERATION_NAME_VALUE_INVOKE_AGENT,
  GEN_AI_OPERATION_NAME_VALUE_PLAN,
]);

// "invoke_workflow" maps to CHAIN rather than AGENT: a workflow is a fixed orchestration of
// steps (a chain), whereas AGENT is reserved for autonomous, LLM-driven control flow. An
// explicit agent identity (agent.* attribute) still takes precedence and yields AGENT; see
// mapSpanKind.
const CHAIN_OPERATIONS: ReadonlySet<string> = new Set([
  GEN_AI_OPERATION_NAME_VALUE_INVOKE_WORKFLOW,
]);

// Terminal operations describe a single concrete model/data call (a leaf in the span tree),
// each with a direct OpenInference span kind. operation.name is authoritative for these — an
// agent.* identity on them is context (which agent owns the call), not a signal to upgrade
// the span to AGENT (see mapSpanKind).
const TERMINAL_OPERATION_SPAN_KINDS: ReadonlyMap<string, OpenInferenceSpanKind> = new Map([
  [GEN_AI_OPERATION_NAME_VALUE_CHAT, OpenInferenceSpanKind.LLM],
  [GEN_AI_OPERATION_NAME_VALUE_TEXT_COMPLETION, OpenInferenceSpanKind.LLM],
  [GEN_AI_OPERATION_NAME_VALUE_GENERATE_CONTENT, OpenInferenceSpanKind.LLM],
  [GEN_AI_OPERATION_NAME_VALUE_EMBEDDINGS, OpenInferenceSpanKind.EMBEDDING],
  [GEN_AI_OPERATION_NAME_VALUE_RETRIEVAL, OpenInferenceSpanKind.RETRIEVER],
  [GEN_AI_OPERATION_NAME_VALUE_EXECUTE_TOOL, OpenInferenceSpanKind.TOOL],
]);

const TOOL_EXECUTION_PREFIXES = [
  ATTR_GEN_AI_TOOL_NAME,
  ATTR_GEN_AI_TOOL_DESCRIPTION,
  ATTR_GEN_AI_TOOL_CALL_ID,
  ATTR_GEN_AI_TOOL_TYPE,
] as const;

const ATTR_GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" as const;
const ATTR_GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" as const;
const ATTR_GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" as const;
const ATTR_GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" as const;
const ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens" as const;
const ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS =
  "gen_ai.usage.cache_creation.input_tokens" as const;

/**
 * A reasoning (thinking) message part.
 *
 * Producers such as `@ai-sdk/otel` already emit `{ type: "reasoning", content: "..." }` parts in
 * `gen_ai.input.messages` / `gen_ai.output.messages`, but the draft OTel GenAI message schemas the
 * types in `__generated__` are derived from do not yet declare the part — hence the local type.
 */
interface ReasoningPart {
  type: "reasoning";
  content?: unknown;
  [k: string]: unknown;
}

// Shared part parsing
type AnyPart = GenAIInputMessagePart | GenAIOutputMessagePart | ReasoningPart;

/**
 * Type guard for a GenAI chat message
 * @param value - The value to check
 * @returns True if the value is a chat message, false otherwise
 */
const isGenAIChatMessage = (value: unknown): value is ChatMessage => {
  if (typeof value !== "object" || value === null) return false;
  if (!("role" in value) || !("parts" in value)) return false;
  if (typeof value.role !== "string" || !Array.isArray(value.parts)) return false;
  if (!value.parts || !Array.isArray(value.parts)) return false;
  return true;
};

/**
 * Normalize a GenAI tool definition into the OpenAI-style tool schema shape expected by OpenInference.
 * @param toolDefinition - The tool definition to normalize
 * @returns The normalized tool definition, or the original value when it cannot be normalized
 */
const normalizeToolDefinition = (toolDefinition: unknown): unknown => {
  if (
    typeof toolDefinition !== "object" ||
    toolDefinition === null ||
    Array.isArray(toolDefinition)
  ) {
    return toolDefinition;
  }

  const definition = toolDefinition as Record<string, unknown>;
  if (typeof definition.function === "object" && definition.function !== null) {
    return definition;
  }

  if (typeof definition.name !== "string") {
    return definition;
  }

  const parameters = definition.parameters ?? definition.inputSchema ?? definition.input_schema;
  const normalizedFunction: Record<string, unknown> = { name: definition.name };
  if (typeof definition.description === "string") {
    normalizedFunction.description = definition.description;
  }
  if (parameters != null) {
    normalizedFunction.parameters = parameters;
  }

  return {
    type: typeof definition.type === "string" ? definition.type : "function",
    function: normalizedFunction,
  };
};

const getSystemInstructionParts = (spanAttributes: Attributes): GenAIInputMessagePart[] | null => {
  const systemInstructions = getString(spanAttributes[ATTR_GEN_AI_SYSTEM_INSTRUCTIONS]);
  if (systemInstructions == null) {
    return null;
  }

  const parsedInstructions = safelyParseJSON(systemInstructions);
  if (Array.isArray(parsedInstructions)) {
    const parts = parsedInstructions.flatMap((part): GenAIInputMessagePart[] => {
      if (
        typeof part === "object" &&
        part !== null &&
        "type" in part &&
        part.type === "text" &&
        "content" in part
      ) {
        return [{ type: "text", content: toStringContent(part.content) }];
      }
      return [];
    });
    return parts.length > 0 ? parts : null;
  }

  return [{ type: "text", content: systemInstructions }];
};

/**
 * Process genai message parts into openinference attributes
 *
 * @param params - The parameters to process the message parts
 */
const processMessageParts = ({
  attrs,
  msgPrefix,
  parts,
}: {
  /** The attributes to mutate with new message attributes */
  attrs: Attributes;
  /** The prefix to add to the attributes */
  msgPrefix: string;
  /** The parts to process */
  parts: AnyPart[] | undefined;
}): void => {
  if (!Array.isArray(parts) || parts.length === 0) return;

  // track the index of the content and tool calls outside the loop
  // this is just in-case we have to skip bad parts
  let contentIndex = 0;
  let toolIndex = 0;

  for (const part of parts) {
    if (!part || typeof part !== "object") continue;
    if (!part.type) continue;

    switch (part.type) {
      case "text": {
        const text = toStringContent(part.content);
        if (text !== undefined) {
          // MESSAGE_CONTENTS entries
          const contentPrefix = `${msgPrefix}${SemanticConventions.MESSAGE_CONTENTS}.${contentIndex}.`;
          set(attrs, `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`, "text");
          set(attrs, `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`, text);
          contentIndex += 1;
        }
        continue;
      }
      case "reasoning": {
        // MESSAGE_CONTENTS entry carrying the reasoning text itself, not the serialized part
        const contentPrefix = `${msgPrefix}${SemanticConventions.MESSAGE_CONTENTS}.${contentIndex}.`;
        set(attrs, `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`, "reasoning");
        if (part.content != null) {
          set(
            attrs,
            `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`,
            toStringContent(part.content),
          );
        }
        contentIndex += 1;
        continue;
      }
      case "tool_call": {
        const id = part.id ?? undefined;
        const name = part.name;
        const args = part.arguments ?? {};
        const toolPrefix = `${msgPrefix}${SemanticConventions.MESSAGE_TOOL_CALLS}.${toolIndex}.`;
        set(attrs, `${toolPrefix}${SemanticConventions.TOOL_CALL_ID}`, id);
        set(attrs, toolPrefix + SemanticConventions.TOOL_CALL_FUNCTION_NAME, name);
        set(
          attrs,
          toolPrefix + SemanticConventions.TOOL_CALL_FUNCTION_ARGUMENTS_JSON,
          safelyJSONStringify(args),
        );
        toolIndex += 1;
        continue;
      }
      case "tool_call_response": {
        const id = part.id ?? undefined;
        const response = toStringContent(part.response);

        set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`, id);
        set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_CONTENT}`, response);
        continue;
      }
      default: {
        // Generic / unknown part type: capture as JSON text content. Only MESSAGE_CONTENTS is
        // written — the flat MESSAGE_CONTENT is an alternative representation of the same message,
        // so setting both duplicates the content in consumers that render each of them.
        const genericPart = part as GenericPart;
        const genericText = toStringContent(genericPart);
        const contentPrefix = `${msgPrefix}${SemanticConventions.MESSAGE_CONTENTS}.${contentIndex}.`;
        set(attrs, `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`, genericPart.type);
        set(attrs, `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`, genericText);
        contentIndex += 1;
      }
    }
  }
};

/**
 * Set OpenInference attributes for a single tool response message.
 *
 * GenAI can group multiple tool_call_response parts under one tool-role message;
 * each response needs its own OpenInference message to avoid overwriting siblings.
 * @param params - The tool response message mapping parameters
 */
const setToolCallResponseMessage = ({
  attrs,
  msgPrefix,
  part,
}: {
  attrs: Attributes;
  msgPrefix: string;
  part: Extract<AnyPart, { type: "tool_call_response" }>;
}): void => {
  const id = part.id ?? undefined;
  const response = toStringContent(part.response);

  set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_ROLE}`, "tool");
  set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`, id);
  set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_CONTENT}`, response);
};

/**
 * Convert GenAI span attributes to OpenInference span attributes
 * @param spanAttributes - The span attributes containing GenAI span attributes to convert
 * @returns The converted OpenInference span attributes
 */
export const convertGenAISpanAttributesToOpenInferenceSpanAttributes = (
  spanAttributes: Attributes,
): Attributes => {
  return merge(
    mapProviderAndSystem(spanAttributes),
    mapAgentAttributes(spanAttributes),
    mapModels(spanAttributes),
    mapSpanKind(spanAttributes),
    mapInvocationParameters(spanAttributes),
    mapInputMessages(spanAttributes),
    mapOutputMessages(spanAttributes),
    mapAgent(spanAttributes),
    mapSystemInstructions(spanAttributes),
    mapToolDefinitions(spanAttributes),
    mapTokenCounts(spanAttributes),
    mapToolExecution(spanAttributes),
    mapInputValue(spanAttributes),
    mapOutputValue(spanAttributes),
  );
};

/**
 * Map provider and system to openinference attributes
 * @remarks TODO: add some heuristics that can map incoming provider names to the correct OpenInference provider name
 * @param spanAttributes - The span attributes containing provider and system to map
 * @returns The mapped provider and system attributes
 */
export const mapProviderAndSystem = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  const provider = getString(spanAttributes[ATTR_GEN_AI_PROVIDER_NAME]);
  set(attrs, SemanticConventions.LLM_SYSTEM, provider);
  set(attrs, SemanticConventions.LLM_PROVIDER, provider);
  return attrs;
};

/**
 * Map GenAI agent attributes to OpenInference agent attributes.
 *
 * Currently only gen_ai.agent.name -> agent.name. gen_ai.agent.id and
 * gen_ai.agent.description (which also drive AGENT span-kind detection in mapSpanKind) are
 * intentionally not mapped, as OpenInference has no corresponding agent attributes for them.
 *
 * @param spanAttributes - The GenAI span attributes to read agent attributes from
 * @returns The mapped OpenInference agent attributes
 */
export const mapAgentAttributes = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  const agentName = getString(spanAttributes[ATTR_GEN_AI_AGENT_NAME]);
  set(attrs, SemanticConventions.AGENT_NAME, agentName);
  return attrs;
};

/**
 * Map model name to openinference attributes
 * @param spanAttributes - The span attributes containing model name to map
 * @returns The mapped model name attributes
 */
export const mapModels = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  const requestModel = getString(spanAttributes[ATTR_GEN_AI_REQUEST_MODEL]);
  const responseModel = getString(spanAttributes[ATTR_GEN_AI_RESPONSE_MODEL]);
  const modelName = responseModel ?? requestModel;
  set(attrs, SemanticConventions.LLM_MODEL_NAME, modelName);
  return attrs;
};

/**
 * Map span kind to openinference attributes
 * @param spanAttributes - The span attributes containing span kind to map
 * @returns The mapped span kind attributes
 */
export const mapSpanKind = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  // default to LLM for now
  let spanKind = OpenInferenceSpanKind.LLM;
  const operationName = getString(spanAttributes[ATTR_GEN_AI_OPERATION_NAME]);
  const terminalSpanKind =
    operationName !== undefined ? TERMINAL_OPERATION_SPAN_KINDS.get(operationName) : undefined;
  const hasAgentAttributes = AGENT_KIND_PREFIXES.some((prefix) => spanAttributes[prefix]);
  if (terminalSpanKind !== undefined) {
    // Concrete leaf operation (chat/text_completion/generate_content/embeddings/retrieval/
    // execute_tool): authoritative. An agent.* identity here is just context (which agent
    // owns the call) and does not upgrade it to AGENT.
    spanKind = terminalSpanKind;
  } else if (
    hasAgentAttributes ||
    (operationName !== undefined && AGENT_OPERATIONS.has(operationName))
  ) {
    // Control-flow span: an agent.* identity, or an agent-class operation, makes it AGENT —
    // even over a CHAIN-class operation like invoke_workflow.
    spanKind = OpenInferenceSpanKind.AGENT;
  } else if (operationName !== undefined && CHAIN_OPERATIONS.has(operationName)) {
    // CHAIN-class operation, only reached when no agent identity is present.
    spanKind = OpenInferenceSpanKind.CHAIN;
  }
  // Tool attributes force TOOL even when the operation says otherwise — covers tool-execution
  // spans not labeled with operation.name = execute_tool.
  if (TOOL_EXECUTION_PREFIXES.some((prefix) => spanAttributes[prefix])) {
    spanKind = OpenInferenceSpanKind.TOOL;
  }

  set(attrs, SemanticConventions.OPENINFERENCE_SPAN_KIND, spanKind);

  return attrs;
};

/**
 * Map invocation parameters to openinference attributes
 * @param spanAttributes - The span attributes containing invocation parameters to map
 * @returns The mapped invocation parameters attributes
 */
export const mapInvocationParameters = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  const requestModel = getString(spanAttributes[ATTR_GEN_AI_REQUEST_MODEL]);
  const maxTokens = getNumber(spanAttributes[ATTR_GEN_AI_REQUEST_MAX_TOKENS]);
  const temperature = getNumber(spanAttributes[ATTR_GEN_AI_REQUEST_TEMPERATURE]);
  const topP = getNumber(spanAttributes[ATTR_GEN_AI_REQUEST_TOP_P]);
  const topK = getNumber(spanAttributes[ATTR_GEN_AI_REQUEST_TOP_K]);
  const presencePenalty = getNumber(spanAttributes[ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY]);
  const frequencyPenalty = getNumber(spanAttributes[ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY]);
  const seed = getNumber(spanAttributes[ATTR_GEN_AI_REQUEST_SEED]);
  const stopSequences = getStringArray(spanAttributes[ATTR_GEN_AI_REQUEST_STOP_SEQUENCES]);
  const invocationParameters: Record<string, unknown> = {};
  if (requestModel) invocationParameters.model = requestModel;
  if (typeof temperature === "number") invocationParameters.temperature = temperature;
  if (typeof topP === "number") invocationParameters.top_p = topP;
  if (typeof topK === "number") invocationParameters.top_k = topK;
  if (typeof presencePenalty === "number") invocationParameters.presence_penalty = presencePenalty;
  if (typeof frequencyPenalty === "number")
    invocationParameters.frequency_penalty = frequencyPenalty;
  if (typeof seed === "number") invocationParameters.seed = seed;
  if (stopSequences && stopSequences.length > 0)
    invocationParameters.stop_sequences = stopSequences;
  if (typeof maxTokens === "number") invocationParameters.max_completion_tokens = maxTokens;
  if (Object.keys(invocationParameters).length > 0) {
    set(
      attrs,
      SemanticConventions.LLM_INVOCATION_PARAMETERS,
      safelyJSONStringify(invocationParameters),
    );
  }
  return attrs;
};

/**
 * Map input value to openinference attributes
 * @param spanAttributes - The span attributes containing input value to map
 * @returns The mapped input value attributes
 */
export const mapInputValue = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  let input = getString(spanAttributes["input"]);
  if (!input) {
    // fallback to deprecated prompt attribute if input is not present
    input = getString(spanAttributes[ATTR_GEN_AI_PROMPT]);
  }
  if (!input) {
    input = getString(spanAttributes[ATTR_GEN_AI_INPUT_MESSAGES]);
  }
  // only set input value and mime type if input is present
  if (input) {
    set(attrs, SemanticConventions.INPUT_VALUE, input);
    set(attrs, SemanticConventions.INPUT_MIME_TYPE, getMimeType(input));
  }
  return attrs;
};

/**
 * Map output value to openinference attributes
 * @param spanAttributes - The span attributes containing output value to map
 * @returns The mapped output value attributes
 */
export const mapOutputValue = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  let output = getString(spanAttributes["output"]);
  if (!output) {
    // fallback to deprecated completion attribute if output is not present
    output = getString(spanAttributes[ATTR_GEN_AI_COMPLETION]);
  }
  if (!output) {
    output = getString(spanAttributes[ATTR_GEN_AI_OUTPUT_MESSAGES]);
  }
  // only set output value and mime type if output is present
  if (output) {
    set(attrs, SemanticConventions.OUTPUT_VALUE, output);
    set(attrs, SemanticConventions.OUTPUT_MIME_TYPE, getMimeType(output));
  }
  return attrs;
};

/**
 * Map GenAI agent attributes to OpenInference attributes.
 * @param spanAttributes - The span attributes containing agent details
 * @returns The mapped agent attributes
 */
export const mapAgent = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  set(attrs, SemanticConventions.AGENT_NAME, getString(spanAttributes[ATTR_GEN_AI_AGENT_NAME]));
  return attrs;
};

/**
 * Map GenAI system instructions into metadata for preservation.
 * @param spanAttributes - The span attributes containing system instructions
 * @returns The mapped metadata attributes
 */
export const mapSystemInstructions = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  const systemInstructions = getString(spanAttributes[ATTR_GEN_AI_SYSTEM_INSTRUCTIONS]);
  if (systemInstructions) {
    set(attrs, `${SemanticConventions.METADATA}.gen_ai.system_instructions`, systemInstructions);

    // Only emit the synthetic system message when there are parts to put in it.
    // mapInputMessages shifts its indexes by one on exactly this condition, so
    // emitting here without parts would claim index 0 while the input messages
    // still start at 0, overwriting the first message's role with "system".
    const parts = getSystemInstructionParts(spanAttributes);
    if (parts != null) {
      const msgPrefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.0.`;
      set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_ROLE}`, "system");
      processMessageParts({ attrs, msgPrefix, parts });
    }
  }
  return attrs;
};

/**
 * Map GenAI tool definitions to OpenInference LLM tool schema attributes.
 * @param spanAttributes - The span attributes containing tool definitions
 * @returns The mapped tool schema attributes
 */
export const mapToolDefinitions = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  const toolDefinitions = getString(spanAttributes[ATTR_GEN_AI_TOOL_DEFINITIONS]);
  const parsedToolDefinitions = safelyParseJSON(toolDefinitions);
  if (Array.isArray(parsedToolDefinitions)) {
    parsedToolDefinitions.forEach((toolDefinition, index) => {
      set(
        attrs,
        `${SemanticConventions.LLM_TOOLS}.${index}.${SemanticConventions.TOOL_JSON_SCHEMA}`,
        safelyJSONStringify(normalizeToolDefinition(toolDefinition)),
      );
    });
  }
  return attrs;
};

/**
 * Map input messages to openinference attributes
 * @param spanAttributes - The span attributes containing input messages to map
 * @returns The mapped input messages attributes
 */
export const mapInputMessages = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  const genAIInputMessages = safelyParseJSON(spanAttributes[ATTR_GEN_AI_INPUT_MESSAGES]);

  if (Array.isArray(genAIInputMessages)) {
    let msgIndex = getSystemInstructionParts(spanAttributes) != null ? 1 : 0;
    (genAIInputMessages as unknown[]).forEach((msg) => {
      if (!isGenAIChatMessage(msg)) return;

      const toolCallResponses = msg.parts.filter(
        (part): part is Extract<AnyPart, { type: "tool_call_response" }> =>
          part?.type === "tool_call_response",
      );

      if (msg.role === "tool" && toolCallResponses.length > 0) {
        toolCallResponses.forEach((part) => {
          const msgPrefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${msgIndex}.`;
          setToolCallResponseMessage({ attrs, msgPrefix, part });
          msgIndex += 1;
        });
        return;
      }

      const msgPrefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${msgIndex}.`;
      // set the message role
      set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_ROLE}`, msg.role);
      // process and set the rest of the message parts
      processMessageParts({ attrs, msgPrefix, parts: msg.parts });
      msgIndex += 1;
    });
  }

  return attrs;
};

/**
 * Map output messages to openinference attributes
 * @param spanAttributes - The span attributes containing output messages to map
 * @returns The mapped output messages attributes
 */
export const mapOutputMessages = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  const genAIOutputMessages = safelyParseJSON(spanAttributes[ATTR_GEN_AI_OUTPUT_MESSAGES]);

  if (Array.isArray(genAIOutputMessages) && genAIOutputMessages.length > 0) {
    // recast as unknown[] for safety, as Array.isArray() retypes to any[]
    (genAIOutputMessages as unknown[]).forEach((msg, msgIndex) => {
      if (!isGenAIChatMessage(msg)) return;
      const msgPrefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.${msgIndex}.`;
      // set the message role
      set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_ROLE}`, msg.role);
      // process and set the rest of the message parts
      processMessageParts({ attrs, msgPrefix, parts: msg.parts });
    });
  }

  return attrs;
};

/**
 * Map usage token counts to openinference attributes
 * @param spanAttributes - The span attributes containing usage token counts to map
 * @returns The mapped usage token counts attributes
 */
export const mapTokenCounts = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  const inputTokens = getNumber(spanAttributes[ATTR_GEN_AI_USAGE_INPUT_TOKENS]);
  const outputTokens = getNumber(spanAttributes[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]);
  const cacheReadInputTokens = getNumber(spanAttributes[ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]);
  const cacheCreationInputTokens = getNumber(
    spanAttributes[ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS],
  );
  if (typeof inputTokens === "number") {
    set(attrs, SemanticConventions.LLM_TOKEN_COUNT_PROMPT, inputTokens);
  }
  if (typeof outputTokens === "number") {
    set(attrs, SemanticConventions.LLM_TOKEN_COUNT_COMPLETION, outputTokens);
  }
  if (typeof inputTokens === "number" && typeof outputTokens === "number") {
    set(attrs, SemanticConventions.LLM_TOKEN_COUNT_TOTAL, inputTokens + outputTokens);
  }
  if (typeof cacheReadInputTokens === "number") {
    set(attrs, SemanticConventions.LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_READ, cacheReadInputTokens);
  }
  if (typeof cacheCreationInputTokens === "number") {
    set(
      attrs,
      SemanticConventions.LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_WRITE,
      cacheCreationInputTokens,
    );
  }
  return attrs;
};

/**
 * Map tool execution to openinference attributes
 * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#execute-tool-span
 * @param spanAttributes - The span attributes containing tool execution to map
 * @returns The mapped tool execution attributes
 */
export const mapToolExecution = (spanAttributes: Attributes): Attributes => {
  const attrs: Attributes = {};
  const toolName = getString(spanAttributes[ATTR_GEN_AI_TOOL_NAME]);
  const toolDescription = getString(spanAttributes[ATTR_GEN_AI_TOOL_DESCRIPTION]);
  const toolCallId = getString(spanAttributes[ATTR_GEN_AI_TOOL_CALL_ID]);
  const toolCallArguments = getString(spanAttributes[ATTR_GEN_AI_TOOL_CALL_ARGUMENTS]);
  const toolCallResult = getString(spanAttributes[ATTR_GEN_AI_TOOL_CALL_RESULT]);
  // parse supported tool details
  set(attrs, SemanticConventions.TOOL_NAME, toolName);
  set(attrs, SemanticConventions.TOOL_DESCRIPTION, toolDescription);
  set(attrs, SemanticConventions.TOOL_CALL_ID, toolCallId);
  set(attrs, SemanticConventions.TOOL_PARAMETERS, toolCallArguments);
  if (toolCallArguments) {
    set(attrs, SemanticConventions.INPUT_VALUE, toolCallArguments);
    set(attrs, SemanticConventions.INPUT_MIME_TYPE, getMimeType(toolCallArguments));
  }
  if (toolCallResult) {
    set(attrs, SemanticConventions.OUTPUT_VALUE, toolCallResult);
    set(attrs, SemanticConventions.OUTPUT_MIME_TYPE, getMimeType(toolCallResult));
  }

  return attrs;
};
