import type { CompilerOptions } from 'typescript'

import chalk from 'chalk'
import * as CommentJson from 'comment-json'
import { detect } from 'detect-package-manager'
import execa from 'execa'
import fs from 'fs'
import fse from 'fs-extra'
import globby from 'globby'
import path from 'path'

import type { CliArgs } from '../types'

import { copyRecursiveSync } from '../utils/copy-recursive-sync'
import { error, info, debug as origDebug, success, warning } from '../utils/log'

type InitNextArgs = Pick<CliArgs, '--debug'> & {
  projectDir?: string
  useDistFiles?: boolean
}
type InitNextResult = { reason?: string; success: boolean; userAppDir?: string }

export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
  args.projectDir = args.projectDir || process.cwd()
  const { projectDir } = args
  const templateResult = await applyPayloadTemplateFiles(args)
  if (!templateResult.success) return templateResult

  const { success: installSuccess } = await installDeps(projectDir)
  if (!installSuccess) {
    return { ...templateResult, reason: 'Failed to install dependencies', success: false }
  }

  // Create or find payload.config.ts
  const createConfigResult = findOrCreatePayloadConfig(projectDir)
  if (!createConfigResult.success) {
    return { ...templateResult, ...createConfigResult }
  }

  // Add `@payload-config` to tsconfig.json `paths`
  await addPayloadConfigToTsConfig(projectDir)

  // Output directions for user to update next.config.js
  const withPayloadMessage = `

  ${chalk.bold(`Wrap your existing next.config.js with the withPayload function. Here is an example:`)}

  const { withPayload } = require("@payloadcms/next");

  const nextConfig = {
    // Your Next.js config
  };

  module.exports = withPayload(nextConfig);

`

  console.log(withPayloadMessage)

  return templateResult
}

async function addPayloadConfigToTsConfig(projectDir: string) {
  const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
  const userTsConfigContent = await fse.readFile(tsConfigPath, {
    encoding: 'utf8',
  })
  const userTsConfig = CommentJson.parse(userTsConfigContent) as {
    compilerOptions?: CompilerOptions
  }
  if (!userTsConfig.compilerOptions && !('extends' in userTsConfig)) {
    userTsConfig.compilerOptions = {}
  }

  if (!userTsConfig.compilerOptions.paths?.['@payload-config']) {
    userTsConfig.compilerOptions.paths = {
      ...(userTsConfig.compilerOptions.paths || {}),
      '@payload-config': ['./payload.config.ts'],
    }
    await fse.writeFile(tsConfigPath, CommentJson.stringify(userTsConfig, null, 2))
  }
}

async function applyPayloadTemplateFiles(args: InitNextArgs): Promise<InitNextResult> {
  const { '--debug': debug, projectDir, useDistFiles } = args

  info('Initializing Payload app in Next.js project', 1)

  const logDebug = (message: string) => {
    if (debug) origDebug(message)
  }

  if (!fs.existsSync(projectDir)) {
    return { reason: `Could not find specified project directory at ${projectDir}`, success: false }
  }

  // Next.js configs can be next.config.js, next.config.mjs, etc.
  const foundConfig = (await globby('next.config.*js', { cwd: projectDir }))?.[0]
  const nextConfigPath = path.resolve(projectDir, foundConfig)
  if (!fs.existsSync(nextConfigPath)) {
    return {
      reason: `No next.config.js found at ${nextConfigPath}. Ensure you are in a Next.js project directory.`,
      success: false,
    }
  } else {
    if (debug) logDebug(`Found Next config at ${nextConfigPath}`)
  }

  const templateFilesPath =
    __dirname.endsWith('dist') || useDistFiles
      ? path.resolve(__dirname, '../..', 'dist/app')
      : path.resolve(__dirname, '../../../../app')

  if (debug) logDebug(`Using template files from: ${templateFilesPath}`)

  if (!fs.existsSync(templateFilesPath)) {
    return {
      reason: `Could not find template source files from ${templateFilesPath}`,
      success: false,
    }
  } else {
    if (debug) logDebug('Found template source files')
  }

  // src/app or app
  const userAppDirGlob = await globby(['**/app'], {
    cwd: projectDir,
    onlyDirectories: true,
  })
  const userAppDir = path.resolve(projectDir, userAppDirGlob?.[0])
  if (!fs.existsSync(userAppDir)) {
    return { reason: `Could not find user app directory inside ${projectDir}`, success: false }
  } else {
    logDebug(`Found user app directory: ${userAppDir}`)
  }

  logDebug(`Copying template files from ${templateFilesPath} to ${userAppDir}`)
  copyRecursiveSync(templateFilesPath, userAppDir, debug)
  success('Successfully initialized.')
  return { success: true, userAppDir }
}

async function installDeps(projectDir: string) {
  const packageManager = await detect({ cwd: projectDir })
  if (!packageManager) {
    throw new Error('Could not detect package manager')
  }

  info(`Installing dependencies with ${packageManager}`, 1)
  const packagesToInstall = [
    'payload',
    '@payloadcms/db-mongodb',
    '@payloadcms/next',
    '@payloadcms/richtext-slate',
    '@payloadcms/ui',
  ].map((pkg) => `${pkg}@alpha`)

  let exitCode = 0
  switch (packageManager) {
    case 'npm': {
      ;({ exitCode } = await execa('npm', ['install', '--save', ...packagesToInstall], {
        cwd: projectDir,
      }))
      break
    }
    case 'yarn':
    case 'pnpm': {
      ;({ exitCode } = await execa(packageManager, ['add', ...packagesToInstall], {
        cwd: projectDir,
      }))
      break
    }
    case 'bun': {
      warning('Bun support is untested.')
      ;({ exitCode } = await execa('bun', ['add', ...packagesToInstall], { cwd: projectDir }))
      break
    }
  }

  if (exitCode !== 0) {
    error(`Failed to install dependencies with ${packageManager}`)
  } else {
    success(`Successfully installed dependencies`)
  }
  return { success: exitCode === 0 }
}
function findOrCreatePayloadConfig(projectDir: string) {
  const configPath = path.resolve(projectDir, 'payload.config.ts')
  if (fs.existsSync(configPath)) {
    return { message: 'Found existing payload.config.ts', success: true }
  } else {
    // Create default config
    // TODO: Pull this from templates
    const defaultConfig = `import path from "path";

import { mongooseAdapter } from "@payloadcms/db-mongodb"; // database-adapter-import
import { slateEditor } from "@payloadcms/richtext-slate"; // editor-import
import { buildConfig } from "payload/config";

export default buildConfig({
  editor: slateEditor({}), // editor-config
  collections: [],
  secret: "asdfasdf",
  typescript: {
    outputFile: path.resolve(__dirname, "payload-types.ts"),
  },
  graphQL: {
    schemaOutputFile: path.resolve(__dirname, "generated-schema.graphql"),
  },
  db: mongooseAdapter({
    url: "mongodb://localhost:27017/next-payload-3",
  }),
});
`

    fse.writeFileSync(configPath, defaultConfig)
    return { message: 'Created default payload.config.ts', success: true }
  }
}
