import {Answers} from 'inquirer'
import type chalk from 'chalk'
import {ChoiceCollection} from 'inquirer'
import {ConfigEnv} from 'vite'
import {DistinctQuestion} from 'inquirer'
import {InlineConfig} from 'vite'
import {Options} from 'ora'
import {Ora} from 'ora'
import {PluginOptions} from 'babel-plugin-react-compiler'
import {SanityClient} from '@sanity/client'
import {Separator} from 'inquirer'
import {TelemetryLogger} from '@sanity/telemetry'
import {TypeGenConfig} from '@sanity/codegen'

declare interface AppConfig {
  /**
   * The ID of your Sanity organization
   */
  organizationId: string
  /**
   * The entrypoint for your Sanity app. Defaults to './src/App'.
   */
  entry?: string
  /**
   * @deprecated - Moved to `deployment.appId`
   */
  id?: string
}

export declare type CliApiClient = (options?: ClientRequirements) => SanityClient

export declare interface CliApiConfig {
  projectId?: string
  dataset?: string
}

export declare interface CliClientOptions {
  cwd?: string
  projectId?: string
  dataset?: string
  useCdn?: boolean
  token?: string
  apiVersion?: string
}

export declare type CliCommandAction<F = Record<string, unknown>> = (
  args: CliCommandArguments<F>,
  context: CliCommandContext,
) => Promise<unknown>

export declare interface CliCommandArguments<F = Record<string, unknown>> {
  groupOrCommand: string
  argv: string[]
  extOptions: F
  argsWithoutOptions: string[]
  extraArguments: string[]
}

export declare interface CliCommandContext {
  output: CliOutputter
  prompt: CliPrompter
  apiClient: CliApiClient
  cliConfigPath?: string
  cliRoot: string
  workDir: string
  corePath?: string
  chalk: typeof chalk
  commandRunner: CliCommandRunner
  fromInitCommand?: boolean
  cliConfig?: CliConfig
  cliPackageManager: CliPackageManager
  telemetry: TelemetryLogger<TelemetryUserProperties>
}

export declare interface CliCommandDefinition<F = Record<string, unknown>> {
  name: string
  group?: string
  signature: string
  description: string
  helpText: string
  action: CliCommandAction<F>
  hideFromHelp?: boolean
}

export declare interface CliCommandGroupDefinition {
  name: string
  signature: string
  isGroupRoot: boolean
  description: string
  hideFromHelp?: boolean
}

export declare interface CliCommandRunner {
  commands: Readonly<(CliCommandDefinition | CliCommandGroupDefinition)[]>
  commandGroups: Readonly<Record<string, (CliCommandDefinition | CliCommandGroupDefinition)[]>>
  runCommand(
    commandOrGroup: string,
    args: CliCommandArguments,
    options: CommandRunnerOptions,
  ): Promise<unknown>
  resolveSubcommand(
    group: (CliCommandDefinition | CliCommandGroupDefinition)[],
    subCommandName: string,
    parentGroupName: string,
  ): ResolvedCliCommand | null
}

export declare interface CliConfig {
  api?: CliApiConfig
  project?: {
    basePath?: string
  }
  /**
   * Wraps the Studio in `<React.StrictMode>` root to aid flagging potential problems related to concurrent features (`startTransition`, `useTransition`, `useDeferredValue`, `Suspense`)
   * Can also be enabled by setting `SANITY_STUDIO_REACT_STRICT_MODE="true"|"false"`.
   * It only applies to `sanity dev` in dev mode, it's ignored in `sanity build` and in production.
   * Defaults to `false`
   */
  reactStrictMode?: boolean
  /**
   * The React Compiler is currently in beta, and is disabled by default.
   * @see https://react.dev/learn/react-compiler
   * @beta
   */
  reactCompiler?: ReactCompilerConfig
  server?: {
    hostname?: string
    port?: number
  }
  graphql?: GraphQLAPIConfig[]
  vite?: UserViteConfig
  /**
   * @deprecated - Moved to deployment.autoUpdates
   */
  autoUpdates?: boolean
  /**
   * @deprecated - Replaced by deployment.appId
   */
  studioHost?: string
  /**
   * Parameter used to configure other kinds of applications.
   * Signals to `sanity` commands that this is not a studio.
   */
  app?: AppConfig
  /**
   * Deployment configuration
   */
  deployment?: {
    /**
     * The ID of your Sanity studio or app. Generated when deploying your studio or app for the first time.
     * Get the appId either by
     * - Checking the output of `sanity deploy`.
     * - Get it from your project's Studio tab in https://www.sanity.io/manage
     */
    appId?: string
    /**
     * Enable auto-updates for studios.
     * {@link https://www.sanity.io/docs/cli#auto-updates}
     */
    autoUpdates?: boolean
  }
  /**
   * Configuration for Sanity media libraries.
   */
  mediaLibrary?: {
    /**
     * The path to the Media Library aspects directory. When using the CLI to manage aspects, this
     * is the directory they will be read from and written to.
     */
    aspectsPath: string
  }
  /**
   * Configuration for Sanity typegen
   */
  typegen?: Partial<TypeGenConfig>
}

