import type {StudioPathLike} from '@sanity/client/csm'
import type {InsertMenuOptions} from '@sanity/insert-menu'
import type {ArrayOptions} from '@sanity/types'
import type {
  ComponentType,
  FunctionComponent,
  HTMLAttributes,
  PropsWithChildren,
  ReactElement,
} from 'react'

/**
 * @public
 */
export declare type CreateDataAttribute<T extends CreateDataAttributeProps> =
  (T extends WithRequired<CreateDataAttributeProps, 'id' | 'type' | 'path'>
    ? {
        /**
         * Returns a string representation of the data attribute
         * @param path - An optional path to concatenate with any existing path
         * @public
         */
        (path?: StudioPathLike): string
        /**
         * Returns a string representation of the data attribute
         * @public
         */
        toString(): string
      }
    : T extends WithRequired<CreateDataAttributeProps, 'id' | 'type'>
      ? (path: StudioPathLike) => string
      : object) & {
    /**
     * Concatenate the current path with a new path
     * @param path - A path to concatenate with any existing path
     * @public
     */
    scope(path: StudioPathLike): CreateDataAttribute<
      T & {
        path: StudioPathLike
      }
    >
    /**
     * Combines the current props with additional props
     * @param props - New props to merge with any existing props
     * @public
     */
    combine: <U extends CreateDataAttributeProps>(props: U) => CreateDataAttribute<T & U>
  }

/**
 * A helper function for creating `data-sanity` attributes by explicitly providing metadata.
 * @returns An object with methods for incrementally adding and scoping metadata, and for generating a data attribute string.
 * @public
 */
export declare function createDataAttribute<T extends CreateDataAttributeProps>(
  props: T,
): CreateDataAttribute<T>

/**
 * The metadata that can be embedded in a data attribute.
 * All values are marked optional in the base type as they can be provided incrementally using the `createDataAttribute` function.
 * @public
 */
export declare interface CreateDataAttributeProps {
  /** The studio base URL, optional */
  baseUrl?: string
  /** The dataset, optional */
  dataset?: string
  /** The document ID, required */
  id?: string
  /** The field path, required */
  path?: StudioPathLike
  /** The project ID, optional */
  projectId?: string
  /** The studio tool name, optional */
  tool?: string
  /** The document type, required */
  type?: string
  /** The studio workspace, optional */
  workspace?: string
}

export declare interface DocumentSchema {
  type: 'document'
  name: string
  title?: string
  icon?: string
  fields: Record<string, SchemaObjectField>
}

/**
 * An element that is safe to parse
 * @internal
 */
export declare type ElementNode = HTMLElement | SVGElement

/**
 *
 * @public
 */
export declare interface HistoryAdapter {
  subscribe: (navigate: HistoryAdapterNavigate) => () => void
  update: (update: HistoryUpdate) => void
}

/**
 *
 * @public
 */
export declare type HistoryAdapterNavigate = (update: HistoryUpdate) => void

/**
 * Preview frame history refresh event, emitted by Presentation Tool
 * @public
 */
export declare type HistoryRefresh =
  | {
      /**
       * source 'manual' means the refresh button were clicked by the user
       */
      source: 'manual'
      /**
       * If true then there's either preview-kit or a loader connected on the page
       */
      livePreviewEnabled: boolean
    }
  | {
      /**
       * source 'mutation' means a document were mutated and the preview might need to refresh
       */
      source: 'mutation'
      /**
       * If true then there's either preview-kit or a loader connected on the page
       */
      livePreviewEnabled: boolean
      /**
       * Select metadata about the document that were mutated
       * If it's prefixed with `drafts.` then it's a draft document, otherwise it's a published document.
       */
      document: {
        /**
         * If it's prefixed with `drafts.` then it's a draft document, otherwise it's a published document.
         */
        _id: string
        /**
         * The document type is frequently used in `revalidateTag` scenarios with Next.js App Router
         */
        _type: string
        /**
         * The document revision, can be used to dedupe requests, as we always send two due to debouncing and handling Content Lake eventual consistency
         */
        _rev: string
        /**
         * If the document has a top level slug field named `slug` with the type `slug`, then it'll be included here
         */
        slug?: {
          current?: string | null
        }
      }
    }

/**
 * Preview frame history update
 * @public
 */
export declare type HistoryUpdate = {
  type: 'push' | 'pop' | 'replace'
  title?: string
  url: string
}

/**
 * @public
 */
export declare type OverlayComponent<
  T extends Record<string, unknown> = Record<string, unknown>,
  P extends OverlayElementParent = OverlayElementParent,
> = ComponentType<OverlayComponentProps<P | undefined> & T>

/**
 * @public
 */
export declare interface OverlayComponentProps<
  P extends OverlayElementParent = OverlayElementParent,
> extends OverlayComponentResolverContext<P> {
  PointerEvents: FunctionComponent<PropsWithChildren<HTMLAttributes<HTMLDivElement>>>
}

/**
 * @public
 */
export declare type OverlayComponentResolver<
  T extends OverlayComponent = OverlayComponent<Record<string, unknown>, any>,
