import type { Jsonifiable } from 'type-fest'

import { cleanStringForModel, stringifyForModel } from './utils'

/**
 * Generic/default OpenAI message without any narrowing applied.
 */
export interface Msg {
  /**
   * The role of the messages author. One of `system`, `user`, `assistant`,
   * 'tool', or `function`.
   */
  role: Msg.Role

  /**
   * The contents of the message. `content` may be null for assistant messages
   * with function calls or `undefined` for assistant messages if a `refusal`
   * was given by the model.
   */
  content?: string | null

  /**
   * The reason the model refused to generate this message or `undefined` if the
   * message was generated successfully.
   */
  refusal?: string | null

  /**
   * The name and arguments of a function that should be called, as generated
   * by the model.
   */
  function_call?: Msg.Call.Function

  /**
   * The tool calls generated by the model, such as function calls.
   */
  tool_calls?: Msg.Call.Tool[]

  /**
   * Tool call that this message is responding to.
   */
  tool_call_id?: string

  /**
   * The name of the author of this message. `name` is required if role is
   * `function`, and it should be the name of the function whose response is in the
   * `content`. May contain a-z, A-Z, 0-9, and underscores, with a maximum length of
   * 64 characters.
   */
  name?: string
}

export interface LegacyMsg {
  content: string | null
  role: Msg.Role
  function_call?: Msg.Call.Function
  tool_calls?: Msg.Call.Tool[]
  tool_call_id?: string
  name?: string
}

/** Used for multimodal chat messages. */
export type ChatMessageContentPart =
  | {
      type: 'text'
      text: string
    }
  // User messages only.
  | {
      type: 'image_url'
      image_url: {
        url: string
        detail?: 'low' | 'high' | 'auto' | (string & {})
      }
    }
  | {
      type: 'input_audio'
      input_audio: {
        data: string
        format: 'mp3' | 'wav' | (string & {})
      }
    }
  // Assistant messages only.
  | {
      type: 'refusal'
      refusal: string
    }

/** Narrowed OpenAI Message types. */
export namespace Msg {
  /** Possible roles for a message. */
  export type Role =
    | 'system'
    | 'developer'
    | 'user'
    | 'assistant'
    | 'function'
    | 'tool'

  export namespace Call {
    /**
     * The name and arguments of a function that should be called, as generated
     * by the model.
     */
    export type Function = {
      /**
       * The arguments to call the function with, as generated by the model in
       * JSON format.
       */
      arguments: string

      /** The name of the function to call. */
      name: string
    }

    /** The tool calls generated by the model, such as function calls. */
    export type Tool = {
      /** The ID of the tool call. */
      id: string

      /** The type of the tool. Currently, only `function` is supported. */
      type: 'function'

      /** The function that the model called. */
      function: Call.Function
    }
  }

  /** Message with text content for the system. */
  export type System = {
    role: 'system'
    content: string
    name?: string
  }

  /** Message with text content for the developer. */
  export type Developer = {
    role: 'developer'
    content: string
    name?: string
  }

  /** Message with text content from the user. */
  export type User = {
    role: 'user'
    name?: string
    content: string
  }

  /** Message with text content from the assistant. */
  export type Assistant = {
    role: 'assistant'
    name?: string
    content: string
  }

  /** Message with refusal reason from the assistant. */
  export type Refusal = {
    role: 'assistant'
    name?: string
    refusal: string
  }

  /** Message with arguments to call a function. */
  export type FuncCall = {
    role: 'assistant'
    name?: string
    content: null
    function_call: Call.Function
  }

  /** Message with the result of a function call. */
  export type FuncResult = {
    role: 'function'
    name: string
    content: string
  }

  /** Message with arguments to call one or more tools. */
  export type ToolCall = {
    role: 'assistant'
    name?: string
    content: null
    tool_calls: Call.Tool[]
  }

  /** Message with the result of a tool call. */
  export type ToolResult = {
    role: 'tool'
    tool_call_id: string
    content: string
  }
}

