import assert from "assert"

import { $, fetch, fs } from "zx"

interface Package {
  name: string
  version: string
}

export interface VersionObject {
  major: number
  minor: number
  patch: number
}

const {
  CI_COMMIT_BRANCH,
  CI_COMMIT_TAG,
  CI_COMMIT_TITLE,
  CI_DEFAULT_BRANCH,
  CI_JOB_TOKEN,
  CI_PIPELINE_SOURCE,
  CI_PROJECT_NAME,
  CI_PROJECT_NAMESPACE,
  CI_SERVER_HOST,
  CI_SERVER_URL,
  // NPM_EMAIL,
  // NPM_TOKEN,
  SSH_KNOWN_HOSTS,
  SSH_PRIVATE_KEY,
} = process.env
const packagePath = "package.json"

function checkVersionObject(
  {
    major: majorReference,
    minor: minorReference,
    patch: patchReference,
  }: VersionObject,
  versionObject: VersionObject | undefined,
): boolean {
  if (versionObject === undefined) {
    return true
  }
  const { major, minor, patch } = versionObject
  if (major < majorReference) {
    return true
  }
  if (major === majorReference) {
    if (minor < minorReference) {
      return true
    }
    if (minor === minorReference) {
      return patch <= patchReference || patch === patchReference + 1
    }
    return minor === minorReference + 1 && patch === 0
  }
  return major === majorReference + 1 && minor === 0 && patch === 0
}

async function commitAndPushWithUpdatedVersions(
  nextVersionObject?: VersionObject | undefined,
) {
  if (nextVersionObject === undefined) {
    // Retrieve next version of project.
    nextVersionObject = await latestVersionObjectFromTags()
    assert.notStrictEqual(nextVersionObject, undefined)
    nextVersionObject!.patch++
  }
  const nextVersion = versionFromObject(nextVersionObject!)

  if ((await $`git diff --quiet --staged`.exitCode) !== 0) {
    let packageJson = await fs.readFile(packagePath, "utf-8")
    packageJson = packageJson.replace(
      /^ {2}"version": "(.*?)",$/m,
      `  "version": "${nextVersion}",`,
    )
    await fs.writeFile(packagePath, packageJson, "utf-8")
  }

  await $`git add .`
  if ((await $`git diff --quiet --staged`.exitCode) !== 0) {
    await $`git commit -m ${nextVersion}`
    await $`git push --set-upstream origin master`
  }
  await $`git tag -a ${nextVersion} -m ${nextVersion}`
  await $`git push --set-upstream --tags`

  // // Note: Don't fail when there is nothing new to publish.
  // if (NPM_EMAIL !== undefined && NPM_TOKEN !== undefined) {
  //   await nothrow($`npm publish`)
  // }
}

async function configureGit() {
  console.log("Configuring git…")

  // Set the Git user name and email.
  await $`git config --global user.email "admin+leximpact-socio-fiscal-ui-ci@tax-benefit.org"`
  await $`git config --global user.name "Leximpact socio-fiscal UI CI"`

  console.log("Git configuration completed.")
}

// async function configureNpm() {
//   console.log("Configuring npm…")

//   // Configure npm to be able to publish packages.
//   if (NPM_EMAIL !== undefined && NPM_TOKEN !== undefined) {
//     await $`echo ${`//registry.npmjs.org/:_authToken=${NPM_TOKEN}`} > ~/.npmrc`
//     await $`echo ${`email=${NPM_EMAIL}`} >> ~/.npmrc`
//   }

//   console.log("Npm configuration completed.")
// }

async function configureSsh() {
  console.log("Configuring ssh…")

  // Note: `eval $(ssh-agent -s)` must be done before calling this script because it
  // creates 2 environment variables (then used by ssh clients).
  // // Install ssh-agent if not already installed, to be able to use git with ssh.
  // if ((await $`which ssh-agent`.exitCode) !== 0) {
  //   await $`apt install -y openssh-client`
  // }
  // // Run ssh-agent (inside the build environment)
  // await $`eval $(ssh-agent -s)`

  // Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
  // Use `tr` to fix line endings which makes ed25519 keys work without
  // extra base64 encoding.
  // https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556
  if (SSH_PRIVATE_KEY !== undefined) {
    await $`echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -`
  }

  if (SSH_KNOWN_HOSTS !== undefined) {
    // Create the SSH directory and give it the right permissions
    await $`mkdir -p ~/.ssh`
    await $`chmod 700 ~/.ssh`
    // Accept the SSH host keys of GitLab server.
    await $`echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts`
    await $`chmod 644 ~/.ssh/known_hosts`
  }

  console.log("Ssh configuration completed.")
}