> = (context: OverlayComponentResolverContext) =>
  | T
  | {
      component: T
      props?: Record<string, unknown>
    }
  | Array<
      | T
      | {
          component: T
          props?: Record<string, unknown>
        }
    >
  | ReactElement
  | undefined
  | void

/**
 * @public
 */
export declare interface OverlayComponentResolverContext<
  P extends OverlayElementParent = OverlayElementParent,
> {
  /**
   * The resolved field's document schema type
   */
  document: DocumentSchema
  /**
   * The element node that the overlay is attached to
   */
  element: ElementNode
  /**
   * The resolved field schema type
   */
  field: OverlayElementField
  /**
   * Whether the overlay is focused or not
   */
  focused: boolean
  /**
   * The Sanity node data that triggered the overlay
   */
  node: SanityNode
  /**
   * The resolved field's parent schema type
   */
  parent: P
  /**
   * A convience property, equal to `field.value.type`
   */
  type: string
}

export declare type OverlayElementField =
  | SchemaArrayItem
  | SchemaObjectField
  | SchemaUnionOption
  | undefined

export declare type OverlayElementParent =
  | DocumentSchema
  | SchemaNode
  | SchemaArrayItem
  | SchemaUnionOption
  | SchemaUnionNode
  | undefined

/**
 * Data resolved from a Sanity node
 * @public
 */
export declare type SanityNode = {
  baseUrl: string
  dataset?: string
  id: string
  isDraft?: string
  path: string
  projectId?: string
  tool?: string
  type?: string
  workspace?: string
}

export declare interface SchemaArrayItem<T extends SchemaNode = SchemaNode> {
  type: 'arrayItem'
  name: string
  title?: string
  value: T
}

export declare interface SchemaArrayNode<T extends SchemaNode = SchemaNode> {
  type: 'array'
  of: SchemaArrayItem<T>
}

export declare interface SchemaBooleanNode {
  type: 'boolean'
  value?: boolean
}

export declare interface SchemaInlineNode {
  type: 'inline'
  /** the name of the referenced type */
  name: string
}

export declare type SchemaNode =
  | SchemaArrayNode
  | SchemaBooleanNode
  | SchemaInlineNode
  | SchemaNullNode
  | SchemaNumberNode
  | SchemaObjectNode
  | SchemaStringNode
  | SchemaUnionNode
  | SchemaUnknownNode

export declare interface SchemaNullNode {
  type: 'null'
}

export declare interface SchemaNumberNode {
  type: 'number'
  value?: number
}

export declare interface SchemaObjectField<T extends SchemaNode = SchemaNode> {
  type: 'objectField'
  name: string
  title?: string
  value: T
  optional?: boolean
}

export declare interface SchemaObjectNode<T extends SchemaNode = SchemaNode> {
  type: 'object'
  fields: Record<string, SchemaObjectField<T>>
  rest?: SchemaObjectNode | SchemaUnknownNode | SchemaInlineNode
  dereferencesTo?: string
}

export declare interface SchemaStringNode {
  type: 'string'
  value?: string
}

export declare interface SchemaUnionNode<T extends SchemaNode = SchemaNode> {
  type: 'union'
  of: SchemaUnionOption<T>[] | SchemaStringNode[] | SchemaNumberNode[]
  options?: SchemaUnionNodeOptions
}

export declare type SchemaUnionNodeOptions = Omit<ArrayOptions, 'insertMenu'> & {
  insertMenu?: Omit<InsertMenuOptions, 'views'> & {
    views?: Array<
      | {
          name: 'list'
        }
      | {
          name: 'grid'
          previewImageUrls?: Record<string, string | undefined>
        }
    >
  }
}

export declare interface SchemaUnionOption<T extends SchemaNode = SchemaNode> {
  type: 'unionOption'
  name: string
  title?: string
  icon?: string
  value: T
}

export declare interface SchemaUnknownNode {
  type: 'unknown'
}

/**
 * @public
 */
export declare function VisualEditing(props: VisualEditingProps): React.ReactElement | null

/**
 * @public
 */
export declare interface VisualEditingOptions {
  /**
   * @alpha
   * This API is unstable and could change at any time.
   */
  components?: OverlayComponentResolver
  /**
   * The history adapter is used for Sanity Presentation to navigate URLs in the preview frame.
   */
  history?: HistoryAdapter
  /**
   * The refresh API allows smarter refresh logic than the default `location.reload()` behavior.
   */
  refresh?: (payload: HistoryRefresh) => false | Promise<void>
  /**
   * The CSS z-index on the root node that renders overlays, tweak it accordingly to what layout you have.
   */
  zIndex?: string | number
}

/**
 * @public
 */
export declare interface VisualEditingProps
  extends Omit<VisualEditingOptions, 'history' | 'refresh'> {
  /**
   * @deprecated The history adapter is already implemented
   */
  history?: never
  /**
   * The refresh API allows smarter refresh logic than the default `location.reload()` behavior.
   * You can call the refreshDefault argument to trigger the default refresh behavior so you don't have to reimplement it.
   */
  refresh?: (
    payload: HistoryRefresh,
    refreshDefault: () => false | Promise<void>,
  ) => false | Promise<void>
}

/**
 * Helper
 * @internal
 */
export declare type WithRequired<T, K extends keyof T> = T & {
  [P in K]-?: T[P]
}

export {}
