import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as github from '@actions/github'
import { DefaultArtifactClient } from '@actions/artifact'
import * as fs from 'node:fs/promises'

/**
 * The main function for the action.
 *
 * @returns {Promise<void>} Resolves when the action is complete.
 */
export async function run() {
  try {
    const comment = core.getBooleanInput('comment-on-pr', { required: false })
    const upload = core.getBooleanInput('generate-artifact', {
      required: false
    })
    const artifactName = core.getInput('artifact-name', { required: false })
    const compare = core.getBooleanInput('do-comparison', { required: false })
    const jobName = core.getInput('job-name', { required: false })
    const baseBranch = core.getInput('base-branch', { required: false })
    const system = core.getInput('system', { required: true })

    if (!comment && !upload) {
      core.error(
        'Both comment-on-pr and generate-artifact were set to false, nothing to do, consider disabling the action instead'
      )
      core.setFailed('Neither commenting nor uploading a report ... why?')
      return
    }

    const flakeInfo = JSON.parse(
      await collectOutput('nix', ['flake', 'show', '--json'])
    )
    core.debug(`nix flake show --json: ${flakeInfo}`)

    const report = await core.group('Generating size report', () =>
      generateReport(flakeInfo, system)
    )

    if (upload) {
      const artifact = new DefaultArtifactClient()
      await fs.writeFile(artifactName, JSON.stringify(report))
      const { id, size } = artifact.uploadArtifact(artifactName, [artifactName])

      core.info(`Uploaded report ${artifactName} (${size}B) with id ${id}`)
    }

    // Done
    if (!comment) {
      return
    }

    // TODO: compare reports and create comment
  } catch (error) {
    // Fail the workflow run if an error occurs
    if (error instanceof Error) core.setFailed(error.message)
  }
}

async function generateReport(flakeInfo, system) {
  const packages = getPackages(flakeInfo, system)
  const hmConfigs = getKeys(flakeInfo, 'homeConfigurations')
  const nixosConfigs = getKeys(flakeInfo, 'nixosConfigurations')

  core.info(`packages: ${packages}`)
  core.info(`homeConfigurations: ${hmConfigs}`)
  core.info(`nixosConfigurations: ${nixosConfigs}`)

  const pkgSizes = await core.group('Calculating size of packages', () =>
    calculateSizeOf(packages, (pkg) => `.#${pkg}`)
  )

  const hmConfigsSizes = await core.group(
    'Calculating size of Home-Manager Configurations',
    () =>
      calculateSizeOf(
        hmConfigs,
        (config) => `.#homeConfigurations.${config}.activationPackages`
      )
  )

  const nixosConfigsSizes = await core.group(
    'Calculating size of NixOS Configurations',
    () =>
      calculateSizeOf(
        nixosConfigs,
        (config) =>
          `.#nixosConfigurations.${config}.config.system.build.toplevel`
      )
  )

  return {
    packages: pkgSizes,
    nixosConfigurations: nixosConfigsSizes,
    homeConfigurations: hmConfigsSizes
  }
}

async function calculateSizeOf(names, nameToInstallable) {
  let sizes = []
  for (const name of names) {
    sizes.push(await calculateSize(nameToInstallable(name)))
  }
  return sizes
}

/**
 * Get the packages from a `nix flake show --json` blob
 *
 * @returns {Array<String>} the packages in the current flake for the specific system
 */
function getPackages(flakeInfo, system) {
  return 'packages' in flakeInfo ? getKeys(flakeInfo.packages, system) : []
}

function getKeys(flakeInfo, key) {
  return key in flakeInfo ? Object.keys(flakeInfo[key]) : []
}

async function calculateSize(installable) {
  const path = await collectOutput('nix', [
    'build',
    '--print-out-paths',
    installable
  ])
  const data = JSON.parse(
    await collectOutput('nix', ['path-info', '--closure-size', '--json', path])
  )
  data.path = path

  return data
}

async function collectOutput(cmd, args) {
  let output = ''
  await exec.exec(cmd, args, {
    listeners: {
      stdout: (data) => {
        output += data.toString()
      }
    }
  })
  return output
}