async function latestVersionObjectFromTags(): Promise<
  VersionObject | undefined
> {
  // Retrieve all tags.
  await $`git fetch --all --tags`

  return (await $`git tag --list`).stdout
    .split("\n")
    .map(objectFromVersion)
    .filter((versionObject) => versionObject !== undefined)
    .sort((versionObject1, versionObject2) =>
      versionObject1.major !== versionObject2.major
        ? versionObject2.major - versionObject1.major
        : versionObject1.minor !== versionObject2.minor
          ? versionObject2.minor - versionObject1.minor
          : versionObject2.patch - versionObject1.patch,
    )[0]
}

async function main() {
  console.log("Starting gitlab-ci.ts…")

  await configureGit()
  // await configureNpm()
  await configureSsh()

  console.log(`Handling "${CI_PIPELINE_SOURCE}" event…`)
  switch (CI_PIPELINE_SOURCE) {
    case "api":
    case "pipeline":
    case "schedule":
    case "trigger":
    case "web": {
      if (CI_COMMIT_BRANCH === CI_DEFAULT_BRANCH) {
        // A pipeline has been triggered (for example when OpenFisca-France has been updated or a user as
        // launched a new pipeline manually or…).
        // => A new version of project must be created if and only if a dependency has
        // changed.

        console.log(`Handling trigger…`)

        await resetGitRepository()

        // Use the latest version of @leximpact/socio-fiscal-openfisca-json
        await $`npm install @leximpact/socio-fiscal-openfisca-json@latest`

        // Test project with the current dependencies (and latest version of @leximpact/socio-fiscal-openfisca-json).
        await $`ln -s example.env .env`
        await $`npm run build`
        // await $`npm test`

        await $`git add .`
        if ((await $`git diff --quiet --staged`.exitCode) !== 0) {
          // `npm install @leximpact/socio-fiscal-openfisca-json@latest` has updated `package-lock.json`.
          // => Generate a new version of leximpact-socio-fiscal-api.
          const pkg = await fs.readJson(packagePath)
          const nextVersionObject =
            await nextVersionObjectFromPackageAndTags(pkg)
          await commitAndPushWithUpdatedVersions(nextVersionObject)
          await triggerDevDeployPipeline()
          await triggerProdDeployPipeline()
        }
      } else {
        console.log(
          `Unhandled event "${CI_PIPELINE_SOURCE}" in branch "${CI_COMMIT_BRANCH}".`,
        )
      }
      break
    }
    case "push": {
      if (CI_COMMIT_BRANCH !== undefined) {
        console.log(`Push to branch ${CI_COMMIT_BRANCH}`)

        if (CI_COMMIT_BRANCH.match(/^\d+_\d+_\d+$/) != null) {
          console.log(
            `Ignoring commit in version branch (for branch ${CI_COMMIT_BRANCH}).`,
          )
        } else if (CI_COMMIT_TITLE?.match(/^\d+\.\d+\.\d+( |$)/) != null) {
          console.log(
            `Ignoring version commit (for version ${CI_COMMIT_TITLE}).`,
          )
        } else {
          await resetGitRepository()

          // Use the latest version of @leximpact/socio-fiscal-openfisca-json
          await $`npm install @leximpact/socio-fiscal-openfisca-json@latest`

          const pkg = await fs.readJson(packagePath)
          const nextVersionObject =
            await nextVersionObjectFromPackageAndTags(pkg)

          // Test project with the current dependencies (and latest @leximpact/socio-fiscal-openfisca-json).
          await $`ln -s example.env .env`
          await $`npm run build`
          // await $`npm test`

          if (CI_COMMIT_BRANCH === CI_DEFAULT_BRANCH) {
            // A merge request has been merged into master (ie content of project has been changed).
            // => Create a new version of project.
            await commitAndPushWithUpdatedVersions(nextVersionObject)
            await triggerDevDeployPipeline()
            await triggerProdDeployPipeline()
          }
        }
      } else if (CI_COMMIT_TAG !== undefined) {
        console.log(`Pushing commit tag "${CI_COMMIT_TAG}"…`)
      } else {
        console.log(`Unhandled push event.`)
      }
      break
    }
    default:
      console.log(
        `Unhandled event "${CI_PIPELINE_SOURCE}" in branch "${CI_COMMIT_BRANCH}".`,
      )
  }
}

