const localeCompare = require('@isaacs/string-locale-compare')('en')
const { join, basename, resolve } = require('path')
const transformHTML = require('./transform-html.js')
const { version } = require('../../lib/npm.js')
const { aliases } = require('../../lib/utils/cmd-list')
const { shorthands, definitions } = require('@npmcli/config/lib/definitions')

const DOC_EXT = '.md'

const TAGS = {
  CONFIG: '<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->',
  USAGE: '<!-- AUTOGENERATED USAGE DESCRIPTIONS -->',
  SHORTHANDS: '<!-- AUTOGENERATED CONFIG SHORTHANDS -->',
}

const assertPlaceholder = (src, path, placeholder) => {
  if (!src.includes(placeholder)) {
    throw new Error(
      `Cannot replace ${placeholder} in ${path} due to missing placeholder`
    )
  }
  return placeholder
}

// Default command loader - loads commands from lib/commands
const defaultCommandLoader = (name) => {
  return require(`../../lib/commands/${name}`)
}

// Load a command using the provided loader or default
const getCommand = (name, commandLoader = defaultCommandLoader) => {
  return commandLoader(name)
}

// Resolve definitions for a command - use definitions if present, otherwise build from params
const resolveDefinitions = (command) => {
  // If command has definitions, use them directly (ignore params)
  if (command.definitions && Object.keys(command.definitions).length > 0) {
    return command.definitions
  }

  // Otherwise build from params using global definitions
  if (command.params) {
    const resolved = {}
    for (const param of command.params) {
      if (definitions[param]) {
        resolved[param] = definitions[param]
      }
    }
    return resolved
  }

  return {}
}

const getCommandByDoc = (docFile, docExt, commandLoader = defaultCommandLoader) => {
  // Grab the command name from the *.md filename
  // NOTE: We cannot use the name property command file because in the case of
  // `npx` the file being used is `lib/commands/exec.js`
  const name = basename(docFile, docExt).replace('npm-', '')

  if (name === 'npm') {
    return {
      name,
      definitions: [],
      usage: 'npm',
    }
  }

  // special case for `npx`:
  // `npx` is not technically a command in and of itself,
  // so it just needs the usage of npm exec
  const srcName = name === 'npx' ? 'exec' : name
  const command = getCommand(srcName, commandLoader)
  const { usage = [''], workspaces } = command
  const usagePrefix = name === 'npx' ? 'npx' : `npm ${name}`

  // Resolve definitions - handles exclusive params expansion
  const commandDefs = resolveDefinitions(command)
  const resolvedDefs = {}
  for (const [key, def] of Object.entries(commandDefs)) {
    resolvedDefs[key] = def
    // Handle exclusive params
    if (def.exclusive) {
      for (const e of def.exclusive) {
        if (!resolvedDefs[e] && definitions[e]) {
          resolvedDefs[e] = definitions[e]
        }
      }
    }
  }

  return {
    name,
    workspaces,
    definitions: name === 'npx' ? {} : resolvedDefs,
    usage: usage?.map(u => `${usagePrefix} ${u}`.trim()).join('\n'),
  }
}

const replaceVersion = (src) => src.replace(/@VERSION@/g, version)

const replaceUsage = (src, { path, commandLoader }) => {
  const replacer = assertPlaceholder(src, path, TAGS.USAGE)
  const { usage, name, workspaces } = getCommandByDoc(path, DOC_EXT, commandLoader)

  const synopsis = ['```bash', usage]

  const cmdAliases = Object.keys(aliases).reduce((p, c) => {
    if (aliases[c] === name) {
      p.push(c)
    }
    return p
  }, [])

  if (cmdAliases.length === 1) {
    synopsis.push('', `alias: ${cmdAliases[0]}`)
  } else if (cmdAliases.length > 1) {
    synopsis.push('', `aliases: ${cmdAliases.join(', ')}`)
  }

  synopsis.push('```')

  if (!workspaces) {
    synopsis.push('', 'Note: This command is unaware of workspaces.')
  }

  return src.replace(replacer, synopsis.join('\n'))
}

// Helper to generate a markdown table from definitions
const generateFlagsTable = (definitionPool) => {
  const rows = Object.keys(definitionPool).map((n) => {
    const def = definitionPool[n]
    const flags = [`\`--${def.key}\``]
    if (def.alias) {
      flags.push(...def.alias.map(a => `\`--${a}\``))
    }
    if (def.short) {
      flags.push(`\`-${def.short}\``)
    }
    const flagsStr = flags.join(', ')
    let defaultVal = def.defaultDescription
    if (!defaultVal) {
      defaultVal = String(def.default)
    }
    let typeVal = def.typeDescription || String(def.type)
    if (def.required) {
      typeVal = `${typeVal} (required)`
    }
    const desc = (def.description || '').replace(/\n/g, ' ').trim()
    return `| ${flagsStr} | ${defaultVal} | ${typeVal} | ${desc} |`
  })

  return [
    '| Flag | Default | Type | Description |',
    '| --- | --- | --- | --- |',
    ...rows,
  ].join('\n')
}

