import {
  Attestation,
  Predicate,
  Subject,
  attest,
  createStorageRecord
} from '@actions/attest'
import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci'
import { formatSubjectDigest } from './subject.js'
import * as core from '@actions/core'
import * as github from '@actions/github'

const OCI_TIMEOUT = 30000
const OCI_RETRY = 3

export type SigstoreInstance = 'public-good' | 'github'
export type AttestResult = Attestation & {
  attestationDigest?: string
  storageRecordIds?: number[]
}

export const createAttestation = async (
  subjects: Subject[],
  predicate: Predicate,
  opts: {
    sigstoreInstance: SigstoreInstance
    pushToRegistry: boolean
    createStorageRecord: boolean
    githubToken: string
  }
): Promise<AttestResult> => {
  // Sign provenance w/ Sigstore
  const attestation = await attest({
    subjects,
    predicateType: predicate.type,
    predicate: predicate.params,
    sigstore: opts.sigstoreInstance,
    token: opts.githubToken
  })

  const result: AttestResult = attestation

  if (subjects.length === 1 && opts.pushToRegistry) {
    const subject = subjects[0]
    const credentials = getRegistryCredentials(subject.name)
    const subjectDigest = formatSubjectDigest(subject)
    const artifact = await attachArtifactToImage({
      credentials,
      imageName: subject.name,
      imageDigest: subjectDigest,
      artifact: Buffer.from(JSON.stringify(attestation.bundle)),
      mediaType: attestation.bundle.mediaType,
      annotations: {
        'dev.sigstore.bundle.content': 'dsse-envelope',
        'dev.sigstore.bundle.predicateType': predicate.type
      },
      fetchOpts: { timeout: OCI_TIMEOUT, retry: OCI_RETRY }
    })

    // Add the attestation's digest to the result
    result.attestationDigest = artifact.digest

    // Because creating a storage record requires the 'artifact-metadata:write'
    // permission, we wrap this in a try/catch to avoid failing the entire
    // attestation process if the token does not have the correct permissions.
    if (opts.createStorageRecord) {
      try {
        const token = opts.githubToken
        const isOrg = await repoOwnerIsOrg(token)
        if (!isOrg) {
          // The Artifact Metadata Storage Record API is only available to
          // organizations. So if the repo owner is not an organization,
          // storage record creation should not be attempted.
          return result
        }

        const registryUrl = getRegistryURL(subject.name)
        const artifactOpts = {
          name: subject.name,
          digest: subjectDigest
        }
        const packageRegistryOpts = {
          registryUrl
        }
        const records = await createStorageRecord(
          artifactOpts,
          packageRegistryOpts,
          token
        )

        if (!records || records.length === 0) {
          core.warning('No storage records were created.')
        }

        result.storageRecordIds = records
      } catch (error) {
        core.warning(`Failed to create storage record: ${error}`)
        core.warning(
          'Please check that the "artifact-metadata:write" permission has been included'
        )
      }
    }
  }

  return result
}

// Call the GET /repos/{owner}/{repo} endpoint to determine if the repo
// owner is an organization. This is used to determine if storage
// record creation should be attempted.
export const repoOwnerIsOrg = async (githubToken: string): Promise<boolean> => {
  const octokit = github.getOctokit(githubToken)
  const { data: repo } = await octokit.rest.repos.get({
    owner: github.context.repo.owner,
    repo: github.context.repo.repo
  })
  return repo.owner?.type === 'Organization'
}

function getRegistryURL(subjectName: string): string {
  let url: URL

  try {
    url = new URL(subjectName)
  } catch {
    url = new URL(`https://${subjectName}`)
  }

  if (url.protocol !== 'https:') {
    throw new Error(
      `Unsupported protocol ${url.protocol} in subject name ${subjectName}`
    )
  }

  return url.origin
}