function maxVersionObject(
  versionObject1: VersionObject,
  versionObject2: VersionObject | undefined,
): VersionObject {
  if (versionObject2 === undefined) {
    return versionObject1
  }
  const { major: major1, minor: minor1, patch: patch1 } = versionObject1
  const { major: major2, minor: minor2, patch: patch2 } = versionObject2
  if (major1 < major2) {
    return versionObject2
  }
  if (major1 > major2) {
    return versionObject1
  }
  if (minor1 < minor2) {
    return versionObject2
  }
  if (minor1 > minor2) {
    return versionObject1
  }
  if (patch1 < patch2) {
    return versionObject2
  }
  if (patch1 > patch2) {
    return versionObject1
  }
  // Both versions are the same. Return one of them.
  return versionObject1
}

async function nextVersionObjectFromPackageAndTags(
  pkg: Package,
): Promise<VersionObject> {
  // Retrieve current version of project.
  const tagVersionObject = (await latestVersionObjectFromTags()) ?? {
    major: 0,
    minor: 0,
    patch: 0,
  }
  const tagVersion = versionFromObject(tagVersionObject)
  let nextVersionObject = {
    ...tagVersionObject,
    patch: tagVersionObject.patch + 1,
  }

  // Ensure that the version numbers of project packages are
  // compatible with last version tag.
  const { version: packageVersion } = pkg
  const packageVersionObject = objectFromVersion(packageVersion)
  assert(
    checkVersionObject(tagVersionObject, packageVersionObject),
    `In ${packagePath}, project version should be compatible with ${tagVersion}, got "${packageVersion}" instead.`,
  )
  nextVersionObject = maxVersionObject(nextVersionObject, packageVersionObject)

  return nextVersionObject
}

function objectFromVersion(
  version: string | undefined,
): VersionObject | undefined {
  if (version === undefined) {
    return undefined
  }
  const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/) as string[]
  if (match === null) {
    return undefined
  }
  return {
    major: parseInt(match[1]),
    minor: parseInt(match[2]),
    patch: parseInt(match[3]),
  }
}

async function resetGitRepository() {
  // Reset current repo, because it may have been tranformed by a previous CI that failed.
  await $`git reset --hard`
  await $`git switch ${CI_COMMIT_BRANCH}`
  await $`git fetch origin ${CI_COMMIT_BRANCH}`
  await $`git reset --hard origin/${CI_COMMIT_BRANCH}`
  await $`git status`
  if (CI_COMMIT_BRANCH === CI_DEFAULT_BRANCH) {
    await $`git remote set-url origin git@${CI_SERVER_HOST}:${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}.git`
  } else {
    await $`git remote set-url origin https://${CI_SERVER_HOST}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}.git`
  }
}

async function triggerDevDeployPipeline() {
  const urlString = new URL(
    `/api/v4/projects/28/trigger/pipeline`,
    CI_SERVER_URL,
  ).toString()
  console.log(
    "Triggering LexImpact socio-fiscal Dev Deploy pipeline on master branch…",
  )
  const response = await fetch(urlString, {
    body: new URLSearchParams({
      ref: "master",
      token: CI_JOB_TOKEN!,
    }).toString(),
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    method: "POST",
  })
  assert(
    response.ok,
    `Unexpected response from ${urlString}: ${response.status} ${
      response.statusText
    }\n${await response.text()}`,
  )
}

async function triggerProdDeployPipeline() {
  const urlString = new URL(
    `/api/v4/projects/29/trigger/pipeline`,
    CI_SERVER_URL,
  ).toString()
  console.log(
    "Triggering LexImpact socio-fiscal Prod Deploy pipeline on master branch…",
  )
  const response = await fetch(urlString, {
    body: new URLSearchParams({
      ref: "master",
      token: CI_JOB_TOKEN!,
    }).toString(),
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    method: "POST",
  })
  assert(
    response.ok,
    `Unexpected response from ${urlString}: ${response.status} ${
      response.statusText
    }\n${await response.text()}`,
  )
}

function versionFromObject({ major, minor, patch }: VersionObject): string {
  return `${major}.${minor}.${patch}`
}

main()
  .then(() => {
    process.exit(0)
  })
  .catch((error: unknown) => {
    console.error(error)
    process.exit(1)
  })