/** Utility functions for creating and checking message types. */
export namespace Msg {
  /** Create a system message. Cleans indentation and newlines by default. */
  export function system(
    content: string,
    opts?: {
      /** Custom name for the message. */
      name?: string
      /** Whether to clean extra newlines and indentation. Defaults to true. */
      cleanContent?: boolean
    }
  ): Msg.System {
    const { name, cleanContent = true } = opts ?? {}
    return {
      role: 'system',
      content: cleanContent ? cleanStringForModel(content) : content,
      ...(name ? { name } : {})
    }
  }

  /** Create a developer message. Cleans indentation and newlines by default. */
  export function developer(
    content: string,
    opts?: {
      /** Custom name for the message. */
      name?: string
      /** Whether to clean extra newlines and indentation. Defaults to true. */
      cleanContent?: boolean
    }
  ): Msg.Developer {
    const { name, cleanContent = true } = opts ?? {}
    return {
      role: 'developer',
      content: cleanContent ? cleanStringForModel(content) : content,
      ...(name ? { name } : {})
    }
  }

  /** Create a user message. Cleans indentation and newlines by default. */
  export function user(
    content: string,
    opts?: {
      /** Custom name for the message. */
      name?: string
      /** Whether to clean extra newlines and indentation. Defaults to true. */
      cleanContent?: boolean
    }
  ): Msg.User {
    const { name, cleanContent = true } = opts ?? {}
    return {
      role: 'user',
      content: cleanContent ? cleanStringForModel(content) : content,
      ...(name ? { name } : {})
    }
  }

  /** Create an assistant message. Cleans indentation and newlines by default. */
  export function assistant(
    content: string,
    opts?: {
      /** Custom name for the message. */
      name?: string
      /** Whether to clean extra newlines and indentation. Defaults to true. */
      cleanContent?: boolean
    }
  ): Msg.Assistant {
    const { name, cleanContent = true } = opts ?? {}
    return {
      role: 'assistant',
      content: cleanContent ? cleanStringForModel(content) : content,
      ...(name ? { name } : {})
    }
  }

  /**
   * Create an assistant refusal message. Cleans indentation and newlines by
   * default.
   */
  export function refusal(
    refusal: string,
    opts?: {
      /** Custom name for the message. */
      name?: string
      /** Whether to clean extra newlines and indentation. Defaults to true. */
      cleanRefusal?: boolean
    }
  ): Msg.Refusal {
    const { name, cleanRefusal = true } = opts ?? {}
    return {
      role: 'assistant',
      refusal: cleanRefusal ? cleanStringForModel(refusal) : refusal,
      ...(name ? { name } : {})
    }
  }

  /** Create a function call message with argumets. */
  export function funcCall(
    funcCall: {
      /** Name of the function to call. */
      name: string
      /** Arguments to pass to the function. */
      arguments: string
    },
    opts?: {
      /** The name descriptor for the message.(message.name) */
      name?: string
    }
  ): Msg.FuncCall {
    return {
      ...opts,
      role: 'assistant',
      content: null,
      function_call: funcCall
    }
  }

  /** Create a function result message. */
  export function funcResult(
    content: Jsonifiable,
    name: string
  ): Msg.FuncResult {
    const contentString = stringifyForModel(content)
    return { role: 'function', content: contentString, name }
  }

  /** Create a function call message with argumets. */
  export function toolCall(
    toolCalls: Msg.Call.Tool[],
    opts?: {
      /** The name descriptor for the message.(message.name) */
      name?: string
    }
  ): Msg.ToolCall {
    return {
      ...opts,
      role: 'assistant',
      content: null,
      tool_calls: toolCalls
    }
  }

  /** Create a tool call result message. */
  export function toolResult(
    content: Jsonifiable,
    toolCallId: string,
    opts?: {
      /** The name of the tool which was called */
      name?: string
    }
  ): Msg.ToolResult {
    const contentString = stringifyForModel(content)
    return {
      ...opts,
      role: 'tool',
      tool_call_id: toolCallId,
      content: contentString
    }
  }

