import { run } from './auxilary.mjs'
import { readFileSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'path'
import { select, input } from '@inquirer/prompts'
import { cwd } from 'node:process'
import chalk from 'chalk'

const invocationDir = resolve(process.env.INIT_CWD)
const workspaceDir = resolve(cwd())

const action = await select({
  message: 'What do you want to do?',
  choices: [
    {
      name: 'List dependencies',
      value: 'list',
      description: 'List all direct dependencies',
    },
    {
      name: 'Check dependencies',
      value: 'check',
      description: 'Check for version inconsistencies',
    },
    {
      name: 'Fix dependencies',
      value: 'fix',
      description: 'Resolve inconsistencies for a dependency',
    },
    {
      name: 'Add dependency',
      value: 'add',
      description: 'Add a new dependency',
    },
    {
      name: 'Remove dependency',
      value: 'remove',
      description: 'Remove a dependency',
    },
    {
      name: 'Change dependency type',
      value: 'change',
      description: 'Change the type of a dependency',
    },
  ],
})

const roots = findPackageRoots()
const jsons = readPackageJsons(roots)
const summary = summarizePackageJsons(jsons)
const problems = identifyProblems(jsons, summary)

if (action === 'list') {
  const good = sortMap(filterMap(summary, ([n]) => !problems.has(n)))
  const bad = sortMap(filterMap(summary, ([n]) => problems.has(n)))

  if (good.size) {
    console.log(chalk.green(`\n${good.size} good dependencies:`))

    for (const [name, versions] of good) {
      const [version, users] = versions.entries().next().value

      const packages = [...users]
        .filter(([r, ts]) => !(r === workspaceDir && ts.length === 1 && ts[0] === 'overrides'))
        .map(([r]) => jsons.get(r).name)

      console.log(`  ${chalk.green(name)}@${version}: ${packages.length}x (${packages.join(', ')})`)
    }
  }

  if (bad.size) {
    console.log(chalk.red(`\n${bad.size} bad dependencies:`))
    printProblems(problems, '  ')
  }
} else if (action === 'check') {
  if (problems.size) {
    printProblems(problems)
    process.exit(1)
  }
} else if (action === 'fix') {
  for (const [name, ps] of problems) {
    console.log(`Fixing ${name}:`)

    for (const p of ps) {
      process.stdout.write(`  ${p.title}: `)

      if (p.fix) {
        p.fix()
        console.log(chalk.green('Fixed'))
      } else {
        console.log(chalk.red('Skipped'))
      }
    }
  }
} else if (action === 'add') {
  const name = (await input({ message: `Name of the package:` })).trim()
  let version

  //if (roots.map(r => jsons.get(r).name))

  if ([...jsons].map(([r, j]) => j.name).includes(name)) {
    version = 'workspace:*'
    console.log('Using version workspace:* for local package')
  } else if (summary.get(name)?.size === 1) {
    version = [...summary.get(name)][0][0]
    console.log(`Reusing currently used version ${version}`)
  } else {
    version = (await input({ message: `Version of the package:` })).trim()
  }

  const type = await select({
    message: 'Which type is the dependency?',
    choices: getDependencyTypeChoices(),
  })

  const flag = invocationDir === workspaceDir ? '-w' : ''
  const identifier = version ? `${name}@${version}` : name

  run(`cd ${invocationDir} && pnpm add ${flag} ${type} ${identifier}`)

  const packageSummary = summarizePackageJson(readPackageJson(invocationDir))
  const versions = packageSummary.get(name)

  if ((versions?.size ?? 2) > 1) {
    console.error(
      `Zero or more than 1 versions specified in ${join(invocationDir, 'package.json')} after pnpm add`
    )
    process.exit(2)
  }

  const workspaceJson = readPackageJson(workspaceDir)
  updatePackageJsonValue(workspaceJson, `pnpm.overrides.${name}`, [...versions][0][0])
  writePackageJson(workspaceDir, workspaceJson)
  run(`cd ${workspaceDir} && pnpm install --no-frozen-lockfile`)
} else if (action === 'remove') {
  const name = await select({
    message: 'Which dependency do you want to remove?',
    choices: getDependencyChoices(),
  })

  const flag = invocationDir === workspaceDir ? '-w' : ''
  run(`cd ${invocationDir} && pnpm remove ${flag} ${name}`)

  const newRoots = findPackageRoots()
  const newJsons = readPackageJsons(newRoots)
  const newSummary = summarizePackageJsons(newJsons)
  const newProblems = identifyProblems(newJsons, newSummary).get(name) ?? []

  const overrideRedundant = newProblems.find((p) => p.id === 'override-redundant')
  overrideRedundant?.fix()
} else if (action === 'change') {
  const name = await select({
    message: 'Which dependency do you want to change?',
    choices: getDependencyChoices(),
  })

  const type = await select({
    message: 'What should the new dependency type be?',
    choices: getDependencyTypeChoices(),
  })

  const flag = invocationDir === workspaceDir ? '-w' : ''
  run(`cd ${invocationDir} && pnpm install ${flag} ${type} ${name}`)

  if (type !== '--save-peer') {
    const newJson = readPackageJson(invocationDir)
    updatePackageJsonValue(newJson, `peerDependencies.${name}`, undefined)
    writePackageJson(invocationDir, newJson) // no need to run pnpm install afterwards for peer dependencies
  }
}

function findPackageRoots() {
  const output = run('pnpm list --recursive --depth -1 --json')
  return JSON.parse(output).map((p) => resolve(p.path))
}

function readPackageJson(root) {
  return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
}

function readPackageJsons(roots) {
  return new Map(roots.map((r) => [r, readPackageJson(r)]))
}

function writePackageJson(root, json) {
  writeFileSync(join(root, 'package.json'), JSON.stringify(json, null, 2), 'utf8')
}

function updatePackageJsonValue(json, name, value) {
  const parents = name.split('.')
  const target = parents.pop()

  for (const parent of parents) {
    if (!(parent in json)) {
      if (value === undefined) return
      json[parent] = {}
    }

    json = json[parent]
  }

  if (value === undefined && json !== undefined) {
    delete json[target]
  } else if (value !== undefined) {
    json[target] = value
  }
}

function summarizePackageJson(json) {
  const summary = new Map()

  const getters = {
    dependencies: (p) => p.dependencies,
    devDependencies: (p) => p.devDependencies,
    peerDependencies: (p) => p.peerDependencies,
    overrides: (p) => p.pnpm?.overrides,
  }

  for (const [type, getter] of Object.entries(getters)) {
    for (const [name, version] of Object.entries(getter(json) ?? {})) {
      const outer = summary.get(name) ?? new Map()
      const inner = outer.get(version) ?? []

      inner.push(type)
      outer.set(version, inner)
      summary.set(name, outer)
    }
  }

  return summary
}

function summarizePackageJsons(roots) {
  const summary = new Map()

  for (const [root, json] of roots) {
    for (const [name, versions] of summarizePackageJson(json)) {
      for (const [version, types] of versions) {
        const outer = summary.get(name) ?? new Map()
        const inner = outer.get(version) ?? new Map()

        inner.set(root, types)
        outer.set(version, inner)
        summary.set(name, outer)
      }
    }
  }

  return summary
}

// TODO: all versions should be exact, app should have parent peer deps as deps (how to find parents?)
function identifyProblems(roots, summary) {
  const problems = new Map()

  function summarize(users) {
    return [...users].map(([r, ts]) => `${roots.get(r).name}: (${ts.join(', ')})`).join(', ')
  }

  for (const [name, versions] of summary.entries()) {
    if (versions.size > 1) {
      const list = problems.get(name) ?? []

      list.push({
        id: 'version-mismatch',
        name: chalk.red(name),
        title: 'Version mismatch detected',
        lines: [...versions].map(([v, us]) => `${chalk.red(v)}: ${summarize(us)}`),
      })

      problems.set(name, list)
    }

    for (const [version, users] of versions.entries()) {
      const workspace = roots.get(workspaceDir).name
      const list = problems.get(name) ?? []

      for (const [root, types] of users) {
        if (types.includes('peerDependencies') && !types.includes('devDependencies')) {
          list.push({
            id: 'peer-missing-dev',
            name: chalk.red(name),
            title: 'Peer dependency missing development dependency',
            lines: [
              `Entry ${name}@${version} required in devDependencies for ${roots.get(root).name}`,
            ],
            fix: () => {
              const flag = invocationDir === workspaceDir ? '-w' : ''
              const identifier = version ? `${name}@${version}` : name

              run(`cd ${invocationDir} && pnpm add ${flag} --save-peer ${identifier}`)
            },
          })
        }
      }

      const override = [...users].find(([u, ts]) => workspaceDir === u && ts.includes('overrides'))

      if (!override) {
        list.push({
          id: 'override-missing',
          name: chalk.red(name),
          title: 'No override specified',
          lines: [`Entry ${name}@${version} required in pnpm.overrides for ${workspace}`],
          fix: () => {
            const json = roots.get(workspaceDir)
            updatePackageJsonValue(json, `pnpm.overrides.${name}`, version)
            writePackageJson(workspaceDir, json)
            run(`cd ${workspaceDir} && pnpm install --no-frozen-lockfile`)
          },
        })
      } else if (users.size === 1 && [...users][0][1].length === 1) {
        list.push({
          id: 'override-redundant',
          name: chalk.red(name),
          title: 'Override specified without use',
          lines: [`Entry ${name}@${version} in pnpm.overrides for ${workspace} but never used`],
          fix: () => {
            const json = roots.get(workspaceDir)
            updatePackageJsonValue(json, `pnpm.overrides.${name}`, undefined)
            writePackageJson(workspaceDir, json)
            run(`cd ${workspaceDir} && pnpm install --no-frozen-lockfile`)
          },
        })
      }

      if (list.length) {
        problems.set(name, list)
      }
    }
  }

  return problems
}

function printProblems(problems, indent = '') {
  for (const [name, versions] of sortMap(problems)) {
    console.log(`${indent}${chalk.red(name)}:`)
    for (const problem of problems.get(name)) {
      const lines = [`${indent}  ${chalk.red(problem.title)}`, ...problem.lines]
      console.log(lines.join(`\n${indent}    `))
    }
  }
}

function filterMap(map, predicate) {
  return new Map([...map].filter(predicate))
}

function sortMap(map) {
  return new Map([...map].sort(([a], [b]) => a.localeCompare(b)))
}

function getDependencyChoices() {
  const json = readPackageJson(invocationDir)
  const summary = summarizePackageJson(json)

  const choices = [...summary]
    .filter(([n, vs]) => {
      const types = new Set([...vs].flatMap(([v, ts]) => ts))
      return types.size > 1 || types.values().next().value !== 'overrides'
    })
    .map(([n, vs]) => ({ name: n, value: n, description: `The ${n} package` }))

  choices.sort((a, b) => a.name.localeCompare(b.name))
  return choices
}

function getDependencyTypeChoices() {
  return [
    {
      name: 'Production',
      value: '--save-prod',
      description: 'Required in production',
    },
    {
      name: 'Development',
      value: '--save-dev',
      description: 'For development only',
    },
    {
      name: 'Peer',
      value: '--save-peer',
      description: 'A peer dependency',
    },
  ]
}