declare type CliConfigResult =
  | {
      config: CliConfig
      path: string
    }
  | {
      config: null
      path: string
    }

declare interface ClientRequirements {
  requireUser?: boolean
  requireProject?: boolean
  api?: {
    projectId?: string
    dataset?: string
    apiHost?: string
    apiVersion?: string
    requestTagPrefix?: string
  }
}

export declare interface CliOutputter {
  print: (...args: unknown[]) => void
  success: (...args: unknown[]) => void
  warn: (...args: unknown[]) => void
  error: (...args: unknown[]) => void
  clear: () => void
  spinner(options: Options | string): Ora
}

/**
 * @internal
 */
declare type CliPackageManager = typeof cliPackageManager

/**
 * @internal
 */
declare const cliPackageManager: {
  getInstallCommand: typeof getInstallCommand
  getPackageManagerChoice: typeof getPackageManagerChoice
  installNewPackages: typeof installNewPackages
}

export declare type CliPrompter = (<T extends Answers = Answers>(
  questions: DistinctQuestion<T>[],
) => Promise<T>) & {
  Separator: typeof Separator
  single: <T = string>(question: SinglePrompt) => Promise<T>
}

export declare type CliStubbedYarn = (args: string[], options?: CliYarnOptions) => Promise<void>

export declare interface CliUserConfig {
  cliLastUpdateCheck?: number
  cliLastUpdateNag?: number
  authToken?: string
  authType?: string
}

export declare interface CliYarnOptions {
  print?: CliOutputter['print']
  error?: CliOutputter['error']
  rootDir?: string
}

export declare interface CommandRunnerOptions {
  cliConfig: CliConfigResult | null
  cliRoot: string
  workDir: string
  corePath: string | undefined
  telemetry: TelemetryLogger<TelemetryUserProperties>
}

/**
 * @deprecated Use `defineCliConfig` instead
 * @beta
 */
export declare function createCliConfig(config: CliConfig): CliConfig

/** @beta */
export declare function defineCliConfig(config: CliConfig): CliConfig

declare interface GetCliClient {
  (options?: CliClientOptions): SanityClient
  /**
   * @internal
   * @deprecated This is only for INTERNAL use, and should not be relied upon outside of official Sanity modules
   * @returns A token to use when constructing a client without a `token` explicitly defined, or undefined
   */
  __internal__getToken: () => string | undefined
}

/** @internal */
export declare const getCliClient: GetCliClient

declare function getInstallCommand(options: {
  workDir: string
  pkgNames?: string[]
  depType?: 'dev' | 'prod' | 'peer'
}): Promise<string>

/**
 * Attempts to resolve the most optimal package manager to use to install/upgrade
 * packages/dependencies at a given path. It does so by looking for package manager
 * specific lockfiles. If it finds a lockfile belonging to a certain package manager,
 * it prioritizes this one. However, if that package manager is not installed, it will
 * prompt the user for which one they want to use and hint at the most optimal one
 * not being installed.
 *
 * Note that this function also takes local npm binary paths into account - for instance,
 * `yarn` can be installed as a dependency of the project instead of globally, and it
 * will use that is available.
 *
 * The user can also select 'manual' to skip the process and run their preferred package
 * manager manually. Commands using this function must take this `manual` choice into
 * account and act accordingly if chosen.
 *
 * @param workDir - The working directory where a lockfile is most likely to be present
 * @param options - Pass `interactive: false` to fall back to npm if most optimal is
 *                  not available, instead of prompting
 * @returns Object of `chosen` and, if a lockfile is found, the `mostOptimal` choice
 */
declare function getPackageManagerChoice(
  workDir: string,
  options:
    | {
        interactive: false
      }
    | {
        interactive?: true
        prompt: CliPrompter
      },
): Promise<{
  chosen: PackageManager
  mostOptimal?: PackageManager
}>