  /** Get the narrowed message from an EnrichedResponse. */
  export function getMessage(
    // @TODO
    response: any
    // response: ChatModel.EnrichedResponse
  ): Msg.Assistant | Msg.Refusal | Msg.FuncCall | Msg.ToolCall {
    const msg = response.choices[0].message as Msg
    return narrowResponseMessage(msg)
  }

  /** Narrow a message received from the API. It only responds with role=assistant */
  export function narrowResponseMessage(
    msg: Msg
  ): Msg.Assistant | Msg.Refusal | Msg.FuncCall | Msg.ToolCall {
    if (msg.content === null && msg.tool_calls != null) {
      return Msg.toolCall(msg.tool_calls)
    } else if (msg.content === null && msg.function_call != null) {
      return Msg.funcCall(msg.function_call)
    } else if (msg.content !== null && msg.content !== undefined) {
      return Msg.assistant(msg.content)
    } else if (msg.refusal != null) {
      return Msg.refusal(msg.refusal)
    } else {
      // @TODO: probably don't want to error here
      console.log('Invalid message', msg)
      throw new Error('Invalid message')
    }
  }

  /** Check if a message is a system message. */
  export function isSystem(message: Msg): message is Msg.System {
    return message.role === 'system'
  }
  /** Check if a message is a developer message. */
  export function isDeveloper(message: Msg): message is Msg.Developer {
    return message.role === 'developer'
  }
  /** Check if a message is a user message. */
  export function isUser(message: Msg): message is Msg.User {
    return message.role === 'user'
  }
  /** Check if a message is an assistant message. */
  export function isAssistant(message: Msg): message is Msg.Assistant {
    return message.role === 'assistant' && message.content != null
  }
  /** Check if a message is an assistant refusal message. */
  export function isRefusal(message: Msg): message is Msg.Refusal {
    return message.role === 'assistant' && message.refusal !== null
  }
  /** Check if a message is a function call message with arguments. */
  export function isFuncCall(message: Msg): message is Msg.FuncCall {
    return message.role === 'assistant' && message.function_call != null
  }
  /** Check if a message is a function result message. */
  export function isFuncResult(message: Msg): message is Msg.FuncResult {
    return message.role === 'function' && message.name != null
  }
  /** Check if a message is a tool calls message. */
  export function isToolCall(message: Msg): message is Msg.ToolCall {
    return message.role === 'assistant' && message.tool_calls != null
  }
  /** Check if a message is a tool call result message. */
  export function isToolResult(message: Msg): message is Msg.ToolResult {
    return message.role === 'tool' && !!message.tool_call_id
  }

  /** Narrow a ChatModel.Message to a specific type. */
  export function narrow(message: Msg.System): Msg.System
  export function narrow(message: Msg.Developer): Msg.Developer
  export function narrow(message: Msg.User): Msg.User
  export function narrow(message: Msg.Assistant): Msg.Assistant
  export function narrow(message: Msg.Assistant): Msg.Refusal
  export function narrow(message: Msg.FuncCall): Msg.FuncCall
  export function narrow(message: Msg.FuncResult): Msg.FuncResult
  export function narrow(message: Msg.ToolCall): Msg.ToolCall
  export function narrow(message: Msg.ToolResult): Msg.ToolResult
  export function narrow(
    message: Msg
  ):
    | Msg.System
    | Msg.Developer
    | Msg.User
    | Msg.Assistant
    | Msg.Refusal
    | Msg.FuncCall
    | Msg.FuncResult
    | Msg.ToolCall
    | Msg.ToolResult {
    if (isSystem(message)) {
      return message
    }
    if (isDeveloper(message)) {
      return message
    }
    if (isUser(message)) {
      return message
    }
    if (isAssistant(message)) {
      return message
    }
    if (isRefusal(message)) {
      return message
    }
    if (isFuncCall(message)) {
      return message
    }
    if (isFuncResult(message)) {
      return message
    }
    if (isToolCall(message)) {
      return message
    }
    if (isToolResult(message)) {
      return message
    }
    throw new Error('Invalid message type')
  }
}