const replaceDefinitions = (src, { path, commandLoader }) => {
  const { definitions: commandDefs, name } = getCommandByDoc(path, DOC_EXT, commandLoader)

  let subcommands = {}
  try {
    const command = getCommand(name, commandLoader)
    subcommands = command.subcommands || {}
  } catch {
    // Command doesn't exist
  }

  // If no definitions and no subcommands, nothing to replace
  if (Object.keys(commandDefs).length === 0 && Object.keys(subcommands).length === 0) {
    return src
  }

  // Assert placeholder is present
  const replacer = assertPlaceholder(src, path, TAGS.CONFIG)

  // If command has subcommands, generate sections for each subcommand
  if (Object.keys(subcommands).length > 0) {
    const subcommandSections = Object.entries(subcommands).map(([subName, SubCommand]) => {
      const subUsage = SubCommand.usage || []
      const subDefs = resolveDefinitions(SubCommand)

      const parts = [`### \`npm ${name} ${subName}\``, '']

      if (SubCommand.description) {
        parts.push(SubCommand.description, '')
      }

      // Add usage/synopsis
      if (subUsage.length > 0) {
        parts.push('#### Synopsis', '', '```bash')
        subUsage.forEach(u => {
          parts.push(`npm ${name} ${subName} ${u}`.trim())
        })
        parts.push('```', '')
      }

      // Add flags section if definitions exist
      if (Object.keys(subDefs).length > 0) {
        parts.push('#### Flags', '')
        parts.push(generateFlagsTable(subDefs), '')
      }

      return parts.join('\n')
    })

    return src.replace(replacer, subcommandSections.join('\n'))
  }

  // For commands without subcommands - commandDefs must be non-empty here
  // (we would have returned early at line 175 if both were empty)
  const paramDescriptions = Object.values(commandDefs)
    .map(def => def.describe())

  return src.replace(replacer, paramDescriptions.join('\n\n'))
}

const replaceConfig = (src, { path }) => {
  const replacer = assertPlaceholder(src, path, TAGS.CONFIG)

  // sort not-deprecated ones to the top
  /* istanbul ignore next - typically already sorted in the definitions file,
   * but this is here so that our help doc will stay consistent if we decide
   * to move them around. */
  const sort = ([keya, { deprecated: depa }], [keyb, { deprecated: depb }]) => {
    return depa && !depb ? 1
      : !depa && depb ? -1
      : localeCompare(keya, keyb)
  }

  const allConfig = Object.entries(definitions).sort(sort)
    .map(([, def]) => def.describe())
    .join('\n\n')

  return src.replace(replacer, allConfig)
}

const replaceShorthands = (src, { path }) => {
  const replacer = assertPlaceholder(src, path, TAGS.SHORTHANDS)

  const sh = Object.entries(shorthands)
    .sort(([shorta, expansiona], [shortb, expansionb]) =>
      // sort by what they're short FOR
      localeCompare(expansiona.join(' '), expansionb.join(' ')) || localeCompare(shorta, shortb)
    )
    .map(([short, expansion]) => {
      // XXX: this is incorrect. we have multicharacter flags like `-iwr` that
      // can only be set with a single dash
      const dash = short.length === 1 ? '-' : '--'
      return `* \`${dash}${short}\`: \`${expansion.join(' ')}\``
    })

  return src.replace(replacer, sh.join('\n'))
}

const replaceHelpLinks = (src) => {
  // replaces markdown links with equivalent-ish npm help commands
  return src.replace(
    /\[`?([\w\s-]+)`?\]\(\/(?:commands|configuring-npm|using-npm)\/(?:[\w\s-]+)\)/g,
    (_, p1) => {
      const term = p1.replace(/npm\s/g, '').replace(/\s+/g, ' ').trim()
      const help = `npm help ${term.includes(' ') ? `"${term}"` : term}`
      return help
    }
  )
}

const transformMan = (src, { data, unified, remarkParse, remarkMan }) => unified()
  .use(remarkParse)
  .use(remarkMan, { version: `NPM@${version}` })
  .processSync(`# ${data.title}(${data.section}) - ${data.description}\n\n${src}`)
  .toString()

const manPath = (name, { data }) => join(`man${data.section}`, `${name}.${data.section}`)

const transformMd = (src, { frontmatter }) => ['---', frontmatter, '---', '', src].join('\n')

module.exports = {
  DOC_EXT,
  TAGS,
  paths: {
    content: resolve(__dirname, 'content'),
    nav: resolve(__dirname, 'content', 'nav.yml'),
    template: resolve(__dirname, 'template.html'),
    man: resolve(__dirname, '..', '..', 'man'),
    html: resolve(__dirname, '..', 'output'),
    md: resolve(__dirname, '..', 'content'),
  },
  usage: replaceUsage,
  definitions: replaceDefinitions,
  config: replaceConfig,
  shorthands: replaceShorthands,
  version: replaceVersion,
  helpLinks: replaceHelpLinks,
  man: transformMan,
  manPath: manPath,
  md: transformMd,
  html: transformHTML,
}