export declare interface GraphQLAPIConfig {
  /**
   * ID of GraphQL API. Only (currently) required when using the `--api` flag
   * for `sanity graphql deploy`, in order to only deploy a specific API.
   */
  id?: string
  /**
   * Name of workspace containing the schema to deploy
   *
   * Optional, defaults to `default` (eg the one used if no `name` is defined)
   */
  workspace?: string
  /**
   * Name of source containing the schema to deploy, within the configured workspace
   *
   * Optional, defaults to `default` (eg the one used if no `name` is defined)
   */
  source?: string
  /**
   * API tag for this API - allows deploying multiple different APIs to a single dataset
   *
   * Optional, defaults to `default`
   */
  tag?: string
  /**
   * Whether or not to deploy a "GraphQL Playground" to the API url - an HTML interface that allows
   * running queries and introspecting the schema from the browser. Note that this interface is not
   * secured in any way, but as the schema definition and API route is generally open, this does not
   * expose any more information than is otherwise available - it only makes it more discoverable.
   *
   * Optional, defaults to `true`
   */
  playground?: boolean
  /**
   * Generation of API to auto-generate from schema. New APIs should use the latest (`gen3`).
   *
   * Optional, defaults to `gen3`
   */
  generation?: 'gen3' | 'gen2' | 'gen1'
  /**
   * Define document interface fields (`_id`, `_type` etc) as non-nullable.
   * If you never use a document type as an object (within other documents) in your schema types,
   * you can (and probably should) set this to `true`. Because a document type _could_ be used
   * inside other documents, it is by default set to `false`, as in these cases these fields
   * _can_ be null.
   *
   * Optional, defaults to `false`
   */
  nonNullDocumentFields?: boolean
  /**
   * Suffix to use for generated filter types.
   *
   * Optional, Defaults to `Filter`.
   *
   */
  filterSuffix?: string
}

declare function installNewPackages(
  options: InstallOptions,
  context: Pick<CliCommandContext, 'output' | 'workDir'>,
): Promise<void>

declare interface InstallOptions {
  packageManager: PackageManager
  packages: string[]
}

/**
 * This is an "inlined" version of Vite's `loadEnv` function,
 * simplified somewhat to only support our use case.
 *
 * Ideally we'd just use `loadEnv` from Vite, but importing it
 * causes bundling issues due to node APIs and downstream dependencies.
 *
 * Vite is MIT licensed, copyright (c) Yuxi (Evan) You and Vite contributors.
 */
export declare function loadEnv(
  mode: string,
  envDir: string,
  prefixes?: string[],
): Record<string, string>

export declare interface PackageJson {
  name: string
  version: string
  scripts?: Record<string, string>
  description?: string
  author?: string
  license?: string
  private?: boolean
  dependencies?: Record<string, string>
  devDependencies?: Record<string, string>
  peerDependencies?: Record<string, string>
  repository?: {
    type: string
    url: string
  }
  engines?: Record<string, string>
}

declare type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun' | 'manual'

/**
 * @beta
 */
export declare type ReactCompilerConfig = Partial<PluginOptions>

export declare interface ResolvedCliCommand {
  command: CliCommandDefinition | CliCommandGroupDefinition
  commandName: string
  parentName?: string
  isGroup: boolean
  isCommand: boolean
}

export {SanityClient}

export declare interface SanityCore {
  requiredCliVersionRange: string
  commands: (CliCommandDefinition | CliCommandGroupDefinition)[]
}

export declare interface SanityJson {
  root?: boolean
  project?: {
    name?: string
    basePath?: string
  }
  api?: CliApiConfig
  __experimental_spaces?: {
    name: string
    title: string
    default?: true
    api: {
      projectId?: string
      dataset?: string
    }
  }[]
  plugins?: string[]
  parts?: {
    name?: string
    path?: string
    implements?: string
    description?: string
  }[]
  env?: {
    production?: SanityJson
    staging?: SanityJson
    development?: SanityJson
  }
}

export declare interface SanityModuleInternal {
  cliProjectCommands: {
    requiredCliVersionRange: string
    commands: (CliCommandDefinition | CliCommandGroupDefinition)[]
  }
}

export declare type SanityUser = {
  id: string
  name: string
  email: string
  profileImage?: string
  tosAcceptedAt?: string
  provider: 'google' | 'github' | 'sanity' | `saml-${string}`
}

export declare type SinglePrompt =
  | (Omit<DistinctQuestion, 'name'> & {
      type: 'list'
      choices: ChoiceCollection
    })
  | (Omit<DistinctQuestion, 'name'> & {
      type: 'confirm'
    })
  | (Omit<DistinctQuestion, 'name'> & {
      type: 'input'
    })

export declare interface TelemetryUserProperties {
  runtime: string
  runtimeVersion: string
  cliVersion: string
  machinePlatform: string
  cpuArchitecture: string
  projectId?: string
  dataset?: string
}

export declare type UserViteConfig =
  | InlineConfig
  | ((config: InlineConfig, env: ConfigEnv) => InlineConfig | Promise<InlineConfig>)

export {}
